python 描述符详解

  1. 描述符的介绍
  2. 结合装饰器

描述符的介绍

只要类中包含了__get__, __set__, __delete__ 中的任意一个,那么这个类就是描述类

也就是对这个实例的取、设置和删除都会执行上面对应的方法。

必须作为类变量进行初始化。

同时包含__get____set__的被称为数据描述符,而只有__get__被称为非数据描述符

对于一个类中的变量调用顺序参考下图:

从上图可以看到调用顺序为:
__getattribute__ > 数据描述符__get__ > 非数据描述符__get__ > 兜底的__getattr__

Example:

class Price:

def __init__(self): self._price = 0
def __get__(self, obj, klass): print('call __get__ method, get: ', self._price) return self._price
def __set__(self, obj, value): print('call __set__ method, set: ', value) if value < 0: raise ValueError('price should not be negative') self._price = value
def __delete__(self, obj): print('call __delete__ method , price to 0') self._price = 0

class Apple:
price = Price()
def __init__(self, price): self.price = price
def __getattribute__(self, __name: str): print("call __getattribute__ method, get: ", __name) return super().__getattribute__(__name)
def __getattr__(self, __name: str): print("call __getattr__ method, get: ", __name)

a1 = Apple(10) a1.no_exist_attr print(a1.price) a1.price = 15 print(a1.price) del a1.price print(a1.price)
# output: #call __set__ method, set: 10 #call __getattribute__ method, get: no_exist_attr #call __getattr__ method, get: no_exist_attr #call __getattribute__ method, get: price #call __get__ method, get: 10 #10 #call __set__ method, set: 15 #call __getattribute__ method, get: price #call __get__ method, get: 15 #15 #call __delete__ method , price to 0 #call __getattribute__ method, get: price #call __get__ method, get: 0 #0

结合装饰器

我们以@property的python简单实现为例。

class mproperty(object):

def __init__(self, fget, fset=None, fdel=None): print("call mproperty.__init__ method, set getter func") self._fget = fget self._fset = fset self._fdel = fdel
def __get__(self, obj, klass): print('call __get__ method') return self._fget(obj)
def __set__(self, obj, val): if not hasattr(self._fset, '__call__'): raise AttributeError("Readonly attribute!") print('call __set__ method, set: ', val) self._fset(obj, val)
def __delete__(self, obj): if not hasattr(self._fdel, '__call__'): raise AttributeError("Can't delete the attribute!") print('call __delete__ method, price to 0') self._fdel(obj)
def setter(self, fset): print("call mproperty.setter method, set setter func") self._fset = fset return self
def deleter(self, fdel): print("call mproperty.delete method, set delete func") self._fdel = fdel return self

class Apple(object):
def __init__(self, price=0): self._price = price
@mproperty def price(self): return self._price
@price.setter def price(self, value): if value < 0: raise ValueError("Price must be greater than 0") self._price = value
@price.deleter def price(self): print("delete price")

print("=== start process ===")
a1 = Apple(10) a1.price a1.price = 15 a1.price del a1.price
# output: #call mproperty.__init__ method, set getter func #call mproperty.setter method, set setter func #call mproperty.delete method, set delete func #=== start process === #call __get__ method #call __set__ method, set: 15 #call __get__ method #call __delete__ method, price to 0 #delete price

如果还不清楚装饰器的原理,可以看我写的这篇

我们来一步一步分析:

  1. 在程序初始化阶段,将执行@mproperty,此时装饰的是price,等价于mpropertry(price), 此时price函数实际上就是mproperty实例了

  2. @price.setter装饰price, 里面的@price现在就是mproperty实例,所以有setter方法。在setter中设置了self._fset后返回self,这样price同样指向的是mproperty实例。

  3. @price.delete装饰price, 同理price指向了同一个mproperty实例,保证了这几个方法都其实是个实例。

  4. 既然都是一个实例,并且mproperty含有__get__ __set__ __delete,是描述器,所以在调用price的时候,就会去调用对应的方法。

  5. 通过self._get(obj)这样的形式再回过来调用Apple.price中的方法

其实这里面的关键点就是setterdelete中的return self到底返回的是什么,成为了什么形式被调用。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 365433079@qq.com