流畅的python,Fluent Python 第二十章笔记 (属性描述符)

倒数第二章 属性描述符

描述符时对多个属性运用相同存取逻辑的一种方式。列如,Django ORM和SQL Alchemy等ORM中的字段类型时描述符,把数据库记录中字段里的数据与Python对象的属性对应起来。

 

描述符示例:验证属性

前面一章节,特性工厂函数借助函数式编程模式避免重复编写读取方法和设定方法,解决这种问题的面向对象方式是描述符类。

 

# 创建描述符
class Quantiy:

    def __init__(self, storage_name):
        # 描述符初始化赋值
        self.storage = storage_name

    def __set__(self, instance, value):
        # 托管属性 描述符示例操作托管示例对属性进行赋值
        if value > 0:
            instance.__dict__[self.storage] = value
        else:
            raise ValueError('value must be > 0')



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

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

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

 

上面的描述符实现的方式,我已经写了一些自己的解释

由于托管属性名称,与存储属性名称一样,所以不需要写__get__了,直接可以通过点的方式读取属性

托管属性,其实就时描述符实例,通过描述符实例的__set__对托管实例的属性进行属性赋值。

 

LineItem类第4版:自动获取储存属性的名称

为了避免在托管类属性赋值描述符实例过程中重复输入属性名,我们可以将每个Quantity实例的stroage_name属性自动初始化给予一个独一无二的字符串。

# 创建描述符
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]

    def __set__(self, instance, value):
        # 托管属性 描述符示例操作托管示例对属性进行赋值
        if value > 0:
            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

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

 

这个脚本不需要赋值托管类属性的描述符实例时添加参数,自动建立一个独一无二的参数。

由于托管属性对实例进行操作时,属性赋值与存储属性名称不一致,所以可以通过setattr与getattr对实例进行操作,不必担心递归调用的问题。但我觉的对托管实例进行操作时,一直用__dict__比较好

这样就没必要激活__setattr__或__getattr__方法。

__get__(self, instance, owner):里面三个参数分别为描述符实例本身(也是托管类属性),托管类实例,托管类。

Django模型字段就时描述符

 

特性工厂与描述符类的比较

# 创建特性工厂函数
def quantiy:
    try:
        quantiy.counter += 1
    except AttributeError:
        quantiy.counter = 0
    # 通过函数属性赋值,定义一个独一无二的名字给函数属性
    storage_name = '_{}:{}'.format('quantity', quantiy.counter)

    def qty_getter(instance):
        return instance.__dict__[storage_name]

    def qty_setter(instance, value):
        if value > 0:
            instance.__dict__[storage_name] = value
        else:
            raise ValueError('value must be > 0')
    # 返回特性
    return property(qty_getter, qty_setter)

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

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

 

上面是特性工厂函数实现的脚本

作者喜欢描述符类,因为描述符类可以使用子类扩展;若想重用工厂函数中的代码,除了复制粘贴,很难有其它方法

与通过函数属性和闭包状态相比,在类属性和实例属性中保持状态更容易理解。

 

LIneItem类第5版:一种新型描述符

这是一种通过继承的方式,针对不同的托管类实例的属性要求,定制不同的描述符过滤条件,这个也是描述符的好处,是特性工厂函数不能实现的。

先上描述符类:

import abc


# 创建一个自动管理和存储属性的描述符类
class AutoStorage:

    __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):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage)

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

# 扩展AutoStorage类的抽象子类,覆盖__set__方法,调用必须由子类事项的validate
class Validate(abc.ABC, AutoStorage):

    def __set__(self, instance, value):
        value = self.validate(instance, value)
        super().__set__(instance, value)

    @abc.abstractmethod
    def validate(self, instance, value):
        '''return validate value or raise ValueError'''

# 给数字不能小于0的属性用的描述符
class Quantity(Validate):
    def validate(self, instance, value):
        if value < 0 :
            raise ValueError('value must be > 0')
        return value

# 给字段参数不能为0的属性用描述符
class NonBlack(Validate):
    def validate(self, instance, value):
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value

 其实上面的代码,就是很好的使用了接口的定义,将通用的接口__set__下面抽象一个通用的方法出来,后面的类只需要重新定义这个方法就好,

这是我的在架构设计的时候缺少的灵感,需要加强学习。

接着上托管类。

import model_v5 as model

class LineItem:
    # 描述符实例赋值给托管类属性
    weight = model.Quantity()
    price = model.Quantity()
    description = model.NonBlack()

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

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

 

 

覆盖型与非覆盖型描述符对比

书中用代码进行了展示,我就不写了

主要意思就是描述符定义了__set__就属于覆盖型描述符

对托管类的实例进行属性操作时,优先使用描述符类的实例进行操作。

描述符有__get__的情况下调用__getattribtue__或__getattr__进行后,读取属性调用__get__操作托管类实例

没有__get__的情况下,直接调用__getattribtue__或__getattr__进行读取属性。

 

如果没有__set__方法的描述符属于非覆盖型描述符

如果托管类设置了同名的实例属性,那描述符会被遮盖,是遮盖,删除实例属性,描述符还能使用。

 

方法是描述符

在类中定义的函数属于绑定方法,因为函数都有__get__方法,所以依附在类上时,相当与描述符。

函数没有__set__方法,所以属于非覆盖型描述符

 

函数会变成绑定方法,这是Python语言底层使用描述符的最好例证。

 

func函数有不少方法与属性,书中的三种,我记录一下

类调用函数:

__get__,传入(对象),返回的就是绑定的方法

传入(None,类),返回的是函数本身

obj.func.__self__

返回obj本身

obj.func.__func__ 就是类.func

实例调用方法__func__返回函数本生

 

描述符用法建议(重点)

使用特性以保持简单

内置的property类创建的其实是覆盖型描述符,__set__方法和__get__方法都实现了,即便不定义设值方法也是如此。特性的__set__方法默认抛出AttributeError:can't set attribute,因此创建只读属性最简单的方式是使用特性,这能避免下一条所述的问题。

 

只读描述符必须有__set__方法

如果使用描述符类实现了只读属性,要记住,__set__和__get__两个方法必须都定义,否则,实例的同名属性会遮盖描述符。只读属性的__set__方法只需抛出AttributeError异常,并提供合适的错误消息

 

用于验证的描述符可以只有__set__方法

对仅用于验证的描述符来说,__set__方法应该检查value参数获得的值,如果有效,使用描述符实例的名称为键,直接在实例的__dict__属性中设置。这样,从实例中读取同名属性的速度很快,因为不用经过__get__处理。

 

仅有__get__方法的描述符可以实现高效缓存

如果只编写了__get__方法,那么创建的是非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。

同名实例属性会遮盖描述符,因此后续访问会直接从实例的__dict__属性中获取值,而不会再粗发描述符的__get__方法。

测试写了一些代码,感觉实际用户不大,(当时水平有限,装饰器的了解还是不够透彻,下面是PythonCookbook书中看来的代码,写的真心不错)

 

class lazyproperty:
 
    def __init__(self, func):
        self.func = func
 
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            value  = self.func(instance)
            # 托管实例,属性赋值
            instance.__dict__[self.func.__name__] =  value
            return value
 
    # 有了__set__就是不可覆盖的描述符了
    # def __set__(self, instance, value):
    #     raise TypeError()
 
import math
 
class Circle:
 
    def __init__(self, radius):
        self.radius = radius
 
    @lazyproperty
    def area(self):
        print('Computing area')
        return math.pi * self.radius ** 2
 
    @lazyproperty
    def perimeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius
 
if __name__ == '__main__':
    c = Circle(4.0)
    print(c.area)
    print(c.area)
    c.area = 50
    print(vars(c))

 

 

非特殊的方法可以被实例属性遮盖

由于函数和方法只实现了__get__方法,它们不会处理同实名的赋值操作。因此,像my_obj.the_method = 7这样的简单赋值之后,后续通过该实例访问the_methed得到的数字7---但是不影响类或其他实例。

然而,特殊方法【魔法函数】不受这个问题的影响。解释器只会在类中寻找特殊的方法,也就是说,repr(x)执行的其实是x.__class__.__repr__(x),因此x的__repr__属性对perp(x)方法调用没有影响

 

只要用到了特殊的方法、类方法、静态方法和特性,他们就不能被实例属性覆盖。

 

posted @ 2020-02-02 22:50  就是想学习  阅读(346)  评论(0编辑  收藏  举报