04-面向对象进阶2
争取一文搞懂描述符(get,set,delete)
何谓描述符?
描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),set(),delete()中的一个,这也被称为描述符协议。
get():调用一个属性时,触发
set():为一个属性赋值时,触发
delete():采用del删除属性时,触发
下面的类就是一个描述符
class Foo:
def __get__(self, instance, owner):
pass
def __set__(self, instance, value):
pass
def __delete__(self, instance):
pass
在python3中Foo是新式类,它实现了三种方法,这个类就被称作一个描述符。
描述符是干什么的
描述符的作用是用来代理另外一个类的属性的(必须把描述符定义成这个类的类属性,不能定义到构造函数中)
class Foo:
def __get__(self, instance, owner):
print('触发get')
def __set__(self, instance, value):
print('触发set')
def __delete__(self, instance):
print('触发delete')
#包含这三个方法的新式类称为描述符,由这个类产生的实例进行属性的调用/赋值/删除,并不会触发这三个方法
f1=Foo()
f1.name='egon'
f1.name
del f1.name
# 输出
啥也没有
从这里也可以看出,操作一个对象的属性,还是用的 _getattr_()、_setattr_() 、_delattr_(),这一点后面还会阐述的。
描述符的使用
class Str:
def __get__(self, instance, owner):
print('Str调用')
print(instance, owner)
def __set__(self, instance, value):
print('Str设置...')
print(instance, value)
def __delete__(self, instance):
print('Str删除...')
print(instance)
class People:
name = Str() # name 属性被代理,将这个类作用于另外一个类的属性来使用
p1 = People()
print('name被代理过,增加:', '-'*20)
p1.name = 'sss'
print('name被代理过,获取:', '-'*20)
p1.name
print('name被代理过,删除:', '-'*20)
del p1.name
print('age没有被代理,增加:', '#'*20)
p1.age = 20
print('age没有被代理,获取:', '#'*20)
p1.age
print('age没有被代理,删除:', '#'*20)
del p1.age
print('name是否为类属性?能否获取地址*****************')
print(People.name)
print('获取p1的属性字典*****************************')
print(p1.__dict__)
print('获取类属性字典*******************************')
print(People.__dict__)
# 输出
name被代理过,增加: --------------------
Str设置...
<__main__.People object at 0x0000027B1876B160> sss
name被代理过,获取: --------------------
Str调用
<__main__.People object at 0x0000027B1876B160> <class '__main__.People'>
name被代理过,删除: --------------------
Str删除...
<__main__.People object at 0x0000027B1876B160>
age没有被代理,增加: ####################
age没有被代理,获取: ####################
age没有被代理,删除: ####################
name是否为类属性?能否获取地址*****************
Str调用
None <class '__main__.People'>
None
获取p1的属性字典*****************************
{}
获取类属性字典*******************************
{'__module__': '__main__', 'name': <__main__.Str object at 0x0000027B1876B460>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}
我看了很多博客,关于描述符的使用细节没有说清楚,虽然没有说错,但是却给人模糊的感觉,上述的代码能够很好的说明描述符
描述符的特点:
- 描述符只能代理类的某一个属性,使用的时候是属性级别
- 被代理的类可以有很多属性,被描述过的在增删查改时才会触发描述符的方法,没有被描述过的,是不会触发描述符的对象
- 描述符的使用是一种语法行为,没有为什么,python解释器规定了想要实现描述符的使用,就必须这么写,不要尝试去想他为什么这么写,内部做了啥
它的特点暂时只能总结这么多,剩下的需要好好描述一下。
描述符深入
我们知道,name=Str()
在python里面的含义是,给name赋了一个Str类型的变量,且name=Str()
写在了类属性里面,因此People的类属性里面一定有一个变量叫作name,它是一个Str类型的变量,于是print(People.name)
的用意就是尝试打印这个类变量,输出是什么?
Str调用 # __get__ 的print('Str调用')的打印行为
None <class '__main__.People'> # __get__ 的print(instance, owner)的打印行为,None是因为我们直接使用的People.name,python解释器传递了一个空People类型的变量,<class '__main__.People'>是因为People这个类传进去了。
None # print(People.name)的打印行为,这个是因为执行__get__方法,他最后不能返回一个name,因此也就是None
那我们看看下面这段代码
class Str:
def __get__(self, instance, owner):
print('Str调用')
print(instance, owner)
def __set__(self, instance, value):
print('Str设置...')
print(instance, value)
def __delete__(self, instance):
print('Str删除...')
print(instance)
class Dog:
pass
class People:
name = Str() # name 属性被代理,将这个类作用于另外一个类的属性来使用
age = 18
dog = Dog()
# p1 = People()
# print('name被代理过,增加:', '-'*20)
# p1.name = 'sss'
# print('name被代理过,获取:', '-'*20)
# p1.name
# print('name被代理过,删除:', '-'*20)
# del p1.name
# print('age没有被代理,增加:', '#'*20)
# p1.age = 20
# print('age没有被代理,获取:', '#'*20)
# p1.age
# print('age没有被代理,删除:', '#'*20)
# del p1.age
# print('name是否为类属性?能否获取地址*****************')
# print(People.name)
# print('获取p1的属性字典*****************************')
# print(p1.__dict__)
# print('获取类属性字典*******************************')
# print(People.__dict__)
print('-------------------------------------------------------------------')
print(People.name)
print(People.age)
print(People.dog)
# 输出
Str调用
None <class '__main__.People'>
None
18
<__main__.Dog object at 0x000002DBF5E0B160>
上面这段代码是更上面的代码的扩展,我们又定义了一个Dog类,并且给People类定义了增加一个age=18和dog=Dog()属性,通过打印结果来看,age和dog这两个类属性正常打印了,而被描述的属性打印不一样了。也就是说描述符改变了People的类属性获取行为。
下面我们温顾一下属性获取顺序
class People:
name = '人类'
source = '地球'
def __init__(self, name, age):
self.name = name
self.age = age
p1 = People('小明', 18)
print('-------对象打印------')
print(p1.name)
print(p1.age)
print(p1.source)
print('-------类打印------')
print(People.name)
print(People.source)
print(People.age)
# 输出
-------对象打印------
小明
18
地球
-------类打印------
人类
地球
Traceback (most recent call last):
File "g:\Project\DRF\test2.py", line 20, in <module>
print(People.age)
AttributeError: type object 'People' has no attribute 'age'
这说明了什么?对象属性和类属性可以同名,如果使用对象获取属性,对象有就拿对象的,对象没有就从类中拿,通过类获取属性,只能拿类的,类有就拿的出来,类没有就只能报错。
为什么要温顾属性获取顺序呢?且看这段代码
这是我们之前定义的一个名为Str的描述符。
python解释器已经将要代理的对象做了绑定,会自动触发,因此触发之时需要哪些参数?self是类Str的实例对象,我们不当做参数。
__del__只有一个,instance,这个参数指向被代理的对象,比如我们要删除p1的name属性,因为描述符已经与name做过绑定,因此name不需要传进去,但是我要知道你让我删除哪个People对象的name属性呀,所以instance是必须的,因此他只需要一个参数。
__set__需要两个参数,我已经知道了我是要操作name属性,name不用传,但是你让我操作哪个People,因此需要instance,我还要知道你想让name等于啥?因此还需要一个value,这个是即将给name的值。
最后一个__get__需要两个参数,一个是instance,这个就不解释了,另一个是owner,为啥要传这个?因为对象.属性
,有个搜索顺序,先去对象里面查找,对象里面没有就去类里面找。所以他需要owner。
描述符与__getattr__等同在
可以看到只有描述符的时候,这三个都执行了
可以看到,只有获取对象的属性的时候才会触发描述符,剩下的两个描述符方法都被屏蔽了,这是因为,获取属性的时候,如果获取失败才会触发__getattr__方法
class People:
def __getattr__(self, key):
print('__getattr__执行了')
def __setattr__(self, key, value):
self.__dict__[key] = value
print('__setattr__执行了')
def __delattr__(self, key):
print('__delattr__执行了')
p1 = People()
print('----- 测试添加属性 -----')
p1.name = 'Jack'
print('----- 测试获取存在的属性 -----')
p1.name
print('----- 测试获取不存在的属性 -----')
p1.age
# 输出
----- 测试添加属性 -----
__setattr__执行了
----- 测试获取存在的属性 -----
----- 测试获取不存在的属性 -----
__getattr__执行了
也就是说获取属性的时候本身就先不走__getattr__,只有当其余的手段不管用了才会触发__getattr__。
描述符的特点
- 描述符只能代理类的某一个属性,使用的时候是属性级别
- 被代理的类可以有很多属性,被描述过的在增删查改时才会触发描述符的方法,没有被描述过的,是不会触发描述符的对象
- 描述符的使用是一种语法行为,没有为什么,python解释器规定了想要实现描述符的使用,就必须这么写
- 描述符的使用会改变类的被描述的属性的获取行为
- 描述符三个方法的参数各不相同,且规定死了
- 描述符方法和__getattr__等三个方法同存时,其余__set__和__del__会被屏蔽
描述符有什么用?
前面说了那么多了,终于知道了细节,和简单的使用,但是如何运用呢?python既然存在这个语法,肯定是有他的道理的。
众所周知,python是弱类型语言,即参数的赋值没有类型限制,下面我们通过描述符机制来实现类型限制功能
class Typed:
def __init__(self, key, key_type):
self.key = key
self.key_type = key_type
def __get__(self, instance, owner):
print('get方法')
# print('instance参数:%s'%instance) # People object
# print('owner参数:%s'%owner)
return instance.__dict__[self.key]
def __set__(self, instance, value):
# 代理的好处:可以对传进来的值进行下一步判断
print('set方法')
# print('instance参数:%s'%instance) # People Object
# print('value参数:%s'%value)
if not isinstance(value, self.key_type):
raise TypeError('%s不是%s' % (value, self.key_type))
instance.__dict__[self.key] = value
def __delete__(self, instance):
print('delete方法')
# print('instance参数:%s'%instance) # People object
instance.__dict__.pop(self.key)
class People:
name = Typed('name', str) # 代理类
age = Typed('age', int)
def __init__(self, name, age, salary):
self.name = name
self.age = age
self.salary = salary
上述的代码对name和age这两个字段做出了限制