13.3.9 @contextlib.contextmanager
装饰器
对于单值生成器函数,即只yield 1个值时,可使用@contextmanager
装饰器,实现上下文管理,将yield返回值与as子句后面的变量绑定,并执行完yield后面的函数语句。
代码示例:
import contextlib
@contextlib.contextmanager # 不使用此装饰器时,不会执行exit函数语句
def foo():
print('enter')
yield 123
print('exit') # 异常情况,不能保证exit语句执行,使用try...finally语句
def foo1():
print('enter')
try:
yield 123
finally: # 通过try...finally语句,保证异常场景,exit语句也能执行
print('exit')
@contextlib.contextmanager
def foo2():
for i in range(3): # yield不止1个值时,将抛RuntimeError: generator didn't stop异常
yield i
if __name__ == '__main__':
# next(foo())
with foo() as f:
print(f)
with foo2() as f:
print(f)
小结: 对于业务逻辑较为复杂的场景,直接使用__enter__
、__exit__
方法更可靠。
13.3.10 @functools.total_ordering
装饰器
比较运算符<、>、<=、>=
如果每一个都在类中实现太麻烦了,通过@total_ordering
装饰器,只需要在类中实现<、>、<=、>=
中的任意一个,即可进行实例的相关比较。
import functools
@functools.total_ordering
class A:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x
def __gt__(self, other):
return self.x > other.x
if __name__ == '__main__':
a1 = A(3)
a2 = A(4)
a3 = A(3)
print(a1 == a2)
print(a1 > a2)
print(a1 <= a2)
print(a1 == a3)
小结:从执行示例可以看出:注释__eq__
方法后,a1==a2
返回值为false,与预期不符。这与前述去重吻合:当没有给出__eq__
时,判断例是否相等,默认比较内存中的id。所以==
必须单独实现,否则比较结果不准确
13.3.11 反射 :__getattr__、__setattr__、__hasattr__
概念:
运行时:指程序被加载到内存中执行的时候。区别于编译时。
反射:reflection,指运行时获取类型定义信息。一个对象能够在运行时,像照镜子一样,反射出其类型信息就是反射
。简单的说,在python中,能够通过一个对象,找出其type、class、attribute或method的能力,称为反射
。
例如:下述Point类的实例p,通过反射能力,在p.__dict__
中找到自己的attribute,并且修改、增加自己的attribute。
通过__dict__
获取、修改属性不优雅,python提供了内建函数: 、
方法 | 意义 | 说明 |
---|---|---|
getattr(object, name[, default]) | 通过name返回 object的属性值。属性不存在返回default,如果没有给出default,抛AttributeError。 | 注意,name必须为字符串类型; getattr搜索顺序,遵从mro搜索顺序,先从自己的 __dict__ 中找,然后找class的… |
setattr(object, name, value) | object的属性存在则覆盖,不存在新增。 | 注意:object为类则新增类属性,为实例则新增实例性。 –动态增加属性,未绑定。增加到实例,则在实例的__dict__中;增加到类,则在类的__dict__中。 |
hasattr(object, name) | 判断对象是否有这个名字的属性 | name必须为字符串类型 |
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
if __name__ == '__main__':
p = Point(2, 3)
# 运用对象的反射能力,在`p.__dict__`中找到自己的attribute,并且修改、增加自己的attribute
print(p.__dict__)
p.__dict__['x'] = 3
p.z = 12
print(p.__dict__)
# 通过反射能力,在`p.__dict__`中找到自己的attribute,并且修改、增加自己的attribute
setattr(Point, 't', 14)
print(p.__dict__)
print(getattr(p, 't'))
if not hasattr(p, 'lab'):
setattr(p, 'lab', lambda x: print(x))
setattr(Point, 'lab1', lambda self: print('lab1'))
print(p.__dict__)
print(Point.__dict__)
p.lab('lab')
p.lab1()
执行结果:
{‘x’: 2, ‘y’: 3}
{‘x’: 3, ‘y’: 3, ‘z’: 12}
{‘x’: 3, ‘y’: 3, ‘z’: 12}
14
{‘x’: 3, ‘y’: 3, ‘z’: 12, ‘lab’: <function at 0x000001D3B1129430>}
{‘module’: ‘main’, ‘init’: <function Point.init at 0x000001D3B1129160>, ‘dict’: <attribute ‘dict’ of ‘Point’ objects>, ‘weakref’: <attribute ‘weakref’ of ‘Point’ objects>, ‘doc’: None, ‘t’: 14, ‘lab1’: <function at 0x000001D3B1129940>}
lab
lab1
小结:动态增加属性的方式,和装饰器修饰一个类、Mixin方式的差异在于,Mixin和装饰器在编译时就决定了;而动态增、删属性的方式是运用反射能力,运行时改变类或实例的属性,更灵活。
练习: 通过getattr/setattr/hasattr
改造命令分发器
class Dispatcher:
def reg(self, cmd: str, fn):
if not hasattr(self, cmd):
setattr(self.__class__, cmd, fn)
else:
raise Exception('Exist')
def run(self):
while True:
cmd = input('enter:')
if cmd == 'q':
return
getattr(self, cmd, self.default_func)()
@classmethod
def default_func(cls):
print('default')
if __name__ == '__main__':
dis = Dispatcher()
dis.reg('cmd1', lambda self: print('cmd1'))
dis.reg('cmd2', lambda self: print('cmd2'))
dis.run()
反射相关的魔术方法:__getattr__、__setattr__、__hasattr__
方法 | 意义 |
---|---|
__getattr__ | 当通过搜索实例、实例的类及祖先类查找不到属性时,就会调用此方法;如果没有这个方法,就会抛AttributeError异常 |
__setattr__ | 通过obj.x=100 方式增加、修改实例的属性都要调用__setattr__ ,包括初始化函数中的实例属性赋值 |
__delattr__ | 通过实例来删除属性时调用此方法,可以阻止通过实例删除属性的操作。但是通过类依然可以删除属性 |
__getattribute__ | 实例所有的属性调用都从这个方法开始,它阻止了属性的查找;该方法应该返回一个值或者抛出AttributeError. 如果return值,则作为属性的查找结果;如果抛出AttributeError,则会直接调用 __getattr__ 方法,表示没有找到属性。除非明确知道 __getattribute__ 用来做什么,否则不要使用此方法 |
示例1:
class Base:
n = 0
class Point(Base):
z = 6
def __init__(self, x, y):
self.x = x
self.y = y
def __getattr__(self, item):
return 'missing {}'.format(item)
def __setattr__(self, key, value):
print('setattr')
# self.__dict__[key] = value
if __name__ == '__main__':
p1 = Point(1, 2)
print(p1.x)
print(p1.z)
print(p1.n)
print(p1.t) # missing t
执行结果:
setattr
setattr
missing x
6
0
missing t
小结:
- 示例中给出了
__setattr__
方法,因此在类初始化时,设置实例属性调用了此方法,但是__setattr__
中没有完成实例__dict__
的操作,所以实例__dict__
没有属性x、y;通过p1.x
访问实例属性x,才调用了__getattr__
。 - 因此:
__setattr__
方法,可以拦截对实例属性的增加、修改操作;如果给出了这个方法,要想增加、修改实例属性生效,必须在方法中实现__dict__
操作。
示例2:
class P:
Z = 4
def __init__(self, x, y):
self.x = x
self.y = y
def __delattr__(self, item):
print('can not delete {}'.format(item))
if __name__ == '__main__':
p2 = P(4, 5)
del p2.x
p2.z = 13
del p2.z
del p2.Z
print(p2.__dict__)
print(P.__dict__)
del P.Z
print(P.__dict__)
执行结果:
can not delete x
can not delete z
can not delete Z
{‘x’: 4, ‘y’: 5, ‘z’: 13}
{‘module’: ‘main’, ‘Z’: 4, …}
{‘module’: ‘main’, ‘init’: <function P.init at 0x000002349845DAF0>, …}
小结: __delattr__
方法,可以阻止通过实例删除属性的操作。但是通过类依然可以删除属性。
示例3:
class Base:
n = 0
class C(Base):
C = 8
def __init__(self, x, y):
self.x = x
self.y = y
def __getattr__(self, item):
return 'missing {}'.format(item)
def __getattribute__(self, item):
# raise AttributeError
return 'getattribute:{}'.format(item)
if __name__ == '__main__':
c = C(3, 4)
print(c.__dict__)
print(c.x)
print(c.c)
print(c.n)
print(C.__dict__)
print(C.C)
执行结果:
getattribute:dict
getattribute:x
getattribute:c
getattribute:n
{‘module’: ‘main’, ‘C’: 8, ‘init’: <function C.init at 0x00000177F7A1DCA0>, ‘getattr’: <function C.getattr at 0x00000177F7A1DD30>, ‘getattribute’: <function C.getattribute at 0x00000177F7A1DDC0>, ‘doc’: None}
8
小结:
- 实例的所有属性访问,第一个都会调用
__getattribute__
方法,它阻止了属性的查找;该方法应该返回一个值或者抛出AttributeError.如果return值,则作为属性的查找结果;如果抛出AttributeError,则会直接调用__getattr__
方法,表示没有找到属性。 - 除非明确知道
__getattribute__
用来做什么,否则不要使用此方法
实例反射总结: 属性的查找顺序:实例调用__getattribute__() -> instance.__dict__ -> instance.__class__.__dict__ -> 继承的祖先类的__dict__ ->调用__getattr__()
13.3.12 描述器:__get__、__set__、__delete__
描述器: 当类的定义中实现了__get__、__set__、__delete__
三个魔术方法中的任意一个时,那么这个类就是一个描述器。
- 当仅实现了
__get__
,称为非数据描述器non data descriptor; - 当同时实现了
__get__
+__set_
或__delete__
就是数据描述器data descriptor 。常见的数据描述器:property。
owner属主: 如果一个类的类属性包含描述器,那么这个类称为owner属主。
1. 描述器通过__get__
方法,对类的实例读取类属性的控制:
__get___(self, instance, owner)
方法:
- instance:属主的实例,当通过类B的实例b读取属性x时,解释器自动传入b和B
- owner:属主,当通过类B读取属性x时,instance为None,只传入B
示例:
class A:
AA = 'aa'
def __init__(self):
print('A.init')
self.a1 = 'a1'
def __get__(self, instance, owner):
print('A:__get__', self, instance, owner)
return self # 通常return self
class B:
x = A()
def __init__(self):
print('B.init')
# self.x = 100
self.y = A()
if __name__ == '__main__':
print(B.x)
print(B.x.AA) # 通过类B读取类属性x,因为x是一个描述器,触发__get__
print('~~~~~~~~~~')
b = B()
print(b.x.AA) # 通过类B的实例b读取类属性x,因为x是一个描述器,触发__get__
print('~~~~~~~~~~')
print(b.y.a1) # 通过实例属性访问类A的实例的属性,不会触发__get__
小结:
- 当类A的实例x是类B的属性时,如果类A中给出了
__get__
方法,则对类B属性x的读取(不管是通过B的实例或B),或者进一步通过属性x访问类A的属性,都会触发__get__
方法;所以一般__get__
方法返回self。 - 当类A的实例是类B的实例的属性时,通过类B的实例属性访问类A的实例的属性的时候,不会触发
__get__
方法
2. 数据描述器通过__set__
或者__delete__
方法,对类的实例修改类属性的控制:
示例:
class C:
CC = 'cc'
def __init__(self):
print('C.init')
self.c1 = 'c1'
def __get__(self, instance, owner):
print('C:__get__', self, instance, owner)
return self # 通常return self
def __set__(self, instance, value):
print('C:__set__', self, instance, value)
class D:
x = C()
def __init__(self):
print('D.init')
self.x = 100
self.y = 123
if __name__ == '__main__':
print('-----------')
print(D.x)
d = D()
print(d.__dict__) # 查看实例d的__dict__是否有属性x
print(D.__dict__)
print('~~~~~~~~~~~~~~')
print(d.x)
d.x = 100 # 尝试为实例d增加属性x
print(d.x) # 查看‘赋值即定义’是否可以增加实例属性x。从结果可以看出,由于类属性x是数据描述器,由于受数据描述器拦截,无法给实例d的__dict__写入x,即增加x属性。
print(d.__dict__)
print('~~~~~~~~~~~~~')
小结:
- 当类D中存在一个标识符为x的类属性,且这个属性x为数据描述器时,则这个类属性x在类D的__dict__的优先级高于类D的实例__dict__的优先级;即通过类D的实例d进行访问、修改属性x操作时(d.x或d.x=100),优先访问
D.__dict__
。 - 所以,一但类属性x为数据描述器,则实例b只能操作类属性x,无标识符为x的实例属性;而操作类属性x又受描述器控制,才会有
d.x=100
时触发了数据描述器的__set__
方法,有点像运算符重载d.x=100 -> d.x.__set__(d, 100)
。 - 本质上:数据描述器
d.x=100 -> d.__dict__.get(x) -> d.__dict__没有x,继续找类的__dict_ ->d.__class__.__dict__[x] -> 找到了,但是x是描述器,类似运算符重载 -> d.__class__.__dict__[x].__set__()
,走不到写d.__dict__
的操作;d.x
也是一样,从自己的__dict__
中找不到x,就找类的,而类属性x又是一个描述器,进而触发了__get__
.
描述器总结: python的方法几乎都实现为非数据描述器(例如staticmethod、classmethod),因此实例可以重新定义一个标识符与类属性一样的实例属性,从而允许单个实例可以获得与同一类的其他实例不同的行为。property函数实现为一个数据描述器,因此被property装饰的类属性z,实例无法定义一个同为标识符z的实例属性。
示例:
class E:
@classmethod
def foo(cls):
pass
@staticmethod
def bar():
pass
@property
def z(self):
return 2
def __init__(self):
self.foo = 100 # foo和、bar方法都为非数据描述器,所以可以直接赋值修改
self.bar = 123
self.z = 'z' # z方法为数据描述器,不能在实例中替换
练习:实现classmethod和staticmethod
import functools
class ClassMethod:
def __init__(self, fn):
print(fn)
self._fn = fn
def __get__(self, instance, owner):
print(self, instance, owner)
# return self._fn(owner)
return functools.partial(self._fn, owner)
class StaticMethod:
def __init__(self, fn):
print(fn)
self._fn = fn
def __get__(self, instance, owner):
print(self, instance, owner)
return self._fn
class A:
@StaticMethod
def foo():
print('static')
@ClassMethod # foo = ClassMethod(foo) -> 新的foo是类ClassMethod的实例, 而ClassMethod非数据描述器,故通过A.foo读取类属性foo时,触发调用__get__,而__get__返回的是固定了参数cls的新的foo。所有最后可以直接foo()执行函数。
def bar(cls):
print(cls.__name__)
if __name__ == '__main__':
f = A.foo
print(f)
f()
b = A.bar
print(b)
b()
业务运用: 检查实例的参数类型