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