Python2.6-原理之类和oop(下)
来自《python学习手册第四版》第六部分
五、运算符重载(29章)
这部分深入介绍更多的细节并看一些常用的重载方法,虽然不会展示每种可用的运算符重载方法,但是这里给出的代码也足够覆盖python这一类功能的所有可能性。运算符重载只是意味着在类方法中拦截内置的操作,当类的实例出现在内置操作中,python自动调用我们自己的方法,并且返回值变成了相应操作的结果:a、运算符重载让类拦截常规的Python运算;b、类可以重载所有Python表达式运算符;c、类也可重载打印、函数调用、属性点号运算等内置运算;d、重载使类实例的行为像内置类型;e、重载是通过提供特殊名称的类方法来实现的。
1、构造函数和表达式:__init__和__sub__。来举个简单的重载例子。例如,下面文件number.py内的Number类提供一个方法来拦截实例的构造函数(__init__),此外还有一个方法捕捉减法表达式(__sub__)。这种特殊的方法是钩子,可与内置运算相绑定:
该代码所见到的__init__构造函数是python中最常用的运算符重载方法,它存在与绝大多数类中。
2、常见的运算符重载方法:在类中,对内置对象(例如,整数和列表)所能做的事,几乎都有相应的特殊名称的重载方法。表29-1列出其中一些最常用的重载方法。事实上,很多重载方法有好几个版本(例如,加法就有__add__、__radd__和__iadd__):
所有重载方法的名称前后都有两个下划线字符,以便把同类中定义的变量名区别开来。特殊方法名称与表达式惑运算的映射关系,是由python语言预先定义好的(在标准语言手册中有说明)。例如:名称__add__按照python语言的定义,无论__add__方法的代码实际在做些什么,总是对应到了表达式+。如果没有定义运算符重载方法的话,它可能继承自超类,就像任何其他的方法一样。运算符重载方法也都是可选的,如果没有编写或继承一个方法,类直接不支持这些运算,并且视图使用它们会引发一个异常。一些内置操作,如打印,有默认的重载方法(继承自3.0中隐含object类),但是,如果没有给出相应的运算符重载方法的话,大多数内置函数会对类实例失效。多数重载方法只用在需要对象行为表现的就像内置类型一样的高级程序中。
3、索引和分片:__getitem__和__setitem__。如果在类中定义了(惑继承了)的话,则对于实例的索引运算,会自动调用__getitem__。当实例X出现在X【i】这样的索引运算中时,python会调用这个实例继承的__getitem__方法(如果有的话),把X作为第一个参数传递,并且方括号内的索引值传给第二个参数。例如,下面的类将返回索引值的平方:
4、拦截分片:除了索引,对于分片表达式也调用__getitem__。正式的说,内置类型以同样的方式处理分片。例如,下面是在一个内置列表上工作的分片,使用了上边界和下边界以及一个stride:
实际上,分片边界绑定到了一个分片对象中,并且传递给索引的列表实现。实际上,我们总是可以手动的传递一个分片对象,分片语法主要是用一个分片对象进行索引的语法糖:
对于带有一个__getitem__的类,这是很重要的,该方法将既针对基本索引(带有一个索引)调用,又针对分片(带有一个分片对象)调用。我们前面的类没有处理分片,因为它的数学假设传递了整数索引,但是,如下类将会处理分片。当针对索引调用的时候,参数像前面一样是一个整数:
当针对分片调用的时候,方法接收一个分片对象,它在一个新的索引表达式中直接传递给嵌套的列表索引:
如果使用的话,__setitem__索引赋值方法类似的拦截索引和分片赋值,它为后者接收了一个分片对象,它可能以同样的方式传递到另一个索引赋值中:
5、实际上,__getitem__可能在甚至比索引和分片更多的环境中自动调用,在3.0之前,类也可以定义__getslice__和__setslice__方法来专门拦截分片获取和赋值;它们将传递一系列的分片表达式,并且优先于__getitem__和__setitem__用于分片。这些特定于分片的方法已经从3.0中移除了,因此,应该使用__getitem__和__setitem__来替代,以考虑到索引和分片对象都可能作为参数。在大多数类中,这不需要任何特殊的代码就能工作,因为索引方法可以在另一个索引表达式的方括号中传递分片对象(就像例子中那样)。此外,不要混淆了3.0中用于索引拦截的__index__方法;需要的时候,该方法针对一个实例返回一个整数值,供转化为数字字符串的内置函数使用:
尽管这个方法并不会拦截像__getitem__这样的实例索引,但它也可以在需要一个整数的环境中应用--包括索引:
该方法在2.6中以同样的方式工作,只不过它不会针对hex和oct内置函数调用(在2.6中使用__hex__和__oct__来拦截这些调用)。
6、索引迭代:__getitem__。新手可能暂时不太领会这里的技巧。但这些技巧都非常有用。for语句的作用是从0到更大的索引值,重复对序列进行索引运算,直到检测到超出边界的衣长。因此,__getitem__也可以是python中一种重载迭代的方式。如果定义了这个方法,for循环每次循环时都会调用类的__getitem__,并持续搭配有更高的偏移值:任何会响应索引运算的内置惑用户定义的对象,同样会响应迭代:
任何支持for循环的类也会自动支持python所有迭代环境,而其中多种环境在前几章看过了。例如,成员关系测试 in 、列表解析、内置函数map、列表和元组赋值运算以及类型构造方法也会自动调用__getitem__(如果定义了的话):
在实际应用中,这个技巧可用于建立提供序列接口的对象,并新增逻辑到内置的序列类型运算。第31章会扩展内置类型的。
7、迭代器对象:__iter__和__next__。虽然上一节的__getitem__技术有效,但它真的只是迭代的一种退而求其次的方法。如今python中所有的迭代环境都会先尝试__iter__方法,在尝试__getitem__。也就是说,它们宁愿使用第14章所学到的迭代协议,然后才是重复对对象进行索引运算。只有在对象不支持迭代协议的时候,才会尝试索引运算。一般来说,也应该有限使用__iter__,它能够比__getitem__更好的支持一般的迭代环境。从技术上来说,迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来师兄的,而这种方法应该返回一个迭代器对象。如果已经提供了,python会重复调用这个迭代器对象的next方法,直到发生StopIteration异常。如果没找到这类__iter__方法,python会改用__getitem__机制,就像之前那样通过偏移量重复索引,知道引发IndexError异常(对于手动迭代来说,一个next内置函数也可以很方便的使用:next(I) 与I.__next__()是相同的)。ps:在2.6中叫做I.next(),而在3.0中叫做I.__next__()。
8、用户定义的迭代器。在__iter__机制中,类就是通过实现第14章和第20章介绍的迭代其协议,来实现用户定义的迭代器的。例如,下面的文件iters.py,定义了用户定义的迭代器类来生成平方值:
这里,迭代器对象就是实例self,因为next方法是这个类的一部分。在较为复杂的场景中,迭代器对象可定义为个别的类或有自己的状态信息的对象,对相同数据支持多种迭代。以python raise语句发出信号表示迭代结束。手动迭代对内置类型也有效:
__getitem__所写的等效代码可能不是很自然,因为for会对所有的0和较高值的偏移值进行迭代。传入的偏移值和所产生的值的范围只有间接的关系(O...N需要映射为start...stop)。因为__iter__对象会在调用过程中明确的保留状态信息,所以比__getitem__具有更好的通用性。另外,有时__iter__迭代其会比__getitem__更复杂和难用。迭代器是用来迭代,不是随机的索引运算。事实上,迭代器根本没有重载索引表达式:
__iter__机制也是我们在__getitem__中所见到的其他所有迭代环境的实现方式(成员关系测试、类型构造函数、序列赋值运算等)。然而,和__getitem__不同的是,__iter__只循环一次,而不是循环多次。例如,Squares类只循环一次,循环之后就变为空。每次新的循环,都得创建一个新的迭代器对象:
ps:如果用生成器函数编写,这个例子可能更简单:
和类不同,这个函数会自动在迭代中存储其状态。当然,这是假设的例子。实际上,可以跳过这两种技术,只用for循环、map或是列表解析,一次创建这个列表。在python中,完成任务最佳而且最快的方法通常也是最简单的方法:
不过在模拟更复杂的迭代时,类会比较好用。
9、有多个迭代器的对象:之前,提到过迭代器对象可以定义成一个独立的类,有其自己的状态信息,从而能够支持相同数据的多个迭代。考虑一下,当步进到字符串这类内置类型时,会发生什么:
在这里,外层循环调用iter从字符串中取得迭代器,而每个嵌套的循环也做相同的事来获得独立的迭代器。因为每个激活状态下的迭代其都有自己的状态信息,而不管其他激活状态下的循环是什么状态。在前面的14、20章都有,例如,生成器函数和表达式。以及map和zip这样的内置函数,都证明是单迭代对象;相反,range内置函数和其他的内置类型(如列表),支持独立位置的多个活跃迭代器。当用类编写用户定义的迭代器时,由我们来决定是支持一个单个的还是多个活跃的迭代。要达到多个迭代器的效果,__iter__只需替迭代器定义新的状态对象,而不是返回self。下面定义了一个迭代器,迭代时,跳过下一个元素。因为迭代器对象会在每次迭代时都重新创建,所以能够支持多个处于激活状态下的循环:
运行时,这个例子工作的和对内置字符串进行嵌套循环一样,因为每个循环都会获得独立的迭代器对象来记录自己的状态信息,所以每个激活状态下的循环都有自己在字符串中的位置:
作为对比,除非我们在嵌套循环中再次调用Squares来获得新的迭代对象,否则之前的Squares例子只支持一个激活状态的迭代。这里,只有SkipObject,但从该对象中创建了许多的迭代器对象。可以使用内置工具达到类似的效果,例如,用第三参数边界值进行分片运算来跳过元素:
不过这并不相同,因为:a、这里的每个分片表达式,实质上是一次把结果列表存储在内存中;b、迭代器则是一次产生一个值,这样使大型结果列表节省了实际的空间。其次,分片产生的新对象,其实我们没有对同一个对象进行多处的循环。为了更接近类,需要事先创建一个独立的对象通过分片运算进行步进:
这样与基于类的解决方法更相似一些,但是,它仍是一次性把分片结果存储在内存中(目前内置分片运算并没有生成器),并且只等效于这里跳过一个元素的特殊情况。因为迭代器能够做类能做的任何事,所以它比例子所展现的更通用。无论应用程序是否需要这种通用性,用户定义的迭代器都是强大的工具,可以让我们把任意对象的外观和用法变得很像本教程所遇到的其他序列和可迭代对象。例如,可以将这项技术用在数据库对象中,通过迭代进行数据库的读取,让多个游标进入同一个查询结果。
10、成员关系:__contains__、__iter__和__geitiem__:迭代器的内容比目前看到的还丰富,运算符重载往往是多个层级的:类可以提供特定的方法,或者用作退而求其次的更通用的替代方法,例如:a、2.6中的比较使用__It_这样的特殊方法来表示少于比较(如果有的话)、或者使用通用的__cmp__。3.0只使用特殊的方法,而不是__cmp_-,如后面说道的;b、布尔测试类似于先尝试一个特定的__bool__(以给出一个明确的Ture/False结果),并且,如果没有它,将会退而求其次到更通用的__len__(一个非零的长度意味着True)。正如后面见到的,2.6也一样起作用,但是,使用名称__nonzero__而不是__bool__。在迭代中,类通常把in成员关系运算符实现为一个迭代,使用__iter__方法或__getitem__方法。要支持更加特定的成员关系,类可能编写一个__contains__方法,当出现的时候,该方法优先于__iter__方法,__iter__方法优先于__getitem__方法。__contains__方法应该把成员关系定义为对一个映射应用键,以及用于序列的搜索。考虑下面的类,它编写了所有3个方法和测试成员关系以及应用于一个实例的各种迭代环境。调用的时候,其方法会打印出跟踪消息:
(print和if在同一个层级)。
这段脚本运行的时候,器输出如下所示,特定的__contains__拦截成员关系,通用的__iter__捕获其他的迭代环境以至__next__重复地被调用,而__getitem__不会被调用:
但是,要观察如果注释掉__contains__方法后代码的输出发生了什么,成员关系现在路由到了通用的__iter__:
正如我们所看到的,__getitem__方法甚至更通用:除了迭代,它还拦截显示索引和分片。分片表达式用包含边界的一个分片对象来触发__getitem__,既针对内置类型,也针对用户定义的类,因此,我们的类中分片是自动化的:
然而,在并非面向序列的、更加现实的迭代用例中,__iter__方法可能很容易编写,因为它不必管理一个整数索引,并且__contains__考虑到作为一种特殊情况优化成员关系。
11、属性引用:__getattr__和__setattr__。__getattr__方法是拦截属性点号运算。更具体的说,当通过对未定义属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果python可以通过其继承树搜索流程找到这个属性,该方法就不会被调用。因为有这种情况,所以__getattr__可以作为钩子来通过通用的方式响应属性请求。例子如下:
这里,empty类和其实例X本身并没有属性,所以对X.age的存取会转至__getattr__方法,self则赋值为实例(X),而attrname则赋值为未定义的属性名称字符串(“age”)。这个类传回一个实际值作为X.age点号表达式的结果(40),让age看起来像实际的属性。实际上,age变成了动态计算的属性。当类不知道该如何处理的属性,__getattr__会引发内置的AttributeError异常,告诉python,那真的是未定义属性名。请求X.name时,会引发错误。当在后两章看到实际的委托和内容属性时,会再看到__getattr__。
12、接上11.有个相关的重载方法__setattr__会拦截所有属性的赋值语句。如果定义了这个方法,self.attr=value会变成self.__setattr__('attr',value)。这一点技巧性很高,因为在__setattr__中对任何self属性做赋值,都会再调用__setattr__,导致了无穷递归循环(最后就是堆栈溢出异常)。如果想使用这个方法,要确定是通过对属性字典做索引运算来赋值任何实例属性的。也就是说,是使用self.__dict__['name'] = x,而不是self.name = x。
13、其他属性管理工具:a、__getattribute__方法拦截所有的属性获取,而不只是那些未定义的,但是,当使用它的时候,必须比使用__getattr__更小心的避免循环;b、Property内置函数运行我们把方法和特定类属性上的获取和设置操作关联起来;c、描述符提供了一个协议,把一个类的__get__和__set__方法与对特定类属性的访问关联起来。在第31章介绍,并在第37章详细的介绍所有属性管理技术。
14、模拟实例属性的私有性:第一部分。下面代码把上一个例子通用化了,让每个子类都有自己的私有变量列表,这些变量名无法通过其实例进行赋值:
实际上,这是python中实现属性私有性(也就是无法在类外对属性名进行修改)的首选方法。虽然python不支持private声明,但类似这种技术可以模拟其主要的目的。不过,这只是一部分的解决方案。为使其更有效,必须增强它的功能,让子类也能够设置私有属性,并且使用__getattr__和包装(有时候称为代理)来检测对私有属性的读取。在第38章将会介绍类装饰器来更加通用的拦截和验证属性,即使私有性可以以此方式模拟,但实际应用中几乎不会这么做。
15、__repr__和__str__会返回字符串表达式形式。下一个例子是已经见过的__init__构造函数和__add__重载方法,也会定义返回实例的字符串表达形式的__repr__方法。字符串格式把self.data对象转换为字符串。如果定义了话,当类的实例打印或转换成字符串时__repr__(或其近亲__str__)就会自动调用。这些方法可替对象定义更好的显示格式,而不是使用默认的实例显示。实例对象的默认显示既无用也不好看:
但是编写或继承字符串表示方法允许我们定制显示:
两个显示的方法,是为了进行用户友好的显示。具体的说:a、打印操作会首先尝试__str__和str内置函数(print运行的内部等价形式)。它通常应该返回一个用户友好的显示;b、__repr__用于所有其他的环境中:用户交互模式下提示回应以及repr函数,如果没有使用__str__,会使用print和str。它通常应该返回一个编码字符串,可以用来重新创建对象,或者给开发者一个详细的显示。__repr__用于任何地方,除了当定义了一个__str__的时候,使用print和str。不过如果没有定义__str__,打印还是使用__repr__,但反过来并不成立,其他环境,例如,交互式响应模式,只是使用__repr__,并且根本不要尝试__str__:
正是因为这一点,如果想让所有环境都有统一的显示,__repr__是最佳选择。不过通过分别定义这两个方法,就可以在不同环境内支持不同显示。例如,终端用户显示使用__str__,而程序员在开发期间则使用底层的__repr__来显示。实际上,__str__只是覆盖了__repr__以得到用户友好的显示环境:
这里提到的两种用法。首先,记住_-str__和__repr_-都必须返回字符串;其他的结果类型不会转换并会引发错误,因此,如果必要的话,确保用一个转换器处理它们。其次根据一个容器的字符串转换逻辑,__str__的用户友好的显示可能只有当对象出现在一个打印操作顶层的时候才应用,嵌套到较大对象中的对象可能用其__repr__或默认方法打印。如下代码说了:
为了确保一个定制显示在所有的环境中都显示而不管容器是什么,可以编写__repr__,而不是__str__;前者在所有的情况下都运行,即便后者不适用的情况也是如此:
在实际应用中,除了__init__以外,__str__(或其近亲__repr__)似乎是python脚本中第二个最常用的运算符重载方法。在可以打印对象并且看见定制显示的任何时候,可能就是使用这两个之一的工具。
16、右侧加法和原处加法:__radd__和__iadd__。从技术上说,前面的例子中出现的__add__方法并不支持+运算右侧使用实例对象。要实现这类表达式,而支持可互换的运算符,可以一并编写__radd__方法。只有当+右侧的对象是类实例,而左边对象不是类实例时,python才会调用__radd__。在其他所有情况下,则由左侧对象调用__add__方法:
__radd__中的顺序与之相反:self 是在+的右侧,而other是在左侧。此外,注意 x 和 y 是同一个类的实例。当不同类的实例混合出现在表达式时,python优先选择左侧的那个类。当两个实例相加的时候,python运行__add__,它反过来通过简化左边的运算数来触发__radd__。在更为实际的类中,其中类类型可能需要在结果中传播,事情可能变得更需要技巧:类型测试可能需要辨别它是否能够安全的转换并由此避免嵌套。例如:下面的代码中如果没有isinstance测试,当两个实例相加并且__add__触发__radd__的时候,我们最终得到一个Computer,其val是另一个Commuter:
17、原地加法:为了也实现+=原处扩展相加,编写一个__iadd__或__add__。如果前者空缺的话,使用后者。实际上,前面小节的Commuter类为此已经支持+=了,但是__iadd__考虑到了更加高效的原处修改:
每个二元运算都有类似的右侧和原处重载方法,它们以相同的方式工作(例如:__mul__,__rmul__和__imul__)。右侧方法在实际中很少用到:只有在需要运算符具有交换性的时候,才会编写它们,并且只有在真正需要支持这样的运算符的时候,才会使用。例如,一个Vector类可能使用这些工具,但是一个Employee或Button类可能不会。
18、call表达式__call__。当调用实例时,使用__call__方法。这不是循环定义:如果定义了,python就会为实例应用函数调用表达式运行__call__方法。这样可以让类实现的外观和用法类似于函数:
更正式的说,在第18章介绍的所有参数传递方式,__call__方法都支持,传递给实例的任何内容都会传递给该方法,包括通常隐式的实例参数。例如,方法定义:
都匹配如下所有的实例调用:
直接效果就是,带有一个__call__的类和实例,支持与常规函数和方法完全相同的参数语法和语义。像这样的拦截调用表达式允许类实例模拟类似函数的外观,但是,也在调用中保持了状态信息以供使用:
在这个示例中,__call__乍一看可能有点怪,一个简单的方法可以提供类似的功能:
然而,当需要为函数的API编写接口时,__call__就变得很有用:这可以编写遵循所需要的函数来调用接口对象,同时又能保留状态信息。事实上,这可能是除了__init__构造函数以及__str__和__repr__显示格式方法外,第三个最常用的运算符重载方法了。
19、函数接口和回调代码。(这里以python安装的时候附带的tkinter gui工具箱为例)tkinter gui工具箱可以将函数注册成事件处理器(也就是回调函数callback)。当事件发生时,tkinter会调用已注册的对象。如果想让事件处理器保存事件之间的状态,可以注册类的绑定方法或者遵循所需接口的实例(使用__call__)。在这一节的代码中,第二个例子中的x.comp和第一个例子中的x都可以用这种方式作为类似于函数的对象传递。这里举个假设__call__例子,应用于gui领域。下列类定义了一个对象,支持函数调用接口,但是也有状态信息,可记住稍后按下按钮后应该变成什么颜色:
现在在gui环境中,即使这个gui期待的事件处理器是无参数的简单函数,还是可以为按钮把这个类的实例注册成事件处理器:
当这个按钮按下时,会把实例对象单词简单的函数来调用,就像下面的调用一样。不过,因它把状态保留成实例的属性,所以知道应该做什么:
实际上,这可能是python语言中保留状态信息的最好方式,比之前针对函数所讨论的技术更好(全局变量、嵌套函数作用域引用以及默认可变参数等)。利用oop,状态的记忆是明确的使用属性赋值运算而实现的。python程序员偶尔还会用两种其他方式,把信息和回调函数联系起来。其中一个选项是使用lambda函数的默认参数:
另一种是使用类的绑定方法:这种对象记住了self实例以及所引用的函数,使其可以在稍后通过简单的函数调用而不需要实例来实现:
当按钮按下时,就好像是gui这么做的,启用changeColor方法来处理对象的状态信息:
这种技巧较为简单,比起__call__重载就不通用了。__call__可让我们把状态信息附加在可调用对象上,所以自然而然的成为了被一个函数记住并调用了另一个函数的实现技术。
20、比较:__It__、__gt__和其他方法。正如表29-1所示,类可以定义方法来捕获所有的6种比较运算符:<、>、<=、>=、==、!=。这些方法通常很容易使用,但是有下面的这些限制:a、与前面讨论的__add__、__radd__对不同,比较方法没有右端形式。相反,当只有一个运算数支持比较的时候,使用其对应方法(例如,__It__和__gt__互为对应);b、比较运算符没有隐式关系。例如,==并不意味着 != 是假的,因此,__eq__和__ne__应该定义为确保两个运算符都正确的作用;c、在2.6中,如果没有定义更为具体的比较方法的话,对所有比较使用一个__cmp__方法。它返回一个小于、等于或大于0的数,以表示比较其两个参数(self和另一个操作数)的结果。这个方法往往使用cmp(x,y)内置函数来计算其结果。__cmp__方法和cmp内置函数都从3.0中删除了:使用更特定的方法来替代。作为一个快速介绍,考虑如下的类和测试代码:
在3.0和2.6下运行的时候,末尾的打印语句显示它们的注释中提到的结果,因为该类的方法拦截并实现了比较表达式。
21、2.6的__cmp__方法(已经从3.0中移除了)。在2.6中,如果没有定义更加具体的方法的话,__cmp__方法作为一种退而求其次的方法:它的整数结果用来计算正在运行的运算符。例如:如下的代码在2.6下产生同样的结果,但是在3.0中失败,因为__cmp__不再可用:
这在3.0中失效是因为__cmp__不再特殊,而不是因为cmp内置函数不再使用。如果我们把前面的类修改为如下的形式,以试图模拟cmp调用,那么代码将在python2.6中工作,但在3.0下无效:
22、布尔测试:__bool__和__len__。正如前面提到的,类可能也定义了赋予其实例布尔特性的方法,在布尔环境中,python首先尝试__bool__来获取一个直接的布尔值,然后,如果没有该方法,就尝试__len__类根据对象的长度确定一个真值。通常,首先使用对象状态或其他信息来生成一个布尔结果:
如果没有这个方法,python退而求其次的求长度,因为一个非空对象看作是真(如,一个非零长度意味着对象是真的,并且一个零长度意味着它为假):
如果两个方法都有,python喜欢__bool__胜过__len__,因为它更具体:
如果没有定义真的方法,对象毫无疑问的看作为真:
23、2.6中的布尔。对于2.6的用户应该在第22中的所有代码中使用__nonzero__而不是__bool__。3.0把2.6的__nonzero__方法重新命名为__bool__,当布尔测试以相同的方式工作(3.0和2.6都是用__len__作为候补)。如果没有使用2.6的名称,本节中第一个测试将会同样的工作,但是,仅仅是因为__bool__在2.6中没有识别为一个特殊的方法名称,并且对象默认看作是真的。:
这在3.0中像宣传的那样有效。然而,在2.6中,__bool__被忽视并且对象总是看作是真:
在2.6中,针对布尔值使用__nonzero__(或者从设置为假的__len__候补方法返回0):
不过,__nonzero__只在2.6中有效;如果在3.0中使用,它将默认的忽略,并且对象将被默认的分类为真,就像是在2.6中使用__bool_-一样。
24、对象析构函数:__del__。每当实例产生时,就会调用__init__构造函数。每当实例空间被收回时(在垃圾收集时),它的对立面__del__,也就是析构函数,就会自动执行:
这里,当brian赋值为字符串时,我们会失去life实例的最后一个引用。因此会触发器析构函数,。这样可以用于一些清理行为(例如,中断服务器的连接)。然而,基于某些原因,在python中,析构函数不像其他oop语言那么常用:a、因为python在实例收回时,会自动收回该实例所拥有的所有空间,对于空间管理来说,是不需要析构函数的;b、无法轻易的预测实例何时收回,通常最好是在有意调用的方法中(try/finally语句)编写代码去终止活动。在某种情况下,系统表中可能还在引用该对象,使析构函数无法执行。ps:实际上,__del__可能会很难使用。例如,直接向sys.stderr打印一条警告消息,而不是触发一个异常事件,这也会从中引发异常,因为垃圾收集器在不可预料的环境下运行。此外,当我们期待垃圾收集的时候,对象间的循环引用可能会阻止其发生。一个可选的循环检测器,是默认可用的,最终可以自动检测这样的对象,但是,只有在它们没有__del__方法的时候才可用。可参见python标准手册对__del__和gc 垃圾收集模块的介绍。
六、类的设计(30章)
这里介绍一些核心的oop概念,以及一些比目前展示过的例子更实际的额外例子。这里有:继承、组合、委托、工厂;类设计的概念,伪私有属性、多继承和边界方法,这部分简单介绍,更详细的需要参考一些oop设计的书籍。
1、python的oop实现可以概括为三个概念,:a、继承,继承是基于python中的属性查找的(在X.name表达式中);b、多态,在X.method方法中,metohd的意义取决于X的类型(类);c、封装,方法和运算符实现行为,数据隐藏默认是一种惯例。之前多次介绍了python中的多态;这是因为python没有类型声明而出现的。因为属性总是在运行期解析,实现相同接口的对象是可互相交换的,所以客户端不需要知道实现它们调用的方法的对象种类。
2、通过调用标记进行重载(或不要):有些oop语言把多态定义成基于参数类型标记(type signature)的重载函数。但是,因为python中没有类型声明,所以这种概念行不通,python中的多态是基于对象接口的,而不是类型。下面是通过参数列表进行重载方法:
这样的代码是会执行的,但是,因为def只是在类的作用域中把对象赋值给变量名,这个方法函数的最后一个定义才是唯一保留的(就好像X=1,然后X=2,结果X将是2)。基于类型的选择,可以使用第4、9章见过的类型测试的想法去编写代码,或者使用低18章的参数列表工具:
通常来说,不需要这么做,第16章说的,应该将程序写成预期的对象接口,而不是特定的数据类型:
3、oop和继承:“是一个(is -a)”关系。这里举个例子,开一家批萨店,需要聘请员工,而且需要创造一个机器人制作批萨,不过也会将机器人看作有薪水的功能齐全的员工。该团队可以通过文件employees.py中的四个类来定义。最通用的类Employee提供共同行为,例如,加薪(giveRaise)和打印(__repr__)。员工有两种,所以Employee有两个子类:Chef和Server。这两个子类都会覆盖继承的work方法来打印更具体的信息。最后,匹萨机器人是由更具体的类来模拟:PizzaRobot是一种Chef,也是一种Employee。以oop来说,成这些关系为"is-a"链接:机器人是一个主厨,而主厨是一个员工,以下是employees.py文件:
当执行此模块中的自我测试代码时,会创建一个名为bob的制作匹萨机器人,从三个类继承变量名:PizzaRobot、Chef、Employee。例如,打印bob会执行Employee.__repr__方法,而给予bob加薪,则会运行Employee.giveRaise,因为继承会在这里找到这个方法:
在这样的类层次中,通常可以创建任何类的实例,而不只是底部的类。例如,这个模块中自我测试程序代码的for循环,创建了四个类的实例。要求工作时,每个反应都不同,因为work方法都各不相同。其实,这些类只是模仿真实世界的对象。work在这里只打印信息。
4、oop和组合:“has-a”关系。对程序员来说,组合就是把其他对象嵌入容器对象内,并使其实现容器方法;对设计师而言,组合是另一种表示问题领域中关系的方式。但是组合不是集合的成员关系,而是组件,也就是整体的组成部分。组合也反映了各组成部分之间的关系,通常称为“has-a”关系,有些oop设计书籍把组合称为聚合(aggregation),或者使用聚合描述容器和所含物之间较弱的依赖关系来区分这两个术语。“组合”就是指内嵌对象集合体。组合类一般都提供自己的接口,并通过内嵌的对象来实现接口。对于这个例子来说,就是有烤炉,服务生,主厨。顾客下单时:服务生接单,主厨制作匹萨等。下面的文件pizzashop.py文件:
PizzaShop类是容器和控制器,其构造函数会创建上一节所编写的员工类实例并将其嵌入。此外,Oven类也在这里定义。当此模块的自我测试程序代码调用PizzaShoprder方法时,内嵌对象会按照顺序进行工作。注意:每份订单创建了新的Customer对象,而且把内嵌的Server对象传给Customer方法。顾客是流动的,但是,服务生是匹萨店的组成部分,另外,员工也涉及了继承关系,组合和继承是互补的工具。当执行这个模块时,匹萨店处理两份订单:一份来自Homer,一份来自Shaggy:
这只是个用来模拟的例子,但是,对象和交互足以代表组合的工作。简明的原则就是,类可以表示任何用一句话表达的对象和关系。只要用类取代名词,方法取代动词。
5、重访流处理器:就更为现实的组合范例而言,可以回忆第22章的oop,写的通用数据流处理器函数的部分代码:
这里,不是使用简单函数,而是编写类,使用组合机制工作,来提供更强大的结构并支持继承。下面的文件streams.py示范了一种编写类的方式:
这个类定义了一个转换器方法,期待子类来填充。以这种方式编码,读取器和写入器对象会内嵌在类实例当中(组合),我们在子类内提供转换器的逻辑,而不是传入一个转换器函数(继承)。文件converters.py:
在这里,Uppercase类继承了类处理的循环逻辑(以及其超类内缩写的其他任何事情)。它只需定i其所特有的事件:数据转换逻辑。当这个文件执行时,会创建并执行实例,而该实例再从文件spam.txt中读取,把该文件对应的大写版本输出到stdout流:
要处理不同种类的流,可以把不同种类的对象传入类的构造调用中。在这里,使用了输出文件,而不是流:
但是,就像之前说的,可以传入包装在类中的任何对象(该对象定义了所需要的输入和输出方法接口),下面的是传入写入器类:
计时原始的Processor超类内的核心处理逻辑什么也不知道,如果跟随这个例子的控制流程,就会发现得到了大写转换(通过继承)以及HTML(通过组合)。处理代码只在意写入器的write方法,而且又定义一个名为convert的方法,并不在意这些调用在做什么。这种逻辑的多态和封装远超过类的威力。Processor超类只提供文件扫描循环。后期可以进行扩充。本教程的重点是继承,不过在实际中,组合和继承用的一样多,都是组织类结果的方式,尤其是在较大型系统中。
6、为什么要在意:类和持续性。pickle和类实例结合起来使用效果很好,而且可以促进类的通用用法,通过pickle或shelve一个类实例,我们得到了包含数据和逻辑的组合的数据存储。例如,类实例可以通过python的pickle或shelve模块,通过单个步骤存储到磁盘上,在第27章使用shelve来存储类的实例,而对象的picke接口很容易使用:
pickle机制把内存中的对象转换成序列化的字节流,可以保存在文件中,也可通过网络发送出去。解出pickle状态则是从字节流转换回同一个内存中的对象,shelve也类似,但是它会自动把对象pickle生成按键读取的数据库,而此数据库会导出类似于字典的接口:
上例中,使用类来模拟员工意味着只需做一点工作,就可以得到员工和商店的简单数据库:把这种实例对象pickle至文件,使其在python程序执行时都能够永久保存:
这一次性的把整个符合的shop对象保存到一个文件中,为了在另一个会话惑程序中再次找回,只要一个步骤,实际上,以这种方式存储的对象保存了状态和行为:
7、oop和委托:“包装”对象。oo的程序员时常会谈到委托(delegation),通常就是指控制器对象内嵌其他对象,而把运算请求传给那些对象。控制器负责管理工作,例如:记录存取等。在python中,委托通常是以__getattr__钩子方法实现的,因为这个方法会拦截对不存在属性的读取,包括类(有时称为代理类)可以使用__getattr__把任意读取转发给被包装的对象。包装类包有被包装对象的接口,而且自己也可以增加其他运算,例如trace.py:
__getattr__会获取属性名称字符串。这个程序代码利用getattr内置函数,以变量名字符串从包裹对象取出属性:getattr(X,N)就像是X.N。只不过N是表达式,可运行时计算出字符串,而不是变量。事实上,getattr(X,N)类似于X.__dict__[N],但前者也会执行继承搜索,就像X.N,而getattr(X,N)则不会。可以使用这个模块包装类的做法,管理任何带有属性的对象的存取:列表、字典甚至是类和实例。在这里,wrapper类只是在每个属性读取时打印跟踪消息,并把属性请求委托给嵌入的wrapped对象:
实际效果就是以包装类内额外的代码来增强被包装的对象的整个接口。可以利用这种方式记录方法调用,把方法调用转给其他惑定制的逻辑等等。ps:在2.6中运算符重载方法通过把内置操作导向__getattr__这样的通用属性拦截方法来运行。例如,直接打印一个包装对象,针对__repr__或__str__调用该方法,随后把调用传递给包装对象。在3.0中,这种情况不再会发生:打印不会触发__getattr__,并且使用一个默认显示。在3.0中,新式类在类中查找运算符重载方法,并且完全忽略常规的实例查找。
8、类的伪私有属性。在第五部分中,知道每个在模块文件顶层赋值的变量名都会导出。在默认情况下,类也是这样:数据隐藏是一个惯例,客户端可以读取惑修改任何它们想要的类或实例的属性。事实上,用CPP术语来说,属性都是“public”和"virtual",在任意地方都可进行读取,并且在运行时进行动态查找。不过python支持变量名压缩(mangling,相当于扩张)的概念,让类内某些变量局部化。压缩后的变量名有时会被误认为是“私有属性”,但这其实只是一种把类所创建的变量名局部化的方式而已:名称压缩并无法阻止类外代码对它的读取。这种功能主要是为了避免实例内的命名空间的冲突,而不是限制变量名的读取。因此,压缩的变量名最好称为"伪私有",而不是"私有"。这个功能一般在多人的项目中编写大型的类的层次,否则可能觉得没什么用,更通俗的说,python程序员用一个单个的下划线来编写内部名称(例如,_X),这只是一个非正式的惯例,让知道这是一个不应该修改的名字。
9、变量名压缩概览:变量名压缩的工作方式:class语句内开头有两个下划线,但结尾没有两个下划线的变量名,会自动扩张,从而包含了类的名称。例如像Spam类内_X这样的变量名会自动变成_Spam__X:原始的变量名会在头部加入一个下划线,然后是所在类名称,因为修改后的变量名包含了所在类的名称,相当于变得独特。不会和同一层级中其他类所创建的类似变量名相冲突。变量名压缩只发生在class语句内,而且只针对开头有两个下划线的变量名。然而,每个开头有两个下划线的变量名都会发生这件事,包括方法名称和实例属性名称(例如:在Spam类内,引用的self._X实例属性会变成self._Spam_X).因为不止有一个类在给一个实例新增属性,所以这种方法是有助于避免变量名冲突的。
10、为什么使用伪私有属性。该功能是为了缓和与实例属性存储方式有关的问题。在python中,所有实例属性最后都会在类树底部的单个实例对象内。这一点和cpp模型大不相同,cpp模型的每个类都有自己的空间来存储其所定义的数据成员。在类方法内,每当方法赋值self的属性时(例如,self.attr = value),就会在该实例内修改或创建该属性(继承搜索只发生在引用时,而不是赋值时)。即使在这个层次中有多个类赋值相同的属性,也是如此,因此有可能发生冲突。例如,假设当一位程序员编写一个类时,他认为属性名称X是在该实例中。在此类的方法内,变量名被设定,然后取出:
假设另一位程序员独立作业,对他写的类也有同样的假设:
这两个类都在各行其事。如果这两个类混合在相同类树中时,问题就产生了:
现在,当每个类说self.X时所得到的值,取决于最后一个赋值是哪一个类。因为所有对self.X的赋值语句都是引用一个i额相同实例,而X属性只有一个(I.X),无论有多少类使用这个属性名。为了保证属性会属于使用它的类,可在类中任何地方使用,将变量名前加上两个下划线,如private.py这个文件所示:
当加上了这样的前缀时,X属性会扩张,从而包含它的类的名称,然后才加到实例中。如果对 I 执行dir,或者在属性赋值后查看其命名空间字典,就会看见扩张后的变量名_C1_X和_C2_X,而不是X。因为扩张让变量名在实例内变得独特,类的编码者可以安全的假设,他们真的拥有任何带有两个下划线的变量名:
这个技巧可避免实例中潜在的变量名冲突,但是,这并不是真正的私有。如果知道所在类的名称,依然可以使用扩张后的变量名(例如,I._C1_X = 77),在能够引用实例的地方,读取这些属性。另一方面,这个功能也保证不太可能意外的访问到类的名称。伪私有属性在较大的框架或工具中也是有用的,既可以避免引入可能在类树中某处偶然隐藏定义的新的方法名,也可以减少内部方法被在树的较低处定义的名称替代的机会。如果一个方法倾向于只在一个可能混合到其他类的类中使用,在前面使用双下划线,以确保该方法不会受到树中的其他名称的干扰,特别是在多继承的环境中:
在类头部行中,超类按照它们从左到右的顺序搜索。在这里,就意味着Sub1首选Tool属性,而不是Super中的那些属性。尽管在这个例子中,我们可能通过切换Sub1类头部列出的超类的顺序,来迫使python首先选择应用程序类的方法,伪私有属性一起解决了这一问题,伪私有名还阻止了子类偶然地重新定义内部的方法名称,就像在Sub2中那样。同样的,这个功能只对较大型的多人项目有用,而且只用于已选定的变量名。不要将代码弄得难以置信的混乱。只当单个类真的需要控制某些变量名时,才使用这个功能。对较为简单的程序来说,就过头了。
11、方法是对象:绑定或无绑定。方法(特别是绑定方法),通常简化了python中的很多设计目标的实现。在第29章学习__call__的时候简单的介绍了绑定方法。这里进行详细的介绍,并且更通用和灵活。在第19章,介绍了函数可以和常规对象一样处理,方法也是一种对象,并且可以用与其他对象大部分相同的方式来广泛的使用,可以对它们赋值,将其传递给函数,存储在数据结构中,等等。由于类方法可以从一个实例或一个类访问,它们实际上在python中有两种形式:a、无绑定类方法对象:无self。通过对类进行点号运算从而获取类的函数属性,会传回无绑定(unbound)方法对象。调用该方法时,必须明确提供实例对象作为第一参数。在3.0中,一个无绑定方法和一个简单的函数是相同的,可以通过类名来调用;在2.6中,它是一种独特的类型,并且不提供一个实例就无法调用;b、绑定实例方法对象:self+函数对。通过对实例进行全运算从而获取类的函数属性,会传回绑定(bound)方法对象。python在绑定方法对象中自动把实例和函数打包,所以,不用传递实例去调用该方法。
12、接上11.这两种方法都是功能齐全的对象,可四处传递,就像字符串和数字。执行时,两者都需要它们在第一参数中的实例(也就是self的值)。这也就是为什么在上一章在子类方法调用超类方法时,要刻意传入实例。从严格意义上来说,这类调用会产生无绑定的方法对象。调用绑定方法对象时,python会自动提供实例,来创建绑定方法对象的实例。也就是说,绑定方法对象通常都可和简单函数对象互换,而且对于原本就是针对函数而编写的接口而言,就很有用了。例如:
现在,在正常操作中,创建了一个实例,在单步中调用了它的方法,从而打印出传入的参数:
不过,绑定方法对象是在过程中产生的,就在方法调用的括号前。事实上,我们可以获取绑定方法,而不用实际进行调用。object.name点号运算是一个对象表达式。在下列代码中,会传回绑定方法对象,把实例(object1)和方法函数(Spam.doit)打包起来。可以把这个绑定方法赋值给另一个变量名,然后像简单函数那样进行调用:
另一方面,如果对类进行点号运算来获得doit,就会得到无绑定方法对象,也就是函数对象的引用值。要调用这类方法时,必须传入实例作为最左侧参数:
扩展一下,如果我们引用的self的属性是引用类中的函数,那么相同规则也适用于类的方法。self.method表达式是绑定方法对象,因为self是实例对象:
大多数时候,通过点号运算取出方法后,就是立即调用,所以不会注意到这个过程中产生的方法对象。但是,如果编写通用方式调用对象的程序代码时,就得小心,特别是要注意无绑定方法:无绑定方法一般需要传入明确的实例对象。
13、在3.0中,无绑定方法是函数。在3.0中,已经删除了无绑定方法的概念。我们在这里所介绍的无绑定方法,在3.0中当作一个简单函数对待。对于大多数用途来说,这对于我们的代码没什么影响;任何一种方式,当通过一个实例来调用一个方法的时候,都会有一个实例传递给该方法的第一个参数。显示类型测试程序可能受到影响,如果打印出一个非实例的类方法,它在2.6中显示“无绑定方法”(unbound method),在3.0中显示“函数”(function)。此外在3.0中,不使用一个实例而是调用一个方法是没有问题的,只要这个方法不期待一个实例,并且通过类调用它而不是通过一个实例调用它。也就是说,只有对通过实例调用,3.0才会向方法传递一个实例,当通过一个类调用的时候,只有在方法期待一个实例的时候,才必须手动传递一个实例:
这里的最后一个测试在2.6中失效,因为无绑定方法默认的需要传递一个实例;它在3.0中有效,因为这样的方法当作一个简单函数对待,而不需要一个实例。尽管这会删除3.0中某些潜在的错误陷阱(比如忘记传入实例),但它允许类方法用作简单的函数,只要它们没有被传递并且不期望一个“self”实例参数。如下的两个调用仍然在3.0和2.6中都失效了,第一个(通过实例调用)自动把一个实例传递给一个并不期待实例的方法,而第二个(通过类调用)不会把一个实例传递给确实期待一个实例的方法:
由于这一修改,对于只通过类名而不通过一个实例调用的、没有一个self参数的方法,在3.0中不再需要下一章介绍的staticmethod装饰器,这样的方法作为简单函数运行,不会接受一个实例参数。在2.6中,这样的调用是错误的,除非手动的传递一个实例。
14、绑定方法和其他可调用对象:绑定方法可以作为一个通用对象处理,就像是简单函数一样,它们可以任意的在一个程序中传递。此外,由于绑定方法在单个的包中组合了函数和实例,因此它们可以像任何其他可调用对象一样对待,并且在调用的时候不需要特殊的语法。例如,如下的例子在一个列表中存储了4个绑定方法对象,并且随后使用常规的调用表达式来调用它们:
和简单函数一样,绑定方法对象拥有自己的內省信息,包括让它们配对的实例对象和方法函数访问的属性。调用绑定方法会直接分配配对:
实际上,绑定方法只是python中众多的可调用对象类型中的一种。正如下面说的,简单函数编写为一个def或lambda,实例继承了一个__call__,并且绑定实例方法都能够以相同的方式对待和调用:
从技术上说,类也属于可调用对象的范畴,但是,我们通常调用它们来产生实例而不是做实际的工作,如下:
绑定方法和python的可调用对象模型,通常都是python的设计朝向一种难以置信的灵活语言方向努力的众多方式中的一些。
15、为什么要在意:绑定方法和回调函数:绑定方法会自动让实例和类方法函数配对,因此可以在任何希望得到的简单函数的地方使用。最常见的使用,就是把方法注册成tkinter gui接口(2。6中叫做tkinter)中事件回调处理器的代码。下面是简单的例子:
要为按钮点击事件注册一个处理器时,通常是将一个不带参数的可调用对象传递给command关键词参数。函数名(和lambda)都可以使用,而类方法只要是绑定方法也可以使用:
在这里,事件处理器是self.handler(一个绑定方法对象),它记住self和MyGui.handler。因为handler稍后因事件而启用时,self会引用原始实例。因此这个方法可以读取在事件间用于保留状态信息的实例的属性。如果利用简单函数,状态信息一般都必须通过全局变量保存,此外可以参考29章的__call__运算符重载的讨论,来了解另一种让类和函数api相容的方式。
16、多重继承:“混合”类:很多基于类的设计都要求组合方法的全异的集合,在class语句中,首行括号内可以列出一个以上的超类。当这么做时,就是在使用所谓的多重继承:类和其实例继承了列出的所有超类的变量名。搜索属性时,python'会由左至右搜索类首行中的超类,直到找到相符者。从技术上说,任何超类本身可能还有一些其他的超类,对于更大的类树,这个搜索可以更复杂一点:a、在传统类中(默认的类,直到3.0),属性搜索处理对所有路径深度优先,直到继承树的顶端,然后从左到右进行;b、在新式类(以及3.0的所有类中),属性搜索处理沿着树层级,以更加广度优先的方式进行。不管哪种方式,当一个类拥有多个超类的时候,它们会根据class语句头部行中列出的顺序从左到右查找。通常来说,多重继承是建模属于一个集合以上的对象的好办法。例如,一个人可以是工程师,作家,音乐家等。因此,可继承这些集合的特性。使用多重继承,对象获得了所有其超类中行为的组合。也许多重继承最常见的用法就是作为“混合”超类的通用方法。这类超类一般都称为混合类:它们提供方法,可以通过继承将其加入应用类。例如,python打印类实例对象的默认方式并不是很好用。从某种意义上说,混合类类似于模块:它们提供方法的包,以便在其客户子类中使用。然而,和模块中的简单函数不同,混合类中的方法也能够访问self实例,以使用状态信息和其他方法。
17、编写混合显示类:python的默认方式来打印一个类实例对象,并不是很有用:
就像29章学习运算符重载的时候看到的,可以提供一个__str__或__repr__方法,以实现制定后的字符串表达式形式。但是,如果不在每个想打印的类中编写__repr__,为什么不在一个通用工具类中编写一次,然后在所有类中继承呢?这就是混合类的用处。在混合类中定义一个显示方法一次,使得能够在想要看到一个定制显示格式的任何地方重用它。:a、第27章的AttrDisplay类在一个通用的__str__方法中格式化了实例属性,但是,它没有爬升类树,并且只是用于但集成模式中;b、第28章的classtree.py定义了函数以爬升和遍历类树,但是,它没有显示对象属性,并且没有架构为一个可继承类。
18、这里在上面的基础上扩展编码一组3个混合类,这3个类充当通用的显示工具,以列出一个类树上所有对象的实例属性、继承属性和属性。
19、接上18;a、用__dict__列出实例属性。从一个简单的例子开始--列出附加给一个实例的属性。如下的类编写在文件lister.py中,它定义了一个名为ListInstance的混合类,它对于将其包含到头部行的所有类都重载了__str__方法。由于ListInstance编写为一个类,所以它成为了一个通用工具,其格式化逻辑可以用于任何子类的实例:
ListInstance使用前面介绍的一些技巧来提取实例的类名和属性:a、每个实例都有一个内置的__class__属性,它引用自己所创建自的类;并且每个类都有一个__name__属性,它引用了头部中的名称,因此,表达式self.__class__.__name__获取了一个实例的类的名称;b、这个类通过直接扫描实例的属性字典(从__dict__中导出),以构建一个字符串来显示所有实例属性的名称和值,从而完成其主要工作。字典的键通过排序,以避免python跨版本的任何排序差异。这些方面,ListInstance类似于第27章的属性显示:实际上,它很大程度上只是一个主题的变体。这里,我们的类显示了两种其他技术:a、通过调用id内置函数显示了实例的内存地址,该函数返回任何对象的地址(根据定义,这是一个唯一的对象标识符,在随后对这一代码的修改中有用);b、它针对其同坐方法使用伪私有命名模式:__attrnames。python通过扩展属性名称以包含类名,从而把这样的名称本地化到其包含类中(在这个例子中,它变成了_ListInstance__attrnames)。对于附加到self的类属性(如方法)和实例属性,都是如此。这种行为在这样的通用工具中很有用,因为它确保了其名称不会与其客户子类中使用的任何名称冲突。
20、接19,没说完的。由于ListInstance定义了一个__str__运算符重载方法,所以派生自这个类的实例在打印的时候自动显示其属性,只给定了比简单地址多一些的信息。如下是使用这个类,在单继承模式中(这段代码在3.0和2.6中一样工作):
我们可以把列表输出获取为一个字符串,而不用str打印出它,并且交互响应仍然使用默认格式:
ListInstance对于我们所编写的任何类都是有用的,即便类已经有了一个或多个超类。这就是多继承的用武之地,通过把ListInstance添加到一个类头部的超类 列表中(例如,混合进去),我们可以仍然继承自己有超类的同时“自由的”获得__str__。文件testmixin.py:
这里,Sub从Super和ListInstance继承了名称,它是自己的名称与其超类中名称的组合。当我们把生成一个Sub实例并打印它,就会自动获得从ListInstance混合进去的定制表示(在这个例子中,这段脚本的输出在3.0和2.6下都是相同的,除了对象地址不同):
ListInstance在它混入的任何类中都有效,因为self引用拉入了这个类的子类的一个实例,而不管它可能是什么。从某种意义上讲,混合类是模块的类等价形式,它是在各种客户中有用的方法包。例如,下面是再次在单继承模式中工作的Lister,它作用于一个不同的类实例之上,使用import,并且带有类之外的属性设置:
它们除了提供这一工具,还像所有的类一样,混入了优化代码维护。例如,如果稍后决定扩展ListInstance的__str__也打印出一个实例继承的所有类属性,是安全的;因为它是一个集成的方法,修改__str__自动的更新导入该类和混合该类的每个子类的显示。
21、接19的。使用dir列出继承的属性。我们的Lister混合类只显示实例属性(例如,附加到实例对象自身的名称)。扩展该类以显示从一个实例可以访问的所有属性,这也是很容易的。这包括它自己以及它所继承自的类。技巧是使用dir内置函数,而不是扫描实例的__dict__字典,后者只是保存了实例属性,但是,在2.2及以后的版本中,前者也收集了所有继承的属性。如下修改后的代码实现了这一方案,我们已经将其重新命名,以便使得测试更简单,但是,如果用这个替代最初的版本,所有已有的客户类将自动选择新的显示:
注意,这段代码省略了__X__名称的值;这些大部分都是内部名称,我们通常不会在这样的通用列表中注意到。这个版本必须使用getattr内置函数来获取属性,通过指定字符串而不是使用实例属性字典索引,getattr使用了继承搜索协议,并且在这里列出的一些代码没有存储到实例自身中。要测试新的版本,修改testmixin.py文件并使用新的类来替代:
这个文件的输出随着每个版本而变化。在2.6中,我们得到如下输出。注意,名称压缩在lister的方法名中其作用(缩减其全部的值显示,以节省篇幅):
在3.0中,更多的属性显示出来,因为所有的类都是“新式的”,并且从隐式的object超类那里继承了名称(关于object的更多的在31章介绍)。由于如此多的名称继承自默认的超类,我们已经在这里省略了很多。自行运行程序以得到完整的列表:
这里注意一点,既然我们也显示继承的方法,我们必须使用__str__而不是__repr__来重载打印。使用__repr__,这段代码将会循环,显示一个方法的值,该值触发了该方法的类的__repr__,从而显示该类。也就是说,如果lister的__repr__试图显示一个方法,显示该方法的类将再次促发lister的__repr__。在这里,自己把__str__修改为__repr__来看看。如果你在这样的环境中使用__repr__,可以使用isinstance来比较属性值的类型和标准库中的types.MethodType,以知道省略哪些项,从而避免循环。
22、接19。列出类树中每个对象的属性。我们的lister没有告诉我们一个继承名称来自哪个类。然而,正如我们在第28章末尾的classtree.py示例中看到的,在代码中爬升类继承树很容易。如下的混合类使用这一名称技术来显示根据属性所在的类来分组的属性,它遍历了整个类树,在此过程中显示了附加到每个对象上的属性。它这样遍历继承树:从一个实例的__class__到其类,然后递归的从类的__bases__到其所有超类,一路扫描对象的__dicts__:
注意,这里使用一个生成器表达式来导向对超类的递归调用,它由嵌套的字符串join方法激活。还要注意,这个版本使用3.0和2.6的字符串格式化方法而不是%来格式化表达式,以使得替代更清晰。当像这样应用很多替代的时候,明确的参数数目可能使得代码更容易理解。简而言之,在这个版本中,我们把如下的第一行与第二行交换:
现在,修改testmixin.py,再次测试新类继承:
在2.6中,该文件的树遍历输出如下所示:
注意,在这一输出中,方法现在在2.6下是无绑定的,因为我们直接从类获取它们,而不是从实例。还注意lister的__visited表把自己的名称压缩到实际的属性字典中;除非我们很不走运,这不会与那里的其他数据冲突。在3.0中,我们再次获取了额外的属性和超类。注意,无绑定的方法在3.0中是简单的函数,正如本章前面说的(再次删除了对象中大多数内置对象以节省篇幅,可以自行运行这段代码以获取完整的列表):
这个版本通过保留一个目前已经访问过的类的表来避免两次列出同样的类对象(这就是为什么一个对象的id包含其中,以充当一个之前显示项的键)。和第24章的过渡性模块重载程序一样,字典在这里用来避免重复和循环,因为类对象可能是字典键。集合也可以提供类似的功能。这个版本还会再次通过省略__X__名称来避免较大的内部对象。如果注释掉这些名称的测试,它们的值将会正常显示。这是在2.6下输出的摘要,带有这一临时性的修改(整个输出很大,并且在3.0中这种情况甚至变得更糟,因此,这些名字可能会有所忽略):
为了更有趣,尝试把这个类混合到更实质的某些内容中,例如python的thinter gui工具箱模块的button类。通常,想要在一个类的头部命名ListTree(最左端),因此,它的__str__会被选取:Button也有一个,并且在多继承中最左端的超类首先搜索。如下的输出十分庞大(18K个字符),因此,自己运行这段代码看看完整的列表(并且,如果在使用2.6,记住应该对模块名使用Tkinter 而不是tkinter):
ps:支持slot:由于它们扫描示例词典,所以这里介绍的ListInstance和ListTree类不能直接支持存储在slot中的属性--slot是一种新的、相对很少使用的选项,在下一章中,示例属性将在一个__slots__类属性中声明。例如,如果在textmixin.py中,我们在Super中赋值__slots__=['data1'],在Sub中赋值__slots__ = ['data3'],只有data2属性通过这两个lister类显示在该实例中:ListTree也会显示data1和data3,但是是作为Super和Sub类对象的属性,并且是它们的值的一种特殊格式(从技术上说,它们都是类级别的描述符)。要更好的支持这些类中的slot属性,把__dict__扫描循环修改为使用下一章给出的代码来迭代__slots__列表,并且使用getattr内置函数来获取值,而不是使用__dict__索引(ListTree已经这么做了)。既然实例只继承最低的类的__slots__,当__slots__列表出现在多个超类中的时候,可以提出一种策略(ListTree已经将它们显示为类属性)。ListInherited对所有这些都是免疫的,因为dir结果组合了__dict__名称和所有类的__slots__名称。而且可以直接允许代码处理基于slot的属性(就像当前所做的那样),而不是将其复杂化为一种少用的、高级的特性。slot和常规的实例属性是不同的名称,在下一章介绍slot。
23、类是对象:通用对象的工厂。有时候,基于类的设计要求要创建的对象来响应条件,而这些条件是在编写程序的时候无法预料的。工厂设计模式允许这样的一种延迟方法。在很大程度上由于python的灵活性,工厂可以采取多种形式,其中的一些根本不会显得特殊。类是对象,可以把类传给会产生任意种类对象的函数,这类函数在oop设计 领域中偶尔称为工厂。这些函数是cpp这类强类型语言的主要工作,但是在python中很容易实现,第17章介绍的apply函数和更新的替代语法,可以用一步调用带有任意构造方法参数的类,从而产生任意种类的实例(这种语法可以调用任何可调用的对象,包括函数、类和方法。这里的factory函数也会运行任何可调用的对象,而不仅仅是类(尽管参数名称是这样)。此外,正如我们在18章看到的,2.6有一种aClass(*args)的替代方法:apply(aClass,args)内置调用,这个在3.0中已经删除了):
这段代码中,定义了一个对象生成器函数,称为factory。它预期传入的是类对象(任何对象都行),还有该类构造函数的一个或多个参数。这个函数使用特殊的“varargs”调用语法来调用该函数并返回实例。例子的其余部分只是定义了两个类,并将其传给factory函数以产生两者的实例。而这就是在python中编写的工厂函数所需要做的事。它适用于任何类以及任何构造函数参数。可能的改进是,在构造函数调用中支持关键词参数。工厂函数能够通过**args参数收集参数,并在类调用中传递它们:
在python中一切都是“对象”,包括类(类在cpp中仅仅是编译器的输入而已)。只有从类衍生的对象才是python中的oop对象。
24、为什么有工厂。回想下第25章以抽象方式介绍的例子processor,以及本章再次作为“has-a”关系的组合例子。这个程序接受读取器和写入器对象来处理任意的数据流。这个例子的原始版本可以手动传入特定的类的实例,例如,FileWriter和SocketReader,来调整正被处理的数据流。后面会传入硬编码的文件、流以及格式对象。在更为动态的场合下,像配置文件或gui这类外部工具可能用来配置流。在这种动态世界中,可能无法在脚本中把流的接口对象的建立方式固定的编写好。但是有可能根据配置文件的内容在运行期间创建它。例如,这个文件可能会提供从模块导入的流的类的字符串名称,以及选用构造函数的调用参数。工厂式的函数或程序代码在这里可能很方便,因为它们可以让我们取出并传入没有预先在程序中硬编码的类。实际上,这些类在编写程序时可能白不存在:
这里,getattr内置函数依然用于取出特定字符串名称的模块属性(很像obj.attr,但attr是字符串)。因为这个程序代码片段是假设的单独的构造函数参数,因此并不见的需要factory或apply:我们能够使用aclass(classarg)直接创建其实例。然而,存在未知的参数列表时,它们就可能有用了,而通用的工厂编码模式可以改进代码的灵活性。
25、与设计相关的其他话题:本章中介绍了继承、复合、委托、多继承、绑定方法和工厂,这些是在python程序中组合类的所有常用模式。在设计模式领域,这些是冰山一角。本书的其他部分的索引:a、抽象超类(第28章);b、装饰器(第31章和第38章);c、类型子类(第31章);d、静态方法和类方法(第31章);e、管理属性(第37章);f、元类(第31章和第39章)。
七、类的高级主体(31章)
本部分将介绍一些与类相关的高级主题,作为第6部分的结束:研究如何建立内置类型的子类、新式类的变化和扩展、静态方法和类方法、函数装饰器等。建议读者能够从事或者研究较大的python oop项目,作为本书的补充。
1、扩展内置类型。除了实现新的种类的对象以外,类也会用扩展python的内置类型的功能,从而支持更另类的数据结构。例如,要为列表增加队列插入和删除方法,可以写些类,包装(嵌入)列表对象,然后导出能够以特殊方式处理该列表的插入和删除的方法,就像30章的委托技术。
2、接1.通过嵌入扩展类型。在16章和18章所写的那些集合函数,下面是它们以python类的形式重生的样子。下面的例子(setwrapper.py)把一些集合函数变成方法,而且新增了一些基本运算符重载,实现了新的集合对象。对于多数类而言,这个类只是包装了python列表,以及附加的集合运算。因为这是类,所以也支持多个实例和子类继承的定制。和我们前面的函数不同,这里使用类允许我们创建多个自包含的集合对象,带有预先设置的数据和行为,而不是手动把列表传入函数中:
要使用这个类,我们生成实例、调用方法,并且像往常一样运行定义的运算符:
重载索引运算让Set类的实例可以充当真正的列表。
3、通过子类扩展类型。所有内置类型现在都能直接创建子类。像list、str、dict以及tuple这些类型转换函数都变成内置类型的名称:虽然脚本看不见,但类型转换调用(例如,list(‘spam’))其实启用了类型的对象构造函数。这样的改变可以通过用户定义的class语句,定制或扩展内置类型的行为:建立类型名称的子类并对其进行定制。类型的子类实例,可用在原始的内置类型能够出现的任何地方。例如,假设对python列表偏移值以0开始计算而不是1开始一直很困扰,我们也可以自己编写自己的子类,定制列表的核心行为。文件typesubclass.py说明了如何去做:
在这个文件中,MyList子类扩展了内置list类型的__getitem__索引运算方法,把索引1 到N映射为实际的0到N-1.它所做的其实就是把提交的索引值减1,之后继续调用超类版本的索引运算,但是这样,就足够了:
此输出包括打印类索引运算的过程。像这样改变索引运算是否是好事是另一回事:MyList类的使用者,对于这种和python序列行为有所偏离的困惑程度可能也都不同。一般来说,用这种方式定制内置类型,可以说是很强大的。例如:这样的编码模式会产生编写集合的另一种方式:作为内置list类型的子类,而不是管理内嵌列表对象的独立类、正如第5章说的,python带有一个强大的内置集合对象,还有常量和解析语法可以生成新的集合。然而,自己编写一个集合,通常仍然是学习类型子集建立过程的一种好方法。下面的setsubclass.py文件内,通过定制list来增加和集合处理相关的方法和运算符。因为其他所有行为都是从内置list超类继承而来的,这样可以得到较短和较简单的替代做法:
下面是文件末尾测试代码的输出。因为创建核心类型的子类是高级功能,这里省略其他的细节:
python中还有更高效的方式,也就是通过字典实现集合:把这里的集合实现中的线性扫描换成字典索引运算(散列),因此运行时会快很多。
4、新式类。在2.2中,引入一种新的类,称为“新式”(new-style)类。本教程这一部分至今为止所谈到的类和新的类相比时,就称为“经典”(classic)类。在3.0中,类的区分已经融合了,但是对于2.X的用户来说,还是有所区别的:a、对于3.0来说,所有的类都是我们所谓的“新式类”,不管它们是否显式的继承自object。所有的类都继承自object,不管是显式的还是隐式的,并且,所有的对象都是object的实例;b、在2.6及其以前的版本中,类必须继承自的类看作是“新式”object(或者其他的内置类型),并且获得所有新式类的特性。也就是当3.0中所有的类都是自动是新式类,所以新式类的特性只是常规的类的特性。然而,在本节中,这里选择进行区分,以便对2.X代码的用户有所区分,这些代码中的类,只有在它们派生自object的时候才具有新式类的特性。在2.6及其之前的版本中,唯一的编码差异是,它们要么从一个内置类型(如list)派生,要么从一个叫做object的特殊内置类派生。如果没有其他合适的内置类型可用,内置名称object就可以作为新式类的超类提供:
通常情况下,任何从object或其他内置类型派生的类,都会自动视为新式类。只要一个内置类型位于超类树中的某个位置,新类也当作一个新式类。不是从内置类型派生出来的类,就会当作经典类来对待。新式类只是和经典类有细微的差别,并且它们之间的区分的方式,对于大多数主要的python用户来说,是无关紧要的,而且,经典类形式在2.6中仍然可用,并且与之前几乎完全一样的工作。实际上,新式类在语法和行为上几乎与经典类完全向后兼容;它们主要只是添加了一些高级的新特性。然而,由于它们修改了一些类行为,它们必须作为一种不同的工具引入,以避免影响到依赖以前的行为的任何已有代码。例如,一些细微的区别,例如钻石模式继承搜索和带有__getattr__这样的管理属性方法的内置运算,如果保持不变的话,可能会导致一些遗留代码失效。
5、新式类变化。新式类在几个方面不同于经典类,其中一些是很细微的,:a、类和类型合并,类现在就是类型,并且类型现在就是类。实际上,这二者基本上同义词。type(I)内置函数返回一个实例所创建自的类,而不是一个通用的实例类型,并且,通常是和 I.__class__相同的。此外,类是type类的实例,type可能子类话为定制类创建,并且所有的类(以及由此所有的类型)继承自object。;b、继承搜索顺序,多继承的钻石模式有一种略微不同的搜索顺序,总体而言,它们可能先横向搜索在纵向搜索,并且先宽度优先搜索,再深度优先搜索;c、针对内置函数的属性获取。__getattr__和__getattribute__方法不再针对内置运算的隐式属性获取而运行。这意味着,它们不再针对__X__运算符重载方法名而调用,这样的名称搜索从类开始,而不是从实例开始;d、新的高级工具。新式类有一组新的类工具,包括slot、特性、描述符和__getattribute__方法。这些工具中的大多数都有非常特定的工具构建目的。在第27章的边栏部分简单的介绍了这些变化的3个,并且,将在第37章的属性管理介绍中以及第38章的私有性装饰器介绍中更深入的回顾它们。这里的a和b可能影响到已有的2.X代码,在介绍新式类之前,更详细的看看这些工具。
6、类型模式变化。在新式类中,类型和类的区别已经完全消失了。类自身就是类型:type对象产生类作为自己的实例,并且类产生它们的类型的实例。实际上,像列表和字符串这样的内置类型和编写为类的用户定义类型之间没有真正的区别。这就是为什么我们可以子类化内置类型,就像本章前面介绍的那样,由于子类化一个列表这样的内置类型,会把一个类变为新式的,因此,它变成了一个用户定义的类型。除了允许子类化内置类型,还有一点变得非常明显的情况,就是当我们进行显式类型测试的时候。使用2.6的经典类,一个类实例的类型是一个通用的“实例”,但是,内置对象的类型要更加特定:
但是,对于2.6中的新式类,一个类实例的类型是它所创建自的类,因为类直接是用户定义的类型,实例的类型是它的类,并且,用户定义的类的类型与一个内置对象类型的类型相同。类现在有一个__class__属性,因为它们也是type的实例:
对于3.0中的所有类都是如此,因为所有的类自动都是新式的,即便它们没有显式的超类。实际上,内置类型和用户定义类型之间的区分,在3.0中消失了:
正如看到的,在3.0中,类就是类型,但是,类型也是类。从技术上说,每个类都是一个元类生成,元类是这样的一个类,它要么是type自身,要么是它定制来扩展或管理生成的类的一个子类。除了影响到进行类型测试的代码,这对于工具开发者来说,是一个重要的钩子。
7、类型测试的隐含意义:除了提供内置类型定制和元类钩子,新的类模式中类和类型的融合,可能会影响到进行类型测试的代码。例如:在3.0中,类实例的类型直接而有意义的比较,并且以与内置类型对象同样的方式进行。下面的代码基于这样一个事实:类现在是类型,并且一个实例的类型是该实例的类:
对于2.6或更早版本中的经典类,比较实例类型几乎是无用的,因为所有的实例都具有相同的“实例”类型。要真正的比较类型,必须要比较实例__class__属性(如果你关注可移植性,这在3.0中也有效,但在那里不是必需的):
并且,正如所期待的,在这方面,2.6中的新式类与3.0中的所有类同样的工作,比较实例类型会自动的比较实例的类:
当然,类型检查通常在python程序中是错误的事情(我们编写对象接口,而不是编写对象类型),并且更加通用的isinstance内置函数很可能是在极少数情况下(即必须查询实例类的类型的情况下)想要使用的。
8、所有对象派生自object。新式类模式中的另一个类型变化是,由于所有的类隐式的或显式的派生自(继承自)类object,并且,由于所有的类型现在都是类,所以每个对象都派生自object内置类,不管是直接的或通过一个超类。考虑3.0中的如下交互模式(在2.6中编写一个显式的object超类,会有等价的效果):
和前面一样,一个类实例的类型就是它所产生自的类,并且,一个类的类型就是type类,因为类和类型都融合了。确实,但是实例和类都派生自内置的object类,因此,每个类都有一个显式或隐式的超类:
对于列表和字符串这样的内置类型来说,也是如此,因为在新模式中,类型就是类,内置类型现在也是类,并且他们的实例也派生自object:
实际上,类型自身派生自object,并且object派生自type,即便二者是不同的对象,一个循环的关系覆盖了对象模型,并且由此导致这样一个事实:类型是生成类的类:
实际上,这种模式导致了比前面的经典类的类型/类区分的几个特殊情况,并且,它允许我们编写假设并使用一个object超类的代码。
9、钻石继承变动。也许新式类中最显著的变化就是,对于所谓的多重继承树的钻石模型(diamond pattern)的继承(也就是有一个以上的超类会通往同一更高的超类)处理方式有点不同。钻石模式是高级设计概念,在python编程中很少用到,并且在本书中目前为止还没有讨论过,所以这里不深究。简单来说,对经典类而言,继承搜索程序是绝对深度优先,然后才是由左至右。python一路往上搜索,深入树的左侧,返回后,才开始找右侧。在新式类中,在这类情况下,搜索相对来说是宽度优先的。python先寻找第一个搜索的右侧的所有超类,然后才一路往上搜索至顶端共同的超类,换句话说,搜索过程先水平进行,然后向上移动。搜索算法也比这里介绍的复杂一些,不过了解这些就够了。因为有这样的变动,较低超类可以重载较高超类的属性,无论它们混入的是哪种多重继承树。此外,当从多个子类访问超类的时候,新式搜索规则避免重复访问同一超类。
10、钻石继承例子。为了说明起见,举一个经典类构成的简单钻石继承模式的例子,这里D是B和C的超类,B和C都导向相同的祖先A:
此外是在超类A中内找到属性的。因为对经典类来说,继承搜索是先往上搜索到最高,然后返回再往右搜索:python会先搜索D、B、A,然后才是C(但是,当attr在A找到时,B之上的就会停止)。这里,对于派生自object这样的内置类的新式类,以及3.0中的所有类,搜索顺序是不同的:python会先搜索C(B的右侧),然后才是A(B之上):也就是先搜索D、B、C,然后才是A(在这个例子中,则会停在C处):
这种继承搜索流程的变化是基于这样的假设:如果在树中较低处混入C,和A相比,可能会比较想获取C的属性。此外,这也是假设C总是要覆盖A的属性:当C独立使用时,可能是真的,但是当C混入经典类钻石模式时,可能就不是这样了。当编写C时,可能根本不知道C会以这样的方式混入。在这个例子中,可能程序员认为C应该覆盖A,尽管如此,新式类先访问C。否则,C将会在钻石环境中基本无意义:它不会定制A,并且只对同名的C使用。
11、明确解决冲突。当然,假设的问题就是这是假设的。如果难以记住这种搜索顺序的偏好,或者如果你想对搜索流程有更多的控制,都可以在树中任何地方强迫属性的选择:通过赋值或者在类混合处指出你想要的变量名:
在这里,经典类树模拟了新式类的搜索顺序:在D中为属性赋值,使其挑选C中的版本,因而改变了正常的继承搜索路径(D.attr位于树中最低的位置)。新式类也能选择类混合处以上的属性来模拟经典类:
如果愿意以这样的方式解决这种冲突,大致上就能忽略搜索顺序的差异,而不依赖假设来决定所编写的类的意义,自然,以这种方式挑选的属性也可以是方法函数(方法是正常可赋值的对象):
在这里,明确在树中较低处赋值变量名以选取方法。我们也可以明确调用所需要的类。在实际应用中,这种模式可能更为常用,尤其是构造函数:
这类在混合点进行赋值运算或调用而做的选择,可以有效的把代码从类的差异性中隔离出。通过这种方式明确的解决冲突,可以确保你的代码不会因以后更新的python版本而有所变化(除了2.6中,新式类需要从object或内置类型派生类以使用新式工具之外)。ps:即使没有经典/新式类的差异,这种技术在一般多重继承场合中也很方便。如果想要左侧超类的一部分以及右侧超类的一部分,可能就需要在子类中明确使用赋值语句,告诉python要选择哪个同名属性。此外,钻石继承模式在有些情况下的问题,比此处所提到的还要多(例如,如果B和C都有所需的构造函数会调用A中的构造器,那该怎么办?),由于这样的语境在python中很罕见,不再本书范围内。
12、搜索顺序变化的范围。总之,默认情况下,钻石模式对于经典类和新式类进行不同的搜索,并且这是一个非向后兼容的变化。此外要记住,这种变化主要影响到多继承的钻石模式情况。新式类继承对于大多数其他的继承树结构都是不变的工作。此外,整个问题不可能在理论上比实践中更重要,因为,新式类搜索直到2.2才足够显著的解决,并且在3.0中才成为标准,它不可能影响到太多的python代码。正如已经提到的,即便没有在自己编写的类中用到钻石模式,由于隐式的object超类在3.0中的每个类之上,所以如今多继承的每个例子都展示了钻石模式。也就是说,在新式类中,object自动扮演了前面讨论的实例中类A的角色。因此,新的类搜索规则不仅修改了逻辑语义,而且通过避免多次访问相同的类而优化了性能。同样,新模式的隐式object超类为各种内置操作提供了默认方法,包括__str__和__repr__显示格式化方法。运行一个dir(object)来看看提供哪些方法。没有一个新式的搜索顺序,在多继承情况中,object中的默认方法将总是覆盖用户编写的类中的重新定义,除非这些重定义总是放在最左边的超类之中。换句话说,新类模式自身使得使用新搜索顺序更关键!
13、新式类的扩展。除了钻石继承搜索模式中的这个改变以外(过于罕见,可以看看就行),新式类还启用了一些更为高级的可能性。下面将对这些额外特性中的每一个给出概览,这些特性在2.6的新式类和3.0的所有类中都可用。
14、slots实例。将字符串属性名称顺序赋给特殊的__slots__类属性,新式类就有可能既限制类的实例将有的合法属性集,又能够优化内存和速度性能。这个特殊属性一般是在class语句顶层内将字符串名称顺序赋给变量__slots__而设置:只有__slots__列表内的这些变量名可赋值为实例属性。然而,就像python中的所有变量名,实例属性名必须在引用前赋值,即使是列在__slots__中也是这样。以下是说明的例子:
slot对于python的动态特性来说是一种违背,而动态特性要求任何名称都可以通过赋值来创建。这个功能看作是捕捉"打字错误"的方式(对于不再__slots__内的非法属性名做赋值运算,就会侦测出来),而且也是最优化机制。如果创建了很多实例并且只有几个属性是必须的话,那么为每个实例对象分配一个命名空间字典可能在内存方面代价过于昂贵。要节省空间和执行速度,slot属性可以顺序存储以供快速查找,而不是为每个实例分配一个字典。
15、slot和通用代码。实际上,有些带有slots的实例也许根本没有__dict__属性字典,使得有些书中缩写的源程序过于复杂(包括本书中一些代码)。工具根据字符串名称通用的列出属性或访问属性,例如,必须小心使用比__dict__更为存储中立的工具,例如getattr、setattr和dir内置函数,它们根据__dict__或__slots__存储应用于属性。在某些情况下,两种属性源代码都需要查询以确保完整性。例如,使用slots的时候,实例通常没有一个属性字典,python使用第37章介绍的类描述符功能来为实例中给的slots属性分配空间。只有slot列表中的名称可以分配给实例,但是,基于 slot的属性仍然可以使用通用工具通过名称来访问或设置。在3.0中,(以及在2,6中派生自object的类中):
没有一个属性命名空间字典,不可能给不是slots列表中名称的实例来分配新的名称:
然而,通过在__slots__中包含__dict__仍然可以容纳额外的属性,从而考虑到一个属性空间字典的需求。在这个例子中,两种存储机智都用到了,但是getattr这样的通用工具允许我们将它们当作单独一组属性对待:
然而,想要通用的列出所有实例属性的代码,可能仍然需要考虑两种存储形式,因为dir也返回继承的属性(这依赖于字典迭代器来收集键):
由于两种都可能忽略,更正确的编码方式如下所示(getattr考虑到默认情况):
16、超类中的多个__slot__列表。注意,上面这段代码只是解决了由一个实例继承的最低__slots__属性中的slot名称。如果类树中的多个类都有自己的__slots__属性,通用的程序必须针对列出的属性开发其他的策略(例如,把slot名称划分为类的属性,而不是实例的属性)。slot声明可能出现在一个类树中的多个类中,但是,它们受到一些限制,除非理解slot作为类级别描述符的实现,否则要说明这些限制有些困难:a、如果一个子类继承自一个没有__slots__的超类,那么超类的__dict__属性总是可以访问的,使得子类中的一个__slots__无意义;b、如果一个类定义了与超类相同的slot名称,超类slot定义的名称版本只有通过直接从超类获取其描述符才能访问;c、由于一个__slots__声明的含义受到它出现其中的类的限制,所以子类将有一个__dict__,除非它们也定义了一个__slots__;d、通常从列出实例属性这方面来说,多类中的slots可能需要手动类树爬升,dir用法,或者把slot名称当作不同的名称领域的政策:
当这种通用性可能的时候,slots可能最好当作类属性来对待,而不是视图让它们表现出与常规类属性一样。
17、类特性。有一种称为特性(property)的机制,提供另一种方式让新式类定义自动调用的方法,来读取或赋值实例属性。这种功能是第29章说过,目前用的很多的__getattr__和__setattr__重载方法的替代做法。特性和这两个方法有类似效果,但是只在读取所需要的动态计算的变量名时,才会发生额外的方法调用。特性(和slots)都是基于属性描述器(attribute descriptor)的新概念(这里不说)。简单来说,特性是一种对象,赋值给类属性名称。特性的产生是以三种方法(获得、设置以及删除运算的处理器)以及通过文档字符串调用内置函数property。如果任何参数以None传递或省略,该运算就不能支持。特性一般都是在class语句顶层赋值【例如,name = property(...)】。这样赋值时,对类属性本身的读取(例如,obj.name)。就会自动传给property的一个读取方法。例如,__getattr_-方法可让类拦截未定义属性的引用:
下面是相同的例子,改用特性来编写。(注意,特性对于所有类可用,但是,对于拦截属性赋值,必须是2.6中object派生的新式对象才有效):
就某些编码任务而言,特性比起传统技术不是那么复杂,而且运行起来更快。例如,当我们新增属性赋值运算支持时,特性就变得更有吸引力:输入的代码更少,对我们不希望动态计算的属性进行赋值运算时,不会发生额外的方法调用:
等效的经典类可能会引发额外的方法调用,而且需要通过属性字典传递属性赋值语句,以避免死循环(或者,对于新式类,会导向object超类的__setattr__):
就这个简单的例子而言,特性似乎是赢家。然而,__getattr__和__setattr__的某些应用依然需要更为动态或通用的接口,超出特性所能直接提供的范围。例如,在大多数情况下,当类编写时,要支持的属性集无法确认,而且甚至无法以任何具体形式存在(例如,委托任意方法的引用给被包装/嵌入对象时).在这种情况下,通用的__getattr__或__setattr__属性处理器外加传入的属性名,会是更好的选择。因为这类通用处理器也能处理较简单的情况,特性大致上就只是选用的扩展功能了。
18、__getattribute__和描述符。__getattribute__方法只适用于新式类,可以让类拦截所有属性的引用,而不局限于未定义的引用(如同__getattr__)。但是,它远比__getattr__和__setattr__难用,而且很像__setattr__多用于循环,但二者的用法不同。除了特性和运算符重载方法,python支持属性描述符的概念,带有__get__和__set__方法的类,分配给类属性并且由实例继承,这拦截了对特定属性的读取和写入访问。描述符在某种意义上是特性的一种更加通用的形式。实际上,特性是定义特性类型描述符的一种简化方式,该描述符运行关于访问的函数。描述符还用来实现前面介绍的slots特性。由于特性、__getattribute__和描述符都是较为高级,放在后面说。
19、元类。新式类的大多数变化和功能增加,都是前面提到的可子类化的类型的概念密切相连,因为在2.2及其以后的版本中,可子类化的类型和新式类与类型和类的合并一起引入。正如看到的,在3.0中,合并完成了:类现在是类型,并且类型也是类。除了这些变化,python还针对编写元类增加了一种更加一致的协议,元类是子类化了type对象并且拦截类创建调用的类。此外,它们还为管理和扩展类对象提供了一种定义良好的钩子。
20、静态方法和类方法。在2.2中,有可能在类中定义两种方法,它们不用一个实例就可以调用:静态方法大致与一个类中的简单的无实例函数类似的工作,类方法传递一个类而不是一个实例。尽管这一功能与前面小节所介绍的新式类一起添加,静态方法和类方法只对经典类有效。要使用这种方法,必须在类中调用特殊的内置函数,分别名为staticmethod和classmethod,或者使用后面的装饰语法来调用。在3.0中,无实例的方法只通过一个类名来调用,而不需要一个staticmethod声明,但是这样的方法却实通过实例来调用。
21、为什么使用特殊方法。类方法通常在其第一个参数中传递一个实例对象,以充当方法调用的一个隐式主体。然而今天,有两种方法来修改这种模式。有时候,程序需要处理与类而不是与实例相关的数据。考虑要记录由一个类创建的实例的数目,或者维护当前内存中一个类的所有实例的列表。这种类型的信息及其处理与类相关,而非与其实例相关。也就是说,这种信息通常存储在类自身上,不需要任何实例也可以处理。对于这样的任务,一个类之外的简单函数编码往往就能胜任,因为它们可以通过类名访问类属性,它们能够访问类数据并且不需要通过一个实例。然而,要更好的把这样的代码与一个类联系起来,并且允许这样的过程像通常一样用继承来定制,在类自身之中编写这类函数将会更好。所以,需要一个类中的方法不仅不传递而且也不期待一个self实例参数。python通过静态方法的概念来支持这样的目标,嵌套在一个类中的没有self参数的简单函数,并且旨在操作类属性而不是实例属性。静态方法不会接受一个自动的self参数,不管是通过一个类还是一个实例调用。它们通常会记录跨所有实例的信息,而不是为实例提供行为。python还支持类方法的概念,这是类的一种方法。传递给它们的第一个参数是一个类对象而不是一个实例,不管是通过一个实例或一个类调用它们。即便是通过一个实例调用,这样的方法也可以通过它们的self类参数来访问类数据。常规的方法(现在正规的方法叫做实例方法)在调用的时候仍然接受一个主题实例,静态方法和类方法则不会。
22、2.6和3.0中的静态方法。静态方法概念在2.6和3.0中是相同的,但是实现需求在3.0中有所发展。还记得2.6和3.0总是给通过一个实例调用的方法传递一个实例。然而,3.0对待从一个类直接获取的方法,与2.6有所不同:a、在2.6中,从一个类获取一个方法会产生一个未绑定方法,没有手动传递一个实例的就不会调用它;b、在3.0中,从一个类获取一个方法会产生一个简单函数,没有给出实例也可以常规的调用。也就是在2.6类方法总是要求传入一个实例,不管是通过一个实例或类调用它们。相反,在3.0中,只有当一个方法期待实例的时候,我们才给它传入一个实例,没有一个self实例参数的方法可以通过类调用而不需要传入一个实例。也就是说,3.0允许类中的简单函数,只要它们不期待并且也不传入一个实例参数。直接效果是:a、在2.6中,必须总是把一个方法声明为静态的,从而不带一个实例而调用它,不管是通过一个类或一个实例调用它;b、在3.0中,如果方法只通过一个类调用的话,不需要将这样的方法声明为静态的,但是,要通过一个实例调用它,必须这么做。
23、接22。例如,假设想使用类属性去计算从一个类产生了多少实例。下面的文件spam.py做出了最初的尝试,它的类把一个计数器存储为类属性,每次创建一个新的实例的时候,构造函数都会对计数器加1,并且,有一个显示计数器值的方法。记住,类属性是由所有实例共享的,所以可以把计数器放在类对象内,从而确保它可以在所有的实例中使用:
printNumInstances方法旨在处理类数据而不是实例数据,它是关于所有实例的,而不是某个特定的实例。因此,想要不必传递一个实例就可以调用它。实际上,不想生成一个实例来获取实例的数目,因为这可能会改变我们想要获取的实例的数目!换句话说,我们想要一个无self的"静态"方法。然而,这段代码是否有效,取决于我们所使用的python,以及我们调用方法的方式,通过类或者通过一个实例。在2.6中(以及更通常的2.X),通过类和实例调用无self方法函数都将失效:
这里的问题在于,2.6中无绑定实例的方法并不完全等同于简单函数,即使在def头部没有参数,该方法在调用的时候仍然期待一个实例,因为该函数与一个类相关。在3.0中(以及随后的3.X中),对一个无self方法的调用使得通过类调用有效,但从实例调用失效:
也就是说,对于printNumInstances这样的无实例方法的调用,在2.6中通过类进行调用将会失效,但是在3.0中有效。另一方面,通过一个实例调用在两个版本中的python都会失效,因为一个实例自动传递给方法,而该方法没有一个参数来接收它:
如果能够使用3.0并且坚持只通过类调用无self方法,就已经有了一个静态方法特性。然而,要允许非self方法在2.6中通过类调用,并且在2.6和3.0中都通过实例调用,需要采取其他设计,或者能够把这样的方法标记为特殊的。
24、静态方法替代方案。如果不能使得一个无self方法称为特殊的,有一些不同的编码结构可以尝试。如果想要调用没有一个实例二访问类成员的函数,可能最简单的就是只在类之外生成他们的简单函数,而不是类方法。通过这种方式,调用中不会期待一个实例。例如,对spam.py的如下修改在3.0和2.6中都有效:
因为类名称对简单函数而言是可读取的全局变量,这样可正常工作。此外,函数名变成了全局变量,这仅适用于这个单一的模块而已。它不会和程序其他文件中的变量名冲突。在python中的静态方法之前,这一结构是通用的方法。由于python已经把模块提供为命名空间分隔工具,因此可以确定通常不需要把函数包装到一个类中,除非它们实现了对象行为。像这里这样的模块中的简单函数,做了无实例类方法的大多数工作,并且已经与类关联起来,因为他们位于同一个模块中。不过这个方法仍然不是理想的,首先,它给该文件的作用域添加了一个额外的名称,该名称只用来处理单个的类。此外,该函数与类的直接关联很小;实际上,它的定义可能在数百行代码之外的位置。可能更糟糕的是,像这样的简单函数不能通过继承定制,由此,他们位于类的命名空间之外:子类不能通过重新定义这样的一个函数来直接替代或扩展它。我们有可能像要像通常那样使用一个常规方法并总是通过一个实例调用它,从而使得这个例子以独立于版本的方式工作:
可惜,正如前面说的,如果没有一个实例可用,并且产生一个实例来改变类数据,就像这里的最后一行所说明的那样,这样的方法完全是无法工作的。更好的解决方法可能是在类中把一个方法标记为不需要一个实例。
25、使用静态和类方法。还有一个选择就是编写和类相关联的简单函数。在2.2中,可以用静态和类方法编写类,两者都不需要在启用时传入实例参数。要设计这个类的方法时,类要调用内置函数staticmethod和classmethod,就像之前讨论过的新式类中提到的那样。它们都把一个函数标记为特殊的,例如,如果是静态方法的话不需要实例,如果是一个类方法的话需要一个类参数,例如:
ps:程序代码中最后两个赋值语句只是重新赋值方法名称smeth和cmeth罢了。在class语句中,通过赋值语句进行属性的建立和修改,所以这些最后的赋值语句会覆盖稍早由def所作的赋值。从技术上说,python现在支持三种类相关的方法:实例、静态和类。此外,3.0也允许类中的简单函数在通过一个类调用的时候充当静态方法的角色而不需要额外的协议,从而扩展这一模式。实例方法是本教程中所见的常规(默认)情况。一定要用实例对象调用实例方法,通过实例调用时,python会把实例自动传给第一个(最左侧)参数。类调用时,需要手动传入实例:
反之,静态方法调用时不需要实力参数。与类之外的简单函数不同,其变量名位于定义所在类的范围内,属于局部变量,而且可以通过继承查找。非实例函数通常在3.0中可以通过类调用,但是在2.6中并非默认的。使用staticmethod内置方法允许这样的方法在3.0中通过一个实例调用,而在2.6中通过类和实例调用(前者在3.0中没有staticmethod也能工作,但是后者不行):
类方法类似,当python自动把类(而不是实例)传入类方法第一个(最左侧)参数中,不管它是通过一个类或一个实例调用:
26、使用静态方法统计实例。现在有了这些内置函数,下面是本节实例统计示例的静态方法等价形式,把方法标记为特殊的,以便不会自动传递一个实例:
使用静态方法内置函数,我们代码现在允许在2.6和3.0中通过类或其任何实例来调用无self方法:
和将printNumInstances移到类之外的做法相比较,这个版本还需要额外的staticmethod调用,然而,这样做把函数名称变成类作用域内的局部变量(不会和模块内的其他变量名冲突),而且把函数程序代码移动靠近其使用的地方(位于class语句中),并且允许子类用集成定制静态方法,这是比超类编码中从文件导入函数更方便的一种方法,下面是子类以及新的测试会话:
此外,类可以继承静态方法而不用重新定义它,它可以没有一个实例而运行,不管定义于类树的什么地方:
27、用类方法统计实例。类方法也可以做上述类似的工作,下面的代码与前面列出的静态方法版本具有相同的行为,但是,它使用一个类方法来把实例的类接收到其第一个参数中。类方法使用通用的自动传递类对象,而不是硬编码类名称:
这个类与前面的版本使用方法相同,但是通过类和实例调用printNumInstances方法的时候,它接受类而不是实例:
当使用类方法的时候,他们接收调用的主题的最具体(低层)的类。当试图通过传入类更新类数据的时候,这具有某些细微的隐藏含义。例如,如果在模块test.py中像前面那样对定制子类化,扩展spam.printNumInstances以显示其cls参数,并且开始一个新的测试会话:
无论何时运行一个类方法的时候,最低层的类传入,即便对于没有自己的类方法的子类:
这里的第一个调用中,通过Sub子类的一个实例调用了一个类方法,并且python传递了最低的类,sub,给该类方法。在这个例子中,由于该方法的sub重定义显式的调用了spam超类的版本,spam中的超类方法在第一个参数中接收自己。但是,对于直接继承类方法的一个对象。看看发生了什么:
这里的最后一个调用把other传递给了spam的类方法。在这个例子中有效的,因为它通过继承获取了在spam中找到的计数器。如果该方法试图把传递的类的数据赋值,它将更新object,而不是spam。在这个特定的例子中,可能spam通过直接编写自己的类名来更新其数据会更好,而不是依赖于传入的类参数。
28、使用类方法统计每个类的实例。由于类方法总是接收一个实例树中的最低类:a、静态方法和显式类名称可能对于处理一个类本地的数据来说是更好的解决方案;b、类方法可能更适合处理对层级中的每个类不同的数据。代码需要管理每个类实例计数器,这可能会更好的利用类方法。在下面的代码中,顶层的超类使用一个类方法来管理状态信息,该信息根据树中的每个类都不同,而且存储在类上,这类似于实例方法管理类实例中状态信息的方式:
静态方法和类方法都有其他高级的作用,在最近的python中,随着装饰器语法的出现,静态方法和类方法的设计都变得更加简单,这一语法允许在2.6和3.0中扩展类,已初始化最后一个示例中numInstances这样的计数器的数据。
30、装饰器和元类:第一部分。上一节的staticmethod调用技术,对于一些人来说比较奇怪,所以新增的函数装饰器(function decorator)让这个运算变得简单一点,提供一个方式,替函数明确了特定的运算模式,也就是将函数包裹了另一层,在另一函数的逻辑内实现。函数装饰器变成了通用的工具:除了静态方法用法外,也可用于新增多种逻辑的函数。例如,可以用来记录函数调用的信息和在出错时检查传入的参数类型等。从某种程度上来说,函数装饰器类似第30章的委托设计模式,但是其设计是为了增强特定的函数或方法调用,而不是整个对象接口。python提供一些内置函数装饰器,来做一些运算,例如,标识静态方法,但是可以编写自己的任意装饰器。虽然不限于使用类,但用户定义的函数装饰器通常也写成类,把原始函数和其他数据当成状态信息。在2.6和3.0中也有更新的相关扩展可用:类装饰器直接绑定到类模式,并且它们的用途与元类有所重叠。
31、函数装饰器基础。从语法上说,函数装饰器是它后边的函数的运行时的声明。函数装饰器是写成一行,就在定义函数或方法的def语句之前,而且由@符号、后面跟着所谓的元函数(metafunction)组成:也就是管理另一个函数(或其他可调用对象)的函数。例如,如今的静态方法可以用下面的装饰器语法编写:
从内部来看,这个语法和下面的写法有相同效果(把函数传递给装饰器,再赋值给最初的变量名):
结果就是,调用方法函数的名称,实际上是触发了它staticmethod装饰器的结果。因为装饰器会传回任何种类的对象,这也可以让装饰器在每次调用上增加一层逻辑。装饰器函数可返回原始函数,或者新对象(保存传给装饰器的原始函数,这个函数将会在额外逻辑层执行后间接的运行)。经过这些添加,有了在2.6和3.0中编写前一节中的静态方法示例的一种更好的方法(classmethod装饰器以同样的方式使用):
记得,staticmethod仍然是一个内置函数;它可以用于装饰语法中,只是因为它把一个函数当作参数并且返回一个可调用对象。实际上,任何这样的函数都可以以这种方式使用,即便是下节介绍的自己编写的用户定义函数。
32、装饰器例子。尽管python提供了很多内置函数,它们可以用作装饰器,我们也可以自己编写定制装饰器。由于广泛应用,(在本教程最后一个部分中介绍,可是在我写的博文中有可能不会出现),这里作为一个快速的示例,看看一个简单的用户定义的装饰器的应用。想想地29章,__call__运算符重载方法为类实例实现函数调用接口。下面的程序通过这种方法定义类,在实例中存储装饰的函数,并捕捉对最初变量名的调用。因为这是类,也有状态信息(记录所作调用的计数器):
因为spam函数是通过tracer装饰器执行的,所以当最初的变量名spam调用时,实际上触发的是类中的__call__方法。这个方法会计算和记录改次调用,然后委托给原始的包裹的函数。注意*name参数语法是如何打包并解开传入的参数的。因此,装饰器可用于包裹携带任意数目参数的任何函数。结果就是新增一层逻辑至原始的spam函数,下面是此脚本的输出:第一列来自tracer类,第二列来自spam函数:
这个装饰器对于任何接收位置参数的函数都有效,但是,它没有返回已装饰函数的结果,没有处理关键字参数,并且不能装饰类方法函数(简单来说,对于其__call__方法将只传递一个tracer实例)。第八部分有各种各样的方式来编写函数装饰器,包括嵌套def语句;其中的一些替代方法比这里给出的更适合于方法。
33、类装饰器和元类。函数装饰器如此有用,以至于2.6和3.0都扩展了这一模式,允许装饰器应用于类和函数,简单来说,装饰器类似于函数装饰器,但是,它们在一条class语句的末尾运行,并且把一个类名重新绑定到一个可调用对象。同样,它们可以用来管理类(在类创建之后),或者当随后创建实例的时候插入一个包装逻辑层来管理实例。代码如下:
被映射为下列相当代码:
类装饰器也可以扩展类自身,或者返回一个拦截了随后的实例构建调用的对象。例如,在本章前面的“用类方法统计每个类的实例”小节的示例中,我们使用这个钩子来自动的扩展了带有实例计数器和任何其他所需数据的类:
元类是一种类似的基于类的高级工具,其用途往往与类装饰器有所重合。它们提供了一种可选的模式,会把一个类对象的创建导向到顶级type类的一个子类,在一条class语句的最后:
在2.6中,效果是相同的,但是编码是不同的,在类头部中使用一个类属性而不是一个关键字参数:
元类通常重新定义type类的__new__或__init__方法,以实现对一个新的类对象的创建和初始化的控制。直接效果就像类装饰器一样,是定义了在类创建时自动运行的代码。两种方法都可以用来扩展一个类或返回一个任意的对象来替代它,几乎是拥有无限的、基于类的可能性的一种协议。
34、类陷阱。大多数类的问题通常都可以浓缩为命名空间的问题(因为类只是多了一些技巧的命名空间而已)。修改类属性的副作用。从理论上来说,类(和类实例)是可改变的对象。就像内置列表和字典一样,可以给类属性赋值,并且进行在原处的修改,同时意味着修改类或实例对象,也会影响对它的多处 引用。这通常是我们想要的(也是对象一般修改其状态的方式),修改类属性时,了解这点很重要,因为所有从类产生的实例都共享这个类的命名空间,任何在类层次所作的修改都会反映在所有实例中,除非实例拥有自己的被修改的类属性版本。因为类、模板以及实例都只是属性命名空间内的对象,一般可通过赋值语句在运行时修改它们的属性。在类主体中,对变量名a的赋值语句会产生属性X.a,在运行时存在于类的对象内,而且会由所有X的实例继承:
到目前为止,都不错,但是,当我们在class语句外动态修改类属性时,将发生什么:这也会修改每个对象从该类继承而来的这个属性。再者,在这个进程或程序执行时,由类所创建的新实例会得到这个动态设置值,无论该类的源代码是什么:
这个功能是好的还是坏的你?在第26章学过,可以修改类的属性而不修改实例,就可以达到相同的目的。这种技术可以模拟其他语言的“记录”或“结构体”。考虑下面不常见但是合法的python程序:
在这里,类X和Y就像“无文件”模块:存储不想发生冲突的变量的命名空间。这是完全合法的python程序设计技巧,但是使用其他人编写的类就不合适了。永远无法知道,修改的类属性会不会对类内部行为产生重要影响。如果要仿真C的结构体,最好是修改实例而不是类,这样的话,影响的只有一个对象:
35、修改可变的类属性也可能产生副作用。这个陷阱其实是前面的陷阱的扩展。由于类属性由所有实例共享,所以如果一个类属性引用一个可变对象,那么从任何实例来原处修改该对象都会立刻影响到所有实例:
这个效果与之前的效果没区别:可变对象通过简单变量来共享,全局变量由函数共享,模块级的对象由多个导入者共享,可变的函数参数由调用者和被调用者共享。所有这些都是通用行为的例子,并且如果从任何引用原处修改共享的对象的话,对一个可变对象的多个引用都将受到影响。在这里,这通过继承发生于所有实例所共享的类属性中,但是,这也是同样的现象在发挥作用。通过对实例属性自身的赋值的不同行为,这可能会更含蓄的发生:
但是,这不是一个问题,它只是需要注意的事情:共享的可变类属性在python程序中可能有很多有效的用途。
36、多重继承:顺序很重要。如果使用多重继承,超类别再class语句首行内的顺序就很重要。python总是会更加超类在首行的顺序,由左至右搜索超类。例如,在第30章多重继承的例子中,假设super类也实现了__str__方法:
我们想要继承Lister的还是SUper的呢,由于继承搜索从左到右,会从先列在sub类首行的那个类取得该方法,假设,先编写ListTree,因为这个类的整个目的就是其定制了的__str__(实际上,当把这个类与拥有自己的一个__str__的tkinter,Button混入的时候,必须这么做)。但现在,假设Super和ListTree各自有其他的同名属性的版本。如果想要使用Super的变量名,也想要使用ListTree的变量名,在类首行的编写顺序就没什么帮助:就得手动对Sub类内的属性名赋值来覆盖继承:
在这里,对sub类中other做赋值运算,会建立sub.ohter,对super.other对象的引用值。因它在树中的位置较低,sub.other实际上会隐藏ListTree.other(继承搜索时)。同样,如果在类首行中先编写super来挑选其中other,就需要刻意的选ListTree中的方法:
多重继承是高级工具,即使掌握了上一段的内容,谨小慎微的使用依然是必须的。否则对于任意关系较远的子类中变量的含义,将会取决于混入的类的顺序。经验是:当混合类尽可能的独立完备时,多重继承的工作状况最好,因为混合类可以应用在各种环境中,因此不应该对树中其他类相关的变量名有任何假设。之前第30章的伪私有__X属性功能可以把类依赖的变量名本地化,限制混合类可以混入的名称,因此会有帮助,例如,在这个例子中,如果ListTree只是要导出特殊的__str__,就可以将其另一个方法命名为__other,从而避免发生与其他类冲突。
37、类、方法以及嵌套作用域,这个陷阱在2.2引入嵌套函数作用域后就消失了,这里作为回顾下,因为这可以示范当一层嵌套是类时,新的嵌套函数作用域会发什么什么。类引入本地作用域,就像函数一样。所以相同的作用域行为也会发生在class语句的主体中。此外,方法是嵌套函数,也有相同的问题。当类进行嵌套时,看起来令人困惑就比较常见了。下面的例子(nester.py),generate函数返回嵌套的spam类的实例。在其代码中,类名称spam是在generate函数的本地作用域中赋值的。但是,在2.2之前,在类的方法函数中,是看不见类名称spam的。方法只能读取其自己的本地作用域,generate所在的模块以及内置变量名:
这个例子可在2.2以后的版本中执行,因为任何所在函数def的本地作用域都会自动被嵌套的def中看见。但是,2.2之前的版本就行不通了。注意,即使在2.2中,方法def还是无法看见所在类的局部作用域。方法def只看得见所在def的局部作用域。这就是为什么方法得通过self实例,或类名称取引用所在类语句中定义的方法和其他属性。例如,方法中的程序代码必须使用self.count或spam.count,不能只是count。如果正在使用2.2版以前的版本,有很多方式可以使用上一个例子,最简单的就是,全局声明,把spam放在所在模块的作用域中。因为方法看得见所在模块中的全局变量名,就能够引用spam:
事实上,这种做法适用于所有python版本。一般而言,如果避免嵌套类和函数,代码都会比较简单。如果想复杂一些,可以完全放弃在方法中引用spam,而是用特殊的__class__属性,来返回实例的类对象:
38、python中基于委托的类:__getattr__和内置函数。在第27章的类教程和第30章的委托介绍中简单的遇到这个问题:使用__getattr__运算符重载方法来把属性获取委托给包装的对象的类,在3.0中将失效,除非运算符重载方法在包装类中重新定义了。在3.0(2.6中,当使用新式类的时候),内置操作没有导向到通用的属性拦截方法,从而隐式的获取运算符重载方法的名称,例如,打印所使用的__str__方法,不会调用__getattr__。相反,3.0在类中查找这样的名字,并且完全略过常规的运行时实例查找机制。为了解决这一点,这样的方法必须在包装类中重定义,要么手动,要么使用工具,或者在超累中重新定义。