我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈
入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈
虚 拟 环 境 搭 建 :👉👉 Python项目虚拟环境(超详细讲解) 👈👈
PyQt5 系 列 教 程:👉👉 Python GUI(PyQt5)文章合集 👈👈
Oracle数据库教程:👉👉 Oracle数据库文章合集 👈👈
优 质 资 源 下 载 :👉👉 资源下载合集 👈👈
Python面向对象_类&补充_描述器
- 描述器
- 作用
- 常见用法
- 实现方法
- 示例代码
- 描述器的实现原理
- 描述器与实例属性同名时,操作优先级
- 描述器中值的存储问题
- 使用类实现装饰器
- 装饰器回顾
- 使用类实现装饰器
- 通过类实现装饰器的注意点
描述器
作用
- 描述器是通过实现特殊方法(
__get__
,__set__
,__delete__
)来拦截属性访问行为的对象 - 描述器是封装了描述(控制)一个属性操作(增/改、删、查)的多个方法的对象
- 当一个描述器对象作为类属性被访问时
- Python会自动调用描述器的
__get__
方法来获取属性值(p[age]
) - 如果同时定义了
__set__
方法,则可以通过赋值操作修改属性的值(p[age] = 36
) - 如果定义了
__delete__
方法,则可以使用del语句删除属性(del p[age]
)
- Python会自动调用描述器的
- 对比示例
class Person(object): def __init__(self): self.age = 18 a = int(input('请输入年龄:')) p = Person() print('你当前的年龄是:', p.age) p.age = a print('你当前的年龄是:', p.age) # 输出 # 你当前的年龄是:18 # 请输入年龄:-100 # 你当前的年龄是: -100 # 这个示例中,age属性没有使用描述器,所以在外部我们可以随意修改,无法对属性的操作进行拦截控制 # 对于用户输入的年龄是否合格,只能在实例化对象之前进行判断,但是每次实例化对象的时候都要加这个判断,会出现很多冗余代码
- 添加描述器示例(以描述器的一种定义方式为例)
class Person(object): def __init__(self): self.__age = 18 def get_age(self): return self.__age def set_age(self, value): # 可以在这里对属性设置的值进行判断 if value < 0 or value > 180: print('你输入的年龄太小或者太大了!') else: self.__age = value def del_age(self): del self.__age age = property(get_age, set_age, del_age) p = Person() print(p.age) p.age = -100 print(p.age) # 输出 # 18 # 你输入的年龄太小或者太大了! # 18
- 图解
常见用法
- 1、 属性访问控制 >> 只读属性 的实现
- 描述器可以用于实现属性访问控制,例如只读属性、只写属性、私有属性等等。下面是一个只读属性的
- 2、类型检查
- 描述器可以用于实现类型检查,例如强制属性值为整数、字符串等等。下面是一个强制属性值为整数的示例代
- 3、 惰性计算
- 描述器可以用于实现惰性计算,例如将一个方法的返回值缓存起来,避免重复计算。下面是一个惰性计算的示例代码:
实现方法
- 方式一
property(get_xxx, set_xxx, del_xxx)
实例对象@property、@xxx.setter、@xxx.deleter
装饰器- 缺点:每1个属性就需要定义3个方法来控制,那3个属性就需要9个方法,很繁琐!
- 方式二
- 自定义类实现
__get__
,__set__
,__delete__
- 注意:
- 在经典类中并不会调用这三个方法,而是创建一个实例属性
- 通过
类.属性
的方式操作属性时,只会调用__get__
方法,不会调用__set__
和__delete__
方法
- 自定义类实现
示例代码
- 方式一示例1:
property(get_xxx, set_xxx, del_xxx)
实例对象class Person(object): def __init__(self): self.__age = 18 def get_age(self): return self.__age def set_age(self, value): # 可以在这里对属性设置的值进行判断 if value < 0 or value > 180: print('你输入的年龄太小或者太大了!') else: self.__age = value def del_age(self): del self.__age age = property(get_age, set_age, del_age) p = Person() print(p.age) # 18 p.age = -100 # 你输入的年龄太小或者太大了! print(p.age) # 18
- 方式一示例2:
@property、@xxx.setter、@xxx.deleter
装饰器class Person(object): def __init__(self): self.__age = 18 @property def age(self): return self.__age @age.setter def age(self, value): if value < 0 or value > 100: print('你输入的年龄太小或者太大了!') else: self.__age = value @age.deleter def age(self): print('删除前可进行删除判断') del self.__age p = Person() print(p.age) # 18 p.age = -100 # 你输入的年龄太小或者太大了! print(p.age) # 18
- 方式二示例:
class Age: def __get__(self, instance, owner): print('__get__方法') def __set__(self, instance, value): print('__set__方法') def __delete__(self, instance): print('__delete__方法') class Person(object): age = Age() # p = Person() # # p.age # __get__方法 # p.age = 100 # __set__方法 # p.age # __get__方法 # del p.age # __delete__方法 Person.age # __get__方法 Person.age = 100 # 不会调用__set__方法 del Person.age # 不会调用__delete__方法 # Person.age # 通过`类.属性`的方式操作属性时,只会调用 `__get__` 方法,不会调用 `__set__` 和 `__delete__` 方法
描述器的实现原理
- 了解描述器的实现原理有助于更深入地理解它的使用和限制。描述器的实现基于Python的属性查找机制。当我们访问一个属性时,Python会按照以下顺序查找属性:
- 1、 在实例对象本身的
__dict__
字典中查找属性,如果存在则返回。 - 2、 在对应类对象的
__dict__
字典中查找属性,如果存在则返回。 - 3、 如果有父类,会再到父类的
__dict__
字典中查找属性,如果存在则返回。 - 4、 重复步骤3,直到找到基类为object
- 5、 如果没找到,有定义了
__getattr__
方法,就会调用这个方法
- 1、 在实例对象本身的
- 可以看到,在整个查找机制中并没有涉及到描述器中的
__get__
方法 - 描述器的
__get__
方法被优先调用的原因- 主要是通过
__getattnibute__
方法来实现 __getattnibute__
方法内部实现过程- 首先会检测一下,是否实现了描述器的
__get__
方法,如果有就会直接调用 - 如果没有实现
__get__
方法,则按照上面的查找机制进行查找
- 首先会检测一下,是否实现了描述器的
- 如果重写了
__getattnibute__
方法,则不会执行描述器的__get__
方法了
- 主要是通过
- 需要注意的是,描述器只对类属性起作用,对实例属性不起作用。如果我们直接访问实例属性,则不会触发描述器的行为。
描述器与实例属性同名时,操作优先级
- 描述器分类
- 资料描述器:实现了
__get__
和__set__
方法的描述器 - 非资料描述器:只实现了
__get__
方法的描述器 - 资料描述器
class Age: def __get__(self, instance, owner): print('__get__方法') def __set__(self, instance, value): print('__set__方法') class Person(object): age = Age() p = Person() p.age # __get__方法 p.age = 10 # __set__方法
- 非资料描述器
class Age: def __get__(self, instance, owner): print('__get__方法') class Person(object): age = Age() p = Person() p.age # __get__方法 p.age = 10
- 资料描述器:实现了
- 实例对象操作类属性的优先级
- 资料描述器 > 实例属性 > 非资料描述器
- 操作优先级示例1:资料描述器与实例属性的优先级
class Age: def __get__(self, instance, owner): print('__get__方法') def __set__(self, instance, value): print('__set__方法') class Person(object): age = Age() def __init__(self): self.age = 10 p = Person() p.age = 100 p.age print(p.__dict__) # 输出结果 # __set__方法 # p = Person() 实例化对象时会自动执行__init__初始化方法,执行 self.age = 10 调用了描述器里面的__set__方法 # __set__方法 # 执行 p.age = 100 时,又自动调用描述器里面的__set__方法 # __get__方法 # 执行 p.age 时,自动调用描述器里面的 __get__方法 # {} # 执行 self.age = 10 时,调用了描述器里重写的 __set__ 方法,仅仅只是打印了__set__字符,没有做其他操作,所以实例中并没有添加 age 属性 # 整个过程都是优先执行了资料描述器里面的__get__方法和__set__方法,并没有执行实例属性
- 操作优先级示例1:实例属性与非资料描述器的优先级
class Person(object): age = Age() def __init__(self): self.age = 10 p = Person() p.age = 100 p.age print(p.__dict__) # 输出结果 {'age': 100} # p = Person() 实例化对象时会自动执行__init__初始化方法,执行了 self.age = 10 ,将age属性直接添加到了实例对象的__dict__字典中, # 并没有调用描述器内部的__set__方法 # 整个执行过程中优先执行了实例属性,并没有执行非资料描述器
描述器中值的存储问题
-
上面我们讲的这些案例中,仅仅只是实现了描述器中的
__get__
、__set__
、__delete__
方法,但是并没有真正实现值的存储,那么我们来看一下如何实现值的传递和存储 -
描述器示例
class Age(object): """描述器""" def __get__(self, instance, owner): pass def __set__(self, instance, value): pass def __delete__(self, instance): pass class Person(object): age = Age() def __init__(self): self.age = 10 p = Person() p.age = 100
-
上面的
Age
描述器中实现了__get__
、__set__
、__delete__
三个方法,在这三个方法中有四个参数,我们来看一下这四个参数分别代表什么# 描述器中值的存储 class Age(object): """描述器""" def __get__(self, instance, owner): print('self:', self) print('instance:', instance) print('owner:', owner) def __set__(self, instance, value): print('value:', value) def __delete__(self, instance): pass class Person(object): age = Age() p = Person() p.age p.age = 199 # 输出结果 # self: <__main__.Age object at 0x000002A15828DFD0> # instance: <__main__.Person object at 0x000002A15828DFA0> # owner: <class '__main__.Person'> # value: 199
-
从输出结果可以看出来四个参数分别代表:
self: # 描述器Age类的实例化对象 instance: # Person类的实例化对象 owner: # Person类 value: # Person类实例化对象的属性值发生变化时的值
-
上面的示例可能还有点抽象,那么看下面这个示例
class Age(object): """描述器""" a = 'Age类属性' def __init__(self): self.a = 'Age实例属性' def __get__(self, instance, owner): print('self:', self.a) print('instance:', instance.a) print('owner:', owner.a) def __set__(self, instance, value): print('value:', value) def __delete__(self, instance): pass class Person(object): age = Age() a = 'Person类属性' def __init__(self): self.a = 'Person实例属性' p = Person() p.age p.age = 199 # 输出结果 # self: Age实例属性 # instance: Person实例属性 # owner: Person类属性 # value: 199
使用类实现装饰器
装饰器回顾
- Python基础进阶_装饰
- 有一段代码可以执行发说说的操作
def fashuoshuo() print('发说说操作') fashuoshuo()
- 新要求:在发说说之前,先做登录验证
- 前提:不能修改原方法的调用方式
- 实现:
- 通过装饰器实现(语法糖模式)
def check(func): def inner(): print('进行登录验证...') func() return inner @check def fashuoshuo(): print('发说说操作') fashuoshuo()
- 语法糖模式原理分解
def check(func): def inner(): print('进行登录验证...') func() return inner # @check def fashuoshuo(): print('发说说操作') fashuoshuo = check(fashuoshuo) # @check语法糖原理 # fashuoshuo这个变量接收到的是check这个方法的返回值:inner函数体 fashuoshuo() # 此时这里执行的fashuoshuo(),实际上是执行的inner()
- 通过装饰器实现(语法糖模式)
使用类实现装饰器
-
我们通过上面的函数装饰器可以看到
@check
这种语法糖模式,实际上就是执行了fashuoshuo = check(fashuoshuo)
-
那么我们现在通过反推的方式来理解类实现装饰器
-
我们先将前面的
def check
函数替换成class check
类class check: pass # @check def fashuoshuo(): print('发说说操作') # @check语法糖的执行原理 fashuoshuo = check(fashuoshuo) # 这里实际上是实例化check类对象,会自动执行类内部的__init__方法 fashuoshuo()
-
在这个案例中
fashuoshuo = check(fashuoshuo)
这一句实际上是实例化check
类的实例对象 -
在实例化示例对象的时候,就会默认调用类中的
__init__
方法,此时,我们还不知道在这个方法中执行什么,所有先不实现任何操作,用占位关键字占位class check: def __init__(self, func): pass # @check def fashuoshuo(): print('发说说操作') # @check语法糖的执行原理 fashuoshuo = check(fashuoshuo) # 这里实际上是实例化check类对象,会自动执行类内部的__init__方法 fashuoshuo()
-
看样子,这样就搞定了…
-
But…
-
报错了!报错在第13行!
fashuoshuo()
… why??? -
…
-
哦!哦!哦!
-
fashuoshuo()
这里的fashuoshuo
已经不是一个函数了,这里是一个实例对象 -
实例对象默认是不允许通过加
()
调用执行的,要想通过加()
调用执行,必须在类内部实现__call__
方法class check: def __init__(self, func): pass def __call__(self, *args, **kwargs): pass # @check def fashuoshuo(): print('发说说操作') # @check语法糖的执行原理 fashuoshuo = check(fashuoshuo) # 这里实际上是实例化check类对象,会自动执行类内部的__init__方法 fashuoshuo()
-
哦豁,没有报错,运行成功了!那么接下来我们就是要实现具体的功能了
-
那么,我们最终要执行的是
def fashuoshuo()
这个函数体,即在__call__
这个方法中执行这个函数体 -
但是,在
__call__
这个方法中怎么拿到这个函数体呢? -
我们在实例化
check
类的实例对象的时候,把fashuoshuo()
这个函数体作为参数传入了进去 -
那么在
__init__
方法中就可以接收到这个函数体,我们把这个函数体保存在实例属性中 -
__call__
方法再通过实例属性就可以执行这个函数体了 -
同样的,最后把要增加的操作执行在调用这个函数体之前就行了
class check: def __init__(self, func): self.f = func def __call__(self, *args, **kwargs): print('登录验证操作......') self.f() # @check def fashuoshuo(): print('发说说操作') # @check语法糖的执行原理 fashuoshuo = check(fashuoshuo) # 这里实际上是实例化check类对象,会自动执行类内部的__init__方法 fashuoshuo()
-
最后用语法糖的模式优化一下
class check: def __init__(self, func): self.f = func def __call__(self, *args, **kwargs): print('登录验证操作......') self.f() @check def fashuoshuo(): print('发说说操作') fashuoshuo()
通过类实现装饰器的注意点
- 1、必须在
__init__
方法中保存要装饰函数的函数体 - 2、在
__call__
方法中执行__init__
方法中保存的函数,并且在执行之前进行其他新增的操作