Python函数式编程:不可变数据结构

开发 后端
在这个由两篇文章构成的系列中,我将讨论如何将函数式编程方法论中的思想引入至 Python 中,来充分发挥这两个领域的优势。

 [[249083]]

不可变性可以帮助我们更好地理解我们的代码。下面我将讲述如何在不牺牲性能的条件下来实现它。

在这个由两篇文章构成的系列中,我将讨论如何将函数式编程方法论中的思想引入至 Python 中,来充分发挥这两个领域的优势。

本文(也就是***篇文章)中,我们将探讨不可变数据结构的优势。第二部分会探讨如何在 toolz 库的帮助下,用 Python 实现高层次的函数式编程理念。

为什么要用函数式编程?因为变化的东西更难推理。如果你已经确信变化会带来麻烦,那很棒。如果你还没有被说服,在文章结束时,你会明白这一点的。

我们从思考正方形和矩形开始。如果我们抛开实现细节,单从接口的角度考虑,正方形是矩形的子类吗?

子类的定义基于里氏替换原则。一个子类必须能够完成超类所做的一切。

如何为矩形定义接口?

  1. from zope.interface import Interface
  2.  
  3. class IRectangle(Interface):
  4.     def get_length(self):
  5.         """正方形能做到"""
  6.     def get_width(self):
  7.         """正方形能做到"""
  8.     def set_dimensions(self, length, width):
  9.         """啊哦"""

如果我们这么定义,那正方形就不能成为矩形的子类:如果长度和宽度不等,它就无法对 set_dimensions 方法做出响应。

另一种方法,是选择将矩形做成不可变对象。

  1. class IRectangle(Interface):
  2.     def get_length(self):
  3.         """正方形能做到"""
  4.     def get_width(self):
  5.         """正方形能做到"""
  6.     def with_dimensions(self, length, width):
  7.         """返回一个新矩形"""

现在,我们可以将正方形视为矩形了。在调用 with_dimensions 时,它可以返回一个新的矩形(它不一定是个正方形),但它本身并没有变,依然是一个正方形。

这似乎像是个学术问题 —— 直到我们认为正方形和矩形可以在某种意义上看做一个容器的侧面。在理解了这个例子以后,我们会处理更传统的容器,以解决更现实的案例。比如,考虑一下随机存取数组。

我们现在有 ISquareIRectangle,而且 ISequereIRectangle 的子类。

我们希望把矩形放进随机存取数组中:

  1. class IArrayOfRectangles(Interface):
  2.     def get_element(self, i):
  3.         """返回一个矩形"""
  4.     def set_element(self, i, rectangle):
  5.         """'rectangle' 可以是任意 IRectangle 对象"""

我们同样希望把正方形放进随机存取数组:

  1. class IArrayOfSquare(Interface):
  2.     def get_element(self, i):
  3.         """返回一个正方形"""
  4.     def set_element(self, i, square):
  5.         """'square' 可以是任意 ISquare 对象"""

尽管 ISquareIRectangle 的子集,但没有任何一个数组可以同时实现 IArrayOfSquareIArrayOfRectangle.

为什么不能呢?假设 bucket 实现了这两个类的功能。

  1. >>> rectangle = make_rectangle(3, 4)
  2. >>> bucket.set_element(0, rectangle) # 这是 IArrayOfRectangle 中的合法操作
  3. >>> thing = bucket.get_element(0) # IArrayOfSquare 要求 thing 必须是一个正方形
  4. >>> assert thing.height == thing.width
  5. Traceback (most recent call last):
  6.   File "<stdin>", line 1, in <module>
  7. AssertionError

无法同时实现这两类功能,意味着这两个类无法构成继承关系,即使 ISquareIRectangle 的子类。问题来自 set_element 方法:如果我们实现一个只读的数组,那 IArrayOfSquare 就可以是 IArrayOfRectangle 的子类了。

在可变的 IRectangle 和可变的 IArrayOf* 接口中,可变性都会使得对类型和子类的思考变得更加困难 —— 放弃变换的能力,意味着我们的直觉所希望的类型间关系能够成立了。

可变性还会带来作用域方面的影响。当一个共享对象被两个地方的代码改变时,这种问题就会发生。一个经典的例子是两个线程同时改变一个共享变量。不过在单线程程序中,即使在两个相距很远的地方共享一个变量,也是一件简单的事情。从 Python 语言的角度来思考,大多数对象都可以从很多位置来访问:比如在模块全局变量,或在一个堆栈跟踪中,或者以类属性来访问。

如果我们无法对共享做出约束,那我们可能要考虑对可变性来进行约束了。

这是一个不可变的矩形,它利用了 attr 库:

  1. @attr.s(frozen=True)
  2. class Rectange(object):
  3.     length = attr.ib()
  4.     width = attr.ib()
  5.     @classmethod
  6.     def with_dimensions(cls, length, width):
  7.         return cls(length, width)

这是一个正方形:

  1. @attr.s(frozen=True)
  2. class Square(object):
  3.     side = attr.ib()
  4.     @classmethod
  5.     def with_dimensions(cls, length, width):
  6.         return Rectangle(length, width)

使用 frozen 参数,我们可以轻易地使 attrs 创建的类成为不可变类型。正确实现 __setitem__ 方法的工作都交给别人完成了,对我们是不可见的。

修改对象仍然很容易;但是我们不可能改变它的本质。

  1. too_long = Rectangle(100, 4)
  2. reasonable = attr.evolve(too_long, length=10)

Pyrsistent 能让我们拥有不可变的容器。

  1. # 由整数构成的向量
  2. a = pyrsistent.v(1, 2, 3)
  3. # 并非由整数构成的向量
  4. b = a.set(1, "hello")

尽管 b 不是一个由整数构成的向量,但没有什么能够改变 a 只由整数构成的性质。

如果 a 有一百万个元素呢?b 会将其中的 999999 个元素复制一遍吗?Pyrsistent 具有“大 O”性能保证:所有操作的时间复杂度都是 O(log n). 它还带有一个可选的 C 语言扩展,以在“大 O”性能之上进行提升。

修改嵌套对象时,会涉及到“变换器”的概念:

  1. blog = pyrsistent.m(
  2.     title="My blog",
  3.     links=pyrsistent.v("github", "twitter"),
  4.     posts=pyrsistent.v(
  5.         pyrsistent.m(title="no updates",
  6.                      content="I'm busy"),
  7.         pyrsistent.m(title="still no updates",
  8.                      content="still busy")))
  9. new_blog = blog.transform(["posts", 1, "content"],
  10.                           "pretty busy")

new_blog 现在将是如下对象的不可变等价物:

  1. {'links': ['github', 'twitter'],
  2.  'posts': [{'content': "I'm busy",
  3.             'title': 'no updates'},
  4.            {'content': 'pretty busy',
  5.             'title': 'still no updates'}],
  6.  'title': 'My blog'}

不过 blog 依然不变。这意味着任何拥有旧对象引用的人都没有受到影响:转换只会有局部效果。

当共享行为猖獗时,这会很有用。例如,函数的默认参数:

  1. def silly_sum(a, b, extra=v(1, 2)):
  2.     extra = extra.extend([a, b])
  3.     return sum(extra)

在本文中,我们了解了为什么不可变性有助于我们来思考我们的代码,以及如何在不带来过大性能负担的条件下实现它。下一篇,我们将学习如何借助不可变对象来实现强大的程序结构。

责任编辑:庞桂玉 来源: Linux中国
相关推荐

2022-07-13 16:38:32

Python可变数据类型不可变数据类型

2017-10-26 08:53:38

前端JavaScript函数式编程

2018-09-21 11:19:30

Lambda架构函数数据系统

2023-09-21 16:13:20

Python数据结构

2024-02-26 10:47:09

Python数据类型开发

2009-09-27 15:23:00

Scala讲座函数式编程Scala

2024-04-02 08:00:00

函数式编程开发

2023-10-20 08:18:17

Python数据类型

2023-12-14 15:31:43

函数式编程python编程

2020-01-21 08:27:50

代码开发Python

2024-04-08 07:58:11

Python数据类型字符串

2024-01-15 07:15:05

函数式编程代码

2019-04-14 22:22:28

Python数据结构算法

2024-03-08 13:06:31

Array数组JavaScrip

2021-07-16 07:57:34

Python数据结构

2022-07-07 09:03:36

Python返回函数匿名函数

2021-06-14 09:34:23

对象存储存储

2021-04-13 09:37:41

Java数据结构算法

2021-03-09 06:30:32

JAVA数据结构算法

2021-03-18 08:44:20

Java数据结构算法
点赞
收藏

51CTO技术栈公众号