5-7-类的高级主题
1. 扩展内置类型
1.1 通过嵌入扩展类型
这个类允许我们创建多个自包含的集合对象,带有预先设置的数据和行为,而不是手动把列表传入函数中:
# setwrapper.py
class Set:
def __init__(self, value=[]):
self.data = []
self.concat(value)
def intersect(self, other):
res = []
for x in self.data:
if x in other: # 获取共有项
res.append(x)
return Set(res) # 返回一个新 Set
def union(self, other):
res = self.data[:]
for x in other:
if not x in res:
res.append(x)
return Set(res)
def concat(self, value):
for x in value:
if not x in self.data:
self.data.append(x)
def __len__(self):
return len(self.data)
def __getitem__(self, key):
return self.data[key]
def __and__(self, other):
return self.intersect(other)
def __or__(self, other):
return self.union(other)
def __repr__(self):
return 'Set:' + repr(self.data)
def __iter__(self):
return iter(self.data)
x = Set([1, 3, 5, 7])
print(x.union(Set([1, 4, 7])))
print(x | Set([1, 4, 6]))
Set:[1, 3, 5, 7, 4]
Set:[1, 3, 5, 7, 4, 6]
重载索引运算让 Set 类的实例可以充当真正的列表。
1.2 通过子类扩展类型
所有内置类型都能直接创建子类,像 list、str、dict 以及 tuple 等类型转换函数都变成内置类型的名称。
可以编写自己的子类,定制列表偏移值以 1 开始计算:
# typesubclass.py
# Map 1..N to 0..N-1
class MyList(list):
def __getitem__(self, offset):
print('(indexing) %s at %s' % (self, offset))
return list.__getitem__(self, offset - 1)
if __name__ == '__main__':
print(list('abc'))
x = MyList('abc')
print(x)
print(x[1])
print(x[3])
x.append('spam')
print(x)
x.reverse()
print(x)
['a', 'b', 'c']
['a', 'b', 'c']
(indexing) ['a', 'b', 'c'] at 1
a
(indexing) ['a', 'b', 'c'] at 3
c
['a', 'b', 'c', 'spam']
['spam', 'c', 'b', 'a']
2. 新式类的扩展
2.1 slots 实例
将字符串属性名称顺序赋值给特殊的 __slots__
类属性,新式类就有可能既限制类的实例将有的合法属性集,又能够优化内存和速度性能。
只有 __slots__
列表内的变量名可赋值为实例属性,实例属性名必须在引用前赋值:
class limiter:
__slots__ = ['age', 'name', 'job']
x = limiter()
x.age
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-3-b903551b2c8f> in <module>()
3
4 x = limiter()
----> 5 x.age
AttributeError: age
x.age = 40
x.age
40
x.ape = 1000 # 不在 __slots__ 中
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-9b5b7f47724b> in <module>()
----> 1 x.ape = 1000 # 不在 __slots__ 中
AttributeError: 'limiter' object has no attribute 'ape'
要节省空间和执行速度,slot 属性可以顺序存储以供快速查找,而不是为每个实例分配一个字典。
Slots 和命名空间字典
有些带有 slots 的实例也许根本没有 __dict__
属性命名空间字典,Python 使用类描述符功能来为实例中的 slot 属性分配空间。只有 slot 列表中的名称可以分配给实例,基于 slot 的属性仍然可以使用通用工具通过名称来访问或设置:
class C:
__slots__ = ['a', 'b']
X = C()
X.a = 1
X.a
1
X.__dict__
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-8-b2412a113e8c> in <module>()
----> 1 X.__dict__
AttributeError: 'C' object has no attribute '__dict__'
getattr(X, 'a') # getattr 和 setattr 仍然可用
1
setattr(X, 'b', 2)
X.b
2
'a' in dir(X)
True
class D:
__slots__ = ['a', 'b'] # 不能给不是 slots 列表中名称的实例来分配新的名称
def __init__(self):
self.d = 4
X = D()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-13-ffbc3ded455c> in <module>()
4 self.d = 4
5
----> 6 X = D()
<ipython-input-13-ffbc3ded455c> in __init__(self)
2 __slots__ = ['a', 'b']
3 def __init__(self):
----> 4 self.d = 4
5
6 X = D()
AttributeError: 'D' object has no attribute 'd'
class D: # 通过在 __slots__ 中包含 __dict__ 仍然可以容纳额外属性
__slots__ = ['a', 'b', '__dict__']
c = 3
def __init__(self): self.d = 4 # d 在 __dict__ 中,a 在 __slots__ 中
X = D()
X.d
4
X.__dict__
{'d': 4}
X.__slots__
['a', 'b', '__dict__']
X.c
3
X.a = 1
X.b = 2
# 通用地列出所有实例属性的代码
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
print(attr, '=>', getattr(X, attr))
d => 4
a => 1
b => 2
__dict__ => {'d': 4}
超类中的多个 __slots__
列表
slot 声明可能出现在一个类树中的多个类中,但是,它们受到一些限制:
- 如果一个子类继承自一个没有
__slots__
的超类,那么超类的__dict__
属性总是可以访问的,使得子类中的一个__slots__
无意义。 - 如果一个类定义了一个与超类相同的 slot 名称,超类 slot 定义的名称版本只有通过直接从超类获取其描述符才能访问。
- 由于一个
__slots__
声明的含义受到它出现其中的类的限制,所以子类将有一个__dict__
,除非它们也定义了一个__slots__
。 - 通常从列出实例属性这方面来讲,多类中的 slots 可能需要手动类树爬升、dir用法,或者把 slot 名称当做不同的名称领域的政策。
class E:
__slots__ = ['c', 'd']
class D(E):
__slots__ = ['a', '__dict__']
X = D()
X.a = 1;X.b = 2;X.c = 3
X.a, X.c
(1, 3)
E.__slots__
['c', 'd']
D.__slots__
['a', '__dict__']
X.__slots__ # 实例继承最底层的 __slots__
['a', '__dict__']
X.__dict__
{'b': 2}
# 其他超类 slots 未访问
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
print(attr, '=>', getattr(X, attr))
b => 2
a => 1
__dict__ => {'b': 2}
2.2 特性:属性访问
特性(property)提供另一种方式让新式类定义自动调用的方法,来读取或赋值实例属性。
尽管特性不支持通用属性路由路由目标,至少在特定属性上是 __getattr__
和 __setattr__
重载方法的替代做法。特性和这两个方法有类似效果,但是只在读取所需要的动态计算的变量名时,才会发生额外的方法调用。
特性和 slots 相关,但是有不同目标,两者都执行未在实例命名空间字典中物理存储的实例属性,都基于描述符概念。但是,slot 管理实例存储,特性拦截访问和任意计算值。
特性基础
特性是一种对象,赋值给类属性名称。特性的产生是通过调用内置函数 property,传入三种方法(get, set 和 delete 操作)以及可选的文档字符串。如果任何参数以 None 传递或省略,则该操作无法支持。
特性一般是在 class 语句顶层赋值(例如,name = property()),特殊的 @ 语法可以自动处理这个步骤。这样赋值时,对类属性本身的读取(例如,obj.name)就会自动传给 property 的一个读取方法。
class properties:
def getage(self):
return 40
age = property(getage, None, None, None) # (get, set, del, docs) 或者使用 @
x = properties()
x.age
40
x.name
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-2-7a8bb287c399> in <module>()
----> 1 x.name
AttributeError: 'properties' object has no attribute 'name'
当我们新增属性赋值运算支持时,特性就变得更有吸引力:输入的代码更少,对我们不希望动态计算的属性进行赋值运算时,不会发生额外的方法调用:
class properties:
def getage(self):
return 40
def setage(self, value):
print('set age: %s' % value)
self._age = value
age = property(getage, setage, None, None)
x = properties()
x.age # 运行 getage
40
x.age = 42 # 运行 setage
set age: 42
x._age # 常规获取:不调用 getage
42
x.age # 运行 getage
40
x.job = 'trainer' # 常规赋值
x.job # 常规获取
'trainer'
等效的经典类可能会引发额外的方法调用,而且需要通过属性字典传递属性赋值语句,以避免死循环。
__getattr__
和 __setattr__
的某些应用依然需要更为动态或通用的接口,超出特性所能直接提供的范围。
2.3 __getattribute__
和描述符:属性工具
__getattribute__
方法只适用于新式类,可以让类拦截所有属性的引用,而不局限于未定义的引用。
Python 支持属性描述符的概念——带有 __get__
和 __set__
方法的类,分配给类属性并且由实例继承,这拦截了对特定属性的读取和写入访问。描述符在某种意义上是特性的一种更加通用的形式。实际上,特性是定义特定类型描述符的一种简化方式,该描述符运行关于访问的函数。
3. 静态方法和类方法
有两种可以在类中定义不需要实例就可以调用的方法:静态方法大致类似于类中简单的无实例函数,类方法传递一个类而不是一个实例。
要启用这些方法模式,必须在类中调用特殊的内置函数 staticmethod 和 classmethod,或者使用特殊的 @name 装饰语法来调用它们。在 Python 3.X 中,对于仅通过类名调用的非实例方法,不需要 staticmethod 声明,但是如果通过实例调用此类方法,则仍然需要静态方法声明。
3.1 为什么使用特殊的方法?
类的方法通常在第一个参数中传递一个实例对象,作为方法调用的隐含主体——这是“面向对象编程”中的“对象”。如今有两种方法可以改变这种模式。
有时,程序需要处理与类相关的数据,而不是实例。信息通常存储在类本身中,并在任何实例之外进行处理。
对于这样的任务,在类之外编写的简单函数通常就足够了——因为它们可以通过类名访问类属性,它们可以访问类数据,而不需要访问实例。但是,为了更好地将此类代码与类关联起来,并且允许这样的处理像往常一样使用继承自定义,最好在类本身内编写这些类型的函数。要实现这一点,我们需要一个类中的方法,这个类没有传递,也不需要一个 self 实例参数。
Python 使用静态方法的概念来支持这些目标——简单的函数,没有嵌套在类中的 self 参数,设计用于处理类属性而不是实例属性。无论通过类还是实例调用,静态方法都不会收到自动的 self 参数。它们通常跟踪跨所有实例的信息,而不是为实例提供行为。
虽然不太常用,但 Python 也支持类方法的概念——这是类的一种方法,传递给它们的第一个参数是一个类对象而不是一个实例,不管它们是通过实例还是类调用。即使是通过实例调用,这样的方法可以通过 self 类参数访问类数据。常规方法(现在正规的叫法是实例方法)在调用时仍然接收一个主题实例,静态方法和类方法则不会。
3.2 Python 3.X 中的静态方法
- 当通过实例获取方法时,生成一个绑定方法。
- 从一个类获取一个方法会产生一个简单函数,没有给出实例也可以常规地调用。
- 如果方法只通过一个类调用的话,我们不需要将这样的方法声明为静态的,但是,要通过一个实例调用它,我们必须这么做。
class Spam:
numInstances = 0
def __init__(self):
Spam.numInstances = Spam.numInstances + 1
def printNumInstances(): # 处理类数据而不是实例数据
print("Number of instances created: ", Spam.numInstances)
a = Spam()
b = Spam()
c = Spam()
Spam.printNumInstances()
Number of instances created: 3
a.printNumInstances() # 无 self 方法的调用从实例调用失效
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-05e2611a4d82> in <module>()
----> 1 a.printNumInstances()
TypeError: printNumInstances() takes 0 positional arguments but 1 was given
3.3 使用静态和类方法
class Methods:
def imeth(self, x): # 常规实例方法:传入一个 self
print(self, x)
def smeth(x): # 静态方法:没有实例传入
print(x)
def cmeth(cls, x): # 类方法:传入类,而不是实例
print(cls, x)
smeth = staticmethod(smeth)
cmeth = classmethod(cmeth)
obj = Methods()
obj.imeth(1) # 通过实例调用,Python 会把实例自动传给第一个参数
<__main__.Methods object at 0x000001AA60911E48> 1
Methods.imeth(obj, 2) # 类调用时,需要手动传入实例
<__main__.Methods object at 0x000001AA60911E48> 2
静态方法调用时不需要实例参数。
Methods.smeth(3) # 静态方法,通过类调用,没有实例传入
3
obj.smeth(4) # 静态方法,通过实例调用,实例没有传入
4
类方法类似,但 Python 自动把类(而不是实例)传入类方法第一个参数中,不管它是通过一个类或一个实例调用:
Methods.cmeth(5)
<class '__main__.Methods'> 5
obj.cmeth(6)
<class '__main__.Methods'> 6
3.4 使用静态方法统计实例
class Spam:
numInstances = 0
def __init__(self):
Spam.numInstances += 1
def printNumInstances():
print("Number of instances: ", Spam.numInstances)
printNumInstances = staticmethod(printNumInstances)
允许子类用继承定制静态方法:
class Sub(Spam):
def printNumInstances():
print("Extra stuff...")
Spam.printNumInstances()
printNumInstances = staticmethod(printNumInstances)
类也可以继承静态方法而不用重新定义它,它可以没有一个实例而运行,不管定义于类树的何处:
class Other(Spam):pass
c = Other()
c.printNumInstances()
Number of instances: 1
3.5 用类方法统计实例
class Spam:
numInstances = 0
def __init__(self):
Spam.numInstances += 1
def printNumInstances(cls):
print("Number of instances: ", cls.numInstances)
printNumInstances = classmethod(printNumInstances)
通过类和实例调用 printNumInstances 方法时,它接受类而不是实例:
a, b = Spam(), Spam()
a.printNumInstances()
Number of instances: 2
Spam.printNumInstances()
Number of instances: 2
当使用类方法的时候,它们接收调用的主体的最具体(底层)的类:
class Sub(Spam):
def printNumInstances(cls):
print("Extra stuff...", cls)
Spam.printNumInstances()
printNumInstances = classmethod(printNumInstances)
x = Sub()
x.printNumInstances()
Extra stuff... <class '__main__.Sub'>
Number of instances: 3
4. 装饰器和元类:第一部分
Python 装饰器提供了一个通用工具,用于添加管理函数和类的逻辑,或者稍后对它们的调用。
更具体地说,这实际上只是在函数和类定义时使用显式语法运行额外处理步骤的一种方式。它有两种方式:
- 函数装饰器——它们为简单函数和类的方法指定了特殊的运算模式,将函数和类封装在一个额外的逻辑层中作为另一个函数实现,通常称为 metafunction。
- 类装饰器——直接绑定到类模式,它们添加了对整个对象及其接口的管理的支持。它们的用途经常与元类有所重叠。
用户定义的函数装饰器通常写成类,把原始函数和其他数据当成状态信息。
4.1 函数装饰器基础
函数装饰器是它后边的函数的运行时的声明。函数装饰器是写成一行,就在定义函数或方法的 def 语句之前,而且由 @ 符号、后面跟着**元函数(metafunction,管理另一函数(或其他可调用对象)的函数)**组成。静态方法可以用下面的装饰器语法编写:
class C:
@staticmethod
def meth():
pass
从内部来看,这个语法和下面的写法有相同的效果(把函数传递给装饰器,再赋值给最初的变量名):
class C:
def meth():
pass
meth = staticmethod(meth)
装饰器函数可返回原函数,或者新对象(保存传给装饰器的原始函数,这个函数将会在额外逻辑层执行后间接地运行)。
通过装饰器,可以编写前一节中的静态方法的一种更好的方法(classmethod、property 装饰器以同样的方法使用):
class Spam:
numInstances = 0
def __init__(self):
Spam.numInstances += 1
@staticmethod
def printNumInstances():
print("Number of instances created: ", Spam.numInstances)
a = Spam()
b = Spam()
c = Spam()
Spam.printNumInstances()
a.printNumInstances()
Number of instances created: 3
Number of instances created: 3
4.2 用户定义函数装饰器
class tracer:
def __init__(self, func):
self.calls = 0
self.func = func
def __call__(self, *args):
self.calls += 1
print('call %s to %s' % (self.calls, self.func.__name__))
return self.func(*args)
@tracer # 等同于 spam = tracer(spam)
def spam(a, b, c): # 将 spam 包装在一个装饰器对象中
return a + b + c
spam(1, 2, 3) # 调用 tracer 包装对象
spam('a', 'b', 'c')
spam(4, 5, 6)
call 1 to spam
call 2 to spam
call 3 to spam
15
spam 函数是通过 tracer 装饰器执行的,所以当最初的变量名 spam 调用时,实际上触发的是类中的 __call__
方法。这个方法会计算和记录该次调用,然后委托给原始的包裹的函数。
结果就是新增一层逻辑至原始的 spam 函数。
4.3 类装饰器和元类
类装饰器类似于函数装饰器,但是,它们在一条 class 语句的末尾运行,并且把一个类名重新绑定到一个可调用对象。它们可以用来管理类,或者当随后创建实例的时候插入一个包装逻辑层来管理实例:
def decorator(aClass):
pass
@decorator
class C:
pass
被映射为下列代码:
def decorator(aClass):
pass
class C:
pass
C = decorator(C)
类装饰器也可以扩展自身,或者返回一个拦截了随后的实例构建调用的对象。
元类提供了一种可选模式,会把一个类对象的创建导向到顶级 type 类的一个子类,在一条 class 语句的最后:
class Meta(type):
def __new__(meta, classname, supers, classdict):
pass
class C(metaclass=Meta): pass
元类通常重新定义 type 类的 __new__
或 __init__
方法,以实现对一个新的类对象的创建和初始化的控制。直接效果就像类装饰器一样,是定义在类创建时自动运行的代码。元类可以不需要是类,这种可能性模糊了该工具和装饰器之间的一些区别,甚至可能使这两者在许多角色中具有功能上的同等性。
类装饰器和元类,都可以自由地扩充类或返回任意对象来替换它——这是一种几乎无限的基于类的定制可能性的协议。元类还可以定义处理它们的实例类的方法,而不是它们的常规实例。
5. super 内置函数
5.1 传统超类调用形式:便携、通用
在需要时通过显式地命名超类来调用超类方法,这种技术在 Python 中是传统的:
class C:
def act(self):
print('spam')
class D(C):
def act(self):
C.act(self) # 显式调用超类
print('eggs')
X = D()
X.act()
spam
eggs
5.2 基本 super 用法
class C:
def act(self):
print('spam')
class D(C):
def act(self):
super().act() # 引用超类,省略 self
print('eggs')
X = D()
X.act()
spam
eggs
如果使用多于一个父类,super 会变得容易出错,甚至无法使用,它不引发异常的多重继承树,但会只选择最左边的超类的方法运行,这可能是也可能不是一个你想要的。
除非你能够确保在软件的整个生命周期中不会在树中向类中添加第二个超类,否则你不能在单继承模式中使用 super。
5.3 super 的优点
- 在运行时更改类树
- 协同多重继承方法调度
运行时更改类树
class X:
def m(self): print('X.m')
class Y:
def m(self): print('Y.m')
class C(X):
def m(self): super().m()
i = C()
i.m()
X.m
C.__bases__ = (Y,) # 运行时改变超类
i.m()
Y.m
协作 super 调用
class A:
def __init__(self):
print('A.__init__')
class B(A):
def __init__(self):
print('B.__init__')
super().__init__()
class C(A):
def __init__(self):
print('C.__init__')
super().__init__()
class D(B, C): pass
x = B()
B.__init__
A.__init__
x = D()
B.__init__
C.__init__
A.__init__
super 并不完全适用于操作符重载或传统编码的多继承树。
使用 super 使程序的行为依赖于 MRO 算法,它对定制、耦合和灵活性的影响也是非常微妙的。如果你不完全理解这个算法(或者有它的应用程序没有实现的目标),那么最好不要依赖它来隐式地触发代码中的操作。
6. 类陷阱
6.1 修改类属性的副作用
在类主体中,对变量名的赋值语句会产生属性,在运行时存在于类的对象内,而且会由所有实例继承。在 class 语句外动态修改类属性时,也会修改每个对象从该类继承而来的这个属性。
6.2 修改可变的类属性也可能产生副作用
如果一个类属性引用一个可变对象,那么从任何实例在原处修改该对象都会立刻影响到所有实例。
6.3 多重继承:顺序很重要
Python 总是会根据超类在首行的顺序,从左至右搜索超类。
6.4 过度包装
如果类层次太深,程序就变得晦涩难懂。