流畅的Python(二十)-属性描述符

本文详细介绍了Python中的描述符(Descriptor)机制,包括描述符的使用、不同版本的LineItem类演示了如何通过描述符实现属性的自动存储、验证和覆盖规则,以及覆盖与非覆盖描述符的区别。
摘要由CSDN通过智能技术生成

一、核心要义

描述符是对多个属性运用相同存取逻辑的一种方式。其是实现了特定协议的类,常见协议包括__get__,__set__,__delete__方法。上一章介绍的property类就实现了完整的描述符协议。

二、代码示例

1、LineItem类第3版

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/17 12:46
# @Author  : Maple
# @File    : 01-LineItem类第3版.py
# @Software: PyCharm

"""一个简单的描述符类"""


class Quantity:

    def __init__(self,storage_name):
        self.storage_name = storage_name

    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.storage_name] = value
        else:
            raise ValueError('value must be >0')


class LineItem:

    weight = Quantity('weight')
    price = Quantity('price')

    def __init__(self,description,weight,price):
        self.description = description
        # 这里赋值的时候,会访问描述符类的set方法
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

    def __repr__(self):
        return 'LineItem =({},{},{})'.format(self.description,self.weight,self.price)

if __name__ == '__main__':

    # 1.传入正常的值
    com = LineItem('Computer',10,2000)
    print(com) # LineItem =(Computer,10,2000)

    # 2.weight负值测试
    try:
        com3 = LineItem('Computer', -10, 1000)
    except ValueError as e:
        print(e)  # value must be >0

2、LineItem类第4版

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/17 9:36
# @Author  : Maple
# @File    : 02-LineItem类第4版.py
# @Software: PyCharm

"""自动获取存储属性的值"""

class Quantity:

    __counter = 0

    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix,index)
        cls.__counter += 1

    def __get__(self, instance, owner):
        if instance is None:
            # 通过托管类(此例中的LineItem)直接访问类属性时(此例中的weight和price时,instance为None,如果直接执行getattr,会报错
            # 因此在此进行一下处理:直接返回描述符类
            return self
        else:
            return getattr(instance,self.storage_name)

    def __set__(self, instance, value):
        if value > 0:
            return setattr(instance,self.storage_name,value)
        else:
            raise ValueError('value must be >0')


class LineItem:

    weight = Quantity()
    price = Quantity()

    def __init__(self,description,weight,price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price


if __name__ == '__main__':

    # 1.初始化
    """初始化流程分析
    1.当执行到self.weight的时候,会去调用Quantity的__set__方法,然后给LineItem实例的'_Quantity#0'属性赋值100
    2.当执行到self.price的时候,会去调用Quantity的__set__方法,然后给LineItem实例的'_Quantity#1'属性赋值100000
    """
    computer = LineItem('Computer',100,100000)
    # 1-1 注意观察实例属性,并非weight和price,而是_Quantity#0和_Quantity#1
    print(computer.__dict__) # {'description': 'Computer', '_Quantity#0': 100, '_Quantity#1': 100000}
    # 1-2 类属性中包含weight和price,并且它们都是Quantity描述符类对象
    print(LineItem.__dict__) # {....'weight': <__main__.Quantity object at 0x000001594A20AB20>, 'price': <__main__.Quantity object at 0x000001594A1E0700>,...}

    # 2.访问属性
    ## 流程分析:会去调用Quantity的__get__方法,然后返回computer中'_Quantity#0'属性的值
    weight = computer.weight
    print(weight) # 100

    # 3.通过托管类直接访问类属性:返回描述符类本身
    print(LineItem.weight) #<__main__.Quantity object at 0x00000266E579AB20>

3、LineItem类第5版

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/17 12:24
# @Author  : Maple
# @File    : 03-LineItem类第5版.py
# @Software: PyCharm


"""创建两个抽象类
1.AutoStorage:自动管理存储属性的描述符类
2.Validated:扩展AutoStorage类的抽象子类,覆盖__set__方法,调用必须由子类实现的validate方法
3.实现类继承Validated,从而可以创建具备不同规则的描述符类
"""

import abc


class AutoStorage:
    __counter = 0

    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix,index)
        cls.__counter += 1

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance,self.storage_name)

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


class Validated(abc.ABC,AutoStorage):
    def __set__(self, instance, value):
        value = self.validate(instance,value)
        super().__set__(instance,value)


    @abc.abstractmethod
    def validate(self,instance,value):
        """retuen validated value or raise ValueError"""



class Quantity(Validated):
    """a number greater than zero"""
    def validate(self,instance,value):
        if value > 0:
            return value
        else:
            raise ValueError('value must be >0')

class NonBlank(Validated):
    """a string with at least one non-space character"""
    def validate(self,instance,value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value can not be empty or blank')
        else:
            return value


class LineItem:

    description = NonBlank()
    weight = Quantity()
    price = Quantity()

    def __init__(self,description,weight,price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

    def __repr__(self):

        return 'LineItem =({},{},{})'.format(self.description,self.weight,self.price)


if __name__ == '__main__':

    # 1.传入正常的值
    com1  = LineItem('Computer',10,1000)
    print(com1) # LineItem =(Computer,10,1000)

    # 2.description传入空值
    try:
        com2 = LineItem('', 10, 1000)
    except ValueError as e:
        print(e) # value can not be empty or blank

    # 3.weight负值测试
    try:
        com3 = LineItem('Computer', -10, 1000)
    except ValueError as e:
        print(e)  # value must be >0

4、覆盖与非覆盖描述符

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/17 11:04
# @Author  : Maple
# @File    : 04-覆盖与非覆盖描述符.py
# @Software: PyCharm

# 将包含parent包的路径添加进系统路径
import sys

sys.path.append(r"D:\01-study\python\fluent_python\20-属性描述符\utils")

from descriptorkinds import print_args,display


class Overriding:
    """也称数据描述符或强制描述符"""
    def __get__(self, instance, owner):
        print_args('get',self,instance,owner)

    def __set__(self, instance, value):
        print_args('set',self,instance,value)

class OverridingNoGet:
    """没有get方法覆盖型描述符"""
    def __set__(self, instance, value):
        print_args('set',self,instance,value)

class NonOverrding:
    """也称非数据描述符或遮盖型描述符"""
    def __get__(self, instance, owner):
        print_args('get',self,instance,owner)

class Managed:
    over = Overriding()
    over_no_get = OverridingNoGet()
    non_over = NonOverrding()

    def spam(self):
        print('--> Managed.spam({})'.format(display(self)))


if __name__ == '__main__':

    print('*****1.覆盖型描述符测试****************')
    # 1.覆盖型描述符测试
    obj = Managed()
    # 1-1.实例访问类属性(是一个描述符实例): 会触发描述符的get方法
    obj.over # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

    # 1-2.类访问类属性(是一个描述符实例):同样会触发描述符的get方法,不过传入get方法的instance为None
    Managed.over # -> Overriding.__get__(<Overriding object>, None, <class Managed>)

    # 1-3.实例属性修改over,会触发描述符的set方法
    obj.over = 7  # -> Overriding.__set__(<Overriding object>, <Managed object>, 7)

    # 1-4.实例属性访问over,仍然会触发描述符的get方法
    obj.over # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

    # 1-5.跳过描述符,直接通过dict赋值属性
    obj.__dict__['over'] = 8
    # 实例属性中确实有一个over实例属性(注意该同名的实例属性与类属性[对应的是描述符over]并不是同一个东西)
    print(vars(obj)) # {'over': 8}

    # 1-6 实例属性访问over,仍然会触发描述符的get方法,而不是直接访问实例属性
    obj.over # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

    print('*****2.没有get方法的覆盖型描述符****************')
    # 2.没有get方法的覆盖型描述符测试
    # 2-1 因为over_no_get描述符并没有get方法,因此下面的操作是直接访问 Managed的类属性
    print(obj.over_no_get) # <__main__.OverridingNoGet object at 0x000001CAA655A370>
    print(Managed.over_no_get) # <__main__.OverridingNoGet object at 0x000001CAA655A370>

    # 2-2.实例给属性赋值,会触发描述符的set方法
    obj.over_no_get = 7 # -> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)

    # 2-3 由于上步操作并没有修改属性,因此以下访问的仍然是Manged的类属性
    print(obj.over_no_get)

    # 2-4 通过实例的__dict__属性设置名为over_no_get的实例属性
    obj.__dict__['over_no_get'] = 7
    # 由于实例属性会覆盖类属性,因此以下会直接读取实例属性的值
    print(obj.over_no_get) # 7
    # 但作为类属性的over_no_get 并没有发生变化
    print(Managed.over_no_get) #<__main__.OverridingNoGet object at 0x000001F24D80A370>

    # 2-5 仍然会触发描述符的set方法(但注意set方法并没有修改实例属性over_no_get的值)
    obj.over_no_get = 9 #-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 9)

    # 2-6 实例属性over_no_get的值并没有发生变化
    print(obj.over_no_get) # 7

    print('*****3.非覆盖型描述符****************')
    # 3.非覆盖型描述符测试
    ## 3-1 实例访问non_over,会触发实例的get方法
    obj.non_over

    ## 3-2 实例修改non_over,因为描述符没有set方法,所以相当于直接修改实例的实例属性
    obj.non_over = 7
    ## ***然后实例属性会覆盖同名的描述符属性:这是一个规则吗?如何理解***
    print(obj.non_over) # 7,不会走描述符的get逻辑

    ## 3-3 通过类访问non_over,描述符仍然存在
    Managed.non_over # -> NonOverrding.__get__(<NonOverrding object>, None, <class Managed>)

    ## 3-4 删除实例属性non_over
    del obj.non_over

    ## 3-5 再次通过实例访问non_over,此时会走描述符的get方法
    obj.non_over # -> NonOverrding.__get__(<NonOverrding object>, <Managed object>, <class Managed>)


    # 4.在类中覆盖描述符
    print('*****4.在类中覆盖描述符****************')
    # 直接通过类修改类属性值(修改之后描述符不存在,变成单纯的类属性,而实例属性的优先级会大于类属性),此时并不会走描述符的set方法[作为对比:通过类读取类属性,会走描述符的get方法]
    # 这种现象也反映了 读写属性的一种不对等
    Managed.over = 1
    Managed.over_no_get = 2
    Managed.non_over = 3
    ## 直接返回实例属性,不会经过描述符get逻辑--描述符已被销毁了
    print(obj.over) #8
    print(obj.over_no_get) #7
    print(obj.non_over) #3

    # 5.方法是描述符
    ## 5-1 通过obj.span获取的是一个`绑定方法对象`,可以观察到是method类型,而不是function
    ## 5-2 该方法的参数:__main__.Managed object at 0x00000186C6D7A400>,是一个Managed(托管类)实例
    print(obj.spam) # <bound method Managed.spam of <__main__.Managed object at 0x00000186C6D7A400>>


    ## 5-3 通过类.spam获取的则是方法本身
    print(Managed.spam) # <function Managed.spam at 0x00000291750E8790>

    ## 5-4 由于函数是非覆盖型(未实现set方法)的描述符,如果通过obj.spam赋值(相当于新增实例属性),会覆盖描述符
    ## 此时spam方法还可以调用
    obj.spam() # --> Managed.spam(<Managed object>)
    ## 相当于给obj新增一个实例属性,spam描述符会被覆盖,因此方法也无法调用
    obj.spam = 10
    try:
        obj.spam()
    except Exception as e:
        print(e) # 'int' object is not callable

    ## 访问spam实例属性的值
    print(obj.spam) # 10
  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值