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}

我看了很多博客,关于描述符的使用细节没有说清楚,虽然没有说错,但是却给人模糊的感觉,上述的代码能够很好的说明描述符

描述符的特点:

  1. 描述符只能代理类的某一个属性,使用的时候是属性级别
  2. 被代理的类可以有很多属性,被描述过的在增删查改时才会触发描述符的方法,没有被描述过的,是不会触发描述符的对象
  3. 描述符的使用是一种语法行为,没有为什么,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__。

描述符的特点

  1. 描述符只能代理类的某一个属性,使用的时候是属性级别
  2. 被代理的类可以有很多属性,被描述过的在增删查改时才会触发描述符的方法,没有被描述过的,是不会触发描述符的对象
  3. 描述符的使用是一种语法行为,没有为什么,python解释器规定了想要实现描述符的使用,就必须这么写
  4. 描述符的使用会改变类的被描述的属性的获取行为
  5. 描述符三个方法的参数各不相同,且规定死了
  6. 描述符方法和__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这两个字段做出了限制

posted @ 2022-02-23 14:04  yaowy  阅读(35)  评论(0编辑  收藏  举报