不可变性可以帮助我们更好地理解我们的代码。下面我将描述如何在不牺牲性能的情况下实现这一目标。在这个由两部分组成的系列中,我将讨论如何将函数式编程方法中的想法引入Python,以实现两全其美。在这篇(也是第一篇)文章中,我们将探讨不可变数据结构的优势。第二部分探讨如何借助toolz库在Python中实现高级函数式编程概念。为什么要使用函数式编程?因为变化的事物更难推理。如果您已经确信改变会带来麻烦,那很好。如果您还不相信,您将在本文结尾找到答案。我们首先考虑正方形和长方形。如果抛开实现细节,单从接口的角度考虑,正方形是不是长方形的子类?子类的定义基于Liskov替换原则。子类必须能够做超类所做的一切。如何为矩形定义接口?fromzope.interfaceimportInterfaceclassIRectangle(Interface):defget_length(self):"""Asquarecandoit"""defget_width(self):"""Asquarecandoit"""defset_dimensions(self,length,width):"""啊"""如果我们这样定义,正方形不能是矩形的子类:如果长宽不相等,就不能响应set_dimensions方法。另一种方法是选择使矩形成为不可变对象。classIRectangle(Interface):defget_length(self):"""Squarecandoit"""defget_width(self):"""Squarecandoit"""defwith_dimensions(self,length,width):"""returnsanewrectangle"""我们现在可以将正方形视为矩形。当调用with_dimensions时,它可以返回一个新的矩形(不一定是正方形),但它本身并没有改变,它仍然是一个正方形。这似乎是一个学术问题——直到我们认为正方形和长方形在某种意义上可以被认为是容器的边。在理解了这个例子之后,我们将处理更多的传统容器来解决更现实的案例。例如,考虑随机访问数组。我们现在有ISquare和IRectangle,ISequere是IRectangle的子类。我们想将矩形放入一个随机访问数组中:可以是任何IRectangle对象"""我们还想将正方形放入随机访问数组中:classIArrayOfSquare(Interface):defget_element(self,i):"""返回一个正方形"""defset_element(self,i,square):"""'square'可以是任何ISquare对象。"""虽然ISquare是IRectangle的子集,但没有数组可以同时实现IArrayOfSquare和IArrayOfRectangle。为什么不?假设bucket实现了这两个类的功能。>>>rectangle=make_rectangle(3,4)>>>bucket.set_element(0,rectangle)#这是IArrayOfRectangle中的合法操作>>>thing=bucket.get_element(0)#IArrayOfSquare要求东西是正方形>>>assertthing.height==thing.widthTraceback(mostrecentcalllast):File"",line1,inAssertionError不能同时实现两种类型的函数,也就是说这两个类不能形成继承关系,即使ISquare是IRectangle的子类。问题来自set_element方法:如果我们实现一个只读数组,那么IArrayOfSquare可以是IArrayOfRectangle的子类。在可变的IRectangle和可变的IArrayOf*接口中,可变性会让类型和子类的思考变得更加困难——放弃转换的能力意味着可以建立我们直觉期望的类型之间的关系。可变性也有作用域的含义。当共享对象在两个地方被代码更改时,就会出现这种问题。一个典型的例子是两个线程同时更改一个共享变量。然而,在单线程程序中,即使在相距很远的两个地方共享一个变量也是一件简单的事情。从Python语言的角度来看,大多数对象都可以从许多地方访问:例如在模块全局变量中,或在堆栈跟踪中,或作为类属性。如果我们不能限制共享,那么我们可能要考虑限制可变性。这是一个使用attr库的不可变矩形:@attr.s(frozen=True)classRectange(object):length=attr.ib()width=attr.ib()@classmethoddefwith_dimensions(cls,length,width):returncls(length,width)这是一个正方形:@attr.s(frozen=True)classSquare(object):side=attr.ib()@classmethoddefwith_dimensions(cls,length,width):returnRectangle(length,width)使用frozen参数,我们可以很容易的让attrs创建的类不可变。__setitem__方法的正确实现是由其他人完成的,我们不可见。修改对象还是很容易的;但我们不可能改变它的性质。too_long=Rectangle(100,4)reasonable=attr.evolve(too_long,length=10)Pyrsistent允许我们拥有不可变的容器。#整数向量a=pyrsistent.v(1,2,3)#非整数向量b=a.set(1,"hello")虽然b不是整数向量,但没有什么可以改变a包含的属性只有整数。如果a有一百万个元素怎么办?b是否复制了999999个元素?Pyssistent具有“大O”性能保证:所有操作都是O(logn)。它还带有可选的C语言扩展,以提升“大O”性能。修改嵌套对象时,涉及到“变形金刚”的概念:m(title="没有更新",content="我很忙"),pyrsistent.m(title="仍然没有更新",content="仍然很忙")))new_blog=blog.transform(["posts",1,"content"],"prettybusy")new_blog现在将是不变的等价于:{'links':['github','twitter'],'posts':[{'content':"I'mbusy",'title':'noupdates'},{'content':'prettybusy','title':'stillnoupdates'}],'title':'Myblog'}但博客保持不变.这意味着任何引用旧对象的人都不会受到影响:转换只会产生局部影响。当共享行为猖獗时,这可能很有用。例如函数的默认参数:defsilly_sum(a,b,extra=v(1,2)):extra=extra.extend([a,b])returnsum(extra)在这篇文章中,我们看到为什么不可能Transmutation帮助我们思考我们的代码以及如何在不增加性能负担的情况下实现它。在下一篇文章中,我们将学习如何使用不可变对象来实现强大的程序结构。