【Python 笔记】Python面向对象

面向对象编程踏上了进化的阶梯,增强了架构化编程,实现了数据与动作的融合:数据层和逻辑层现在由一个可以创建这些对象的简单抽象层来描述。现实世界中的问题和实体完全暴露了本质,从中提供的一种抽象,可以用来进行相似编码,或者编入能与系统中对象进行交互的对象中。提供了这样一些对象的定义,实例即是这些定义的实现。二者对面向对象设计(object-oriented design, OOD)来说都是重要的,OOD仅意味来创建你采用面向对象方式架构来创建系统。

1. OOD 与 OOP

  面向对象设计(OOD)不会特别要求面向对象编程语言。OOD可以由纯结构化语言来实现,比如C。当一门语言内建OO特性,OO编程开会更加方便高效。
Python从一开始设计就是面向对象的,但并非日常编程所必须。Python面向对象编程(OOP)具有强大能力,在Python中使用OOP可以提高许多效率。

2. 常用术语

  1. 抽象/实现
  2. 封装/接口
  3. 合成
  4. 派生/继承/继承结构
  5. 泛华/特化
  6. 多态
  7. 自省/反射

3. 类

3.1 创建类

  Python 类使用class关键字来创建。

class ClassNmae(bases):
    'class documentation string'  # '类文档字符串'
    class_suite  # 类体

bases 是基类。

3.2 声明与定义

  对于Python,声明与定义没什么区别。

4. 类属性

  属性就是属于另一个对象的数据或者函数元素。

3.1 类的数据属性

  类数据属性仅仅是所定义类的变量。

>>> class C(object):
...     foo = 100
...
>>> print(C.foo)
100
>>> C.foo = C.foo + 1
>>> print(C.foo)
101

  上面的代码中,看不到任何类实例的引用。

3.2 Methods

  方法,比如下面,类MyClass中的myNoActionMethod方法,仅仅是一个作为类定义一部分定义的函数(这使得方法成为类属性)。这表示myNoActionMethod仅应用在MyClass类型的对象(实例)上。这里,myNoActionMethod是通过句点属性标识法与它的实例绑定的。

  1. 方法
>>> class MyClass(object):
...     def myNoActionMethod(self):
...         pass
... 
>>> mc = MyClass()
>>> mc.myNoActionMethod()

  任何像函数一样对myNoActionMethod自身调用都将失败:

>>> myNoAction()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'myNoAction' is not defined

  引发了NameError异常,因为全局名字空间中,没有这样的函数存在。

  由类对象调用此方法也失败了:

>>> MyClass.myNoActionMethod()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: myNoActionMethod() missing 1 required positional argument: 'self'

  TypeError异常看起来让人困惑,因为这种方法是类的一个属性。
下面进行解释。

  1. 绑定(绑定及非绑定方法)
    Python严格要求,没有实例,方法不能被调用。这种限制即Python所描述的绑定概念(binding),在此,方法必须绑定(到一个实例)才能直接被调用。

3.3 决定类的属性

>>> class MyClass(object):
...     'MyClass class definition'          # 类定义
...     myVersion = '1.1'                   # 静态数据
...     def showMyVersion(self):            # 方法
...         print(MyClass.myVersion)
... 

  查看类的属性,有两种方法。

  最简单的是使用dir()内建函数。

>>> dir(MyClass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'myVersion', 'showMyVersion']

  另外通过访问类的字典属性__dict__,这是所有类都具备的特殊属性之一。

>>> MyClass.__dict__
mappingproxy({'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x7fa5e37c5048>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>})
>>> print(MyClass.__dict__)
{'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x7fa5e37c5048>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>}

  可以看出,dir()返回的仅是对象的属性的一个名字列表,而__dict__返回的是一个字典,它的键(key)是属性名,键值(value)是相应的属性对象的数据值。

3.4 特殊的类属性

C.__name__类C的名字(字符串)
C.__doc__类C的文档字符串
C.__bases__类C的所有父类构成的元祖
C.__dict__类C的属性
C.__module__类C定义所在的模块
C.__class__实例C对应的类(仅新式类)
>>> MyClass.__name__
'MyClass'
>>> MyClass.__doc__
'MyClass class definition'
>>> MyClass.__bases__
(<class 'object'>,)
>>> print(MyClass.__dict__)
{'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x7fa5e37c5048>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>}
>>> MyClass.__module__
'__main__'
>>> MyClass.__class__
<class 'type'>

  __name__是给定类的字符名字。

>>> stype = type('What is your quest?')
>>> stype
<class 'str'>
>>> stype.__name__
'str'
>>> 
>>> type(3.14159265)
<class 'float'>
>>> type(3.14159265).__name__      # 得到类型名(字符串表示)
'float'

  __doc__是类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行(header line)后的字符串。文档字符串不能被派生类继承。派生类必须含有它们自己的文档字符串。

  __bases__用来处理继承,它包含了一个所有父类组成的元祖。

  __dict__包含一个字典,由类的数据属性组成。

  Python支持模块间的类继承。

>>> class C(object):
...     pass
... 
>>> C
<class '__main__.C'>
>>> C.__module__
'__main__'

  类C的全名是“__mian__.C”。如果类C位于一个导入的模块中,如mymod,像下面的。

>>> from mymod import C
>>> C
<class 'mymod.C'>
SyntaxError: invalid syntax
>>> C.__module__
'mymod'

5. 实例

  如果说类是一种数据结构定义类型,那么实例则声明了一个这样类型的量。

5.1 初始化:通过调用类对象创建实例

  其它的OOP提供new关键字,用过new来创建类的实例。Python的方式更加简单,在定义类后,调用类就创建了一个实例。

>>> class MyClass(object):
...     pass
... 
>>> mc = MyClass()

  当使用函数记法调用一个类时,解释器会实例化该对象,将这个实例返回。

5.2 __init__()“构造器”方法

  当类被调用,实例化的第一步是创建实例对象。一旦对象创建了,Python检查是否实现了__init__()方法。默认情况下,如果没有定义(或覆盖)特殊方法__init__(),对实例不会施加任何特别的操作。任何所需的特定操作,都需要手动实现__init__(),覆盖它的默认行为。如果__init__()没有实现,则返回它的对象,实例化过程完毕。

  然而,如果__init__()已经被实现,那么它将被调用,实例对象作为第一个参数(self)被传递进去,像标准方法调用一样。调用类时,传进的任何参数都交给了__init__()。实际中,可以想象成这样,把创建实例的调用当成是对构造器的调用。

  总之,(a)你没有通过调用new来创建实例,你也没有定义一个构造器. 是Python为你创建了对象;(b)__init__(),是在解释器为你创建一个实例后调用的第一个方法,在你开始使用它之前,这一步可以让你做些准备工作.

  __init__()是很多为类定义的特殊方法之一. 其中一些特殊方法是预定义的,缺省情况下,不进行任何操作,比如__init__(),要定制,必须对它进行重载,还有些方法,可能要按需要去实现. 本章中,我们会讲到很多这样的特殊方法. 你将会经常看到__init__()的使用,在此,就不举例说明了.

5.3 __new__()“构造器”方法

  与__init__()相比,__new__()方法更像一个真正的构造器. 类型和类在版本2.2就统一了,Python用户可以对内建类型进行派生. 因此,需要一种途径来实例化不可变对象,比如,派生字符串,数字,等等.

  在这种情况下,解释器则调用类的__new__()方法,一个静态方法,并且传入的参数是在类实例化操作时生成的. __new__()会调用父类的__new__()来创建对象(向上代理).

  __new__()和__init__()在类创建时,都传入了(相同)参数. 11.3节中有个例子使用了__new__().

5.4 __del__()“解构器”方法

  同样,有一个相应的特殊结构其(destructor)方法名为__del__(). 然而,由于Python具有垃圾对象回收机制(靠应用计数),这个函数要直到该实例对象所有的引用都被清除后才会执行. Python中的解构器是在实例释放前提供特殊处理功能的方法,它们通常没有被实现,因为实例很少被显示释放.

举例

  在下面的例子中,我们分别创建(并覆盖)init()和__del__()构造及解构函数. 然后,初始化类并给同样的对象分配很多别名. id()内建函数可用来确定引用同一对象的三个别名. 最后一步是使用del语句清除所有的别名,显示何时,调用了多少次解构器.

>>> class P():                       
...     def __del__(self):           
...         pass
... 
>>> class C(P):                          # 类声明
...     def __init__(self):              # 构造器
...         print('initialized')
...     def __del__(self):               # 解构器
...         P.__del__(self)              # 调用父类解构器打印
...         print('deleted')
... 
>>> c1 = C()                             # 实例初始化
initialized
>>> c2 = c1                              # 创建另外一个别名
>>> c3 = c1                              # 创建第三个别名
>>> id(c1), id(c2), id(c3)               # 同一对象所有引用
(140445373906056, 140445373906056, 140445373906056)
>>> del c1                               # 清除一个引用 
>>> del c2                               # 清除另外一个引用
>>> del c3                               # 清除最终引用
deleted                                  # 解构器最后调用

注意,在上面的例子中,解构器是在类C实例所有的引用都被被清除掉后,才被调用的,比如,当引用计数已减少到0. 如果你预期你的__del__()方法会被调用,却实际上没有被调用,这意味着,你的实例对象由于某些原因,其引用计数不为0,这可能有别的对它的引用,而你并不知道这些让你的对象还活着的引用所在.

另外,要注意,解构器只能被调用一次,一旦引用计数为0,则对象就被清除了. 这非常合理,因为系统中任何对象都只被分配及解构一次》

总结:

  • 不要忘记首先调用父类的__del__().
  • 调用 del x 不表示调用了 x.__del__() ——前面也看到,它仅仅是减少x的引用计数》
  • 如果你有一个循环引用或其它的原因,让一个实例的引用逗留不去,该对象的__del__()可能永远不会被执行.
  • __del__()未捕获的异常会被忽略掉(因为一些在__del__()用到的变量或许已经被删除了).不要在__del__()中干与实例没有任何关系的事情.
  • 除非你知道你正在干什么,否则不要去实现__del__().
  • 如果你定义了__del__,并且实例是某个循环的一部分,垃圾回收器将不会终止这个循环——你需要自己显示调用del.

核心笔记:跟踪实例
Python没有提供任何内部机制来跟踪一个类有多少个实例被创建了,或者记录这些实例是些什么东西. 如果需要这些功能,你可以显式加入一些代码到类定义或者__init__()和__del__()中去. 最好的方式是使用一个静态成员来记录实例的个数. 靠保存它们的引用来跟踪实例对象是很危险的,因为你必须合理管理这些引用,不然,你的引用可能没办法释放(因为还有其它的引用)!看下面的例子:

class InstCt(object):
   count = 0
  
   def __init__(self):
       InstCt.count += 1
       
   def __del__(self):
       InstCt.count -= 1
       
   def howMany(self):
       return InstCt.count
>>> a = InstCt()
>> b = InstCt()
>> b.howMany()
2
>> a.howMany()
2
>> del b
>> a.howMany()
1
>> del a
>> InstCt.count
0
>>

6. 实例属性

  实例仅拥有数据属性(方法严格来说是类属性),后者只是与某个类的实例相关联的数据值,并且可以通过句点属性标识法来访问. 这些值独立于其它实例或类. 当一个实例被释放后,它的属性同时也被清除了.

6.1 “实例化”实例属性(或创建一个更好的构造器)

  设置实例的属性可以在实例创建后任意时间进行,也可以在能够访问实例的代码中进行。构造器__init()__是设置这些属性的关键点之一。

核心笔记:实例属性
  能够在“运行时”创建实例属性,时Python类的优秀特性之一,从C++或Java转过来的人会被小小的震惊一下,因为C++或Java中所有属性在使用前都必须明确定义/声明.
  Python不仅是动态类型,而且在运行时,允许这些对象属性的动态创建。这种特性让人爱不释手。当然,我们必须提醒读者,创建这样的属性时,必须谨慎.
  一个缺陷是,属性在条件语句中创建,如果该条件语句块并未被执行,属性也就不存在,而你在后面的代码中试着去访问这些属性,就会有错误发生。故事的精髓是告诉我们,Python 让你体验从未用过的特性,但如果你使用它了,你还是要小心为好.

  1. 在构造器中首先设置实例属性

  构造器是最早可以设置实例属性的地方,因为__init__()是实例创建后第一个被调用的方法。再没有比这更早的可以设置实例属性的机会了。一旦__init__()执行完毕,返回实例对象,即完成了实例化过程。

  1. 默认参数提供默认的实例安装

  在实际应用中,带默认参数的__init__()提供一个有效的方式来初始化实例。在很多情况下,默认值表示设置实例属性的最常见的情况,如果提供了默认值,我们就没必要显式给构造器传值了。需要明白一点,默认参数应当是不变的对象;像列表(list)和字典(dictionary)这样的可变对象可以扮演静态数据,然后在每个方法调用中来维护它们的内容。

  例1 描述了如何使用默认构造器行为来帮助我们计算在美国一些大都市中的旅馆中寄宿时,租房总费用。

  代码的主要目的是来帮助某人计算出每日旅馆租房费用,包括所有州销售税和房税。缺省为旧金山附近的普通区域,它有8.5%销售税及10%的房间税。每日租房费用没有缺省值,因此在任何实例被创建时,都需要这个参数。

  例1 使用缺省参数进行实例化

  定义一个类来计算这个假想旅馆租房费用。__init__()构造器对一些实例属性进行初始化。
calcTotal()方法用来决定是计算每日总的租房费用还是计算所有天全部的租房费。

hotel.py

class HotelRoomCalc(object):
    'Hotel room rate calculator'
    def __init__(self, rt, sales=0.085, rm=0.1):
        '''
        HotelRoomCalc default arguments:
        sales tax == 8.5%
        room tax == 10%
        '''
        self.salesTax = sales
        self.roomTax = rm
        self.roomRate = rt

    def calcTotal(self, days=1):
        'Calculate total; default to daily rate'
        daily = round((self.roomRate * (1 + self.roomTax + self.salesTax)), 2)
        return float(days) * daily

  设置工作是由__init__()在实例化之后完成的,如上第4 到8 行,其余部分的核心代码是calcTotal()方法,从第10 到14 行。__init__()的工作即是设置一些参数值来决定旅馆总的基本租房费用(不包括住房服务,电话费,或其它偶发事情)。calcTotal()可以计算每日所有费用,如果提供了天数,那么将计算整个旅程全部的住宿费用。内建的round()函数可以大约计算出最接近的费用(两个小数位)。下面是这个类的用法:

>>> sfo = HotelRoomCalc(299)
>>> sfo.calcTotal()
354.31
>>> sfo.calcTotal(2)
708.62
>>> sea = HotelRoomCalc(189, 0.086, 0.058)
>>> sea.calcTotal()
216.22
>>> sea.calcTotal(4)
864.88
>>> wasWkDay = HotelRoomCalc(169, 0.045, 0.02)
>>> wasWkEnd = HotelRoomCalc(119, 0.045, 0.02)
>>> wasWkDay.calcTotal(5) + wasWkEnd.calcTotal()
1026.6299999999999

  最开始的两个假想例子都是在旧金山(San Francisco), 使用了默认值,然后是在西雅图(Seattle) , 这里我们提供了不同的销售税和房间税率。最后一个例子在华盛顿特区(Washington.D.C)。经过计算更长的假想时间,来扩展通常的用法:停留五个工作日,外加一个周六,此时有特价,假定是星期天出发回家。

  不要忘记,函数所有的灵活性,比如默认参数,也可以应用到方法中去。在实例化时,可变长度参数也是一个好的特性(当然,这要根据应用的需要)

  1. __init__() 应当返回 None

  你也知道,采用函数操作符调用类对象会创建一个类实例,也就是说这样一种调用过程返回的对象就是实例,下面示例可以看出:

>>> class MyClass(object):
...     pass
... 
>>> mc = MyClass()
>>> mc
<__main__.MyClass object at 0x000000CA65386E80>

  如果定义了构造器,它不应当返回任何对象,因为实例对象是自动在实例化调用后返回的。相应地,__init__()就不应当返回任何对象(应当为None);否则,就可能出现冲突,因为只能返回实例。试着返回非None 的任何其它对象都会导致TypeError 异常:

>>> class MyClass:
...     def __init__(self):
...         print('initialized')
...         return 1
...     
>>> mc = Myclass()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'Myclass' is not defined

6.2 查看实例属性

  内建函数dir()可以显示类属性,同样还可以打印所有实例属性:

>>> class C(object):
...     pass
... 
>>> c = C()
>>> c.foo = 'roger'
>>> c.bar = 'shrubber'
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']

  与类相似,实例也有一个__dict__特殊属性(可以调用vars()并传入一个实例来获取),它是实例属性构成的一个字典:

>>> c.__dict__
{'foo': 'roger', 'bar': 'shrubber'}

6.3 特殊的实例属性

  实例仅有两个特殊属性(见下表)。对于任意对象 I:

I.__class__实例化 I 的类
I.__dict__I 的属性

  使用类 C 及其实例 c 来看看这些特殊实例属性:


>>> class C(object):        # 定义类
...     pass
... 
>>> c = C()                 # 创建实例
>>> dir(c)                  # 返回基类属性,实例还没有属性
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> c.__dict__              # 实例还没有属性
{}
>>> c.__class__             # 实例化 c 的类
<class '__main__.C'>

  可以看到,c 目前还没有数据属性,添加一些再检查 __dict__ 属性。

>>> c.foo = 1
>>> c.bar = 'SPAM'
>>> '%d can of %s please' % (c.foo, c.bar)
'1 can of SPAM please'
>>> c.__dict__
{'foo': 1, 'bar': 'SPAM'}

  __dict__ 属性由一个字典组成,包含一个实例的所有属性。键是属性名,值是属性相应的数据值。字典中仅有实例属性,没有类属性或特殊属性。

核心风格:修改__dict__
对类和实例来说,尽管__dict__属性是可修改的,但还是建议你不要修改这些字典,除非你知道你的目的。这些修改可能会破坏你的OOP,造成不可预料的副作用。使用熟悉的句点属性标识来访问及操作属性会更易于接受。需要你直接修改__dict__属性的情况很少,其中之一是你要重载__setattr__特殊方法。实现__setattr__()本身是一个冒险的经历,满是圈套和陷阱,例如无穷递归和破坏实例对象。这个故事还是留到下次说吧。

6.4 内建类型属性

  内建类型也是类。用 dir() 考察它们有没有像类或实例一样的属性。

>>> x = 3 + 0.14j
>>> x.__class__
<class 'complex'>
>>> dir(x)
['__abs__', '__add__', '__bool__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', 'conjugate', 'imag', 'real']
>>> 
>>> [type(getattr(x, i)) for i in ('conjugate', 'imag', 'real')]
[<class 'builtin_function_or_method'>, <class 'float'>, <class 'float'>]

  已知一个复数的属性,可以访问它的数据属性,调用它的方法:

>>> x.imag
0.14
>>> x.real
3.0
>>> x.conjugate()
(3-0.14j)

  访问 __dict__ 会失败,因为在内建类型中,不存在这个属性:

>>> x.__dict__
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'complex' object has no attribute '__dict__'

6.5 实例属性 vs 类属性

  我们已在 4.1 节中描述了类数据属性。这里简要提一下,类属性仅是与类相关的数据值,和实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使在多次实例化中调用类,它们的值都保持不变。不管如何,静态成员不会因为实例而改变它们的值,除非实例中显式改变它们的值。(实例属性与类属性的比较,类似于自动变量和静态变量,但这只是笼统的类推。在你对自动变量和静态变量还不是很熟的情况下,不要深究这些。

  类和实例都是名字空间。类是类属性的名字空间,实例则是实例属性的。

  关于类属性和实例属性,还有一些方面需要指出。你可采用类来访问类属性,如果实例没有同名的属性的话,你也可以用实例来访问。

  1. 访问类属性

  类属性可通过类或实例来访问。下面的示例中,类C 在创建时,带一个version 属性,这样通过类对象来访问它是很自然的了,比如,C.version。当实例c 被创建后,对实例c 而言,访问c.version 会失败,不过Python 首先会在实例中搜索名字version,然后是类,再就是继承树中的基类。本例中,version 在类中被找到了:

>>> class C(object):            # 定义类
...     version = 1.2           # 静态成员
...     
>>> c = C()                     # 实例化
>>> C.version                   # 通过类来访问
1.2
>>> c.version                   # 通过实例来访问
1.2
>>> C.version += 0.1            # 通过类(只能这样)来更新
>>> C.version                   # 类访问
1.3
>>> c.version                   # 实例访问
1.3

  然而,我们只有当使用类引用version 时,才能更新它的值,像上面的C.version 递增语句。如果尝试在实例中设定或更新类属性会创建一个实例属性c.version,后者会阻止对类属性C.versioin 的访问,因为第一个访问的就是c.version,这样可以对实例有效地“遮蔽”类属性C.version,直c.version 被清除掉。

  1. 从实例中访问类属性须谨慎

  与通常Python变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话)并且对其赋值。如果类属性中存在同名的属性,又去的副作用即产生(经典类和新式类都存在)。

>>> class Foo(object):
...     x = 1.5
...     
>>> foo = Foo()
>>> foo.x
1.5
>>> foo.x = 1.7        # 更新类属性
>>> foo.x              # 似乎已更新
1.7
>>> Foo.x              # 类属性没有变,只是创建了一个新的实例属性
1.5
>>> foo.__dict__
{'x': 1.7}
>>> foo.__dict__['x']
1.7
>>> Foo.__dict__
mappingproxy({'__module__': '__main__', 'x': 1.5, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})
>>> Foo.__dict__['x']
1.5

上述代码创建了名为 x 的新实例属性,它覆盖了对类属性的引用。

>>> del foo.x
>>> foo.x
1.5
>>> foo.x += .2
>>> foo.x
1.7
>>> Foo.x
1.5

同样创建了一个新的实例属性,类属性原封不动(深入理解 Python 相关知识:属性已存在于类字典 [__dict__] 中。通过赋值,其被加入到实例的 __dict__ 中了)。

在类属性可变的情况下,一切都不同了:

>>> class Foo(object):
...     x = {2003: 'poe2'}
...     
>>> foo Foo()
  File "<input>", line 1
    foo Foo()
          ^
SyntaxError: invalid syntax
>>> foo = Foo()
>>> foo.x
{2003: 'poe2'}
>>> foo.x[2004] = 'valid path'
>>> foo.x
{2003: 'poe2', 2004: 'valid path'}
>>> Foo.x                                   # 生效了!
{2003: 'poe2', 2004: 'valid path'}
>>> del foo.x                               # 没有遮蔽,所以不能删除掉
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: x
  1. 类属性持久性
    静态成员,顾名思义,任凭整个实例(及其属性)的如何进展,它都不理不睬(因此独立于实例)。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响到所有的实例:

>>> class C(object):
...     spam = 100
...     
>>> c1 = C()
>>> c1.spam
100
>>> C.spam += 100
>>> C.spam
200
>>> c1.spam
200
>>> c2 = C()
>>> c2.spam
200
>>> del c1
>>> c2.spam
200
>>> C.spam
200
>>> C.spam += 200
>>> c2.spam
400

核心提示:使用类属性来修改自身(不是实例属性)
正如上面所看到的那样,使用实例属性来试着修改类属性是很危险的。原因在于实例拥有它们自已的>属性集,在Python中没有明确的方法来指示你想要修改同名的类属性,比如,没有global关键字可以用来在一个函数中设置一个全局变量(来代替同名的局部变量)。修改类属性需要使用类名,而不是实例名。

7. 绑定和方法调用

Pythong 中绑定(binding)主要与方法调用相关连。
首先,方法仅仅是类内部定义的函数(这意味着方法是类属性而不是实例属性)。
其次,方法只有在其所属的类拥有实例时,才能被调用。当存在一个实例时,方法才被认为是绑定到那个实例了。没有实例时方法就是未绑定的。
最后,任何一个方法定义中的第一个参数都是变量 self它表示调用此方法的实例对象

笔记: self 是什么?
self 变量用于在实例方法中引用方法所绑定的实例。因为方法的实例在任何方法调用中总是作为第一个参数传递的, self 被选中用来代表实例。你必须在方法声明中放上 self(你可能已经注意到这点),但可以在方法中不适用实例(self)。如果你的方法中没有用到 self ,那么请考虑创建一个常规函数,除非你有特别的原因。毕竟,你的方法代码没有使用实例,没有与类关联其功能,这使得它看起来更像一个常规函数。在其他面向对象语言中, self 可能被称为 this

7.1 调用绑定方法

通过实例调用类的方法,即调用绑定的方法。 所属 MyClass 类的方法 foo() 和 实例 mc,调用绑定的方法:mc.foo()。InstanceName.Method()实例名.方法())即调用绑定的方法。调用绑定的方法 self 不需要明确地传入。

7.2 调用非绑定方法

通过类调用类的方法,即调用非绑定的方法。所属 MyClass 类的方法 foo() 和实例 mc,调用非绑定的方法:MyClass.foo(self, …)。ClassName.Method(self, …)类名.方法(self, …))即调用非绑定的方法。调用非绑定的方法必须传递 self 参数。

调用非绑定方法并不经常用到。需要调用一个还没有任何实例的类中的方法的一个主要场景是:你在派生一个子类,而且你需要覆盖父类的方法,这是你需要调用那个父类中想要覆盖掉的构造方法。这里给出一个例子:

class AddrBookEntry(object):
    """address book entry class"""

    def __init__(self, nm, ph):
        self.name = nm
        self.phone = ph
        print('Created instance for:', self.name)

    def updatePhone(self, newph):
        self.phone = newph
        print('Updated phone# for:', self.name)


class EmpAddrBookEntry(AddrBookEntry):
    """Employee Address Book Entry class"""

    def __init__(self, nm, ph, em):
        AddrBookEntry.__init__(self, nm, ph)
        self.empid = id
        self.email = em

8. 静态方法和类方法

经典类和新式类(new-style)都可以使用静态方法和类方法。一对内建函数被引入,用于将作为类定义的一部分的某一方法声明“标记”(tag),“强制类型转换”(cast)或者“转换”(convert)为这两种类型的方法之一。
通常的方法需要一个实例(self)作为第一个参数,并且对于(绑定的)方法调用来说,self 是自动传递给这个方法的。而对于类方法而言,需要类而不是实例作为第一个参数,它是由解释器传给方法。类不需要特别的命名,类似 self ,不过很多人使用 cls 作为变量名字。

8.1 staticmethod() 和 classmethod() 内建函数

在经典类中创建静态方法和类方法的例子(也可用于新式类中):

class TestStaticMethod:
    def foo():
        print('calling static method foo()')

    foo = staticmethod(foo)

class TestClassMethod:
    def foo(cls):
        print('calling class method foo()')
        print('foo() is part of class:', cls.__name__)

    foo = classmethod(foo)

对应的内建函数被转换成它们相应的类型,并且重新赋值给了相同的变量名。如果没有调用这两个函数,二者都会在 Python 编译器中产生错误,显示需要带 self 的常规方法声明。现在,我们可以通过类或者实例调用这些函数,这没什么不同:

>>> tsm = TestStaticMethod()
>>> TestStaticMethod.foo()
calling static method foo()
>>> tsm.foo()
calling static method foo()
>>> tcm = TestClassMethod()
>>> TestClassMethod.foo()
calling class method foo()
foo() is part of class: TestClassMethod
>>> tcm.foo()
calling class method foo()
foo() is part of class: TestClassMethod

赋值给不同的变量名,Python 解释器会报错:

class TestStaticMethod:
    def foo():
        print('calling static method foo()')

    faa = staticmethod(foo)


class TestClassMethod:
    def foo(cls):
        print('calling class method foo()')
        print('foo() is part of class:', cls.__name__)

    faa = classmethod(foo)

结果如下:

>>> tsm = TestStaticMethod()
>>> TestStaticMethod.foo()
calling static method foo()
>>> TestStaticMethod.faa()
calling static method foo()
>>> tsm.foo()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: foo() takes 0 positional arguments but 1 was given
>>> tsm.faa()
calling static method foo()
>>> tsm.faa
<function TestStaticMethod.foo at 0x000000170945D048>
>>> tcm = TestClassMethod()
>>> TestClassMethod.foo()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: foo() missing 1 required positional argument: 'cls'
>>> tcm.foo()
Traceback (most recent call last):
calling class method foo()
  File "<input>", line 1, in <module>
  File "F:/A1_SLAM/03__projects/learn_python/staticmethod.py", line 11, in foo
    print('foo() is part of class:', cls.__name__)
AttributeError: 'TestClassMethod' object has no attribute '__name__'
>>> TestClassMethod.faa()
calling class method foo()
foo() is part of class: TestClassMethod
>>> TestClassMethod.faa
<bound method TestClassMethod.foo of <class '__main__.TestClassMethod'>>
>>> tcm.faa()
calling class method foo()
foo() is part of class: TestClassMethod
>>> tcm.faa
<bound method TestClassMethod.foo of <class '__main__.TestClassMethod'>>

8.2 使用函数修饰符

通过使用装饰器,我们可以避免像上面那样的重新赋值:

class TestStaticMethod:
    @staticmethod
    def foo():
        print('calling static method foo()')


class TestClassMethod:
    @classmethod
    def foo(cls):
        print('calling class method foo()')
        print('foo() is part of class:', cls.__name__)

9. 组合

一个类被定义后,目标就是要把他当成一个模块使用,并把这些对象嵌入到代码中去,同其他类型及逻辑执行流混合使用。有两种方法可以在代码中利用类。第一种是组合(Composition)。就是让不同的类混合并加入到其他类中,类增加功能和代码的重用性。我可以在一个大点的类中创建我自己的类的实例,实现一些其他属性和方法来增强原来的类对象。另一种方法是通过派生,我们将在下一节中讨论。
举例来说,如果在设计的过程中,为Name,Phone创建了单独的类,那么最后我们可能想把这些工作继承到AddrBookEntry类中去,而不是重新设计每一个需要的类。这样就节省了时间和精力,而且最后的结果是容易维护的代码——块代码中的bug被修正,将反映到整个应用中。
这样的类可能包含以Name实例,再加一个Phone实例,以及其它如StreetAddress、Email(home、work等),还可能需要一些Date实例(birthday、wedding、anniversary等)。
下面给出一个例子

class Name(object):

    def __init__(self, name):
        self.first_name = name.split(' ')[0]
        self.last_name = name.split(' ')[1]
        self.full_name = name


class Phone(object):

    def __init__(self, phone):
        self.phone = phone


class NewAddBookEntry(object):

    def __init__(self, nm, ph):
        self.name = Name(nm)                         # 创建 Name 实例
        self.full_name = self.name.full_name
        self.first_name = self.name.first_name
        self.last_name = self.name.last_name
        self.phone = Phone(ph)                       # 创建 Phone 实例
        print('Created instance for: Mr.', self.last_name, '--', self.full_name)


if __name__ == '__main__':
    new_entry = NewAddBookEntry('James Harden', '18012345678')

输出结果

Created instance for: Mr. Harden -- James Harden

NewAddrBookEntry类由它自身和其它类组合而成。这就在一个类和其它组成类之间定义了一种“有一个”(has-a)的关系。比如,我们的NewAddrBookEntry类“有一个”Name类实例和一个Phone实例。
创建复合对象就可以实现这些附加的功能,并且很有意义,因为这些类都不相同。每一个类管理它们自己的名字空间和行为。不过当对象之间有更接近的关系时,派生的概念可能对我的程序来说更有意义,特别是当我需要一些相似的对象,但却有少许不同功能的时候。

10. 子类和派生

当类之间有显著的不同,并且(较小的类)是较大的类所需要的组件时,组合表现得很好,但当我们设计“相同的类担忧一些不同的功能”时,派生就是一个更加合理的选择了。
OOP的更强大功能之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其他代码片段。OOD允许类特征在子孙类或子类中进行继承。这些子类从基类(或称祖先类、超类)继承它们的核心属性。而且,这些派生可能会扩展到多代。从同一个父类派生出来的这些类(或者是在类树图中水平相邻)是同胞关系。父类和所有高层类都被认为是祖先。

创建子类
创建子类的语法看起来与普通(新式)类没有区别,一个类名,后跟一个或多个需要从其中派生的父类:

class SubClassName(ParentClass1[, ParentClass2, ...]):
    """optional class documentation string"""
    class_suite

如果你的类没有从任何祖先类派生,可以使用object作为父类的名字。经典类的声明唯一不同之处在于其没有从祖先类派生——此时,没有圆括号:

class ClassicClassWithouSuperclasses:
    pass
    

至此,我们已经看到了一些类和子类的例子,下面还有一个简单的例子:

class Parent(object):                            # 调用父类
    def parentMethod(self):
        print('calling parent method')


class Child(Parent):                             # 调用子类
    def childMethod(self):
        print('calling parent method')

p = Parent()                                    # 父类的实例
p.parentMethod()
calling parent method
c = Child()                                       
c.childMethod()                                   
calling parent method
c.parentMethod()
calling parent method

>>> p = Parent()                                # 父类的实例
>>> p.parentMethod()
calling parent method
>>> c = Child()                                 # 子类的实例
>>> c.childMethod()                             # 子类调用它的方法                         
calling parent method
>>> c.parentMethod()                            # 子类调用父类的方法
calling parent method

11. 继承

继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。
举个例子如下。P是一个没有属性的简单类。C从P继承而来(因此是它的子类),也没有属性:

class P(object):
    pass

class C(P):
    pass
>>> c = C()
>>> c.__class__
<class '__main__.C'>
>>> C.__bases__
(<class '__main__.P'>,)

因为P没有属性,C没有继承到什么。下面我们给P添加一些属性:

class P:
    """P class"""
    def __init__(self):
        print('Created an instance of', self.__class__.__name__)


class C(P):
    pass

现在所创建的P有文档字符串(__doc__)和构造器,当我们实例化P时它被执行,如下面的交互会话所示:

>>> p = P()
Created an instance of P
>>> p.__class__
<class '__main__.P'>
>>> P.__bases__
(<class 'object'>,)

“created an instance”是由__init__()直接输出的。我们也可以显示更多关于父类的信息。我们现在来实例化C,展示__init__()(构造)方法在执行过程中是如何继承的:

>>> c = C()
Created an instance of C
>>> c.__class__
<class '__main__.C'>
>>> C.__bases__
(<class '__main__.P'>,)
>>> C.__doc__
>>> c.__doc__

C没有声明__init__()方法,然而在类C的实例c被创建时,还是会有输出信息。原因在于C继承了P的__init__()。__bases__元祖列出了其父类P。需要注意的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来。

11.1 __bases__类属性

对任何(子)类,它是一个包含其父类(patent)的集合的元祖。注意,我们明确指出“父类”是相对所有基类(它包括了所有祖先类)而言的。
在Python2.x中,那些没有父类的类,它们的__bases__属性为空。
在Python3.x中,取消了经典类,仅保留新式类。

python2.x

# -*- coding:utf-8 -*-
# !/usr/bin/env python2


class A(object):   # 声明新式类
    pass


class B():         # 声明经典类
    pass


class C:           # 声明经典类
    pass
>>> A.__bases__
(<type 'object'>,)
>>> B.__bases__
()
>>> C.__bases__
()

python3.x

# -*- coding:utf-8 -*-
# !/usr/bin/env python3


class A(object):
    pass


class B():
    pass


class C:
    pass
>>> A.__bases__
(<class 'object'>,)
>>> B.__bases__
(<class 'object'>,)
>>> C.__bases__
(<class 'object'>,)

class A(object):
    pass

class B(A):
    pass

class C(B):
    pass

class D(B, A):
    pass

交互结果

>>> A.__bases__
(<class 'object'>,)
>>> C.__bases__
(<class '__main__.B'>,)
>>> D.__bases__
(<class '__main__.B'>, <class '__main__.A'>)

在上面的例子中,尽管C是A和B的子类(通过B传递继承关系),但C的父类是B,这从它的声明中可以看出,所以,只有B会在C.__bases__中显示出来。另一方面,D是从两个类A和B中继承而来的。

class E(A, B):
    pass
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

11.2 通过继承覆盖方法

我们在P中再写一个函数,然后在其子类中对它进行覆盖。

class P(object):
    def foo(self):
        print('Hi, Iam P-foo()')
>>> p = P()
>>> p.foo()
Hi, Iam P-foo()

现在来创建子类C,从父类P派生

class C(P):
    def foo(self):
        print('Hi, I am C-foo')
>>> c = C()
>>> c.foo()
Hi, I am C-foo

尽管C继承了P的foo()方法,但因为C定义了它自己的foo()方法,所以P中的foo()(Override)。覆盖方法的原因之一是,你的子类可能需要这个方法具有特定或不同的功能。所以,你接下来的问题肯定是:“我还能否调用那个被我覆盖的基类方法呢?”
答案是肯定的,但是这时就需要你去调用一个未绑定的基类方法,明确给出子类的实例,例如下边:

>>> P.foo(c)
Hi, Iam P-foo()

注意,我们上面已经有了一个P的实例p,但上面的这个例子并没有用它。我们不需要P的实例调用P的方法,因为已经有一个P的子类的实例c可用。典型情况下,你不会以这种方式调用父类方法,你会在子类的重写方法里显示地调用基类方法。

class C(P):
    def foo(self):
        P.foo(self)
        print('Hi, I am C-foo()')

注意,在这个(未绑定)方法调用中我们显示地传递了 self。一个更好的办法是使用 super()内建方法:

class C(P):
    def foo(self):
        super(C, self).foo()
        print('Hi, I am C-foo()')

super()不但能找到基类方法,而且还为我们传进 self,这样我们就不需要做这些事了。现在我们只要调用子类的方法,它会帮你完成一切:

>>> c = C()
>>> c.foo()
Hi, Iam P-foo()
Hi, I am C-foo()

重写__init__不会自动调用基类的__init__
类似于上面的覆盖非特殊方法,当从一个带构造器 __init()__ 的类派生,如果你不去覆盖 __init__(),它将会被继承并自动调用。但如果你在子类中覆盖了 __init__(),子类被实例化时,基类的 __init__() 就不会被自动调用。

class P(object):
    def __init__(self):
        print("Calling Ps constructor")

class C(P):
    def __init__(self):
        print("Calling C's consructor")
class P(object):
    def __init__(self):
        print("Calling Ps constructor")

class C(P):
    def __init__(self):
        print("Calling C's consructor")
>>> c = C()
Calling C's consructor

如果你还想调用基类的 __init__(),你需要像上边我们刚说的那样,明确指出,使用一个子类的实例去调用基类(未绑定)方法。相应地更新类C,会出现线面预期的执行结果:

class C(P):
    def __init__(self):
        P.__init__(self)
        print("Calling C's constructor")
>>> c = C()
Calling P's constructor
Calling C's constructor

上面的例子中,子类的 __init__() 方法首先调用了基类的 __init__() 方法。这是相当普遍(不是强制)的做法,用来设置初始化基类,然后可以执行子类内部的设置。这个负责之所以有意义的原因是,你希望被继承的类的对象在子类构造器运行前能够很好地被初始化或作好准备工作,因为它(子类)可能需要或设置继承属性。
Python使用基类名来调用类方法,对应在Java中,是关键词super来实现,这就是super()内建函数引入到Python中的原因,这样你就可以“依葫芦画瓢”了:

class C(P):
    def __init__(self):
        super(C, self).__init__()
        print("Calling C's constructor")

使用 super() 的漂亮之处在于,你不需要明确给出任何基类名字……“跑腿儿”的事,它帮你干了!使用 super() 的重点,使你不需要明确提供父类。这意味着如果你改变了类继承关系,你只需要改一行代码(class 语句本身)而不必再大量代码中去查找所有被修改的那个类的名字。


11.3 从标准类型派生

经典类中,一个最大的问题是,不能对标准类型进行子类化。幸运的是,在2.2以后的版本中,随着类型(types)和类(class)的统一和新式类的引入,这一点已经被修正。下面,介绍两个子类化 Python 类型的相关例子。其中一个是可变类型,另一个是不可变类型

1. 不可变类型的例子
假定你想在金融应用中,应用一个处理浮点数的子类。每次你得到一个贷币值(浮点数给出的),你都需要通过四舍五入,变为带两位小数位的数值。(当然,Decimal 类比起标准浮点类型来说是个用来精确保存浮点值的更佳方案,但你还是需要[有时候]对其进行舍入操作!)你的类开始可以这样写:

class RoundFloat(float):
    def __new__(cls, val)
        return float.__new__(cls, round(val, 2))

我们覆盖了__new__()特殊方法来定制我们的对象,使之和标准Python 浮点数(float)有一些区别:我们使用round()内建函数对原浮点数进行舍入操作,然后实例化我们的float,RoundFloat。我们是通过调用父类的构造器来创建真实的对象的,float.new()。注意,所有的__new()方法都是类方法,我们要显式传入类传为第一个参数,这类似于常见的方法如__init()中需要的self。

现在的例子还非常简单,比如,我们知道有一个float,我们仅仅是从一种类型中派生而来等等.通常情况下,最好是使用super()内建函数去捕获对应的父类以调用它的__new()__方法,下面,对它进行这方面的修改:

class RoundFloat(float):
    def __new__(cls, val):
        return super(RoundFloat, cls).__new__(cls, round(val, 2))

这个例子还远不够完整,所以,请留意本章我们将使它有更好的表现。下面是一些样例输出:

>>> RoundFloat(1.5955)
1.6
>>> RoundFloat(1.5945)
1.59
>>> RoundFloat(-1.9955)
-2.0

2. 可变类型的例子
子类化一个可变类型与此类似,你可能不需要使用__new__() (或甚至__init__()),因为通常设置不多。一般情况下,你所继承到的类型的默认行为就是你想要的。下例中,我们简单地创建一个新的字典类型,它的keys()方法会自动排序结果:

class SortedKeyDict(dict):
    def keys(self):
        return sorted(super(SortedKeyDict, self).keys())

回忆一下,字典(dictionary)可以由dict(),dict(mapping),dict(sequence_of_2_tuples),或者dict(**kwargs)来创建,看看下面使用新类的例子:

d = SortedKeyDict((('zheng-cai', 67), ('hui-jun', 68),('xin-yi', 2)))
print 'By iterator:'.ljust(12), [key for key in d]
print 'By keys():'.ljust(12), d.keys()

把上面的代码全部加到一个脚本中,然后运行,可以得到下面的输出:

By iterator: ['zheng-cai', 'xin-yi', 'hui-jun']
By keys(): ['xin-yi', 'hui-jun', 'zheng-cai']

在上例中,通过keys 迭代过程是以散列顺序的形式,而使用我们(重写的)keys()方法则将keys 变为字母排序方式了。

一定要谨慎,而且要意识到你正在干什么。如果你说,“你的方法调用super()过于复杂”,取而代之的是,你更喜欢keys()简简单单(也容易理解)…,像这样:

def keys(self):
return sorted(self.keys())

11.4 多重继承

同C++一样,Python 允许子类继承多个基类。这种特性就是通常所说的多重继承。概念容易,但最难的工作是,如何正确找到没有在当前(子)类定义的属性。当使用多重继承时,有两个不同的方面要记住。首先,还是要找到合适的属性。另一个就是当你重写方法时,如何调用对应父类方法以“发挥他们的作用”,同时,在子类中处理好自己的义务。我们将讨论两个方面,但侧重后者,讨论方法解析顺序。
方法解释顺序(MRO)
在Python 2.2 以前的版本中,算法非常简单:深度优先,从左至右进行搜索,取得在子类中使用的属性。其它Python 算法只是覆盖被找到的名字,多重继承则取找到的第一个名字。

由于类,类型和内建类型的子类,都经过全新改造, 有了新的结构,这种算法不再可行. 这样一种新的MRO 算法被开发出来,在2.2 版本中初次登场,是一个好的尝试,但有一个缺陷(看下面的核心笔记)。这在2.3 版本中立即被修改,也就是今天还在使用的版本。

精确顺序解释很复杂,超出了本文的范畴,但你可以去阅读本节后面的参考书目提到的有关内
容。这里提一下,新的查询方法是采用广度优先,而不是深度优先。

核心笔记:
Python 2.2 使用一种唯一但不完善的MROPython 2.2 是首个使用新式MRO 的版本,它必须取代经典类中的算法,原因在上面已谈到过。在2.2 版本中,算法基本思想是根据每个祖先类的继承结构,编译出一张列表,包括搜索到的类,按策略删除重复的。然而,在Python 核心开发人员邮件列表中,有人指出,在维护单调性方面失败过(顺序保存),必须使用新的C3 算法替换,也就是从2.3 版开始使用的新算法。

下面的示例,展示经典类和新式类中,方法解释顺序有什么不同。

简单属性查找示例

下面这个例子将对两种类的方案不同处做一展示。脚本由一组父类,一组子类,还有一个子孙类组成。

class P1: #(object):                # parent class 1 父类1
    def foo(self):
        print 'called P1-foo()'
        
class P2: #(object):                # parent class 2 父类2
    def foo(self):
        print 'called P2-foo()'
    def bar(self):
        print 'called P2-bar()'

class C1(P1, P2):                   # child 1 der. from P1, P2 #子类1,从P1,P2 派生
    pass

class C2(P1, P2):                   # child 2 der. from P1, P2 #子类2,从P1,P2 派生
    def bar(self):
        print 'called C2-bar()'

class GC(C1, C2):                  # define grandchild class #定义子孙类
    pass                           # derived from C1 and C2 #从C1,C2 派生

Alt
图13-2 父类,子类及子孙类的关系图,还有它们各自定义的方法

在图13-2 中,我们看到父类,子类及子孙类的关系。P1 中定义了foo(),P2 定义了foo()和bar(),C2 定义了bar()。下面举例说明一下经典类和新式类的行为。

经典类
首先来使用经典类。通过在交互式解释器中执行上面的声明,我们可以验证经典类使用的解释顺序,深度优先,从左至右:

>>> gc = GC()
>>> gc.foo() # GC ==> C1 ==> P1
called P1-foo()
>>> gc.bar() # GC ==> C1 ==> P1 ==> P2
called P2-bar()

当调用foo()时,它首先在当前类(GC)中查找。如果没找到,就向上查找最亲的父类,C1。查找未遂,就继续沿树上访到父类P1,foo()被找到。

同样,对bar()来说,它通过搜索GC,C1,P1 然后在P2 中找到。因为使用这种解释顺序的缘故,C2.bar()根本就不会被搜索了。
现在,你可能在想,“我更愿意调用C2 的bar()方法,因为它在继承树上和我更亲近些,这样才会更合适。”在这种情况下,你当然还可以使用它,但你必须调用它的合法的全名,采用典型的非绑定方式去调用,并且提供一个合法的实例:

>>> C2.bar(gc)
called C2-bar()

新式类
取消类P1 和类P2 声明中的对(object)的注释,重新执行一下。新式方法的查询有一些不同:

>>> gc = GC()
>>> gc.foo() # GC ==> C1 ==> C2 ==> P1
called P1-foo()
>>> gc.bar() # GC ==> C1 ==> C2
called C2-bar()

与沿着继承树一步一步上溯不同,它首先查找同胞兄弟,采用一种广度优先的方式。当查找foo(),它检查GC,然后是C1 和C2,然后在P1 中找到。如果P1 中没有,查找将会到达P2。foo()的底线是,包括经典类和新式类都会在P1 中找到它,然而它们虽然是同归,但殊途!

然而,bar()的结果是不同的。它搜索GC 和C1,紧接着在C2 中找到了。这样,就不会再继续搜索到祖父P1 和P2。这种情况下,新的解释方式更适合那种要求查找GC 更亲近的bar()的方案。当然,如果你还需要调用上一级,只要按前述方法,使用非绑定的方式去做,即可。

>>> P2.bar(gc)
called P2-bar()

新式类也有一个__mro__属性,告诉你查找顺序是怎样的:

>>> GC.__mro__
(<class '__main__.GC'>, <class '__main__.C1'>, <class
'__main__.C2'>, <class '__main__.P1'>, <class
'__main__.P2'>, <type 'object'>)

菱形效应为难MRO
经典类方法解释不会带来很多问题。它很容易解释,并理解。大部分类都是单继承的,多重继承只限用在对两个完全不相关的类进行联合。这就是术语mixin 类(或者“mix-ins”)的由来。

为什么经典类MRO 会失败
在版本2.2 中,类型与类的统一,带来了一个新的“问题”,波及所有从object(所有类型的祖先类)派生出来的(根)类,一个简单的继承结构变成了一个菱形。从Guido van Rossum 的文章中得到下面的灵感,打个比方,你有经典类B 和C,C 覆盖了构造器,B 没有,D 从B 和C 继承而来:

class B:
    pass
    
class C:
    def __init__(self):
        print "the default constructor"

class D(B, C):
    pass

当我们实例化D,得到:

>>> d = D()
the default constructor

图13.3 为B,C 和D 的类继承结构,现在把代码改为采用新式类的方式,问题也就产生了:

class B(object):
    pass
class C(object):
    def __init__(self):
        print "the default constructor"

在这里插入图片描述
图13.3 继承的问题是由于在新式类中,需要出现基类,这样就在继承结构中,形成了一个菱形。D 的实例上溯时,不应当错过C,但不能两次上溯到A(因为B 和C 都从A 派生)。去读读Guidovan Rossum 的文章中有关"协作方法"的部分,可以得到更深地理解。

代码中仅仅是在两个类声明中加入了(object),对吗?没错,但从图中,你可以看出,继承结构已变成了一个菱形;真正的问题就存在于MRO 了。如果我们使用经典类的MRO,当实例化D 时,不再得到C.__init__()之结果…而是得到object.__init__()!这就是为什么MRO 需要修改的真正原因。

尽管我们看到了,在上面的例子中,类GC 的属性查找路径被改变了,但你不需要担心会有大量的代码崩溃。经典类将沿用老式MRO,而新式类将使用它自己的MRO。还有,如果你不需要用到新式类中的所有特性,可以继续使用经典类进行开发,不会有问题的。

总结
经典类,使用深度优先算法。因为新式类继承自object,新的菱形类继承结构出现,问题也就接着而来了,所以必须新建一个MRO。

你可以在下面的链接中读在更多有关新式类、MRO 的文章:

Guido van Rossum 的有关类型和类统一的文章:
http://www.python.org/download/releases/2.2.3/descrintro
PEP 252:使类型看起来更像类
http://www.python.org/doc/peps/pep-0252
“Python 2.2 新亮点” 文档
http://www.python.org/doc/2.2.3/whatsnew
论文:Python 2.3 方法解释顺序
http://python.org/download/releases/2.3/mro/

12. 类、实例和其他对象的内建函数

12.1 issubclass()

issubclass() 布尔函数判断一个类是另一个类的子类或子孙类。它有如下语法:

issubclass(sub, sup)

issubclass() 返回True 的情况:给出的子类sub 确实是父类sup 的一个子类(反之,则为False)。这个函数也允许“不严格”的子类,意味着,一个类可视为其自身的子类,所以,这个函数如果当sub 就是sup,或者从sup 派生而来,则返回True。(一个“严格的”子类是严格意义上的从一个类派生而来的子类。)

从Python 2.3 开始,issubclass()的第二个参数可以是可能的父类组成的tuple(元组),这时,只要第一个参数是给定元组中任何一个候选类的子类时,就会返回True。

12.2 isinstance()

isinstance() 布尔函数在判定一个对象是否是另一个给定类的实例时,非常有用。它有如下语法:

isinstance(obj1, obj2)

isinstance()在obj1 是类obj2 的一个实例,或者是obj2 的子类的一个实例时,返回True(反之,则为False),看下面的例子:

>>> class C1(object):pass
... 
>>> class C2(object):pass
... 
>>> c1 = C1()
>>> c2 = C2()
>>> isinstance(c1, C1)
True
>>> isinstance(c2, C1)
False
>>> isinstance(c1, C2)
False
>>> isinstance(c2, C2)
True
>>> isinstance(C2, c2)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and types

注意:第二个参数应当是类;不然,你会得到一个TypeError。但如果第二个参数是一个类型对象,则不会出现异常。这是允许的,因为你也可以使用isinstance()来检查一个对象obj1 是否是obj2 的类型,比如:

>>> isinstance(4, int)
True
>>> isinstance(4, str)
False
>>> isinstance('4', str)
True

如果你对Java 有一定的了解,那么你可能知道Java 中有个等价函数叫instanceof(),但由于性能上的原因,instanceof()并不被推荐使用。调用Python 的isinstance()不会有性能上的问题,主要是因为它只用来来快速搜索类族集成结构,以确定调用者是哪个类的实例,还有更重要的是,它是用C 写的!

同issubclass()一样,isinstance()也可以使用一个元组(tuple)作为第二个参数。这个特性是从Python 2.2 版本中引进的。如果第一个参数是第二个参数中给定元组的任何一个候选类型或类的实例时,就会返回True。你还可以在595 页,第13.16.1 节中了解到更多有isinstance()的内容。

12.3 hasattr(), getattr(),setattr(), delattr()

*attr()系列函数可以在各种对象下工作,不限于类(class)和实例(instances)。然而,因为在类和实例中使用极其频繁,就在这里列出来了。需要说明的是,当使用这些函数时,你传入你正在处理的对象作为第一个参数,但属性名,也就是这些函数的第二个参数,是这些属性的字符串名字。换句话说,在操作obj.attr 时,就相当于调用*attr(obj,‘attr’…)系列函数------下面的例子讲得很清楚。

hasattr()函数是Boolean 型的,它的目的就是为了决定一个对象是否有一个特定的属性,一般用于访问某属性前先作一下检查。getattr()和setattr()函数相应地取得和赋值给对象的属性,getattr()会在你试图读取一个不存在的属性时,引发AttributeError 异常,除非给出那个可选的默认参数。setattr()将要么加入一个新的属性,要么取代一个已存在的属性。而delattr()函数会从一个对象中删除属性。

下面一些例子使用到了*attr()系列函数:

>>> class myClass(object):
...     def __init__(self):
...         self.foo = 100
...         
>>> myInst = myClass()
>>> hasattr(myInst, 'foo')
True
>>> getattr(myInst, 'foo')
100
>>> hasattr(myInst, 'bar')
False
>>> getattr(myInst, 'bar')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'myClass' object has no attribute 'bar'
>>> getattr(c, 'bar', 'oops!')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'c' is not defined
>>> getattr(myInst, 'bar', 'oops!')
'oops!'
>>> setattr(myInst, 'bar', 'my attr')
>>> dir(myInst)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']
>>> getattr(myInst, 'bar')        # same as myInst.bar
'my attr'
>>> delattr(myInst, 'foo')
>>> dir(myInst)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar']
>>> hasattr(myInst, 'foo')
False

12.4 dir()

我们用dir()列出一个模块所有属性的信息。现在你应该知道dir()还可以用在对象上。

在Python 2.2 中, dir()得到了重要的更新。因为这些改变,那些\ __members__和__methods__数据属性已经被宣告即将不支持。dir()提供的信息比以前更加详尽。根据文档,“除了实例变量名和常用方法外,它还显示那些通过特殊标记来调用的方法,像__iadd__(+=),__len__(len()),__ne__(!=)。” 在Python 文档中有详细说明。

  • dir()作用在实例上(经典类或新式类)时,显示实例变量,还有在实例所在的类及所有它的基类中定义的方法和类属性。
  • dir()作用在类上(经典类或新式类)时,则显示类以及它的所有基类的__dict__中的内容。但它不会显示定义在元类(metaclass)中的类属性。
  • dir()作用在模块上时,则显示模块的__dict__的内容。(这没改动)。
  • dir()不带参数时,则显示调用者的局部变量。(也没改动)。
  • 关于更多细节:对于那些覆盖了__dict__或__class__属性的对象,就使用它们;出于向后兼容的考虑,如果已定义了__members__和__methods__,则使用它们。

12.5 super()

super()函数在Python2.2 版本新式类中引入。这个函数的目的就是帮助程序员找出相应的父类,然后方便调用相关的属性。一般情况下,程序员可能仅仅采用非绑定方式调用祖先类方法。使用super()可以简化搜索一个合适祖先的任务,并且在调用它时,替你传入实例或类型对象。

在第13.11.4 节中,我们描述了文档解释顺序(MRO),用于在祖先类中查找属性。对于每个定义的类,都有一个名为__mro__的属性,它是一个元组,按照他们被搜索时的顺序,列出了备搜索的类。语法如下:

super(type[, obj])

给出type,super()“返回此type 的父类”。如果你希望父类被绑定,你可以传入obj 参数(obj必须是type 类型的).否则父类不会被绑定。obj 参数也可以是一个类型,但它应当是type 的一个子类。通常,当给出obj 时:

  • 如果 obj 是一个实例,isinstance(obj,type)就必须返回True
  • 如果 obj 是一个类或类型,issubclass(obj,type)就必须返回True

事实上,super()是一个工厂函数,它创造了一个super object,为一个给定的类使用__mro__去查找相应的父类。很明显,它从当前所找到的类开始搜索MRO。更多详情,请再看一下Guido vanRossum 有关统一类型和类的文章,他甚至给出了一个super()的纯Python 实现,这样,你可以加深其印象,知道它是如何工作的!

最后想到… super() 的主要用途, 是来查找父类的属性, 比如,super(MyClass,self).__init__()。如果你没有执行这样的查找,你可能不需要使用super()。

有很多如何使用super()的例子分散在本章中。记得阅读一下第13.11.2 节中有关super()的重要提示,尤其是那节中的核心笔记。

12.6 vars()

vars()内建函数与dir()相似,只是给定的对象参数都必须有一个__dict__属性。vars()返回一个字典,它包含了对象存储于其__dict__中的属性(键)及值。如果提供的对象没有这样一个属性,则会引发一个TypeError 异常。如果没有提供对象作为vars()的一个参数,它将显示一个包含本地名字空间的属性(键)及其值的字典,也就是,locals()。我们来看一下例子,使用类实例调用vars():

>>> class C(object):
...     pass
... 
>>> c = C()
>>> c.foo = 100
>>> c.bar = 'Python'
>>> c.__dict__
{'foo': 100, 'bar': 'Python'}
>>> vars(c)
{'foo': 100, 'bar': 'Python'}

表13.3 概括了类和类实例的内建函数。

表13.3 类,实例及其它对象的内建函数

内建函数描述
issubclass(sub, sup)如果类sub 是类sup 的子类,则返回True,反之,为False。
isinstance(obj1, obj2)如果实例obj1 是类obj2 或者obj2 子类的一个实例;或者如果obj1是obj2 的类型,则返回True;反之,为False。
hasattr(obj, attr)如果obj 有属性attr(用字符串给出),返回True,反之,返回表13.3 类,实例及其它对象的内建函数
getattr(obj, attr[, default])获取obj 的attr 属性;与返回obj.attr 类似;如果attr不是obj 的属性,如果提供了默认值,则返回默认值;不然,就会引发一个AttributeError 异常。
setattr(obj, attr, val)设置obj 的attr 属性值为val,替换任何已存在的属性值;不然,就创建属性;类似于obj.attr=val
delattr(obj, attr)从obj 中删除属性attr(以字符串给出);类似于delobj.attr。
dir(obj=None)返回obj 的属性的一个列表;如果没有给定obj,dir()则显示局部名字空间空间中的属性,也就是locals().keys()
super(type, obj=None) a返回一个表示父类类型的代理对象;如果没有传入obj,则返 回的super 对象是非绑定的;反之,如果obj 是一个type , issubclass(obj,type) 必为True ; 否则,isinstance(obj,type)就必为True。
vars(obj=None)返回obj 的属性及其值的一个字典;如果没有给出obj,vars()显示局部名字空间字典(属性及其值),也就是locals()。

a. Python2.2 中新增;仅对新式类有效

13. 用特殊方法定制类

  我们已在本章前面部分讲解了方法的两个重要方面:首先,方法必须在调用前被绑定(到它们相应类的某个实例中);其次,有两个特殊方法可以分别作为构造器和解构器的功能,分别名为__init__()__del__()

  事实上,__init__()__del__()只是可自定义特殊方法集中的一部分。它们中的一些有预定义的默认行为,而其它一些则没有,留到需要的时候去实现。这些特殊方法是Python 中用来扩充类的强有力的方式。它们可以实现:

  • 模拟标准类型
  • 重载操作符

  特殊方法允许类通过重载标准操作符+,*, 甚至包括分段下标及映射操作操作[] 来模拟标准类型。如同其它很多保留标识符,这些方法都是以双下划线(__)开始及结尾的。表13.4 列出了所有特殊方法及其它的描述。

表13.4 用来定制类的特殊方法

特殊方法描述
基本定制型
C.__init__(self[, arg1, ...])构造器(带一些可选的参数)
C.__new__(self[, arg1, ...])a构造器(带一些可选的参数);通常用在设置不变数据类 型的子类。
C.__del__(self)解构器
C.__str__(self)可打印的字符输出;内建str()及print 语句
C.__repr__(self)运行时的字符串输出;内建repr() 和‘‘ 操作符
C.__unicode__(self)bUnicode 字符串输出;内建unicode()
C.__call__(self, *args)表示可调用的实例
C.__nonzero__(self)为object 定义False 值;内建bool() (从2.2 版开始)
C.__len__(self)“长度”(可用于类);内建len()
对象(值)比较c
C.__cmp__(self, obj)对象比较;内建cmp()
C.__lt__(self, obj) and小于/小于或等于;对应<及<=操作符
C.__gt__(self, obj) and大于/大于或等于;对应>及>=操作符
C.__eq__(self, obj) and等于/不等于;对应==,!=及<>操作符
属性
C.__getattr__(self, attr)获取属性;内建getattr();仅当属性没有找到时调用
C.__setattr__(self, attr, val)设置属性
C.__delattr__(self, attr)删除属性
C.__getattribute__(self, attr) a获取属性;内建getattr();总是被调用
C.__get__(self, attr) a(描述符)获取属性
C.__set__(self, attr, val) a(描述符)设置属性
C.__delete__(self, attr) a(描述符)删除属性
定制类/模拟类型
数值类型:二进制操作符
C.__*add__(self, obj)加;+操作符
C.__*sub__(self, obj)减;-操作符
C.__*mul__(self, obj)乘;*操作符
C.__*div__(self, obj)除;/操作符
C.__*truediv__(self, obj) eTrue 除;/操作符
C.__*floordiv__(self, obj) eFloor 除;//操作符
C.__*mod__(self, obj)取模/取余;%操作符
C.__*divmod__(self, obj)除和取模;内建divmod()
C.__*pow__(self, obj[, mod])乘幂;内建pow();**操作符
C.__*lshift__(self, obj)左移位;<<操作符
C.__*rshift__(self, obj)右移;>>操作符
C.__*and__(self, obj)按位与;&操作符
C.__*or__(self, obj)按位或;|操作符
C.__*xor__(self, obj)按位与或;^操作符
数值类型:一元操作符
C.__neg__(self)一元负
C.__pos__(self)一元正
C.__abs__(self)绝对值;内建abs()
C.__invert__(self)按位求反;~操作符
数值类型:数值转换
C.__complex__(self, com)转为complex(复数);内建complex()
C.__int__(self)转为int;内建int()
C.__long__(self)转为long;内建long()
C.__float__(self)转为float;内建float()
数值类型:基本表示法(String)
C.__oct__(self)八进制表示;内建oct()
C.__hex__(self)十六进制表示;内建hex()
数值类型:数值压缩
C.__coerce__(self, num)压缩成同样的数值类型;内建coerce()
C.__index__(self)g在有必要时,压缩可选的数值类型为整型(比如:用于切片 索引等等)
序列类型
C.__len__(self)序列中项的数目
C.__getitem__(self, ind)得到单个序列元素
C.__setitem__(self, ind,val)设置单个序列元素
C.__delitem__(self, ind)删除单个序列元素
C.__getslice__(self, ind1,ind2)得到序列片断
C.__setslice__(self, i1, i2,val)设置序列片断
C.__delslice__(self, ind1,ind2)删除序列片断
C.__contains__(self, val) f测试序列成员;内建in 关键字
C.__*add__(self,obj)串连;+操作符
C.__*mul__(self,obj)重复;*操作符
C.__iter__(self) e创建迭代类;内建iter()
映射类型
C.__len__(self)mapping 中的项的数目
C.__hash__(self)散列(hash)函数值
C.__getitem__(self,key)得到给定键(key)的值
C.__setitem__(self,key,val)设置给定键(key)的值
C.__delitem__(self,key)删除给定键(key)的值
C.__missing__(self,key)给定键如果不存在字典中,则提供一个默认值
  • a. Python 2.2 中新引入;仅用于新式类中。
  • b. Python 2.3 中新引入。
  • c. 除了cmp()外,其余全是在Python 新引入的。
  • d. “*” 代表’’(self OP obj), ‘r’(obj OP
    self),或’i’(原位(in-place)操作, Py2.0 新增), 例如 __add__, __radd__, or
    __iadd__.
  • e. Python 2.2 中新引入。
  • f. “*” either nothing (self OP obj),
    “r” (obj OP self ), or “i” for in-place operation (new in Python
    1.6), i.e., __add__, __radd__, or __add__.
  • g. Python 2.5 中新引入。

基本的定制和对象(值)比较特殊方法在大多数类中都可以被实现,且没有同任何特定的类型模型绑定。延后设置,也就是所谓的Rich 比较,在Python2.1 中加入。属性组帮助管理您的类的实例属性。这同样独立于模型。还有一个,__getattribute__(),它仅用在新式类中,我们将在后面的章节中对它进行描述。

特殊方法中数值类型部分可以用来模拟很多数值操作,包括那些标准(一元和二进制)操作符,类型转换,基本表示法,及压缩。也还有用来模拟序列和映射类型的特殊方法。实现这些类型的特殊方法将会重载操作符,以使它们可以处理你的类类型的实例。

另外,除操作符__*truediv__()__*floordiv__()在Python2.2 中加入,用来支持Python 除操作符中待定的更改—可查看5.5.3 节。基本上,如果解释器启用新的除法,不管是通过一个开关来启动Python,还是通过"from __future__ import division",单斜线除操作(/)表示的将是ture除法,意思是它将总是返回一个浮点值,不管操作数是否为浮点数或者整数(复数除法保持不变)。双斜线除操作(//)将提供大家熟悉的浮点除法,从标准编译型语言像C/C++及Java 过来的工程师一定对此非常熟悉。同样,这些方法只能处理实现了这些方法并且启用了新的除操作的类的那些符号。

表格中,在它们的名字中,用星号通配符标注的数值二进制操作符则表示这些方法有多个版本,在名字上有些许不同。星号可代表在字符串中没有额外的字符,或者一个简单的“r”指明是一个右结合操作。没有“r”,操作则发生在对于self OP obj 的格式; “r”的出现表明格式obj OP self。比如,__add__(self,obj)是针对self+obj 的调用,而__radd__(self,obj)则针对obj+self 来调用。

增量赋值,起于Python 2.0,介绍了“原位”操作符。一个“i”代替星号的位置,表示左结合操作与赋值的结合,相当是在self=self OP obj。举例,__iadd__(self,obj)相当于self=self+obj的调用。

随着Python 2.2 中新式类的引入,有一些更多的方法增加了重载功能。然而,在本章开始部分提到过,我们仅关注经典类和新式类都适应的核心部分,本章的后续部分,我们介绍新式类的高级特性。

13.1 简单定制 (RoundFloat2)

我们的第一个例子很普通。在某种程度上,它基于我们前面所看到的从Python 类型中派生出的派生类RoundFloat。这个例子很简单。事实上,我们甚至不想去派生任何东西(当然,除 object 外)…我们也不想采用与 floats 有关的所有“好东西”。不,这次,我们想创建一个苗条的例子,这样你可以对类定制的工作方式有一个更好的理解。这种类的前提与其它类是一样的:我们只要一个类来保存浮点数,四舍五入,保留两位小数位。

class RoundFloatManual(object):
    def __init__(self, val):
        assert isinstance(val, float), "Value must be a float!"
        self.value = round(val, 2)

这个类仅接收一个浮点值----它断言了传递给构造器的参数类型必须为一个浮点数----并且将其保存为实例属性值。让我们来试试,创建这个类的一个实例:

>>> rfm = RoundFloatManual(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
AssertionError: Value must be a float!
>>> rfm = RoundFloatManual(4.2)
>>> rfm
<__main__.RoundFloatManual object at 0x0000023945236D90>
>>> print(rfm)
<__main__.RoundFloatManual object at 0x0000023945236D90>

你已看到,它因输入非法,而“噎住”,但如果输入正确时,就没有任何输出了。可是,当把这个对象转存在交互式解释器中时,看一下发生了什么。我们得到一些信息,却不是我们要找的。(我们想看到数值,对吧?)调用 print 语句同样没有明显的帮助。

不幸的是,print(使用str())和真正的字符串对象表示(使用repr())都没能显示更多有关我们对象的信息。一个好的办法是,去实现__str__()__repr__()二者之一,或者两者都实现,这样我们就能“看到”我们的对象是个什么样子了。换句话说,当你想显示你的对象,实际上是想看到有意义的东西,而不仅仅是通常的Python 对象字符串(K)。让我们来添加一个__str()__方法,以覆盖默认的行为:

class RoundFloatManual(object):
    def __init__(self, val):
        assert isinstance(val, float), "Value must be a float!"
        self.value = round(val, 2)

    def __str__(self):
        return str(self.value)

现在我们得到下面的:

>>> rfm = RoundFloatManual(5.590464)
>>> rfm
<__main__.RoundFloatManual object at 0x0000023944DDEFD0>
>>> print(rfm)
5.59
>>> rfm = RoundFloatManual(5.5964)
>>> rfm
<__main__.RoundFloatManual object at 0x0000023945236D90>
>>> print(rfm)
5.6

我们还有一些问题…一个问题是仅仅在解释器中转储(dump)对象时,仍然显示的是默认对象符号,但这样做也算不错。如果我们想修复它,只需要覆盖__repr__()。因为字符串表示法也是Python对象,我们可以让__repr__()__str__()的输出一致。

为了完成这些,只要把__str__()的代码复制给__repr__()。这是一个简单的例子,所以它没有真正对我们造成负面影响,但作为程序员,你知道那不是一个最好的办法。如果__str__()中存在bug,那么我们会将bug 也复制给__repr__()了。

最好的方案,在__str__()中的代码也是一个对象,同所有对象一样,引用可以指向它们,所以,我们可以仅仅让__repr__()作为__str__()的一个别名:

class RoundFloatManual(object):
    def __init__(self, val):
        assert isinstance(val, float), "Value must be a float!"
        self.value = round(val, 2)

    def __str__(self):
        return str(self.value)

    __repr__ = __str__

在带参数5.5964 的第二个例子中,我们看到它舍入值刚好为5.6,但我们还是想显示带两位小数的数。来玩玩一个更好的妙计吧,看下面:

>>> rfm = RoundFloatManual(5.5964)
>>> rfm
5.60
>>> print(rfm)
5.60

例13.2 基本定制(roundFloat2.py)

#!/usr/bin/env python

class RoundFloatManual(object):

    def __init__(self, val):
        assert isinstance(val, float), "Value must be a float!"
        self.value = round(val, 2)

    def __str__(self):
        return '%.2f' % self.value
    
    __repr__ = __str__

在本章开始部分,最初的RoundFloat 例子,我们没有担心所有细致对象的显示问题;原因是__str__()__repr__()作为 float 类的一部分已经为我们定义好了。我们所要做的就是去继承它们。增强版本“手册”中需要另外的工作。你发现派生是多么的有益了吗?我们甚至不需要知道解释器在继承树上要执行多少步才能找到一个已声明的你正在使用却没有考虑过的方法。我们将在例13.2中列出这个类的全部代码。

现在开始一个稍复杂的例子。

13.2 数值定制 (Time60)

作为第一个实际的例子,我们可以想象需要创建一个简单的应用,用来操作时间,精确到小时和分。我们将要创建的这个类可用来跟踪职员工作时间,ISP 用户在线时间,数据库总的运行时间(不包括备份及升级时的停机时间),在扑克比赛中玩家总时间,等等。

在Time60 类中,我们将整数的小时和分钟作为输入传给构造器。

class Time60(object):
    def __init__(self, hr, min):
        self.hr = hr
        self.min = min

显示

同样,如前面的例子所示,在显示我们的实例的时候,我们需要一个有意义的输出,那么就要覆盖__str__()(如果有必要的话,__repr__()也要覆盖)。我们都习惯看小时和分,用冒号分隔开的格式,比如,“4:30”,表示四个小时,加半个小时(4 个小时及30 分钟):

    def __str__(self):
        return '%d:%d' % (self.hr, self.min)

用此类,可以实例化一些对象。在下面的例子中,我们启动一个工时表来跟踪对应构造器的计费小时数:

>>> mon = Time60(10, 30)
>>> tue = Time60(11, 15)
>>> print(mon, tue)
10:30 11:15

输出不错,正是我们想看到的。下一步干什么呢?可考虑与我们的对象进行交互。比如在时间片的应用中,有必要把Time60 的实例放到一起让我们的对象执行所有有意义的操作。我们更喜欢像这样的:

>>> mon + tue
21:45

加法

Python 的重载操作符很简单。像加号(+),我们只需要重载__add__()方法,如果合适,还可以用__radd__()__iadd__()。稍后有更多有关这方面的描述。实现__add__()听起来不难----只要把分和小时加在一块。大多数复杂性源于我们怎么处理这个新的总数。如果我们想看到“21:45”,就必须认识到这是另一个 Time60 对象,我们没有修改 mon 或 tue,所以,我们的方法就应当创建另一个对象并填入计算出来的总数。

实现__add__()特殊方法时,首先计算出个别的总数,然后调用类构造器返回一个新的对象:

    def __add__(self, other):
        return self.__class__(self.hr + other.hr, self.min + other.min)

和正常情况下一样,新的对象通过调用类来创建。唯一的不同点在于,在类中,你一般不直接调用类名, 而是使用self__class__属性,即实例化self 的那个类,并调用它。由于self.__class__与Time60 相同,所以调用self.__class__()与调用Time60()是一回事。

不管怎样,这是一个更面向对象的方式。另一个原因是,如果我们在创建一个新对象时,处处使用真实的类名,然后,决定将其改为别的名字,这时,我们就不得不非常小心地执行全局搜索并替换。如果靠使用self.__class__,就不需要做任何事情,只需要直接改为你想要的类名。

好了,我们现在来使用加号重载,“增加”Time60 对象:

>>> mon = Time60(10, 30)
>>> tue = Time60(11, 15)
>>> mon + tue
<__main__.Time60 object at 0x0000022C791FF1C0>
>>> print(mon + tue)
21:45

哎哟,我们忘记添加一个别名__repr____str__了,这很容易修复。你可能会问,“当我们试着在重载情况下使用一个操作符,却没有定义相对应的特殊方法时还有很多需要优化和重要改良的地方,会发生什么事呢?” 答案是一个TypeError 异常:

>>> mon - tue
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'Time60' and 'Time60'

原位加法

有了增量赋值(在Python 2.0 中引入),我们也许还有希望覆盖“原位”操作,比如,__iadd__()。这是用来支持像mon += tue 这样的操作符,并把正确的结果赋给mon。重载一个__i*__()方法的唯一秘密是它必须返回self。把下面的片断加到我们例子中,以修复上面的repr()问题,并支持增量赋值:

    __repr__ = __str__

    def __iadd__(self, other):
        self.hr += other.hr
        self.min += other.min
        return self

下面是结果输出:

>>> mon =Time60(10,30)
>>> tue =Time60(11,15)
>>> mon
10:30
>>> id(mon)
1330866355456
>>> mon += tue
>>> id(mon)
1330866355456
>>> mon
21:45

注意,使用id()内建函数是用来确定一下,在原位加的前后,我们确实是修改了原来的对象,而没有创建一个新的对象。对一个具有巨大潜能的类来说,这是很好的开始。在例13.3 中给出了Time60 的类的完全定义。

例13.3 中级定制(time60.py)

#!/usr/bin/env python

class Time60(object):
    """Time60 - track hours and minutes"""
    def __init__(self, hr, min):
        self.hr = hr
        self.min = min

    def __str__(self):
        return '%d:%d' % (self.hr, self.min)

    def __add__(self, other):
        return self.__class__(self.hr + other.hr, self.min + other.min)

    __repr__ = __str__

    def __iadd__(self, other):
        self.hr += other.hr
        self.min += other.min
        return self

升华

现在暂不管它了,但在这个类中,还有很多需要优化和改良的地方。比如,如果我们不传入两个分离的参数,而传入一个2 值元组给构造器作为参数,是不是更好些呢?如果是像“10:30”这样的字符串的话,结果会怎样?

答案是肯定的,你可以这样做,在Python 中很容易做到,但不是像很多其他面向对象语言一样通过重载构造器来实现.Python 不允许用多个签名重载可调用对象.所以实现这个功能的唯一的方式是使用单一的构造器,并由isinstance()和(可能的)type()内建函数执行自省功能。

能支持多种形式的输入,能够执行其它操作像减法等,可以让我们的应用更健壮,灵活。当然这些是可选的,就像“蛋糕上的冰”,但我们首先应该担心的是两个中等程度的缺点:1.当比十分钟还少时,格式并不是我们所希望的,2. 不支持60 进制(基数60)的操作:

>>> wed = Time60(12, 5)
>>> wed
12:5
>>> thu = Time60(10, 30)
>>> fri = Time60(8, 45)
>>> thu + fri
18:75

显示wed 结果是“12:05”,把thu 和fri 加起来结果会是“19:15”。修改这些缺陷,实现上面的改进建议可以实际性地提高你编写定制类技能。

13.3 迭代器 (RandSeq 和 AnyIter)

例13.4 随机序列迭代器(randseq.py)

#!/usr/bin/env python

from random import choice


class RandSeq(object):
    def __init__(self, seq):
        self.data = seq

    def __iter__(self):
        return self

    def __next__(self):
        return choice(self.data)

RandSeq

迭代器可以一次一个的遍历序列(或者是类似序列对象)中的项,它可以利用一个类中的__iter__()next()方法,来创建一个迭代器。我们在此展示两个例子。

第一个例子是RandSeq(RANDom SEQuence 的缩写)。我们给我们的类传入一个初始序列,然后让用户通过next()去迭代(无穷)。

__init__()方法执行前述的赋值操作。__iter__()仅返回self,这就是如何将一个对象声明为迭代器的方式,最后,调用 next() (python3 调用 __next__())来得到迭代器中连续的值。这个迭代器唯一的亮点是它没有终点。

这个例子展示了一些我们可以用定制类迭代器来做的与众不同的事情。一个是无穷迭代。因为我们无损地读取一个序列,所以它是不会越界的。每次用户调用next()时,它会得到下一个迭代值,但我们的对象永远不会引发StopIteration 异常。我们来运行它,将会看到下面的输出:

>>> from randseq import RandSeq
... for eachItem in RandSeq(('rock', 'paper', 'scissors')):
...     print(eachItem)
rock
paper
scissors
rock
rock
    ...

例13.5 任意项的迭代器 (anyIter.py)

#!/usr/bin/env python

class AnyIter(object):
    def __init__(self, data, safe=False):
        self.safe = safe
        self.iter = iter(data)

    def __iter__(self):
        return self

    def __next__(self, howmany=1):
        retval = []
        for eachItem in range(howmany):
            try:
                retval.append(self.iter.__next__())
            except StopIteration:
                if self.safe:
                    break
                else:
                    raise
        return retval

AnyIter

在第二个例子中,我们的确创建了一个迭代器对象,我们传给__next()__方法一个参数,控制返回条目的数目,而不是去一次一个地迭代每个项。下面是我们的代码(ANY number of items ITERator):

和RandSeq 类的代码一样,类AnyIter 很容易领会。我们在上面描述了基本的操作…它同其它
迭代器一样工作,只是用户可以请求一次返回N 个迭代的项,而不仅是一个项。

我们给出一个迭代器和一个安全标识符(safe)来创建这个对象。如果这个标识符(safe)为真(True),我们将在遍历完这个迭代器前,返回所获取的任意条目,但如果这个标识符为假(False),则在用户请求过多条目时,将会引发一个异常。错综复杂的核心在于__next()__,特别是它如何退出的(14-21 行)。

__next()__的最后一部分中,我们创建用于返回的一个列表项,并且调用对象的next()方法来获得每一项条目。如果我们遍历完列表,得到一个 StopIteration 异常,这时则检查安全标识符(safe)。如果不安全(即,self.safe=False),则将异常抛还给调用者(raise);否则, 退出(break)并返回(return)已经保存过的所有项。

>>> a = AnyIter(range(10))
>>> i = iter(a)
>>> for j in range(1,5):
...     print(j, ':', i.__next__(j))
...     
1 : [0]
2 : [1, 2]
3 : [3, 4, 5]
4 : [6, 7, 8, 9]

上面程序的运行没有问题,因为迭代器正好符合项的个数。当情况出现偏差,会发生什么呢?让我们首先试试“不安全(unsafe)”的模式,这也就是紧随其后创建我们的迭代器:

>>> i = iter(a)
>>> i.__next__(14)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "F:/04_Code/oss/oss_master/my_run/learn_core_python/anyiter.py", line 15, in __next__
    retval.append(self.iter.__next__())
StopIteration

因为超出了项的支持量,所以出现了 StopIteration 异常,并且这个异常还被重新引发回调用者(第20 行)。如果我们使用“安全(safe)”模式重建迭代器,再次运行一次同一个例子的话,我们就可以在项失控出现前得到迭代器所得到的元素:

>>> a = AnyIter(range(10), True)
>>> i = iter(a)
>>> i.__next__(14)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

13.4 *多类型定制 (NumStr)

现在创建另一个新类,NumStr,由一个数字-字符对组成,相应地,记为n 和s,数值类型使用整型(integer)。尽管这组顺序对的“合适的”记号是(n,s),但我们选用[n::s]来表示它,有点不同。暂不管记号,这两个数据元素只要我们模型考虑好了,就是一个整体。可以创建我们的新类了,叫做NumStr,有下面的特征:

初始化

类应当对数字和字符串进行初始化;如果其中一个(或两)没有初始化,则使用0 和空字符串,也就是,n=0 且s=’’,作为默认。

加法

我们定义加法操作符,功能是把数字加起来,把字符连在一起;要点部分是字符串要按顺序相连。比如,NumStr1=[n1::s1]NumStr2=[n2::s2]。则NumStr1+NumStr2 表示[n1+n2::s1+s2],其中,代表数字相加及字符相连接。

乘法

类似的, 定义乘法操作符的功能为, 数字相乘, 字符累积相连, 也就是,NumStr1*NumStr2=[n1*n::s1*n]

False 值

当数字的数值为0 且字符串为空时,也就是当NumStr=[0::'']时,这个实体即有一个false 值。

比较

比较一对NumStr 对象,比如,[n1::s1] vs. [n2::s2],我们可以发现九种不同的组合(即,n1>n2 and s1<s2n1==n2 and s1>s2,等等)。对数字和字符串,我们一般按照标准的数值和字典顺序的进行比较,即,如果obj1<obj2,普通比较cmp(obj1,obj2)的返回值是一个小于0 的整数,当obj1>obj2 时,比较的返回值大于0,当两个对象有相同的值时,比较的返回值等于0。

我们的类的解决方案是把这些值相加,然后返回结果。有趣的是cmp()不会总是返回-1,0,或1。上面提到过,它是一个小于,等于或大于0 的整数。

为了能够正确的比较对象,我们需要让__cmp__()在(n1>n2) 且 (s1>s2)时,返回 1,在(n1<n2)且(s1<s2)时,返回-1,而当数值和字符串都一样时,或是两个比较的结果正相反时(即(n1<n2)且(s1>s2),或相反),返回0.反之亦然。

例13.6 多类型类定制 (numstr.py)

根据上面的特征,我们列出numstr.py 的代码,执行一些例子:

>>> a = NumStr(3, 'foo')
>>> b = NumStr(3, 'goo')
>>> c = NumStr(2, 'foo')
>>> d = NumStr()
>>> e = NumStr(string='boo')
>>> f = NumStr(1)
>>> a
[3 :: 'foo']
>>> b
[3 :: 'goo']
>>> c
[2 :: 'foo']
>>> d
[0 :: '']
>>> e
[0 :: 'boo']
>>> f
[1 :: '']
>>> a < b
True
>>> b < c
False
>>> a == a
True
>>> b * 2
[6 :: 'googoo']
>>> a * 3
[9 :: 'foofoofoo']
>>> b + e
[3 :: 'gooboo']
>>> e + b
[3 :: 'boogoo']
>>> bool(d)
False
>>> bool(e)
True
>>> cmp(a,b)
-1
>>> cmp(a,c)
1
>>> cmp(a,a)
0

逐行解释

第1-7 行
脚本的开始部分为构造器__init__(),通过调用NumStr()时传入的值来设置实例,完成自身初始化。如果有参数缺失,属性则使用false 值,即默认的 0 或空字符,这取决于参数情况。

一个重要怪癖是命名属性时,双下划线的使用。我们在下一节中会看到,这是在信息隐藏时,强加一个级别,尽管不够成熟。程序员导入一个模块时,就不能直接访问到这些数据元素。我们正试着执行一种 OO 设计中的封装特性,只有通过存取函数才能访问。如果这种语法让你感觉有点怪异,不舒服的话,你可以从实例属性中删除所有双下划线,程序同样可以良好地运行。

所有的由双下划线(__)开始的属性都被“混淆”(mangled)了,导致这些名字在程序运行时很难被访问到。但是它们并没有用一种难于被逆向工程的方法来“混淆”。事实上,“混淆”属性的方式已众所周知,很容易被发现。这里主要是为了防止这些属性在被外部模块导入时,由于被意外使用而造成的名字冲突。我们将名字改成含有类名的新标志符,这样做,可以确保这些属性不会被无意“访问”。更多信息,请参见 14 节中关于私有成员的内容。

第9-12 行
我们把顺序对的字符串表示形式确定为“[num::'str']”,这样不论我们的实例用str()还是包含在 print 语句中时候,我们都可以用__str__()来提供这种表示方式。我们想强调一点,第二个元素是一个字符串,如果用户看到由引号标记的字符串时,会更加直观。要做到这点,我们使用“repr()”表示法对代码进行转换,把“%s”替换成“%r”。这相当于调用repr()或者使用单反引号来给出字符串的可求值版本–可求值版本的确要有引号:

>>> print a
[3 :: 'foo']

如果在 self.__string 中没有调用repr()(去掉单反引号或使用“%s”)将导致字符串引号丢失:

        return '[%d :: %s]' % (self.__num, self.__string)

现在对实例再次调用print,结果:

>>> print a
[3 :: foo]

没有引号,看起来会如何呢?不能信服“foo”是一个字符串,对吧?它看起来更像一个变量。连作者可能也不能确定。(我们快点悄悄回到这一变化之前,假装从来没看到这个内容。)

代码中__str__()函数后的第一行是把这个函数赋给另一个特殊方法名,__repr__。我们决定我们的实例的一个可求值的字符串表示应当与可打印字符串表示是一样的。而不是去定义一个完整的新函数,成为__str__()的副本,我们仅去创建一个别名,复制其引用。当你实现__str__()后,一旦使用那个对象作为参数来应用内建str()函数,解释器就会调用这段代码.对__repr__()repr()也一样。

如果不去实现__repr__(),我们的结果会有什么不同呢?如果赋值被取消,只有调用str()print 语句才会显示对象的内容。而可求值字符串表示恢复成默认的Python 标准形式<...some_object_ information...>

>>> a = NumStr(3, 'foo')
>>> print a
[3 :: 'foo']
>>> a
<numstr.NumStr object at 0x0000000003213848>

第14-21 行
我们想加到我们的类中的一个特征就是加法操作,前面已提到过。Python 用于定制类的特征之一是,我们可以重载操作符,以使定制的这些类型更“实用”。调用一个函数,像“add(obj1,obj2)”是为“add”对象obj1ojb2,这看起来好像加法,但如果能使用加号(+)来调用相同的操作是不是更具竞争力呢?像这样,obj1+obj2

重载加号,需要去为self(SELF)和其它操作数实现(OTHER)__add__()__add__()函数考虑Self+Other 的情况,但我们不需要定义__radd__()来处理Other+Self,因为这可以由Other 的__add__()去考虑。数值加法不像字符串那样结果受到(操作数)顺序的影响.

加法操作把两个部分中的每一部分加起来,并用这个结果对形成一个新的对象----通过将结果做为参数调用self.__class__()来实例化(同样,在前面已解释过).碰到任何类型不正确的对象时,我们会引发一个TypeError 异常.

第23-29 行
我们也可以重载星号[靠实现__mul__()],执行数值乘法和字符串重复,并同样通过实例化来创建一个新的对象。因为重复只允许整数在操作数的右边,因此也必执行此规则。基于同样的原因,我们在此也没有实现__rmul__()

第31-32 行
Python 对象任何时候都有一个Boolean 值。对标准类型而言,对象有一个false 值的情况为:它是一个类似于0 的数值,或是一个空序列,或者映射。就我们的类而言,我们选择数值必须为0,字符串要为空 作为一个实例有一个false 值的条件。覆盖__nonzero__()方法,就是为此目的。其它对象,像严格模拟序列或映射类型的对象,使用一个长度为 0 作为 false 值。这些情况,你需要实现__len__()方法,以实现那个功能。

第34-41 行
__norm_cval() (“normalize cmp() value 的缩写”)不是一个特殊方法。它是一个帮助我们重载__cmp__()的助手函数:唯一的目的就是把cmp()返回的正值转为1,负值转为-1。cmp()基于比较的结果,通常返回任意的正数或负数(或0),但为了我们的目的,需要严格规定返回值为-1,0 和1。对整数调用cmp()及与0 比较,结果即是我们所需要的,相当于如下代码片断

def __norm_cval(self, cmpres):
    if cmpres < 0:
        return -1
    elif cmpres > 0:
        return 1
    else:
        return 0

两个相似对象的实际比较是比较数字,比较字符串,然后返回这两个比较结果的和。

14. 私有化

默认情况下,属性在Python 中都是“public”,类所在模块和导入了类所在模块的其他模块的代码都可以访问到。很多OO 语言给数据加上一些可见性,只提供访问函数来访问其值。这就是熟知的实现隐藏,是对象封装中的一个关键部分。

大多数OO 语言提供“访问控制符”来限定成员函数的访问。

双下划线(__)
Python 为类元素(属性和方法)的私有性提供初步的形式。由双下划线开始的属性在运行时被“混淆”,所以直接访问是不允许的。实际上,会在名字前面加上下划线和类名。比如,以例13.6(numstr.py)中的self.__num 属性为例,被“混淆”后,用于访问这个数据值的标识就变成了self._NumStr__num。把类名加上后形成的新的“混淆”结果将可以防止在祖先类或子孙类中的同名
冲突。
尽管这样做提供了某种层次上的私有化,但算法处于公共域中并且很容易被“击败”。这更多的是一种对导入源代码无法获得的模块或对同一模块中的其他代码的保护机制.
这种名字混淆的另一个目的,是为了保护__XXX 变量不与父类名字空间相冲突。如果在类中有一个__XXX 属性,它将不会被其子类中的__XXX 属性覆盖。(回忆一下,如果父类仅有一个XXX 属性,子类也定义了这个,这时,子类的XXX 就是覆盖了父类的XXX,这就是为什么你必须使用PARENT.XXX来调用父类的同名方法。) 使用__XXX,子类的代码就可以安全地使用__XXX,而不必担心它会影响到父类中的__XXX。

单下划线(_)
与我们在第十二章发现的那样,简单的模块级私有化只需要在属性名前使用一个单下划线字符。这就防止模块的属性用“from mymodule import *”来加载。这是严格基于作用域的,所以这同样适合于函数。
在Python 2.2 中引进的新式类,增加了一套全新的特征,让程序员在类及实例属性提供保护的多少上拥有大量重要的控制权。尽管Python 没有在语法上把private,protected,friend 或protected friend 等特征内建于语言中,但是可以按你的需要严格地定制访问权。我们不可能涵盖所有的内容,但会在本章后面给你一些有关新式类属性访问的建议。

15. 授权

15.1 包装

“包装”在Python 编程世界中经常会被提到的一个术语。它是一个通用的名字,意思是对一个已存在的对象进行包装,不管它是数据类型,还是一段代码,可以是对一个已存在的对象,增加新的,删除不要的,或者修改其它已存在的功能。

在Python 2.2 版本前,从Python 标准类型子类化或派生类都是不允许的。即使你现在可以对新式类这样做,这一观念仍然很流行。你可以包装任何类型作为一个类的核心成员,以使新对象的行为模仿你想要的数据类型中已存在的行为,并且去掉你不希望存在的行为;它可能会要做一些额外的事情。这就是“包装类型”。在附录中,我们还将讨论如何扩充Python,包装的另一种形式。

包装包括定义一个类,它的实例拥有标准类型的核心行为。换句话说,它现在不仅能唱能跳,还能够像原类型一样步行,说话。图15-4 举例说明了在类中包装的类型看起像个什么样子。在图的中心为标准类型的核心行为,但它也通过新的或最新的功能,甚至可能通过访问实际数据的不同方法得到提高。

类对象(其表现像类型)

你还可以包装类,但这不会有太多的用途,因为已经有用于操作对象的机制,并且在上面已描述过,对标准类型有对其进行包装的方式。你如何操作一个已存的类,模拟你需要的行为,删除你不喜欢的,并且可能让类表现出与原类不同的行为呢?我们前面已讨论过,就是采用派生。
在这里插入图片描述

15.2 实现授权

授权是包装的一个特性,可用于简化处理有关dictating 功能,采用已存在的功能以达到最大限度的代码重用。

包装一个类型通常是对已存在的类型的一些定制。我们在前面提到过,这种做法可以新建,修改或删除原有产品的功能。其它的则保持原样,或者保留已存功能和行为。授权的过程,即是所有更新的功能都是由新类的某部分来处理,但已存在的功能就授权给对象的默认属性。

实现授权的关键点就是覆盖__getattr__()方法,在代码中包含一个对getattr()内建函数的调用。特别地,调用getattr()以得到默认对象属性(数据属性或者方法)并返回它以便访问或调用。特殊方法__getattr__()的工作方式是,当搜索一个属性时,任何局部对象首先被找到(定制的对象)。如果搜索失败了,则__getattr__()会被调用,然后调用getattr()得到一个对象的默认行为。

换言之,当引用一个属性时,Python 解释器将试着在局部名称空间中查找那个名字,比如一个自定义的方法或局部实例属性。如果没有在局部字典中找到,则搜索类名称空间,以防一个类属性被访问。最后,如果两类搜索都失败了,搜索则对原对象开始授权请求,此时,__getattr__()会被调用。

包装对象的简例

看一个例子。这个类已乎可以包装任何对象,提供基本功能,比如使用repr()和str()来处理字符串表示法。另外定制由get()方法处理,它删除包装并且返回原始对象。所以保留的功能都授权给对象的本地属性,在必要时,可由__getattr__()获得。

下面是包装类的例子:

class WrapMe(object):
    def __init__(self, obj):
        self.__data = obj

    def get(self):
        return self.__data

    def __repr__(self):
        return `self.__data`

    def __str__(self):
        return str(self.__data)

    def __getattr__(self, attr):
        return getattr(self.__data, attr)

在第一个例子中,我们将用到复数,因为所有 Python 数值类型,只有复数拥有属性:数据属性,及conjugate()内建方法(求共轭复数,译者注!)。记住,属性可以是数据属性,还可以是函数或方法:

>>> wrappedComplex = WrapMe(3.5+4.2j)
>>> wrappedComplex  # wrapped object: repr() 包装的对象:repr()
(3.5+4.2j)
>>> wrappedComplex.real  # real attribute 实部属性
3.5
>>> wrappedComplex.imag  # imaginary attribute 虚部属性
42.2
>>> wrappedComplex.conjugate()  # conjugate() method conjugate()方法
(3.5-4.2j)
>>> wrappedComplex.get()  # actual object 实际对象
(3.5+4.2j)

一旦我们创建了包装的对象类型,只要由交互解释器调用repr(),就可以得到一个字符串表示。然后我们继续访问了复数的三种属性,我们的类中一种都没有定义。在例子中,寻找实部,虚部及共轭复数的定义…they are not there!

对这些属性的访问,是通过getattr()方法,授权给对象.最终调用get()方法没有授权,因为它是为我们的对象定义的----它返回包装的真实的数据对象。

下一个使用我们的包装类的例子用到一个列表。我们将会创建对象,然后执行多种操作,每次授权给列表方法。

>>> wrappedList = WrapMe([123, 'foo', 45.67])
>>> wrappedList.append('bar')
>>> wrappedList.append(123)
>>> wrappedList
[123, 'foo', 45.67, 'bar', 123]
>>> wrappedList.index(45.67)
2
>>> wrappedList.count(123)
2
>>> wrappedList.pop()
123
>>> wrappedList
[123, 'foo', 45.67, 'bar']

注意,尽管我们正在我们的例子中使用实例,它们展示的行为与它们包装的数据类型非常相似。然后,需要明白,只有已存在的属性是在此代码中授权的。

特殊行为没有在类型的方法列表中,不能被访问,因为它们不是属性。一个例子是,对列表的切片操作,它是内建于类型中的,而不是像append()方法那样作为属性存在的。从另一个角度来说,切片操作符是序列类型的一部分,并不是通过__getitem__()特殊方法来实现的。

>>> wrappedList[3]
Traceback (innermost last): File "<stdin>", line 1, in ?
File "wrapme.py", line 21, in __getattr__
return getattr(self.data, attr)
AttributeError: __getitem__

AttributeError 异常出现的原因是切片操作调用了__getitem__()方法,且__getitme__()没有作为一个类实例方法进行定义,也不是列表对象的方法。回忆一下,什么时候调用getattr()呢?当在实例或类字典中的完整搜索失败后,就调用它来查找一个成功的匹配。你在上面可以看到,对getattr()的调用就是失败的那个,触发了异常。

然而,我们还有一种"作弊"的方法,访问实际对象[通过我们的get()方法]和它的切片能力.

>>> realList = wrappedList.get()
>>> realList[3]
'bar'

你现在可能知道为什么我们实现get()方法了----仅仅是为了我们需要取得对原对象进行访问这种情况,我们可以从访问调用中直接访问对象的属性,而忽略局部变量(realList):

>>> wrappedList.get()[3]
'bar'

get()方法返回一个对象,随后被索引以得到切片片断。

>>> f = WrapMe(open('/etc/motd'))
>>> f
<wrapMe.WrapMe object at 0x40215dac>
>>> f.get()
<open file '/etc/motd', mode 'r' at 0x40204ca0>
>>> f.readline()
'Have a lot of fun...\012'
>>> f.tell()
21
>>> f.seek(0)
>>> print f.readline(),
Have a lot of fun...
>>> f.close()
>>> f.get()
<closed file '/etc/motd', mode 'r' at 0x40204ca0>

一旦你熟悉了对象的属性,你就能够开始理解一些信息片断从何而来,能够利用新得到的知识来重复功能:

>>> print "<%s file %s, mode %s at %x>" % \
... (f.closed and 'closed' or 'open', 'f.name',
'f.mode', id(f.get()))
<closed file '/etc/motd', mode 'r' at 80e95e0>

这总结了我们的简单包装类的例子。我们还刚开始接触使用类型模拟来进行类自定义。你将会发现你可以进行无限多的改进,来进一步增加你的代码的用途。一种改进方法是为对象添加时间戳。在下一小节中,我们将对我们的包装类增加另一个维度(dimension):

更新简单的包裹类

创建时间,修改时间,及访问时间是文件的几个常见属性,但没人说,你不能为对象加上这类信息。毕竟,一些应用能因有这些额外信息而受益。

如果你对使用这三类时间顺序(chronological)数据还不熟,我们将会对它们进行解释。创建时间(或’ctime’)是实例化的时间,修改时间(或’mtime’)指的是核心数据升级的时间[通常会调用新的set()方法],而访问时间(或’atime’)是最后一次对象的数据值被获取或者属性被访问时的时间戳。

更新我们前面定义的类,可以创建一个模块twrapme.py,看例13.7。

如何更新这些代码呢?好,首先,你将会发现增加了三个新方法:gettimeval()gettimestr(),及set()。我们还增加数行代码,根据所执行的访问类型,更新相应的时间戳。

例13.7 包装标准类型 (twrapme.py)

类定义包装了任何内建类型,增加时间属性;get()set(),还有字符串表示的方法;并授权所有保留的属性,访问这些标准类型。

#!/usr/bin/env python

from time import time, ctime

class TimedWrapMe(object):

    def __init__(self, obj):
        self.__data = obj
        self.__ctime = self.__mtime = \
        self.__atime = time()

    def get(self):
        self.__atime = time()
        return self.__data

    def gettimeval(self, t_type):
        if not isinstance(t_type, str) or \
                t_type[0] not in 'cma':
            raise TypeError, \
                "argument of 'c', 'm', or 'a' req'd"
        return getattr(self, '_%s__%stime' % \
                       (self.__class__.__name__, t_type[0]))

    def gettimestr(self, t_type):
        return ctime(self.gettimeval(t_type))

    def set(self, obj):
        self.__data = obj
        self.__mtime = self.__atime = time()

    def __repr__(self):  # repr()
        self.__atime = time()
        return `self.__data`

    def __str__(self):  # str()
        self.__atime = time()
        return str(self.__data)

    def __getattr__(self, attr):  # delegate
        self.__atime = time()
        return getattr(self.__data, attr)

gettimeval()方法带一个简单的字符参数,“c”,“m”或“a”,相应地,对应于创建,修改或访问时间,并返回相应的时间,以一个浮点值保存。gettimestr()仅仅返回一个经time.ctime()函数格式化的打印良好的字符串形式的时间。

为新的模块作一个测试驱动。我们已看到授权是如何工作的,所以,我们将包装没有属性的对象,来突出刚加入的新的功能。在例子中,我们包装了一个整数,然后,将其改为字符串。

>>> timeWrappedObj = TimedWrapMe(932)
>>> timeWrappedObj.gettimestr('c')
‘Wed Apr 26 20:47:41 2006'
>>> timeWrappedObj.gettimestr('m')
'Wed Apr 26 20:47:41 2006'
>>> timeWrappedObj.gettimestr('a')
'Wed Apr 26 20:47:41 2006'
>>> timeWrappedObj
932
>>> timeWrappedObj.gettimestr('c')
'Wed Apr 26 20:47:41 2006'
>>> timeWrappedObj.gettimestr('m')
'Wed Apr 26 20:47:41 2006'
>>> timeWrappedObj.gettimestr('a')
'Wed Apr 26 20:48:05 2006'

你将注意到,一个对象在第一次被包装时,创建,修改,及最后一次访问时间都是一样的。一旦对象被访问,访问时间即被更新,但其它的没有动。如果使用set()来置换对象,则修改和最后一次访问时间会被更新。例子中,最后是对对象的读访问操作。

>>> timeWrappedObj.set('time is up!')
>>> timeWrappedObj.gettimestr('m')
'Wed Apr 26 20:48:35 2006'
>>> timeWrappedObj
'time is up!'
>>> timeWrappedObj.gettimestr('c')
'Wed Apr 26 20:47:41 2006'
>>> timeWrappedObj.gettimestr('m')
'Wed Apr 26 20:48:35 2006'
>>> timeWrappedObj.gettimestr('a')
'Wed Apr 26 20:48:46 2006'

改进包装一个特殊对象

下一个例子,描述了一个包装文件对象的类。我们的类与一般带一个异常的文件对象行为完全一样:在写模式中,字符串只有全部为大写时,才写入文件。

这里,我们要解决的问题是,当你正在写一个文本文件,其数据将会被一台旧电脑读取。很多老式机器在处理时,严格要求大写字母,所以,我们要实现一个文件对象,其中所有写入文件的文本会自动转化为大写,程序员就不必担心了。

事实上,唯一值得注意的不同点是并不使用open()内建函数,而是调用 CapOpen 类时行初始化。尽管,参数同open()完全一样。

这个类扩充了Python FAQs 中的一个例子,提供一个文件类对象,定制write()方法,同时,给
文件对象授权其它的功能。

例13.8 包装文件对象 (capOpen.py)

#!/usr/bin/env python

class CapOpen(object):
    def __init__(self, fn, mode='r', buf=-1):
        self.file = open(fn, mode, buf)

    def __str__(self):
        return str(self.file)

    def __repr__(self):
        return 'self.file'

    def write(self, line):
        self.file.write(line.upper())

    def __getattr__(self, attr):
        return getattr(self.file, attr)

例13.8 展示那段代码,文件名是capOpen.py。下面看一下例子中是如何使用这个类的:

>>> f = CapOpen('/tmp/xxx', 'w')
>>> f.write('delegation example\n')
>>> f.write('faye is good\n')
>>> f.write('at delegating\n')
>>> f.close()
>>> f
<closed file '/tmp/xxx', mode 'w' at 12c230>

可以看到,唯一不同的是第一次对CapOpen()的调用,而不是open()。如果你正与一个实际文件对象,而非行为像文件对象的类实例进行交互,那么其它所有代码与你本该做的是一样的。除了write(),所有属性都已授权给文件对象。为了确定代码是否正确,我们加载文件,并显示其内容。(注:可以使用open()CapOpen(),这里因在本例中用到,所以选用CapOpen()。)

>>> f = CapOpen('/tmp/xxx', 'r')
>>> for eachLine in f:
... print eachLine,
...
DELEGATION EXAMPLE FAYE IS GOOD
AT DELEGATING

16. 新式类的高级特性 (Python 2.2+)

16.1 新式类的通用特性

由于类型和类的统一,这些特性中最重要的是能够子类化Python 数据类型。其中一个副作用是,所有的Python 内建的 “casting” 或转换函数现在都是工厂函数。当这些函数被调用时,你实际上是对相应的类型进行实例化。

下面的内建函数,跟随Python 多日,都已“悄悄地”(也许没有)转化为工厂函数:

  • int(), long(), float(), complex()
  • str(), unicode()
  • list(), tuple()
  • type()

还有,加入了一些新的函数来管理这些“散兵游勇”:

  • basestring()1
  • dict()
  • bool()
  • set()2, frozenset()2
  • object()
  • classmethod()
  • staticmethod()
  • super()
  • property()
  • file()

这些类名及工厂函数使用起来,很灵活。不仅能够创建这些类型的新对象,它们还可以用来作为基类,去子类化类型,现在还可以用于isinstance()内建函数。使用isinstance()能够用于替换用烦了的旧风格,而使用只需少量函数调用就可以得到清晰代码的新风格。比如,为测试一个对象是否是一个整数,旧风格中,我们必须调用type()两次或者import 相关的模块并使用其属性;但现在只需要使用isinstance(),甚至在性能上也有所超越:

OLD (not as good):

  • if type(obj) == type(0)…
  • if type(obj) == types.IntType…

BETTER:

  • if type(obj) is type(0)…

EVEN BETTER:

  • if isinstance(obj, int)…
  • if isinstance(obj, (int, long))…
  • if type(obj) is int…

记住:尽管isinstance()很灵活,但它没有执行“严格匹配”比较----如果 obj 是一个给定类型的实例或其子类的实例,也会返回True。但如果想进行严格匹配,你仍然需要使用 is 操作符。

  1. Python 2.3 中新增。
  2. Python 2.4 中新增。

16.2 __slots__类属性

字典位于实例的“心脏”。__dict__属性跟踪所有实例属性。举例来说,你有一个实例inst.它有一个属性foo,那使用inst.foo 来访问它与使用inst.__dict__['foo']来访问是一致的。

字典会占据大量内存,如果你有一个属性数量很少的类,但有很多实例,那么正好是这种情况。为内存上的考虑,用户现在可以使用__slots__属性来替代__dict__

基本上,__slots__是一个类变量,由一序列型对象组成,由所有合法标识构成的实例属性的集合来表示。它可以是一个列表,元组或可迭代对象。也可以是标识实例能拥有的唯一的属性的简单字符串。任何试图创建一个其名不在__slots__中的名字的实例属性都将导致 AttributeError 异常:

>>> class SlottedClass(object):
...    __slots__ = ('foo', 'bar')
...    
>>> c = SlottedClass()
>>> c.foo = 42
>>> c.xxx = "don't think so"
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'SlottedClass' object has no attribute 'xxx'

这种特性的主要目的是节约内存。其副作用是某种类型的"安全",它能防止用户随心所欲的动态增加实例属性。带__slots__属性的类定义不会存在__dict__了(除非你在__slots__中增加’__dict__'元素)。更多有关__slots__的信息,请参见 Python(语言)参考手册中有关数据模型章节。

16.3 特殊方法__getattribute__()

Python 类有一个名为__getattr__()的特殊方法,它仅当属性不能在实例的__dict__或它的类(类的__dict__),或者祖先类(其__dict__)中找到时,才被调用。我们曾在实现授权中看到过使用__getattr__()

很多用户碰到的问题是,他们想要一个适当的函数来执行每一个属性访问,不光是当属性不能找到的情况。这就是__getattribute__()用武之处了。它使用起来,类似__getattr__(),不同之处在于,当属性被访问时,它就一直都可以被调用,而不局限于不能找到的情况。

如果类同时定义了__getattribute__()__getattr__()方法,除非明确从__getattribute__()调用,或__getattribute__()引发了AttributeError 异常,否则后者不会被调用.

如果你将要在此(译者注:__getattribute__()中)访问这个类或其祖先类的属性,请务必小心。如果你在__getattribute__()中不知何故再次调用了__getattribute__(),你将会进入无穷递归。为避免在使用此方法时引起无穷递归,为了安全地访问任何它所需要的属性,你总是应该调用祖先类的同名方法;比如,super(obj,self).__getattribute__(attr)。此特殊方法只在新式类中有效。同__slots__一样,你可以参考Python(语言)参考手册中数据模型章节,以得到更多有关__getattribute__()的信息。

16.4 描述符

如你的对象有代理,并且这个代理有一个“get”属性(实际写法为__get__),当这个代理被调用时,你就可以访问这个对象了。当你试图使用描述符(set)给一个对象赋值或删除一个属性(delete)时,这同样适用。
556

17. 相关模块和文档

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值