Loading... 现有一个学生类`Student`,学生类里的属性记录着当前学生的成绩 ```python class Student: def __init__(self, score): self.score = score ``` 现在对实例化这个学生类 ```python student = Student(100) student = Student(200) student = Student('222') ``` 因为这个类是给其他使用的,所以你不知道这个参数是不是数字型。然后你就给`score`的赋值加上了约束条件。 ```python class Student(object): def get_score(self): return self._score def set_score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value ``` ```python student = Student() student.set_score(199) # 抛出异常,score must between 0 ~ 100! student.set_score('100') # 抛出异常,score must be an integer! student.set_score(100) print(student.get_score()) # 获取score的值 # 100 ``` 现在每次设置`score`的值,都需要调用函数`set_score()`,如果可以直接访问`score`就好了。 ## @property > Python内置的`@property`装饰器就是负责把一个方法变成属性调用 ```python class Student(object): @property def score(self): return self._score @score.getter def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value ``` 加上`@property`装饰器后,现在的使用就可以像实例属性一样方便了 ```python student = Student() student.socre = 100 # 相当于调用 student.set_score(100) print(student.score) # 100 ``` 好的,继续深入了解,现在学生类需要保存三门功课的成绩,并且成绩范围都是 `0 - 100` 学习了`@property`,所以可以这样写: ```python class Student(object): def __init__(self, chinese, math, english): self.chinese = chinese self.math = math self.english = english @property def chinese(self): return self._chinese @chinese.setter def chinese(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._chinese = value @property def math(self): return self._math @math.setter def math(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._math = value @property def english(self): return self._english @english.setter def english(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._english = value ``` 输出: ```python student = Student(chinese=90, math=90, english=200) # 抛出异常 """ <ipython-input-20-898909dcebcd> in english(self, value) 39 raise ValueError('score must be an integer!') 40 if value < 0 or value > 100: ---> 41 raise ValueError('score must between 0 ~ 100!') 42 self._english = value """ ``` --- 效果是实现了,但是仅仅三个学科,就需要这么多的代码 代码冗余有很多重复的代码,有没有更优雅的方法来实现呢? ## 描述器 > 一般地,一个描述器是一个包含 “绑定行为” 的对象,对其属性的访问被描述器协议中定义的方法覆盖。这些方法有:[`__get__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__get__),[`__set__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__set__) 和 [`__delete__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__delete__)。如果某个对象中定义了这些方法中的任意一个,那么这个对象就可以被称为一个描述器。 > > 学习描述器不仅能提供接触到更多工具集的途径,还能更深地理解 Python 工作的原理并更加体会到其设计的优雅性。 > > 描述器是一个强大而通用的协议。 它们是特征属性(`@property`)、方法静态方法(`@staticmethod`)、类方法(`@classmethod`)和 [`super()`](https://docs.python.org/zh-cn/3/library/functions.html#super) 背后的实现机制。 它们在 Python 内部被广泛使用来实现自 2.2 版中引入的新式类。 描述器简化了底层的 C 代码并为 Python 的日常程序提供了一组灵活的新工具。 https://docs.python.org/zh-cn/3/howto/descriptor.html ### 定义描述类 通过修改上方的代码,来了解如何定义一个描述类。 ```python class Score(object): def __init__(self, default=0): self._score = default def __get__(self, obj, owner): return self._score def __set__(self, obj, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value ``` ### 描述器协议 - `__get__(self, obj, type=None) -> value`用于访问属性。它返回属性的值,若值不存在、不合法等都可以抛出对应的异常 - `__set__(self, obj, value) -> None`将在属性分配操作中调用,不返回内容 - `__delete__(self, obj) -> None`控制删除操作,不返回内容 **注意:定义这些方法中的任何一个的对象被视为描述器,并在被作为属性时==覆盖其默认行为==。** **1、描述器类别:** **数据描述器**:一个对象定义了`__set__()`和`__delete__()`,则它被视为数据描述器 **非数据描述器**:仅定义了`__get__()`的描述器 **2、区别:** 如果实例的字典具有数据描述器同名的条目,则数据描述器优先。 如果实例的字典具有与非数据描述器同名的条目,则字典条目优先。 **3、优先级:** 优先级:数据描述器 > 字典条目 > 非数据描述器 **使用描述器的类** ```python class Student(object): chinese = Score() math = Score() english = Score() def __init__(self, chinese, math, english): self.chinese = chinese self.math = math self.english = english ``` **非数据描述器** ```python class Score(object): def __init__(self, default=0): self._score = default def __get__(self, obj, owner): return self._score ``` **输出:** ```python s = Student(100, 90, 90) print(s.english) # 500 # 访问的实例属性 del s.english # 将实例属性删除之后 print(s.english) # 0 # 访问的才是描述器的值 ``` **结论:**非数据描述器对象与类实例属性同名,优先使用实例属性 **数据描述器** ```python class Score(object): def __init__(self, default=0): self._score = default def __get__(self, obj, owner): return self._score def __set__(self, obj, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value def __delete__(self, obj): print("数据描述器的删除方法被调用了") ``` **输出:** ```python s = Student(100, 90, 90) print(s.english) # 90 del s.english # 数据描述器的删除方法被调用了 ``` **结论:数据描述器对象与类实例属性同名时,优先使用数据描述器** **注意:为了使数据描述器成为只读的,应该同时定义 [`__get__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__get__) 和 [`__set__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__set__) ,并在 [`__set__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__set__) 中引发 [`AttributeError`](https://docs.python.org/zh-cn/3/library/exceptions.html#AttributeError) 。用引发异常的占位符定义 [`__set__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__set__) 方法使其成为数据描述器。** ### 调用描述类 1. 描述器可以通过其方法名直接调用,例如:`d.__get__(obj)` 2. 更常见的是**在属性访问时自动调用描述器**, 调用的细节取决于`obj`是对象还是类 - 对于对象,机制是调用`object.__getattribute__()`, 例如: `student.math` 转换为 `type(student).__dict__['math'].__get__(student, type(student))` 用中文翻译一下流程: 1. 获取当前实例的所属类并访问字典条目拿到对应的变量 `type(student).__dict__['math']`,这个拿到的变量就是 **描述器** 2. 调用描述器的`__get__(实例, 实例所属类)`方法获取数据 > 这个实现通过一个优先级链完成 > > 这个机制赋予数据描述器优先于实例变量的优先级,也赋予实例变量优先于非数据描述器的优先级 > > **额外补充:** > > 如果既定义了`__getattribute__`也定义了`__getattr__`,后者不会执行,除非 [`__getattribute__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__getattribute__) 显式地调用它或是引发了 [`AttributeError`](https://docs.python.org/zh-cn/3/library/exceptions.html#AttributeError) > ```python def __getattribute__(self, item): raise AttributeError def __getattr__(self, item): return 10000000 # 结果:10000000 ``` - 对于类,机制是 `Student.__getattribute__()` 中将 `Student.math` 转换为 `Student.__dict__['math'].__get__(None, Student)` 。 **重点总结:** - 描述器由 [`__getattribute__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__getattribute__) 方法调用 - 重写 [`__getattribute__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__getattribute__) 会阻止描述器的自动调用 - [`object.__getattribute__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__getattribute__) 和 `type.__getattribute__()` 会用不同的方式调用 [`__get__()`](https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__get__). - 数据描述器始终会覆盖实例字典。 - 非数据描述器会被实例字典覆盖。 ### 描述类的访问规则 > 属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。例如,`a.x` 的查找顺序会从 `a.__dict__['x']` 开始,然后是 `type(a).__dict__['x']`,接下来依次查找 `type(a)` 的基类,不包括元类。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。 ## 基于描述器实现`@property` `@property`的基本用法 ```python class Student(object): @property def score(self): return self._score @score.getter def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value ``` 结合『描述协议』来自己实现类`property`特性 ```python class TestProperty(object): def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): print("in __set__") if obj is None: return self if self.fget is None: raise AttributeError return self.fget(obj) def __set__(self, obj, value): print("in __set__") if self.fset is None: raise AttributeError self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__) ``` 接着修改`Student`类 ```python class Student(object): @TestProperty def score(self): return self._score @score.getter def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value ``` 输出结果 ```python s = Student() s.score = 100 print(s.score) """ int __set__ in __set__ 100 """ ``` 简易描述下执行过程: 1. 使用`TestProperty`装饰后,`score`不再是一个函数,而是`TestProperty`的一个实例。被`@score.getter`装饰的函数变成了一个`TestProperty`的实例。本质是调用`TestProperty.setter`来产生新的`TestProperty`实例并赋值给`score` 2. 对于`@TestProperty` 和 `@score.getter`构建的是两个不同的实例。它们同属`TestProperty`的实例。当对`score`赋值时,会进入`TestProperty.__set__`,当访问`score`时会进入`TestProperty.__get__`  ## 基于描述器实现`@staticmethod` > 静态方法可以在类不进行实例化的情况下调用 ### 使用`@staticmethod` ```python class Demo(object): @staticmethod def test(value:int): return value + 1 ``` 等价于 ```python class Demo(object): def test(value:int): return value + 1 test = staticmethod(test, value) ``` > `test`作为参数,使用`staticmethod`构建一个描述器实例 > > `test`成为了`staticmethod`的实例 > > 当有访问`test`时,触发`staticmethod.__get__` ### 实现`@TestStaticMethod` ```python class TestStaticMethod(object): def __init__(self, func): self.func = func def __get__(self, obj, objtype): print("in staticmethod __get__") return self.func ``` 使用: ```python class Demo(object): @TestStaticMethod def test(value:int): return value + 1 print(Demo.test(10)) """ 输出: in staticmethod __get__ 11 """ ``` 自己构建了`@staticmethod`,对`staticmethod`的理解一下子明朗不少 > 在类中,与`self`实例不接触的函数,可以选择使用`@staticmethod`改成静态方法 > > 通过上面的示例,可以看到就是将被修饰函数饶了个圈子拿出来使用,从而避开实例化 ## 基于描述器实现`@classmethod` ### 使用`@classmethod` ```python class Demo(object): @classmethod def test(cls, value): pass ``` 等价于 ```python class Demo(obejct): def test(cls, value): pass test = classmethod(test) ``` ### 实现`@TestClassMethod` ```python class TestClassMethod(object): def __init__(self, func): self.func = func def __get__(self, obj, objtype): print("in classmethod __get__") def new_func(*args): return self.func(objtype, *args) return new_func ``` 使用: ```python class Demo(object): @TestClassMethod def test(cls, value): pass Demo.test(10) """ 输出: in classmethod __get__ """ ``` 现在知道这个表示当前类的`cls`怎么来的了吧! 访问`test`即触发`TestClassMethod.__get__(obj, objtype)`,传入了实例和类。 这个`objtype`类就传给了`func`的第一个参数,所以`cls`在第一位表示当前类。 可以试试看,自己定义`cls`在参数中的位置 ```python class TestClassMethod(object): def __init__(self, func): self.func = func def __get__(self, obj, objtype): print("in classmethod __get__") def new_func(*args): # 交换了其他参数与类的位置 return self.func(*args, objtype) return new_func class Demo(object): @TestClassMethod def test(value, cls): # 交换了其他参数与类的位置 print(isinstance(Demo(), cls)) Demo.test(10) """ 输出: in classmethod __get__ True """ ``` ## 参考文章 (1) [廖雪峰Python教程-使用@property](https://www.liaoxuefeng.com/wiki/1016959663602400/1017502538658208#0) (2) [这个 Python 知识点,90% 的人都得挂~](https://mp.weixin.qq.com/s/JYifxS3kDXLQvajA5OLfIw) (3) [Python文档-描述器使用指南](https://docs.python.org/zh-cn/3/howto/descriptor.html#id2) Last modification:August 24th, 2020 at 11:47 pm © 允许规范转载