Python描述符的一些补充

最近有空在看新版的《Effective Python》一书,看到了一些描述符的使用。

补充一些我的一些自己看法

 

当一个实例成为一个类的属性时,当这个类有__get__属性,__set__属性,这个类的实例成为类的属性就称为描述符

我们的日常使用中,函数就属于描述符。因为创建函数的类有__get__属性

首先,我来解释一下,覆盖性与非覆盖型的描述符区别

覆盖与非覆盖,是相对实例属性来说的

# 创建描述符
class Quantiy:
    # 外部定一个描述符类属性
    __count = 0

    def __init__(self):
        # 描述符初始化赋值
        cls = self.__class__
        prefil = cls.__name__
        index = cls.__count
        # 设立一个独一无二的名称
        self.storage = '_{}#{}'.format(prefil, index)
        cls.__count += 1

    def __get__(self, instance, owner):
        return 'This is quantiy'


class T1:
    q = Quantiy()

    def __init__(self):
        self.q = 1
        self.hello = 666

    def hello(self):
        return 'h_func'

if __name__ == '__main__':
    t = T1()
    print(t.q)
    print(t.hello)

  

上面的代码运行,输出还是self的实例属性,实例通过点的方式取属性,还是获取实例自身__dict__中的属性

# 创建描述符
class Quantiy:
    # 外部定一个描述符类属性
    __count = 0

    def __init__(self):
        # 描述符初始化赋值
        cls = self.__class__
        prefil = cls.__name__
        index = cls.__count
        # 设立一个独一无二的名称
        self.storage = '_{}#{}'.format(prefil, index)
        cls.__count += 1

    def __get__(self, instance, owner):
        return 'This is quantiy'

    def __set__(self, instance, value):
        ...

class T1:
    q = Quantiy()

    def __init__(self):
        self.q = 1
        self.hello = 666

    def hello(self):
        return 'h_func'

if __name__ == '__main__':
    t = T1()
    print(t.q)
    print(t.hello)

  

当在描述符中加入了__set__之后,对于实例通过点的方式取值或者设置同名的实例属性时,会设置该类的同名属性优先,也就是对描述符[实例]进行操作

后面经过研究,其实还挺复杂的逻辑,网上找到一个坐着写的挺好的。

参考链接:https://halfclock.github.io/2019/06/04/python-descriptor_02/

他对覆盖型与非覆盖型的描述符有着明确的说明

各类描述符的使用场景

全覆盖型描述符

这里指实现了 __set__ 和 __get__ 协议的描述符。

能够使用全覆盖型描述符的场景,通常还需要考虑是否使用特性。这部分可以参照上一篇博文最后的总结。

此类描述符还有另一个使用的场景,即只读属性,只读属性的 __set__ 只需要抛出指定的异常即可。

必须设置 __get__ 的原因是,防止用户使用 __dict__ 直接对实例属性进行修改,因为覆盖型描述符不管是否有实例属性,在读值时都会访问 __get__ 方法。

半覆盖型描述符

这里指没有 __get__ 方法的覆盖型描述符。

此类描述符通常用于验证属性。

即检查用户给的 value 是否符合系统定义的规则,如果符合规则,才将之存储至实例属性中,当需要拿到实例属性时,不用通过 __get__ ,直接访问实例属性即可能快速的拿到需要的值。

非覆盖型描述符

这里指没有 __set__ 方法的覆盖型描述符。

此类描述符适合使用在第一次访问需要加载数据(花费时间长)的场景。———— 高效缓存

因为第一次访问实例属性时,调用描述符实例的 __get__ 方法,在该方法中加载数据,然后将加载完成的数据(value),使用 obj. attr = value 赋给实例属性。

之后再访问实例属性就无需加载数据,不再访问描述符实例的 __get__ 方法了,直接访问实例属性即可。

总结

本篇博文与上一篇博文总结了属性描述符是什么、怎么使用、以及何时使用的问题。

指出了属性描述符是实现了描述符协议的类、其实例通常被托管类类属性所承载、并且根据是否实现 __set__ 方法,分为覆盖型描述符和非覆盖型描述符,他们分别应用于只读属性、属性验证和高效缓存中。

 以上的内容,我是复制了它的博客内容,确实让我对Python的描述符有了更加深入的认识。
 
 
下面是我对描述符使用的增强案例
# 创建描述符
class Quantiy:
    # 外部定一个描述符类属性
    __count = 0

    def __init__(self):
        # 描述符初始化赋值
        cls = self.__class__
        prefil = cls.__name__
        index = cls.__count
        # 设立一个独一无二的名称
        self.storage = '_{}#{}'.format(prefil, index)
        cls.__count += 1

    def __get__(self, instance, owner):
        return instance.__dict__[self.storage + str(id(instance))]

    def __set__(self, instance, value):
        # 托管属性 描述符示例操作托管示例对属性进行赋值
        if value > 0:
            # 增强属性赋值,通过计算器与实例的id作为每个实例的属性唯一码
            instance.__dict__[self.storage + str(id(instance))] = value
        else:
            raise ValueError('value must be > 0')


class LineItem:
    # 描述符实例赋值给托管类属性
    weight = Quantiy()
    price = Quantiy()

    def __init__(self, description, weight, price):
        # 储存实例,托管实例中存储自身托管属性的属性 self.weight与self.price
        self.description = description
        # 这个按照书中的说法叫特性的赋值方法了,不是属性赋值了。
        self.weight = weight
        self.price = price

    @property
    def subtoall(self):
        return self.weight * self.price

  

上面的代码是摘抄至流畅的Python一书,由于Python模块在导入执行后,类的描述符只会初始化一次,通过描述符的加工器给实例属性的__dict__中赋值属性我给加上了实例的id码,做唯一。

这样,基本确保了每次实例出来的对象通过获取的属性不会出问题

前面我的理解错了,其实对于不同的实例,通过描述符赋值相同的属性名是正确的,这并不会影响各个实例的属性之间干扰

# 创建描述符
class Quantiy:
    # 外部定一个描述符类属性
    __count = 0

    def __init__(self):
        # 描述符初始化赋值
        cls = self.__class__
        prefil = cls.__name__
        index = cls.__count
        # 设立一个独一无二的名称
        self.storage = '_{}#{}'.format(prefil, index)
        cls.__count += 1

    def __get__(self, instance, owner):
        # return instance.__dict__[self.storage + str(id(instance))]
        return instance.__dict__[self.storage]

    def __set__(self, instance, value):
        # 托管属性 描述符示例操作托管示例对属性进行赋值
        if value > 0:
            # 增强属性赋值,通过计算器与实例的id作为每个实例的属性唯一码
            # instance.__dict__[self.storage + str(id(instance))] = value
            instance.__dict__[self.storage] = value
        else:
            raise ValueError('value must be > 0')


class LineItem:
    # 描述符实例赋值给托管类属性
    weight = Quantiy()
    price = Quantiy()

    def __init__(self, description, weight, price):
        # 储存实例,托管实例中存储自身托管属性的属性 self.weight与self.price
        self.description = description
        # 这个按照书中的说法叫特性的赋值方法了,不是属性赋值了。
        self.weight = weight
        self.price = price

    @property
    def subtoall(self):
        return self.weight * self.price

  这样是对的

如果通过秒速符对实例的__dict__属性进行操作,可以理解为描述符就是一个工具而已,上面的方式,没有用到描述符实例内部的属性来保存相对托管类的实例的属性。

确实非常不错

 

下面是我摘抄至《Effective Python》书中使用描述的方式,他直接将描述符实例放入托管类,并且没有进行__init__的托管类实例化函数的操作

所以他相关的实例属性[伪],其实托管类的实例__dict__中还是空的

# Example 14
from weakref import WeakKeyDictionary

class Grade:
    def __init__(self):
        self._values = WeakKeyDictionary()

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return self._values.get(instance, 0)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError(
                'Grade must be between 0 and 100')
        self._values[instance] = value


# Example 15
class Exam:
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
print(f'First  {first_exam.writing_grade} is right')
print(f'Second {second_exam.writing_grade} is right')
print(first_exam.__dict__)
print(second_exam.__dict__)

  

两种方式的操作,我还是更加喜欢第一种,话说流畅的Python一书确实很多操作与解释很棒,最近太懒了。哈哈,要么等新版的流畅的Python一书出来,再看一本

希望我英语能够学好,下次直接看新版的英文原书

 

 参照书<effective python>书中第50章节的代码参考,通过__set_name__初始化描述符属性,可以获取属性赋值时的变量名值

# Example 11
class Field:
    def __init__(self):
        print('__init__')
        self.name = None
        self.internal_name = None

    # 自动触发在__init__之后
    def __set_name__(self, owner, name):
        print('__set_name__')
        # Called on class creation for each descriptor
        self.name = name
        self.internal_name = '_' + name

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        setattr(instance, self.internal_name, value)


# Example 2
class Customer:
    # Class attributes
    first = Field()



# Example 3
cust = Customer()
print(f'Before: {cust.first!r} {cust.__dict__}')
cust.first = 'Euclid'
print(f'After:  {cust.first!r} {cust.__dict__}')

 

 

 
 
posted @ 2022-06-23 00:48  就是想学习  阅读(39)  评论(0编辑  收藏  举报