读书笔记:《流畅的Python》第20章 属性描述符

本文详细介绍了Python中的描述符,包括描述符如何用于属性管理和验证,以及它们如何与实例属性交互。通过示例展示了描述符的实现,如Quantity和NonBlank,用于限制LineItem类的weight和price属性必须大于零,以及方法作为描述符的特性。文章还探讨了覆盖型和非覆盖型描述符的区别,并提供了使用特性工厂函数替代描述符的实现方式。
# 第20章 属性描述符
"""
描述符: 是对多个属性运用相同存取逻辑的一种方式
    描述符是实现了特定协议的类
    包括__get__.__set__.__delete__
    property实现了完整的描述符协议
    使用描述符的python功能还有staticmethod\classmethod装饰器
    理解描述符是精通python的关键
"""

# 20.1描述符示例:验证属性

# 20.1.1 LineItem类第三版:一个简单的描述符类
# 实现了__get__.__set__.__delete__的类是描述符,描述符的用法是创建一个实例,作为另一个类的类属性
"""
定义
描述符类:实现描述符协议的类,如下例中的Quantity
托管类:把描述符类的实例申明为类属性的类,如下例中的LineItem
描述符实例:描述符类的各个实例,申明为托管类的类属性
托管实例:托管类的实例,如下例中的LineItem类的实例
存储属性:托管实例中存储自身托管属性的属性,如下例中的LineItem类的实例的weight和price属性
托管属性:托管类中由描述符实例处理的公开属性
值存储在存储属性中,这种属性和描述符实例不同,描述符属性都是类属性
"""
# 示例20-1 bulkfood_v3.py:使用Quantity描述符管理LineItem类的属性

# 20.1.2 LineItem类第四版:自动获取存储属性的名称
# 为了避免重复输入属性的名称,为每一个Quantity实例的storage_name属性生成一个独一无二的字符串
# 为了生成storage_name 我们以"_Quantity#"为前缀,后面拼接一个整数
# Quantity.__counter类属性的当前值,每次把一个新的描述符实例依附到类上,都会递增这个值

# 示例 20-2 bulkfood_v4.py:每个Quantity描述符都有独一无二的storage_name
# 示例 20-3 bulkfood_v4b.py:通过托管类调用时,__get__方法返回描述符的引用
"""
通常,我们不会在使用描述符的模块中定义描述符类
而是在一个单独的模块中定义,以便在整个应用中使用
"""
# 示例 20-4 bulkfood_v4c.py :整洁的LineItem类,Quantity在model_v4c模块中

# 使用上一章的特性工厂函数实现和描述符类相同的功能
# 示例20-5 bulkfood_v4prop.py:使用特性工厂函数实现和描述符类相同的功能
# 描述符类可以使用子类扩展

# 20.1.3 LineItem类第5版:一种新型描述符
"""
在上述虚构的有机食物网店遇到一个问题:
    有个商品的描述信息为空,导致无法下单
    为解决这个问题,再创建一个描述符--NonBlank
    它和Quantity描述符很像,只是验证逻辑不同
回想Quantity描述符
    功能是管理托管实例的存储属性,以及验证用于设置那两个属性的值
    
重构这个描述符类,并创建两个基类
    AutoStorage
        自动管理存储属性的描述符类
    Validated
        扩展AutoStorage类的抽象子类,覆盖__set__方法,调用必须由子类实现的validate方法
"""

# 示例20-6 model_v5.py:重构构的描述符类
'''model_v5脚本的用户不需要知道全部细节
只需知道,可以实用Quantity和NonBlank自动验证属性的值'''

# 示例20-7 bulkfood_v5.py:使用Quantity和NonBlank描述符的LineItem类

# 20.2 覆盖型和非覆盖型描述符对比
"""
python存取属性的方式特别不对等:
    通过实例读取属性时,通常返回实例中定义的属性
    如果实例中没有指定的属性,那么会获取类属性
    而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类属性

根据是否定义了__set__方法,描述符可以分为覆盖型和非覆盖型

"""
# 示例20-8 descriptorkinds.py:几个简单的类用于研究描述符的覆盖行为

# 20.2.1覆盖型描述符
"""
实现了__set__方法的描述符属于覆盖型描述符
因为虽然描述符是类属性,但是实现了__set__方法的话,会覆盖对实例属性的赋值操作"""

# 20.2.2没有__get__方法的覆盖型描述符
"""
覆盖型描述符通常会实现__set__和__get__方法,不过也可以只实现__set__方法
此时,只有写操作会由描述符处理,通过实例读取描述符会返回描述符本身
因为没有处理读值操作的__get__方法
如果直接通过实例的__dict__属性创建同名属性,以后再设置这个属性时,
仍会由__set__方法处理,但是读取这个属性,就会直接从属性中返回这个值,而不会返回描述符对象
"""
# 20.2.3非覆盖型描述符

# 20.2.4 在类中覆盖描述符
"""不管描述符是不是覆盖型
    为类属性赋值都能覆盖描述符,这是一种猴子补丁技术"""

# 20.3方法是描述符
"""
在类中定义的函数属于绑定方法(bound method)
因为用户定义的函数都有__get__方法,所以依附到类上时,就相当于描述符
"""
# 示例20-14 method_is_descriptor.py:Text类,继承自UserString类

# 20.4 描述符与用法建议
"""结论:
    1.使用特性以保持简单
        内置的property类创建的其实是覆盖型描述符 __set__和__get__都实现了
    2.只读描述符必须由__set__方法
        否则实例的同名属性会掩盖描述符
    3.用于验证的描述符可以只有__set__方法
    4.仅有__get__方法的描述符可以实现高效缓存
    5.非特殊的方法可以被实例属性覆盖
    
"""

"""
描述符的文档字符串用于注解托管类中各个描述符实例
"""
# 20.5 描述符的文档字符串和覆盖删除操作
示例20-1 bulkfood_v3.py:使用Quantity描述符管理LineItem类的属性
# 示例20-1 bulkfood_v3.py:使用Quantity描述符管理LineItem类的属性
class Quantity:  # 1
    def __init__(self,storage_name):
        self.storage_name = storage_name  # 2
    def __set__(self, instance, value):  # 3
        if value > 0 :
            # 错误示范
            # self.__dict__[self.storage_name] = value 不能把值存储在描述符实例中
            instance.__dict__[self.storage_name] = value  # 4
        else:
            raise ValueError('value must be > 0')
class LineItem:
    weight = Quantity('weight')  # 5 这里需要重复输入weight,如果能weight = Quantity()就好了
    price = Quantity('price')  # 6

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

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

"""
解释:
    1.描述符基于协议实现,无需创建子类
    2.Quantity有一个storage_name属性,这是托管实例中存储值的属性的名称
    3.尝试为托管属性赋值时会触发__set__方法,self是描述符实例,instance是托管实例,value是要设置的值
    4.直接处理托管实例的__dict__属性,如果使用内置方法setattr()会触发无限递归
    5.第一个描述符实例绑定给weight属性
    6.第二个描述符实例绑定给price属性
    7.余下的代码和上一章中的特性工厂函数相同
   
"""
if __name__ == '__main__':
    truffle = LineItem('White truffle',100,0)  # ValueError: value must be > 0

 

# 示例 20-2 bulkfood_v4.py:每个Quantity描述符都有独一无二的storage_name
# 示例 20-2 bulkfood_v4.py:每个Quantity描述符都有独一无二的storage_name

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

    def __get__(self,instance,ower):  # 5
        # ower是托管类的引用
        return getattr(instance,self.storage_name)  # 6 因为名称不同,所以不会触发描述符,不会无限递归

    def __set__(self, instance, value):
        if value > 0 :
            setattr(instance,self.storage_name,value)  # 7
        else:
            raise ValueError('value must be > 0')
class LineItem:
    weight = Quantity()  #8
    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

"""
说明:
    1.__counter是Quantity类的类属性,用来统计Quantuty实例的数量
    2.cls是Quantity类的引用
    3.每一个描述符实例都是独一无二的,如_Quantity#0
    4.递增__counter属性的值
    5.要实现__get__方法,因为托管属性的名称和storage_name不同
    6.使用内置的getattr方法从instance中获取存储属性的值
    7.使用内置的setattr把值存储在instance中
    8.不用把托管属性的名称传给Quantity构造方法,这是一版的目标
"""

if __name__ == '__main__':
    thing = LineItem('thing',10,20)
    print(thing.weight, thing.price)
    print(getattr(thing, '_Quantity#0'))
    print(getattr(thing, '_Quantity#1'))
    thing.price = 100
    print(thing.price)

    print(LineItem.weight)
# 示例 20-3 bulkfood_v4b.py:通过托管类调用时,__get__方法返回描述符的引用

 

# 示例 20-3 bulkfood_v4b.py:通过托管类调用时,__get__方法返回描述符的引用

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,ower):
        # ower是托管类的引用
        if instance is None:
            return self  # 如果不是通过实例调用,返回描述符自身
        else: # 否则像之前一样返回托管属性的值
            return getattr(instance,self.storage_name)

    def __set__(self, instance, value):
        if value > 0 :
            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__':
    thing = LineItem('thing',10,20)
    print(thing.weight, thing.price)
    print(getattr(thing, '_Quantity#0'))
    print(getattr(thing, '_Quantity#1'))
    thing.price = 100
    print(thing.price)

    print(LineItem.weight)
# 示例 20-4 bulkfood_v4c.py :整洁的LineItem类,Quantity在model_v4c模块中

 

# 示例 20-4 bulkfood_v4c.py :整洁的LineItem类,Quantity在model_v4c模块中
import model_v4c as model
class LineItem:
    weight = model.Quantity()
    price = model.Quantity()

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

    def subtotal(self):
        return self.weight * self.price
# 示例20-5 bulkfood_v4prop.py:使用特性工厂函数实现和描述符类相同的功能
# 示例20-5 bulkfood_v4prop.py:使用特性工厂函数实现和描述符类相同的功能
def quantity():
    try:
        quantity.counter += 1  # 把counter定义为函数自身的属性
    except AttributeError:
        quantity.counter = 0  # 如果属性未定义,将其值设置为0
    storage_name = '_{}:{}'.format('quantity',quantity.counter)  # 将创建一个局部变量借助闭包保存它的值

    def qty_getter(instance):
        return getattr(instance,storage_name)

    def qty_setter(instance,value):
        if value > 0 :
            setattr(instance,storage_name,value)
        else:
            raise ValueError('value must be >0')
    return property(qty_getter,qty_setter)

class LineItem:
    weight = quantity('weight')
    price = quantity('price')
    def __init__(self,description,weight,price):
        self.description = description
        self.weight = weight
        self.price = price
    def subtotal(self):
        return self.weight * self.price

 

# model_v4c.py 在单独的模块中定义描述符类
# model_v4c.py 在单独的模块中定义描述符类
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,ower):
        # ower是托管类的引用
        if instance is None:
            return self  # 如果不是通过实例调用,返回描述符自身
        else: # 否则像之前一样返回托管属性的值
            return getattr(instance,self.storage_name)

    def __set__(self, instance, value):
        if value > 0 :
            setattr(instance,self.storage_name,value)
        else:
            raise ValueError('value must be > 0')
# 示例20-6 model_v5.py:重构构的描述符类
# 示例20-6 model_v5.py:重构构的描述符类
import abc
class AutoStorage:  #1 AutoStorage提供了之前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,ower):
        # ower是托管类的引用
        if instance is None:
            return self  # 如果不是通过实例调用,返回描述符自身
        else: # 否则像之前一样返回托管属性的值
            return getattr(instance,self.storage_name)

    def __set__(self, instance, value):
        setattr(instance,self.storage_name,value) #2 ..验证部分除外

class Validated(abc.ABC,AutoStorage):  # 3 抽象类也继承AutoStorage
    def __set__(self, instance, value):
        value = self.validate(instance,value)  # 4 把验证操作委托给validate方法
        super().__set__(instance,value)  # 5 然后把返回的值传给超类的__set__方法
    @abc.abstractmethod
    def validate(selfs,instance,value):  # 6 validate是抽象方法
        '''return validated value or raise ValueError'''

class Quantity(Validated):  # 7 Quantity和NonBlank都继承自Validated
    '''a number greater than zero'''
    def validate(selfs,instance,value):
        if value <= 0:
            raise ValueError('value must be > 0 ')
        return value

class NonBlank(Validated):
    '''a string with at least one non-space character'''
    def validate(selfs,instance,value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value  # 8 要求具体的方法返回验证后的值,借机可以清理\转换和规范化接收的数据

if __name__ == '__main__':
    help(Quantity)

 

# 示例20-7 bulkfood_v5.py:使用Quantity和NonBlank描述符的LineItem类
# 示例20-7 bulkfood_v5.py:使用Quantity和NonBlank描述符的LineItem类
import model_v5 as model
class LineItem:
    weight = model.Quantity()
    price = model.Quantity()
    description = model.NonBlank()

    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__':
        # stuff = LineItem('',10,0)
        help(LineItem.weight)

 

# 示例20-8 descriptorkinds.py:几个简单的类用于研究描述符的覆盖行为
# 示例20-8 descriptorkinds.py:几个简单的类用于研究描述符的覆盖行为
def cls_name(obj_or_cls):
    cls = type(obj_or_cls)
    if cls is type:
        cls = obj_or_cls
    return cls.__name__.split('.')[-1]


def display(obj):
    cls = type(obj)
    if cls is type:
        return '<class {}>'.format(obj.__name__)
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return '<{} object>'.format(cls_name(obj))


def print_args(name, *args):
    pseudo_args = ', '.join(display(x) for x in args)
    print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))


class Overriding:  # 1 有__get__和__set__的典型覆盖型描述符
    '''也称数据描述符或强制描述符'''

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)  # 2 在这个示例中,各个描述符的每个方法都调用了print_args函数

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


class OverridingNoGet:  # 3
    '''没有__get__方法的覆盖性描述符'''

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


class NonOverriding:  # 4 没有__set__
    '''也称非数据描述符或非覆盖型描述符'''

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)


class Managed:  #5 托管类,使用各个描述符的一个实例
    over = Overriding()
    over_no_get = OverridingNoGet()
    non_over = NonOverriding()

    def spam(self):  # 6 spam方法放在这里是为了对比,因为方法也是描述符
        print('-> Managed.spam({})'.format(display(self)))

if __name__ == '__main__':
    # 覆盖型描述符的行为
    """
    obj = Managed()
    print(obj.over)  # 触发__get__方法,第二个参数的值是托管实例obj
    print(Managed.over)  # 触发描述符的__get__方法,第二个参数(instance)的值是None
    obj.over = 7  # 为obj.over赋值触发__set__方法,最后一个参数是7
    print(obj.over)  # 读取obj.over,扔会触发描述符的__get__方法
    obj.__dict__['over'] = 8  # 跳过描述符,直接通过__dict__属性设置值
    print(vars(obj)) # 确认值在obj.__dict__属性中,在over键下
    print(obj.over) #然而,即使是名为over的实例属性,Managed.over描述符扔会覆盖读取obj.over这个操作
    """
    # 没有__get__方法的覆盖性描述符
    """
    obj = Managed()
    print(obj.over_no_get)
    print(Managed.over_no_get)
    obj.over_no_get = 7
    print(obj.over_no_get)
    obj.__dict__['over_no_get'] = 9
    print(obj.over_no_get)
    obj.over_no_get = 7
    print(obj.over_no_get)
    """
    # 非覆盖型描述符
    """obj = Managed()
    print(obj.non_over)
    obj.non_over = 7
    print(obj.non_over)
    print(Managed.non_over)
    del obj.non_over
    print(obj.non_over)"""
    # 通过类可以覆盖任何描述符
    """obj = Managed()
    Managed.over = 1
    Managed.over_no_get = 2
    Managed.non_over = 3
    print(obj.over, obj.over_no_get, obj.non_over)  # 描述符不见了"""
    # 方法是非覆盖型描述符
    obj = Managed()
    print(obj.spam)  # <bound method Managed.spam of <__main__.Managed object at 0x00000245DD80BA90>>
    print(Managed.spam)  # <function Managed.spam at 0x00000217BD3B0670>
    obj.spam = 7  # 如果为obj.spam赋值,会掩盖类属性,导致无法通过obj实例访问spam方法
    print(obj.spam)
# 示例20-14 method_is_descriptor.py:Text类,继承自UserString类
# 示例20-14 method_is_descriptor.py:Text类,继承自UserString类
import collections
class Text(collections.UserString):
    def __repr__(self):
        return 'Text({!r})'.format(self.data)
    def reverse(self):
        return self[::-1]

word = Text('forward')
print(repr(word))
print(repr(word.reverse()))
print(repr(Text.reverse(Text('backword'))))  #  在类上调用方法相当于调用函数
print(type(Text.reverse), type(word.reverse)) #类型是不同的,一个是function一个是method
# Text.reverse相当于函数,甚至可以处理Text之外的对象
print(list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')])))
# 函数都是非覆盖性描述符,在函数上调用__get__方法时,传入实例,得到的是绑定到那个实例上的方法
print(Text.reverse.__get__(word))
# 在函数上调用__get__方法时,传入实例是None,得到的是函数本身
print(Text.reverse.__get__(None, word))
print(word.reverse) # word.reverse其实会调用Text.reverse.__get__(word)返回对应的绑定方法
print(word.reverse.__self__)#word.reverse有个__self__属性,其值为调用这个方法的实例的引用
print(word.reverse.__func__ is Text.reverse) # 绑定方法的__func__属性时依附在托管类上那个原始函数的引用

 


35岁学python,也不知道为了啥?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值