2. 类的装饰器
Python 中一切皆对象,装饰器同样适用于类:
无参数装饰器
def foo(obj):print('foo 正在运行~')return obj@foo # Bar=foo(Bar)
class Bar:pass
print(Bar())
foo 正在运行~
<__main__.Bar object at 0x0000000004ED1B70>
有参数装饰器
利用装饰器给类添加类属性:
def deco(**kwargs): # kwargs:{'name':'tom', 'age':1}def wrapper(obj): # Dog、Catfor key, val in kwargs.items():setattr(obj, key, val) # 设置类属性return objreturn wrapper@deco(name='tom', age=1) # @wrapper ===> Dog=wrapper(Dog)
class Dog:pass
print(Dog.__dict__) # 查看属性字典
print(Dog.name, Dog.age)@deco(name='john')
class Cat:pass
print(Cat.__dict__)
print(Cat.name)
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Dog' objects>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': None, 'name': 'tom', 'age': 1}
tom 1
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Cat' objects>, '__weakref__': <attribute '__weakref__' of 'Cat' objects>, '__doc__': None, 'name': 'john'}
john
@deco(name='tom', age=1)
首先执行 deco(name='tom', age=1)
,返回 wrapper
。再接着执行 @wrapper
,相当于 Dog = wrapper(Dog)
。最后利用 setattr(obj, key, value)
为类添加属性。
描述符+装饰器实现类型检查
class Descriptor:def __init__(self, key, expected_type):self.key = key # 'name'self.expected_type = expected_type # strdef __get__(self, instance, owner): # self:Descriptor对象, instance:People对象,owner:Peoplereturn instance.__dict__[self,key]def __set__(self, instance, value):if isinstance(value, self.expected_type):instance.__dict__[self.key] = valueelse:raise TypeError('你传入的%s不是%s' % (self.key, self.expected_type))def deco(**kwargs): # kwargs:{'name': 'str', 'age': 'int'}def wrapper(obj): # obj:Peoplefor key, val in kwargs.items():setattr(obj, key, Descriptor(key, val)) # key:name、age val:str、int return objreturn wrapper@deco(name=str, age=int) # @wrapper ==> People = wrapper(People)
class People:
# name = Descriptor('name', str)def __init__(self, name, age, color):self.name = nameself.age = ageself.color = color
p1 = People('rose', 18, 'white')
print(p1.__dict__)p2 = People('rose', '18', 'white')
{'__module__': '__main__', '__init__': <function People.__init__ at 0x00000000051971E0>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None, 'name': <__main__.Descriptor object at 0x00000000051BFDD8>, 'age': <__main__.Descriptor object at 0x00000000051D1518>}
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-18-010cd074c06d> in <module>()30 print(People.__dict__)31
---> 32 p2 = People('rose', '18', 'white')<ipython-input-18-010cd074c06d> in __init__(self, name, age, color)25 def __init__(self, name, age, color):26 self.name = name
---> 27 self.age = age28 self.color = color29 p1 = People('rose', 18, 'white')<ipython-input-18-010cd074c06d> in __set__(self, instance, value)11 instance.__dict__[self.key] = value12 else:
---> 13 raise TypeError('你传入的%s不是%s' % (self.key, self.expected_type))14 15 def deco(**kwargs): # kwargs:{'name': 'str', 'age': 'int'}TypeError: 你传入的age不是<class 'int'>
在利用 setattr()
设置属性的时候,对 value 进行类型检查。
3. 自定制 property
静态属性 property 可以让类或类对象,在调用类函数属性时,类似于调用类数据属性一样。下面我们利用描述符原理来模拟 property。
把类设置为装饰器,装饰另一个类的函数属性:
class Foo:def __init__(self, func):self.func = funcclass Bar:@Foo # test = Foo(test)def test(self):pass
b1 = Bar()
print(b1.test)
<__main__.Foo object at 0x00000000051DFEB8>
调用 b1.test
返回的是 Foo 的实例对象。
利用描述符模拟 property
class Descriptor: # 1"""描述符"""def __init__(self, func): # 4 # func: areaself.func = func # 5def __get__(self, instance, owner): # 13"""调用 Room.areaself: Descriptor 对象instance: Room 对象(即 r1或Room中的 self)owner: Room 本身"""res = self.func(instance) #14 # 实现调用 Room.area,area需要一个位置参数 self,即 r1return res # 17class Room: # 2def __init__(self, name, width, height): # 7self.name = name # 8self.width = width # 9self.height = height # 10# area = Descriptor(area),Descriptor 对象赋值给 area,实际是给 Room 增加描述符# 设置 Room 的属性字典@Descriptor # 3 def area(self): # 15"""计算面积"""return self.width * self.height # 16r1 = Room('卧室', 3, 4) # 6
print(Room.__dict__) # 11
print(r1.area) # 12 调用 Descriptor.__get__()
{'__module__': '__main__', '__init__': <function Room.__init__ at 0x0000000005206598>, 'area': <__main__.Descriptor object at 0x000000000520C6A0>, 'test': <property object at 0x00000000051FFDB8>, '__dict__': <attribute '__dict__' of 'Room' objects>, '__weakref__': <attribute '__weakref__' of 'Room' objects>, '__doc__': None}
12
首先执行 @Descriptor
,相当于 area = Descriptor(area)
,类似于给类 Room 设置 area属性。area 属性又被 Descriptor 代理(描述)。
所有当执行 r1.area
的时候,触发调用 Descriptor.__get__()
方法,然后执行 area() 函数
,并返回结果。
到目前为止,模拟 property 已经完成了百分之八十。唯一不足的是:property 除了可以使用实例对象调用外,还可以使用类调用。只不过返回的是 property 这个对象:
class Room:...@propertydef test(self):return 1
print(Room.test)
<property object at 0x000000000520FAE8>
那么我们也用类调用看看:
print(Room.area)
AttributeError: 'NoneType' object has no attribute 'width'
发现类没有 width
这个属性,这是因为我们在使用 __get__()
方法时,其中 instance 参数接收的是类对象,而不是类本身。当使用类去调用时,instance = None
。
因此在使用模拟类调用时,需要判断是否 instance 为 None:
def __get__(self, instance, owner):if instance == None:return self...
print(Room.area)
<__main__.Descriptor object at 0x0000000005212128>
````
发现返回的是 Descriptor 的对象,与 property 的返回结果一样。到此为止,我们使用描述符成功地模拟 property 实现的功能。**实现延迟计算功能**要想实现延迟计算功能,只需每计算一次便缓存一次到实例属性字典中即可:
```python
def __get__(self, instance, owner):if instance == None:return selfres = self.func(instance) setattr(instance, self.func.__name__, res)return res...
print(r1.area) # 首先在自己的属性字典中找,自己属性字典中有 area 属性。因为已经缓存好上一次的结果,所有不需要每次都去计算
总结
- 描述符可以实现大部分 Python 类特性中的底层魔法,包括
@property、@staticmethod、@classmethod
,甚至是__slots__
属性。 - 描述符是很多高级库和框架的重要工具之一,通常是使用到装饰器或元类的大小框架中的一个组件。
4. 描述符自定制
4.1 描述符自定制类方法
类方法 @classmethod
可以实现类调用函数属性。我们也可以描述符模拟出类方法实现的功能:
class ClassMethod:def __init__(self, func):self.func = funcdef __get__(self, instance, owner):def deco(*args, **kwargs):return self.func(owner, *args, **kwargs)return decoclass Dog:name = 'tom'@ClassMethod # eat = ClassMethod(eat) 相当于为 Dog 类设置 eat 类属性,只不过它被 Classmethod 代理def eat(cls, age):print('那只叫%s的狗,今年%s岁,它正在吃东西' % (cls.name, age))print(Dog.__dict__)
Dog.eat(2) # Dog.eat:调用类属性,触发 __get__,返回 deco。再调用 deco(2)
{'__module__': '__main__', 'name': 'tom', 'eat': <__main__.ClassMethod object at 0x00000000052D1278>, '__dict__': <attribute '__dict__' of 'Dog' objects>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': None}
那只叫tom的狗,今年2岁,它正在吃东西
类 ClassMethod 被定义为一个描述符,@ClassMethod
相当于 eat = ClassMethod(eat)
。因此也相当于 Dog 类设置了类属性 eat,只不过它被 ClassMethod 代理。
运行 Dog.eat(2)
,其中 Dog.eat
相当于调用 Dog 的类属性 eat,触发 __get__()
方法,返回 deco 。最后调用 deco(2)
。
4.2 描述符自定制静态方法
静态方法的作用是可以在类中定义一个函数,该函数的参数与类以及类对象无关。下面我们用描述符来模拟实现静态方法:
class StaticMethod:def __init__(self, func):self.func = funcdef __get__(self, instance, owner):def deco(*args, **kwargs):return self.func(*args, **kwargs)return decoclass Dog:@StaticMethoddef eat(name, color):print('那只叫%s的狗,颜色是%s的' % (name, color))print(Dog.__dict__)
Dog.eat('tom', 'black')d1 = Dog()
d1.eat('lila', 'white')
print(d1.__dict__)
{'__module__': '__main__', 'eat': <__main__.StaticMethod object at 0x00000000052EF940>, '__dict__': <attribute '__dict__' of 'Dog' objects>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': None}
那只叫tom的狗,颜色是black的
那只叫lila的狗,颜色是white的
{}
类以及类对象属性字典中,皆没有 name 和 color 参数,利用描述符模拟静态方法成功。
5. property 用法
静态属性 property 本质是实现了 get、set、delete 三个方法。
class A:@propertydef test(self):print('get运行')@test.setterdef test(self, value):print('set运行')@test.deleterdef test(self):print('delete运行')a1 = A()
a1.test
a1.test = 'a'
del a1.test
get运行
set运行
delete运行
另一种表现形式:
class A:def get_test(self):print('get运行')def set_test(self, value):print('set运行')def delete_test(self):print('delete运行')test = property(get_test, set_test, delete_test)a1 = A()
a1.test
a1.test = 'a'
del a1.test
get运行
set运行
delete运行
应用
class Goods:def __init__(self):self.original_price = 100self.discount = 0.8@propertydef price(self):"""实际价格 = 原价 * 折扣"""new_price = self.original_price * self.discountreturn new_price@price.setterdef price(self, value):self.original_price = value@price.deleterdef price(self):del self.original_priceg1 = Goods()
print(g1.price) # 获取商品价格
g1.price = 200 # 修改原价
print(g1.price)
del g1.price
80.0
160.0