19、Python3 魔术方法

作者: Brinnatt 分类: python 术 发布时间: 2023-03-30 10:30

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,访问到了描述器的数据。

给我们的感觉好像是这么一个属性查找顺序:

  1. 实例的 __dict__ 优先于非数据描述器。

  2. 数据描述器优先于实例的 __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 进行数据校验。

思路:

  1. 写函数,在 __init__ 中先检查,如果不合格,直接抛异常

  2. 装饰器使用 inspect 模块完成

  3. 描述器

# 写函数检查
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
标签云