19、Python3 魔术方法
19.1、特殊属性
属性 | 含义 |
---|---|
__name__ |
类、函数、方法等的名字 |
__module__ |
类定义所在的模块名 |
__class__ |
对象或类所属的类 |
__bases__ |
类的基类的元组,顺序为它们在基类列表中出现的顺序 |
__doc__ |
类、函数的文档字符串,如果没有定义则为None |
__mro__ |
类的mro,class.mro()返回的结果保存在__mro__ 中 |
__dict__ |
类或实例的属性,可写的字典 |
19.2、查看属性
方法 | 意义 |
---|---|
__dir__ |
返回类或者对象的所有成员名称列表。 dir()函数就是调用 __dir__() 。如果提供 __dir__() ,则返回属性的列表,否则会尽量从__dict__ 属性中收集信息。 |
如果 dir([obj])
参数 obj 包含方法 __dir__()
,该方法将被调用。如果参数 obj 不包含 __dir__()
,该方法将最大限度地收集参数信息。
dir()
对于不同类型的对象具有不同的行为:
-
如果对象是模块对象,返回的列表包含模块的属性名。
-
如果对象是类型或者类对象,返回的列表包含类的属性名,及它的基类的属性名。
-
否则,返回的列表包含对象的属性名,它的类的属性名和类的基类的属性名。
19.3、魔术方法
分类:
- 创建、初始化与销毁
__init__
与__del__
- hash
- bool
- 可视化
- 运算符重载
- 容器和大小
- 可调用对象
- 上下文管理
- 反射
- 描述器
- 其他杂项
19.3.1、hash
方法 | 意义 |
---|---|
__hash__ |
内建函数hash() 调用的返回值,返回一个整数。如果定义这个方法该类的实例就可hash。 |
class A:
def __init__(self, name, age=18):
self.name = name
def __hash__(self):
return 1
def __repr__(self):
return self.name
print(hash(A('tom')))
print((A('tom'), A('tom')))
print([A('tom'), A('tom')])
print('~~~~~~~~~~~~~~~~~~~~~~~~~~~')
s = {A('tom'), A('tom')}
print(s) # 去重了吗
print({tuple('t'), tuple('t')})
print({('tom',), ('tom',)})
print({'tom', 'tom'})
上例中 set 为什么不能剔除相同的 key?
class A:
def __init__(self, name, age=18):
self.name = name
def __hash__(self):
return 1
def __eq__(self, other): # 这个函数作用
return self.name == other.name
def __repr__(self):
return self.name
print(hash(A('tom')))
print((A('tom'), A('tom')))
print([A('tom'), A('tom')])
print('~~~~~~~~~~~~~~~~~~~~~~~~~~~')
s = {A('tom'), A('tom')}
print(s) # 去重了吗
print({tuple('t'), tuple('t')})
print({('tom',), ('tom',)})
print({'tom', 'tom'})
方法 | 意义 |
---|---|
__eq__ |
对应 == 操作符,判断 2 个对象是否相等,返回 bool 值 |
__hash__
方法只是返回一个 hash 值作为 set 的 key,但是 去重
,还需要 __eq__
来判断 2 个对象是否相等。
hash 值相等,只是 hash 冲突,不能说明两个对象是相等的。
因此,一般来说提供 __hash__
方法是为了作为 set 或者 dict 的 key,所以 去重
要同时提供 __eq__
方法。
不可 hash 对象 isinstance(p1, collections.Hashable)
一定为 False。
去重
需要提供 __eq__
方法。
思考:list 类实例为什么不可 hash?
源码中有一句 __hash__
= None,也就是如果调用 __hash__()
相当于 None(),一定报错。
所有类都继承 object,而这个类是具有 __hash__()
方法的,如果一个类不能被 hash,就把 __hash__
设置为 None。
练习:设计二维坐标类 Point,使其成为可 hash 类型,并比较 2 个坐标的实例是否相等?
from typing import Hashable
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
return self.x == other.x and self.y == other.y
p1 = Point(4, 5)
p2 = Point(4, 5)
print(1, '-->', hash(p1))
print(2, '-->', hash(p2))
print(3, '-->', p1 is p2)
print(4, '-->', p1 == p2) # True 使用 __eq__
print(5, '-->', hex(id(p1)), hex(id(p2)))
print(6, '-->', set((p1, p2)))
print(7, '-->', isinstance(p1, Hashable))
19.3.2、bool
方法 | 意义 |
---|---|
__bool__ |
内建函数 bool() 操作,或者把对象放在逻辑表达式的位置判断,都会调用这个函数返回布尔值。没有定义__bool__() ,就找__len__() 返回长度,非0为真。如果__len__() 也没有定义,那么所有实例都返回真。 |
class A: pass
print(1, '-->', bool(A()))
if A():
print(2, '-->', 'Real A')
class B:
def __bool__(self):
return False
print(3, '-->', bool(B))
print(4, '-->', bool(B()))
if B():
print(5, '-->', 'Real B')
class C:
def __len__(self):
return 0
print(6, '-->', bool(C()))
if C():
print(7, '-->', 'Real C')
19.3.3、可视化
方法 | 意义 |
---|---|
__repr__ |
内建函数repr()对一个对象获取字符串表达。 调用 __repr__ 方法返回字符串表达,如果__repr__ 也没有定义,就直接返回 object 的定义,就是显示内存地址信息。 |
__str__ |
str() 函数、内建函数 format()、print()函数调用,需要返回对象的字符串表达。如果没有定义,就去调用__repr__ 方法返回字符串表达,如果__repr__ 没有定义,就直接返回对象的内存地址信息。 |
__bytes__ |
bytes()函数调用,返回一个对象的bytes表达,即返回bytes对象。 |
class A:
def __init__(self, name, age=18):
self.name = name
self.age = age
def __repr__(self):
return 'repr: {}, {}'.format(self.name, self.age)
def __str__(self):
return 'str: {} {}'.format(self.name, self.age)
def __bytes__(self):
import json
return json.dumps(self.__dict__).encode()
print(1, '-->', A('tom')) # print 函数使用 __str__
print(2, '-->', [A('tom')]) # []使用__str__,但其内部使用__repr__
print(3, '-->', ([str(A('tom'))])) # []使用__str__,str()函数也使用__str__
print(4, '-->', 'str:a,1') # 字符串直接输出没有引号
s = '1'
print(5, '-->', s)
print(6, '-->', ['a'], (s,)) # 字符串在基本数据类型内部输出有引号
print(7, '-->', {s, 'a'})
print(8, '-->', bytes(A('tom')))
19.3.4、运算符重载
operator 模块提供以下的特殊方法,可以将类的实例使用下面的操作符来操作。
运算符 | 特殊方法 | 含义 |
---|---|---|
<, <=, ==, >, >=, != |
__lt__ , __le__ , __eq__ , __gt__ , __ge__ , __ne__ |
比较运算符 |
+, -, *, /, %, //, **, divmod |
__add__ , __sub__ , __mul__ , __truediv__ , __mod__ , __floordiv__ , __pow__ , __divmod__ |
算数运算符,移位、位运算也有对应的方法 |
+=, -=, *=, /=, %=, //=, **= |
__iadd__ , __isub__ , __imul__ , __itruediv__ , __imod__ , __ifloordiv__ , __ipow__ |
class A:
def __init__(self, name, age=18):
self.name = name
self.age = age
def __sub__(self, other):
return self.age - other.age # 返回的是一个int数据
def __isub__(self, other):
return A(self.name, self - other) # 返回的还是一个对象
tom = A('tom')
jerry = A('jerry', 16)
print(1, '-->', type(tom - jerry), tom - jerry) # 返回的是一个int数据
print(2, '-->', jerry - tom, jerry.__sub__(tom))
print(3, '-->', id(tom))
tom -= jerry # 返回的还是一个对象
print(4, '-->', type(tom), tom.age, id(tom))
练习:
完成 Point 类设计,实现判断点相等的方法,并完成向量的加法。
在直角坐标系里面,定义原点为向量的起点,两个向量和与差的坐标分别等于这两个向量相应坐标的和与差,若向量的表示为 (x, y) 形式。
A(X1,Y1) B(X2,Y2),则 A+B=(X1+X2, Y1+Y2), A-B=(X1-X2, Y1-Y2)
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def add(self, other):
return (self.x + other.x, self.y + other.y)
def __str__(self):
return '<Point: {}, {}>'.format(self.x, self.y)
p1 = Point(1, 1)
p2 = Point(1, 1)
points = (p1, p2)
print(1, '-->', points[0].add(points[1]))
# 运算符重载
print(points[0] + points[1])
print(p1 == p2)
往往用面向对象实现的类,需要做大量的运算,而运算符是数学上最常见的表达方式。
例如,上例中对 +
进行了运算符重载,实现了 Point 类的二元操作,重新定义为 Point+Point
。
提供运算符重载,比直接提供加法方法要更加适合该领域内使用者的习惯。
int 类,几乎实现了所有操作符,可以作为参考。
19.3.5、@functools.total_ordering
__lt__
, __le__
, __eq__
, __gt__
, __ge__
是比较大小必须实现的方法,但是全部写完太麻烦,使用 @functools.total_ordering
装饰器就可以大大简化代码。
但是要求 __eq__
必须实现,其它方法 __lt__
, __le__
, __gt__
, __ge__
实现其一。
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __gt__(self, other):
return self.age > other.age
tom = Person('tom', 20)
jerry = Person('jerry', 16)
print(tom > jerry)
print(tom < jerry)
print(tom >= jerry)
print(tom <= jerry)
上例中大大简化代码,但是一般来说,实现等于或者小于方法也就够了,其它可以不实现。这个装饰器只是看着很美好,但是可能会带来性能问题,建议需要什么方法就自己创建,少用这个装饰器。
19.3.6、容器相关方法
方法 | 意义 |
---|---|
__len__ |
内建函数len(),返回对象的长度(>=0的整数),如果把对象当作容器类型看,就如同list或者dict。 bool()函数调用的时候,如果没有 __bool__() 方法,则会看__len__() 方法是否存在,存在返回非0为真。 |
__iter__ |
迭代容器时调用,返回一个新的迭代器对象。 |
__contains__ |
in 成员运算符,没有实现,就调用__iter__ 方法遍历。 |
__getitem__ |
实现self[key]访问。序列对象,key接受整数为索引,或者切片。 对于set和dict,key为hashable。key不存在引发KeyError异常。 |
__setitem__ |
和__getitem__ 的访问类似,是设置值的方法。 |
__missing__ |
字典或其子类使用__getitem__() 调用时,key不存在执行该方法。 |
class A(dict):
def __missing__(self, key):
print('Missing key:', key)
return 0
a = A()
print(a['k'])
思考:为什么空字典、空字符串、空元组、空集合、空列表等可以等效为 False?
练习:将购物车类改造成方便操作的容器类。
class Cart:
def __init__(self):
self.items = []
def __len__(self):
return len(self.items)
def additem(self, item):
self.items.append(item)
def __iter__(self):
return iter(self.items)
def __getitem__(self, index): # 索引访问
return self.items[index]
def __setitem__(self, key, value): # 索引赋值
self.items[key] = value
def __str__(self):
return str(self.items)
def __add__(self, other):
self.items.append(other)
return self
cart = Cart()
cart.additem(1)
cart.additem('abc')
cart.additem('3')
# 长度、bool
print(1, '-->', len(cart))
print(2, '-->', bool(cart))
# 迭代
for x in cart:
print(x)
# in
print(3, '-->', 3 in cart)
print(4, '-->', 2 in cart)
# 索引操作
print(5, '-->', cart[1])
cart[1] = 'xyz'
print(cart)
# 链式编程实现加法
print(cart + 4 + 5 + 6)
print(cart.__add__(17).__add__(18))
19.3.7、可调用对象
Python 中一切皆对象,函数也不例外。
def hello():
print(hello.__module__, hello.__name__)
hello()
# 等价于
hello.__call__()
函数即对象,对象 hello 加上(),就是调用对象的 __call__()
方法。
方法 | 意义 |
---|---|
__call__ |
类中定义一个该方法,实例就可以像函数一样调用 |
可调用对象:定义一个类,并实例化得到其实例,将实例像函数一样调用。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __call__(self, *args, **kwargs):
return '<Point {}:{}>'.format(self.x, self.y)
p = Point(4, 5)
print(p)
print(p())
练习:定义一个斐波那契数列的类,方便调用,计算第 n 项。
class Fib:
def __init__(self):
self.items = [0, 1, 1]
def __call__(self, index):
if index < 0:
raise IndexError('Wrong Index')
if index < len(self.items):
return self.items[index]
for i in range(3, index + 1):
self.items.append(self.items[i - 1] + self.items[i - 2])
return self.items[index]
print(Fib()(10))
上例中,增加迭代的方法、返回容器长度、支持索引的方法:
class Fib:
def __init__(self):
self.items = [0, 1, 1]
def __len__(self):
return len(self.items)
def __iter__(self):
return iter(self.items)
def __getitem__(self, index):
if index < 0:
raise IndexError('Wrong Index')
if index < len(self.items):
return self.items[index]
for i in range(len(self), index + 1):
self.items.append(self.items[i - 1] + self.items[i - 2])
return self.items[index]
def __call__(self, index):
return self[index]
def __str__(self):
return str(self.items)
__repr__ = __str__
fib = Fib()
print(fib(5), len(fib)) # 全部计算
print(fib(10), len(fib)) # 部分计算
for x in fib:
print(x)
print(fib[5], fib[6]) # 索引访问,不计算
可以看出使用类来实现斐波那契数列也是非常好的实现,还可以缓存数据,便于检索。
19.3.8、上下文管理
文件 IO 操作可以对文件对象使用上下文管理,使用 with...as
语法。
with open('access.log') as f:
pass
仿照上例写一个自己的类,实现上下文管理:
class Point:
pass
with Point() as p: # AttributeError: __enter__
pass
提示属性错误,没有 __enter__
,看来需要这个属性。
19.3.8.1、上下文管理对象
当一个对象同时实现了 __enter__()
和 __exit__()
方法,它就属于上下文管理的对象。
方法 | 意义 |
---|---|
__enter__ |
进入与此对象相关的上下文。如果存在该方法,with 语法会把该方法的返回值绑定到 as 子句中指定的变量上。 |
__exit__ |
退出与此对象相关的上下文。 |
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit')
with Point() as f:
print('do sth')
实例化对象的时候,并不会调用 enter,进入 with 语句块调用 __enter__
方法,然后执行语句体最后离开 with 语句块的时候,调用 __exit__
方法。
with 可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。
19.3.8.2、上下文管理的安全性
看看异常对上下文的影响。
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit')
with Point() as f:
raise Exception('error')
print('do sth')
输出结果:
init
enter
exit
Traceback (most recent call last):
File "D:\JetBrains\pythonProject\main.py", line 13, in <module>
raise Exception('error')
Exception: error
可以看出 enter 和 exit 照样执行,上下文管理是安全的。
极端的例子:
调用 sys.exit()
,它会退出当前解释器。
打开 Python 解释器,在里面敲入 sys.exit()
,窗口直接关闭了。也就是说碰到这一句,Python 运行环境直接退出了。
import sys
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit')
with Point() as f:
sys.exit(-100)
print('do sth')
print('outer')
从执行结果来看,依然执行了__exit__
函数,哪怕是退出 Python 运行环境。
说明上下文管理很安全。
19.3.8.3、with 语句
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit')
p = Point()
with p as f:
print(p == f) # 为什么不相等
print('do sth.')
问题在于 __enter__
方法上,它将自己的返回值赋给 f。修改上例。
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit')
p = Point()
with p as f:
print(p == f) # 为什么不相等
print('do sth.')
__enter__
方法返回值就是上下文中使用的对象,with 语法会把它的返回值赋给 as 子句的变量。
19.3.8.4、上下文参数
__enter__
方法没有其他参数。
__exit__
方法有 3 个参数:
__exit__(self, exc_type, exc_val, exc_tb):
这三个参数都与异常有关。
如果该上下文退出时没有异常,这3个参数都为None。
如果有异常,参数意义如下:
exc_type
,异常类型。
exc_value
,异常值。
exc_tb
,异常的追踪信息。
__exit__
方法返回一个等效 True 的值,则压制异常;否则,继续抛出异常。
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(exc_type)
print(exc_val)
print(exc_tb)
print('exit')
return 'abc'
p = Point()
with p as f:
raise Exception('New Error')
print('do sth.')
print('outer')
19.3.8.5、练习
为加法函数计时。
方法1、使用装饰器显示该函数的执行时长。
方法2、使用上下文管理方法来显示该函数的执行时长。
import time
def add(x, y):
time.sleep(2)
return x + y
装饰器实现:
import time
import datetime
from functools import wraps
def timeit(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
print('{} took {}s'.format(fn.__name__, delta))
return ret
return wrapper
@timeit
def add(x, y):
time.sleep(2)
return x + y
print(add(4, 5))
上下文实现:
import time
import datetime
def add(x, y):
time.sleep(2)
return x + y
class Timeit:
def __init__(self, fn):
self.fn = fn
def __enter__(self):
self.start = datetime.datetime.now()
return self.fn
def __exit__(self, exc_type, exc_val, exc_tb):
delta = (datetime.datetime.now() - self.start).total_seconds()
print('{} took {}'.format(self.fn.__name__, delta))
with Timeit(add) as fn:
print(fn(4, 6))
print(add(4, 7))
使用可调用对象实现:
import time
import datetime
def add(x, y):
time.sleep(2)
return x + y
class Timeit:
def __init__(self, fn):
self.fn = fn
def __enter__(self):
self.start = datetime.datetime.now()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
delta = (datetime.datetime.now() - self.start).total_seconds()
print('{} took {}'.format(self.fn.__name__, delta))
def __call__(self, x, y):
print(x, y)
return self.fn(x, y)
with Timeit(add) as instance:
print(instance(5, 6))
根据上面的代码,能不能把类当做装饰器用?
import time
import datetime
class TimeIt:
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
self.start1 = datetime.datetime.now()
ret = self.fn(*args, **kwargs)
self.delta1 = (datetime.datetime.now() - self.start1).total_seconds()
print('{} took {}s in call'.format(self.fn.__name__, self.delta1))
return ret
@TimeIt
def add(x, y):
"""This is add function"""
time.sleep(2.7)
return x + y
add(4, 5)
print(add.__doc__)
思考:如何解决文档字符串问题?
方法一:直接修改 __doc__
class TimeIt:
def __init__(self, fn):
self.fn = fn
# 把函数对象的文档字符串赋给类
self.__doc__ = fn.__doc__
方法二:使用 functools.wraps 函数
class TimeIt:
def __init__(self, fn):
self.fn = fn
# 把函数对象的文档字符串赋给类
# self.__doc__ = fn.__doc__
# update_wrapper(self,fn)
wraps(fn)(self)
上面的类即可以用在上下文管理,又可以用做装饰器。
19.3.9、上下文应用场景
增强功能:在代码执行的前后增加代码,以增强其功能。类似装饰器的功能。
资源管理:打开了资源需要关闭,例如文件对象、网络连接、数据库连接等。
权限验证:在执行代码之前,做权限的验证,在 __enter__
中处理。
19.3.10、contextlib.contextmanager
contextlib.contextmanager
它是一个装饰器实现上下文管理,装饰一个函数,而不用像类一样实现 __enter__
和 __exit__
方法。
对下面的函数有要求,必须有 yield,也就是这个函数必须返回一个生成器,且只有 yield 一个值。
也就是这个装饰器接收一个生成器对象作为参数。
import contextlib
@contextlib.contextmanager
def foo():
print('enter') # 相当于__enter__()
yield # yield 5, yield的值只能有一个,作为__enter__方法的返回值
print('exit') # 相当于__exit__()
with foo() as f:
# raise Exception()
print(f)
f 接收 yield 语句的返回值。
上面的程序看似不错,但是,增加一个异常试一试,发现不能保证 exit 的执行,怎么办?
增加 try finally。
import contextlib
@contextlib.contextmanager
def foo():
print('enter') # 相当于__enter__()
try:
yield # yield 5, yield的值只能有一个,作为__enter__方法的返回值
finally:
print('exit') # 相当于__exit__()
with foo() as f:
raise Exception()
print(f)
上例这么做有什么意义呢?
在 yield 发生处为生成器函数增加了上下文管理:
-
把 yield 之前的当做
__enter__
方法执行。 -
把 yield 之后的当做
__exit__
方法执行。 -
把 yield 的值作为
__enter__
的返回值。
import contextlib
import datetime
import time
@contextlib.contextmanager
def add(x, y):
start = datetime.datetime.now()
try:
yield x + y
finally:
delta = (datetime.datetime.now() - start).total_seconds()
print(delta)
with add(4, 5) as f:
# raise Exception()
time.sleep(2.5)
print(f)
总结:
如果业务逻辑简单可以使用函数加 contextlib.contextmanager 装饰器方式,如果业务复杂,用类的方式加 __enter__
和 __exit__
方法更方便。
19.3.11、反射
运行时,区别于编译时,指的是程序被加载到内存中执行的时候。
反射,reflection,指的是运行时获取类型定义信息。
一个对象能够在运行时,像照镜子一样,反射出其类型信息。
简单说,在 Python 中,能够通过一个对象,找出其 type、class、attribute 或 method 的能力,称为反射或者自省。
具有反射能力的函数有:type()、isinstance()、callable()、dir()、getattr()。
19.3.11.1、反射相关的函数和方法
需求:有一个Point类,查看它实例的属性,并修改它。动态为实例增加属性。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return 'Point({}, {})'.format(self.x, self.y)
def show(self):
print(self.x, self.y)
p = Point(4, 5)
print(1, '-->', p)
print(2, '-->', p.__dict__)
p.__dict__['y'] = 16
print(3, '-->', p.__dict__)
p.z = 10
print(4, '-->', p.__dict__)
print(5, '-->', dir(p))
print(6, '-->', p.__dir__())
上例通过属性字典 __dict__
来访问对象的属性,本质上也是利用反射的能力。
但是,上面的例子中,访问的方式不优雅,Pthon 提供了内置的函数。
内建函数 | 意义 |
---|---|
getattr(object, name[,default]) | 通过 name 返回 object 的属性值。当属性不存在,将使用 default 返回,如果没有 default,则抛出 AttributeError。name 必须为字符串。 |
setattr(object, name, value) | object 的属性存在,则覆盖,不存在,新增。 |
hasattr(object, name) | 判断对象是否有这个名字的属性,name 必须为字符串。 |
用上面的方法来修改上例的代码:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return 'Point({}, {})'.format(self.x, self.y)
def show(self):
print(self)
p1 = Point(4, 5)
p2 = Point(10, 10)
print(1, '-->', repr(p1), repr(p2))
print(2, '-->', p1.__dict__)
setattr(p1, 'y', 16)
setattr(p1, 'z', 10)
print(3, '-->', getattr(p1, '__dict__'))
# 动态调用方法
if hasattr(p1, 'show'):
getattr(p1, 'show')()
# 动态增加方法,给类增加
if not hasattr(Point, 'add'):
setattr(Point, 'add', lambda self, other: Point(self.x + other.x, self.y + other.y))
print(4, '-->', Point.add)
print(5, '-->', p1.add)
print(6, '-->', p1.add(p2)) # 绑定
# 为实例增加方法,未绑定
if not hasattr(p1, 'sub'):
setattr(p1, 'sub', lambda self, other:Point(self.x - other.x, self.y-other.y))
print(7, '-->', p1.sub(p1, p1))
print(8, '-->', p1.sub)
# add 在谁里面,sub在谁里面
print(9, '-->', p1.__dict__)
print(10, '-->', Point.__dict__)
思考:这种动态增加属性的方式和装饰器修饰一个类、Mixin 方式的差异?
这种动态增删属性的方式是运行时改变类或者实例的方式,但是装饰器或 Mixin 都是定义时就决定了,因此反射能力具有更大的灵活性。
练习:命令分发器,通过名称找对应的函数执行。
思路:名称找对象的方法。
class Dispatch:
def __init__(self):
self._run()
def cmd1(self):
print("I'm cmd1")
def cmd2(self):
print("I'm cmd2")
def _run(self):
while True:
cmd = input('Plz input a command: ').strip()
if cmd == 'quit':
break
getattr(self, cmd, lambda: print('Unknow Command {}'.format(cmd)))()
Dispatch()
上例中使用 getattr 方法找到对象的属性,比自己维护一个字典来建立名称和函数之间的关系好多了。
19.3.11.2、反射相关的魔术方法
__getattr__()
、__setattr__()
、__delattr__()
这三个魔术方法,分别测试。
19.3.11.2.1、__getattr__()
class Base:
n = 0
class Point(Base):
z = 6
def __init__(self, x, y):
self.x = x
self.y = y
def show(self):
print(self.x, self.y)
def __getattr__(self, item):
return 'missing {}'.format(item)
p1 = Point(4, 5)
print(1, '-->', p1.x)
print(2, '-->', p1.z)
print(3, '-->', p1.n)
print(4, '-->', p1.t) # missing
一个类的属性会按照继承关系找,如果找不到,就会执行 __getattr__()
方法,如果没有这个方法,就会抛出 AttributeError 异常表示找不到属性。
查找属性顺序为:
instance.__dict__
--> instance.__class__.__dict__
--> 继承的祖先类(直到object).__dict__
-->找不到--> 调用__getattr__()
。
19.3.11.2.2、__setattr__()
class Base:
n = 0
class Point(Base):
z = 6
def __init__(self, x, y):
self.x = x
self.y = y
def show(self):
print(self.x, self.y)
def __getattr__(self, item):
return 'missing {}'.format(item)
def __setattr__(self, key, value):
print("setattr {}={}".format(key, value))
p1 = Point(4, 5)
print(1, '-->', p1.x) # why missing?
print(2, '-->', p1.z)
print(3, '-->', p1.n)
print(4, '-->', p1.t) # missing
p1.x = 50
print(5, '-->', p1.__dict__)
p1.__dict__['x'] = 60
print(6, '-->', p1.__dict__)
print(7, '-->', p1.x)
输出:
setattr x=4
setattr y=5
1 --> missing x
2 --> 6
3 --> 0
4 --> missing t
setattr x=50
5 --> {}
6 --> {'x': 60}
7 --> 60
实例通过 .
设置属性,如同 self.x = x
,就会调用 __setattr__()
,属性要加到实例的 __dict__
中,就需要自己完成。
class Point(Base):
z = 6
def __init__(self, x, y):
self.x = x
self.y = y
def show(self):
print(self.x, self.y)
def __getattr__(self, item):
return 'missing {}'.format(item)
def __setattr__(self, key, value):
print("setattr {}={}".format(key, value))
self.__dict__[key] = value
__setattr__()
方法,可以拦截对实例属性的增加、修改操作,如果要设置生效,需要自己操作实例的 __dict__
。
19.3.11.2.3、__delattr__()
class Point:
Z = 5
def __init__(self, x, y):
self.x = x
self.y = y
def __delattr__(self, item):
print("Can not del {}".format(item))
p = Point(14, 5)
del p.x
p.z = 15
del p.z
del p.Z
print(Point.__dict__)
print(p.__dict__)
del Point.Z
print(Point.__dict__)
可以阻止通过实例删除属性的操作。但是通过类依然可以删除属性。
19.3.11.2.4、__getattribute__
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 __getattribute__(self, item):
return item
p1 = Point(4, 5)
print(1, '-->', p1.__dict__)
print(2, '-->', p1.x)
print(3, '-->', p1.z)
print(4, '-->', p1.n)
print(5, '-->', p1.t)
print(6, '-->', Point.__dict__)
print(7, '-->', Point.z)
输出:
1 --> __dict__
2 --> x
3 --> z
4 --> n
5 --> t
6 --> {'__module__': '__main__', 'z': 6, '__init__': <function Point.__init__ at 0x000001B5FF48BAC0>, '__getattr__': <function Point.__getattr__ at 0x000001B5FF769EA0>, '__getattribute__': <function Point.__getattribute__ at 0x000001B5FF7697E0>, '__doc__': None}
7 --> 6
实例所有的属性访问,第一个都会调用 __getattribute__
方法,它阻止了属性的查找,该方法应该返回(计算后的)值或者抛出一个 AttributeError 异常。
它的 return 值将作为属性查找的结果,它如果抛出 AttributeError 异常,则会直接调用 __getattr__
方法,因为表示属性没有找到。
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 __getattribute__(self, item):
# raise AttributeError("Not fount")
# return self.__dict__[item] # RecursionError: maximum recursion depth exceeded
return object.__getattribute__(self, item)
p1 = Point(4, 5)
print(1, '-->', p1.__dict__)
print(2, '-->', p1.x)
print(3, '-->', p1.z)
print(4, '-->', p1.n)
print(5, '-->', p1.t)
print(6, '-->', Point.__dict__)
print(7, '-->', Point.z)
__getattribute__
方法,为了避免在该方法中无限的递归,它的实现应该永远调用基类的同名方法以访问需要的任何属性,例如 object.__getattribute__(self,name)
。
注意,除非你明确地知道 __getattribute__
方法用来做什么,否则不要使用它。
总结:
魔术方法 | 意义 |
---|---|
__getattr__() |
当通过搜索实例、实例的类及祖先类查不到属性,就会调用此方法。 |
__setattr__() |
通过 . 访问实例属性,进行增加、修改都要调用它。 |
__delattr__() |
当通过实例来删除属性时调用此方法。 |
__getattribute__ |
实例所有的属性调用都从这个方法开始。 |
属性查找顺序:
实例调用__getattribute__()
--> instance.__dict__
--> instance.__class__.__dict__
--> 继承祖先类(直到object).__dict__
--> 调用__getattr__()
。
19.3.12、描述器
19.3.12.1、描述器概念
用到 3 个魔术方法:__get__()
、__set__()
、__delete__()
。
方法签名如下:
object.__get__(self, instance, owner)
object.__set__(self, instance, value)
object.__delete__(self, instance)
self 指当前实例,调用者。
instance 是 owner 的实例。
owner 是属性所属的类。
请思考下面程序的执行流程是什么?
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
class B:
x = A()
def __init__(self):
print('B.init')
print('-' * 20)
print(B.x.a1)
print('*' * 20)
b = B()
print(b.x.a1)
输出:
A.init
--------------------
a1
********************
B.init
a1
可以看出执行的先后顺序吧?
类加载的时候,类变量需要先生成,而类 B 的 x 属性是类 A 的实例,所以类 A 先初始化,所以打印 A.init
。
然后执行到打印 B.x.a1
。
然后实例化并初始化 B 的实例 b。
打印 b.x.a1
,会查找类属性 b.x,指向 A 的实例,所以返回 A 实例的属性 a1 的值。
看懂执行流程了,再看下面的程序,对类 A 做一些改造。如果在类 A 中实现 __get__
方法,看看变化。
class A:
def __init__(self):
self.a1 = 'a1'
print(1, '-->', 'A.init')
def __get__(self, instance, owner):
print(2, '-->', "A.__get__ {} {} {}".format(self, instance, owner))
class B:
x = A()
def __init__(self):
print(3, '-->', 'B.init')
print('-' * 20)
print(4, '-->', B.x)
# print(5, '-->', B.x.a1) # AttributeError: 'NoneType' object has no attribute 'a1'
print('*' * 20)
b = B()
print(6, '-->', b.x)
# print(7, '-->', b.x.a1) # AttributeError: 'NoneType' object has no attribute 'a1'
输出:
1 --> A.init
--------------------
2 --> A.__get__ <__main__.A object at 0x0000021A810B3EB0> None <class '__main__.B'>
4 --> None
********************
3 --> B.init
2 --> A.__get__ <__main__.A object at 0x0000021A810B3EB0> <__main__.B object at 0x0000021A810B3E20> <class '__main__.B'>
6 --> None
因为定义了 __get__
方法,类 A 就是一个描述器,对类 B 或者类 B 的实例的 x 属性读取,成为对类 A 的实例的访问,就会调用 __get__
方法。
如何解决上例中访问报错的问题,问题应该来自 __get__
方法。
self, instance, owner 这三个参数,是什么意思?
<__main__.A object at 0x0000021A810B3EB0> None <class '__main__.B'>
<__main__.A object at 0x0000021A810B3EB0> <__main__.B object at 0x0000021A810B3E20> <class '__main__.B'>
self 就是 A 的实例。
owner 就是 B 类。
instance 说明:
- None 表示没有B类的实例,对应调用B.x
- <__main__.B object at 0x0000021A810B3E20> 表示B类的实例,对应调用B().x
使用返回值解决。返回 self,就是 A 的实例,该实例有 a1 属性,返回正常。
class A:
def __init__(self):
self.a1 = 'a1'
print(1, '-->', 'A.init')
def __get__(self, instance, owner):
print(2, '-->', "A.__get__ {} {} {}".format(self, instance, owner))
return self # 解决返回None的问题
class B:
x = A()
def __init__(self):
print(3, '-->', 'B.init')
print('-' * 20)
print(4, '-->', B.x)
print(5, '-->', B.x.a1)
print('*' * 20)
b = B()
print(6, '-->', b.x)
print(7, '-->', b.x.a1)
如果说在类 B 的实例上设置一个属性指向类 A 的实例,可以吗?
class A:
def __init__(self):
self.a1 = 'a1'
print(1, '-->', 'A.init')
def __get__(self, instance, owner):
print(2, '-->', "A.__get__ {} {} {}".format(self, instance, owner))
return self # 解决返回None的问题
class B:
x = A()
def __init__(self):
print(3, '-->', 'B.init')
self.b = A() # 实例属性也指向一个A的实例
print('-' * 20)
print(4, '-->', B.x)
print(5, '-->', B.x.a1)
print('*' * 20)
b = B()
print(6, '-->', b.x)
print(7, '-->', b.x.a1)
print(8, '-->', b.b) # 并没有触发__get__
从运行结果可以看出,只有类属性是类的实例才可以触发 __get__
。
19.3.12.2、描述器定义
Python3 中,一个类实现了 __get__
、__set__
、__delete__
三个方法中的任何一个方法,就是描述器。
如果仅实现了 __get__
,就是非数据描述符 non-data descriptor;
同时实现了 __get__
、__set__
就是数据描述符 data descriptor。
如果一个类的类属性设置为描述器,那么这个类被称为 owner 属主。
19.3.12.3、属性的访问顺序
为上例中的类 B 增加实例属性 x:
class A:
def __init__(self):
self.a1 = 'a1'
print(1, '-->', 'A.init')
def __get__(self, instance, owner):
print(2, '-->', "A.__get__ {} {} {}".format(self, instance, owner))
return self # 解决返回None的问题
class B:
x = A()
def __init__(self):
print(3, '-->', 'B.init')
self.x = 'b.x' # 增加实例属性 x
print('-' * 20)
print(4, '-->', B.x)
print(5, '-->', B.x.a1)
print('*' * 20)
b = B()
print(6, '-->', b.x)
print(7, '-->', b.x.a1) # AttributeError: 'str' object has no attribute 'a1'
b.x
访问到了实例的属性,而不是描述器。
继续修改代码,为类 A 增加 __set__
方法。
class A:
def __init__(self):
self.a1 = 'a1'
print(1, '-->', 'A.init')
def __get__(self, instance, owner):
print(2, '-->', "A.__get__ {} {} {}".format(self, instance, owner))
return self # 解决返回None的问题
def __set__(self, instance, value):
print(2.1, '-->', 'A.__set__ {} {} {}'.format(self, instance, value))
self.data = value
class B:
x = A()
def __init__(self):
print(3, '-->', 'B.init')
self.x = 'b.x' # 增加实例属性 x
print('-' * 20)
print(4, '-->', B.x)
print(5, '-->', B.x.a1)
print('*' * 20)
b = B()
print(6, '-->', b.x)
print(7, '-->', b.x.a1) # 返回 a1
print(8, '-->', b.x.data)
输出:
1 --> A.init
--------------------
2 --> A.__get__ <__main__.A object at 0x000001F4F7FE3FD0> None <class '__main__.B'>
4 --> <__main__.A object at 0x000001F4F7FE3FD0>
2 --> A.__get__ <__main__.A object at 0x000001F4F7FE3FD0> None <class '__main__.B'>
5 --> a1
********************
3 --> B.init
2.1 --> A.__set__ <__main__.A object at 0x000001F4F7FE3FD0> <__main__.B object at 0x000001F4F7FE3E20> b.x
2 --> A.__get__ <__main__.A object at 0x000001F4F7FE3FD0> <__main__.B object at 0x000001F4F7FE3E20> <class '__main__.B'>
6 --> <__main__.A object at 0x000001F4F7FE3FD0>
2 --> A.__get__ <__main__.A object at 0x000001F4F7FE3FD0> <__main__.B object at 0x000001F4F7FE3E20> <class '__main__.B'>
7 --> a1
2 --> A.__get__ <__main__.A object at 0x000001F4F7FE3FD0> <__main__.B object at 0x000001F4F7FE3E20> <class '__main__.B'>
8 --> b.x
返回变成了 a1,访问到了描述器的数据。
给我们的感觉好像是这么一个属性查找顺序:
-
实例的
__dict__
优先于非数据描述器。 -
数据描述器优先于实例的
__dict__
。
__delete__
方法有同样的效果,有了这个方法,就是数据描述器。
尝试着增加下面的代码,看看字典的变化:
b.x = 500,这是调用数据描述器的 __set__
方法,或调用非数据描述器的实例覆盖。
B.x = 600,赋值即定义,这是覆盖类属性。
19.3.12.4、描述器本质
Python 真的会做的这么复杂吗,再来一套属性查找顺序规则?看看非数据描述器和数据描述器,类 B 及其 __dict__
的变化。
屏蔽和不屏蔽 __set__
方法,看看变化。
class A:
def __init__(self):
self.a1 = 'a1'
print(1, '-->', 'A.init')
def __get__(self, instance, owner):
print(2, '-->', "A.__get__ {} {} {}".format(self, instance, owner))
return self # 解决返回None的问题
# def __set__(self, instance, value):
# print(2.1, '-->', 'A.__set__ {} {} {}'.format(self, instance, value))
# self.data = value
class B:
x = A()
def __init__(self):
print(3, '-->', 'B.init')
self.x = 'b.x' # 增加实例属性 x
self.y = 'b.y'
print('-' * 20)
print(4, '-->', B.x)
print(5, '-->', B.x.a1)
print('*' * 20)
b = B()
print(6, '-->', b.x)
# print(7, '-->', b.x.a1) # 返回 a1
print(b.y)
print('字典')
print(b.__dict__)
print(B.__dict__)
# 屏蔽 __set__ 方法结果如下:
字典
{'x': 'b.x', 'y': 'b.y'}
{'__module__': '__main__', 'x': <__main__.A object at 0x00000239418B3FD0>, '__init__': <function B.__init__ at 0x00000239418C9F30>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
# 不屏蔽 __set__ 方法结果如下:
字典
{'y': 'b.y'}
{'__module__': '__main__', 'x': <__main__.A object at 0x000002C4A99E3FD0>, '__init__': <function B.__init__ at 0x000002C4A99F9FC0>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
原来不是什么数据描述器优先级高,而是把实例的属性从 __dict__
中给去除掉了,造成了该属性如果是数据描述器优先访问的假象。说到底,属性访问顺序从来就没有变过。
19.3.13、Python 中的描述器
描述器在 Python 中应用非常广泛。
Python 的方法(包括 staticmethod() 和 classmethod()) 都实现为非数据描述器。因此,实例可以重新定义和覆盖方法。这允许单个实例获取与同一类的其他实例不同的行为。
property() 函数实现为一个数据描述器。因此,实例不能覆盖属性的行为。
class A:
@classmethod
def foo(cls): # 非数据描述器
pass
@staticmethod # 非数据描述器
def bar():
pass
@property # 数据描述器
def z(self):
return 5
def getfoo(self): # 非数据描述器
return self.foo
def __init__(self): # 非数据描述器
self.foo = 100
self.bar = 200
# self.z = 300
a = A()
print(a.__dict__)
print(A.__dict__)
foo、bar 都可以在实例中覆盖,但是z不可以。
19.3.14、练习
19.3.14.1、类方法staticmethod实现
实现 StaticMethod 装饰器,完成 staticmethod 装饰器的功能。
# 类staticmethod 装饰器
class StaticMethod: # 怕冲突改名
def __init__(self, fn):
self._fn = fn
def __get__(self, instance, owner):
return self._fn
class A:
@StaticMethod
# stmtd =StaticMethod(stmtd)
def stmtd():
print('static method')
A.stmtd()
A().stmtd()
19.3.14.2、类方法classmethod实现
实现 ClassMethod 装饰器,完成 classmethod 装饰器的功能。
# 类classmethod装饰器
class ClassMethod: # 怕冲突改名
def __init__(self, fn):
self._fn = fn
def __get__(self, instance, owner):
ret = self._fn(owner)
return ret
class A:
@ClassMethod
# clsmtd = ClassMethod(clsmtd)
# 调用应该是A.clsmtd()或者A().clsmtd()
def clsmtd(cls):
print(cls.__name__)
print(A.__dict__)
A.clsmtd
# A.clsmtd() # TypeError: 'NoneType' object is not callable
A.clsmtd()
的意思就是 None(),一定报错,怎么修改?
A.clsmtd()
其实就应该是 A.clsmtd(cls)()
,应该怎么处理?
A.clsmtd = A.clsmtd(cls)
,应该用到 partial 偏函数:
from functools import partial
# 类classmethod装饰器
class ClassMethod: # 怕冲突改名
def __init__(self, fn):
self._fn = fn
def __get__(self, instance, cls):
ret = partial(self._fn, cls)
return ret
class A:
@ClassMethod
# clsmtd = ClassMethod(clsmtd)
# 调用应该是A.clsmtd()或者A().clsmtd()
def clsmtd(cls):
print(cls.__name__)
print(A.__dict__)
print(A.clsmtd)
A.clsmtd()
19.3.14.3、实例的数据校验
对实例的数据进行校验。
class Person:
def __init__(self, name:str, age:int):
self.name = name
self.age = age
对上面类实例的属性 name、age 进行数据校验。
思路:
-
写函数,在
__init__
中先检查,如果不合格,直接抛异常 -
装饰器使用 inspect 模块完成
-
描述器
# 写函数检查
class Person:
def __init__(self, name: str, age: int):
params = ((name, str), (age, int))
if not self.ckdata(params):
raise TypeError()
self.name = name
self.age = age
def ckdata(self, params):
for p, t in params:
if not isinstance(p, t):
return False
return True
p = Person('tom', '20')
这种方法耦合度太高。装饰器的方式,前面写过类似的,这里不再赘述。
描述器方式:需要使用数据描述器,写入实例属性的时候做检查。
class Typed:
def __init__(self, name, type):
self.name = name
self.type = type
def __get__(self, instance, owner):
if instance is not None:
return instance.__dict__[self.name]
return self
def __set__(self, instance, value):
if not isinstance(value, self.type):
raise TypeError(value)
instance.__dict__[self.name] = value
class Person:
name = Typed('name', str) # 不优雅,这是为了刻意让实例属性重名从而触发__set__数据描述器
age = Typed('age', int) # 不优雅,这是为了刻意让实例属性重名从而触发__set__数据描述器
def __init__(self, name: str, age: int):
self.name = name
self.age = age
p = Person('tom', '20')
代码看似不错,但是有硬编码,能否直接获取形参类型,使用 inspect 模块。
先做个实验 params =inspect.signature(Person).parameters
,看看返回什么结果。
完整代码如下:
import inspect
class Typed:
def __init__(self, name, type):
self.name = name
self.type = type
def __get__(self, instance, owner):
if instance is not None:
return instance.__dict__[self.name]
return self
def __set__(self, instance, value):
if not isinstance(value, self.type):
raise TypeError(value)
# 一旦触发__set__数据描述器,instance的字典会移除那个触发__set__的属性,所以必须手动重新注入
instance.__dict__[self.name] = value
def typeassert(cls):
params = inspect.signature(cls).parameters
print(params)
for name, param in params.items():
print(param.name, param.annotation)
if param.annotation != param.empty:
# 注入类属性,让实例属性重名从而触发__set__数据描述器
setattr(cls, name, Typed(name, param.annotation))
return cls
@typeassert
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __repr__(self):
return "{} is {}".format(self.name, self.age)
# p = Person('tom', '20') # TypeError: 20
p = Person('tom', 20)
print(p)
可以把上面的函数装饰器改为类装饰器,如何写?
import inspect
class Typed:
def __init__(self, type):
self.type = type
def __get__(self, instance, owner):
pass
def __set__(self, instance, value):
print(1, '-->', 'T.set', self, instance, value)
if not isinstance(value, self.type):
raise ValueError(value)
class TypeAssert:
def __init__(self, cls):
self.cls = cls # 记录着被包装的Person类
params = inspect.signature(self.cls).parameters
print(2, '-->', params)
for name, param in params.items():
print(3, '-->', name, param.annotation)
if param.annotation != param.empty:
# 注入类属性,让实例属性重名从而触发__set__数据描述器
setattr(self.cls, name, Typed(param.annotation))
print(4, '-->', self.cls.__dict__)
def __call__(self, name, age):
return self.cls(name, age) # 装饰器完善功能后,要还原被装饰的Person对象
@TypeAssert
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
p1 = Person('tom', 18)
print(id(p1))
p2 = Person('tom', 20)
print(id(p2))
# p3 = Person('tom', '20') # ValueError: 20