Python - 属性描述符

描述符概念

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

描述符是实现了动态协议的类。这个协议包括__get__、_set_、__delelte__方法。property 类实现了完整的描述符协议。通常,动态协议可以部分实现。其实,我们在真实的代码中见到的大多数描述符只实现了__get__方法和__set__方法,还有很多只实现了其中一个方法。

描述符是Python 独有的功能,不仅能在应用程序中使用,在语言的基础设施中也会用到。用户定义的函数就是描述符。我们将看到,描述符可以把方法变成绑定方法或非绑定方法,这取决于方法的调用方式

Demo:

import numbers

class IntFiled:

    def __get__(self, instance, owner):
        return self.value

    # 对age的类型做限制
    def __set__(self, instance, value):
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")
        self.value = value


class User:
    age = IntFiled()   # 关键点,变量名要和__init__中的属性一致
    def __init__(self, age):
        self.age = age

if __name__ == '__main__':
    user =  User(18) 
    print(user.age)

Debug 看下 instance 和 value 的值:

描述符示例:属性验证

特性工厂函数借助函数式编程模式避免重复编写读值方法和设值方法。特性工厂函数是高阶函数,在闭包中存储storage_name 等设置。由参数决定创建哪些存取函数,再使用存取函数构建自定义的特性实例。解决这种问题的面向对象的方式是描述符类

LineItem类第三版:一个简单的描述符

描述符的用法是创建一个实例,作为另一个类的类属性。

  • 描述符类:Quantity
  • 托管类: LineItem
  • 储存属性:托管实例中存储自身托管属性的属性,如LineItem的weight和price属性是储存属性
  • 托管属性:托管类中由描述符实例处理的公开属性,值存储在储存属性中。也就是说,描述符实例和储存属性为托管属性建立了基础
# 示例23-1 bulkfood_v3.py Quantity  不接受负值
class Quantity: # 1
    def __init__(self, storage_name):
        self.storage_name = storage_name # 2

    def __set__(self, instance, value): # 3
        if value > 0:
            instance.__dict__[self.storage_name] = value # 4
        else:
            raise ValueError('value mast be > 0')
        
    def __get__(self, instance, owner): # 5
        return instance.__dict__[self.storage_name]

  1. 描述符基于协议实现,无须子类化
  2. Quantity 实例有一个storange_name 属性,这是托管实例中用于存储值得储存属性名称
  3. 尝试为托管属性赋值时,调用__set__方法。这里,self是描述符实例(LineItem.weight或LineItem.price),instance是托管实例(LineItem实例),value是要设定得值
  4. 必须把属性得值直接存入__dict__。调用setattr(instance,self.storage_name) 将再次调用__set__方法,导致无限递归
  5. 需要实现__get__方法,因为托管属性得名称可能与storange_name不同。owner参数稍后解释

__get__方法有必要实现,因为用户可能编写如下代码:

  class House:
    rooms = Quantity('number_of_roomrs')

在这个House类中,托管属性是rooms,而存储属性是number_of_rooms。对于一个名为chaos_manor 得House示例,读写chaos_manor.rooms都经过依附在rooms上得Quantity描述符。但是读写chaos_manor.number_of_rooms 会绕过该描述符

注意,__get__方法接受3个参数:self、instance、owenr。owenr 参数是对托管类(例如LineItem)得引用。在希望描述符支持获取类属性时会用到--比如说模拟Python获取类属性,但是在实例中未找到指定名称得属性时得默认行为。

如果通过类获取托管属性(例如LineItem.weight),那么描述符得__get__方法收到得instance参数值为None

为了支持内省和其他元编程技巧,当通过类属性获取托管属性时,__get__方法最好返回描述符实例。为此,应像下面这样编写__get__方法

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

示例32-2 bulkfood_v3.py:在LineItem中使用Quantity描述符管理属性
class LineItem:
    weight = Quantity('weight')  # 1
    price = Quantity('price')  # 2

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

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

1.第一个描述符实例管理weight属性
2.第二个描述符实例管理price属性

实例23-2 中得代码可能像预期那样运行,禁止以0美元销售松露

item = LineItem('White truffle', 100, 0)

Traceback (most recent call last):
  File "E:\PyProject\study\test_08.py", line 34, in <module>
    l = LineItem('White truffle', 100, 0)
  File "E:\PyProject\study\test_08.py", line 27, in __init__
    self.price = price
  File "E:\PyProject\study\test_08.py", line 17, in __set__
    raise ValueError('value mast be > 0')
ValueError: value mast be > 0

tips: 编写描述符得__get__和__set__方法时,要记住self参数和instance 参数得意思,self是描述符实例,instace 是托管实例。管理实例属性得描述符应该把值存储在托管实例中,因此,Python才为描述符中得方法提供了instace参数。

测试托管属性名称和存储属性名称不一致:

可以正常访问weight_test

LineItem类第4版:为存储属性自动命名

为了避免在描述符实例中重复输入属性名,我们将实现__set_name__方法,设置各个Quantity实例得storage_name。特殊方法__set_name__在Python3.6 中加入了属性描协议。解释器会在class主体中找到得每个描述符上调用__set_name__方法,当前前提是描述符实现了该方法

class Quantity:

    def __set_name__(self, owner, name): # 1
        self.storage_name = name # 2

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

    # 不需要__get__

class LineItem:
    weight = Quantity()   # 5
    price = Quantity()  

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
  1. self 是描述符实例,owner 是托管类,name 是托管属性
  2. 与示例23-1 中得__init__做法一致
  3. 这里得__set__与示例23-1 中得一摸一样
  4. 不需要实现__get__方法,因此存储属性得名称与托管属性名称一致。表达式product.price直接从LineItem实例中获取price属性。
  5. 现在,不用把托管属性得名称传给Quantity构造函数。这正是这一版得目标

测试托管属性名称和存储属性名称不一致:

直接报错:

看着实例23-3,你或许会想,仅仅为了托管两个属性,有必要写这么多代码吗?但是,要知道,现在描述符得逻辑抽象到单独得代码单元中了:Quantity类。通常,我们不在使用描述符得模块中定义描述符,而是在一个单独的实用工具模块中定义,以方便在整个应用程序中重用 ---- 如果是在开发库或者框架,那么甚至可以在多个应用程序中使用。

# 示例23-4 bulkfood_v4c.py 整洁的LineItem类,Quantity 描述类现在位于导入的model_v4c模块中
import model_v4c as model

class LineItem:
    weight = model.Quantity()   # 5
    price = model.Quantity()  

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

由于描述符通过类实现,因此可以利用继承重用部分代码来创建新描述符

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

我们虚构的有机食物网店遇到一个问题,不知怎么回事儿,有个商品描述信息为空,导致无法下单。为了避免出现这个问题,我们将再创建一个描述符NonBlank。在设置NonBlank的过程中,我们发现它与Quantity 描述符很像,只是验证逻辑不同。

据此,我们判断需要重构,定义一个抽象类Validated,覆盖__set__方法。调用必须由子类实现的validate方法。然后,重写Quantity 类,通过继承Validated 类并只编写validate 放来实现NonBlank。

Validated、Quantity、NonBlank 这3个类之间的关系体现了《设计模式》一书中提出的模板方法

模板方法是一些抽象的操作定义算法,而子类将重定义这些操作以提供具体的行为。

在示例23-5中,Validated.__set__是模板方法,self.valiete 是抽象操作

# 示例23-5 model_v5.py: 抽象基类Validated

import abc

class Validated(abc.ABC):
    
    def __set_name__(self, owner, name):
        self.storage_name = name
         
    
    def __set__(self, instance, value): 
        value = self.validate(self.storage_name,value) # 1
        instance.__dict__[self.storage_name] = value  # 2
        
        
    @abc.abstractmethod
    def validate(self, name,value): # 3
        """返回通过验证的值, 或者抛出ValueError"""
  1. set 方法把验证操作委托给 validate方法
  2. ....然后使用返回的value 更新存储的值
  3. validate 是一个抽象方法,即模板方法。
class Quantity(Validated):
    """数值要大于零"""

    def validate(self, name, value):
        if value <= 0:
            raise ValueError(f'{self.storage_name} must be >0')
        return value


class NonBlank(Validated):
    """字符串至少要包含一个非空字符"""

    def validate(self, name, value):  # 1
        value = value.strip()
        if not value: # 2
            raise ValueError(f'{name} cannot be blank') # 3

  1. 实现抽象方法 Validated.validate 要求的模板方法
  2. 去除头尾的空白后,如果什么也没剩下,则拒绝提供的值
  3. 具体的validate 方法必须返回通过验证的值,防止后续需要清理,转换或规范化接收到的数据。这里,返回的是去除头尾的空白之后的value
import model_v4c as model # 1

class LineItem:
    description = model.NonBlank() # 2
    weight = model.Quantity()   
    price = model.Quantity()  

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
  1. 导入model_v5 模块,指定一个更友好的名称
  2. 使用model.NonBlank 描述符,其余的代码没变

本章所举的几个LineItem 示例演示了描述符的典型用途,即管理数据属性。Quantity 这种描述符叫做覆盖型描述符,因为描述符的__set__方法使用托管实例中的同名属性覆盖了(插手接管)了要设置的属性。除此之外,还有非覆盖型描述符。

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

根据是否实现__set__方法,描述符可以分为两大类。实现__set__ 方法的类是覆盖型描述符,未实现__set__方法的类则是非覆盖型描述符。

为了观察两类描述符的差异,需要用到几个类。我们将使用23-8中的代码作为接下来几节的试验台。

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

### 辅助函数 , 仅用于展示 ###


def cls_name(obj_or_cls):
    """ 获取类名称 """
    cls = type(obj_or_cls)  # 如果是类,cls此时为type,如果是obj,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 f'<class {obj.__name__}>'  # obj为类对象
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return f'<{cls_name(obj)} object>'  # 如果是实例对象


def print_args(name, *args):
    """name: 方法名称
       args:方法参数
    """
    pseudo_args = ', '.join(display(x) for x in args)
    print(f'-> {cls_name(args[0])}.__{name}__({pseudo_args})')



### 对于这个示例重要的类 ###
class Overriding:  # 1
    """也叫数据描述符或强制描述符""" 

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

    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 NonOveridingding: # 4
    """ 也叫非数据描述符或遮盖型描述符 """

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


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

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

覆盖型描述符

# 示例23-9 一个覆盖描述符的行为
>>> obj = Managed() # 1                
>>> obj.over # 2
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
>>> Managed.over # 3
-> Overriding.__get__(<Overriding object>, None, <class Managed>)
>>> obj.over = 7 # 4
-> Overriding.__set__(<Overriding object>, <Managed object>, 7)
>>> obj.over # 5
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
>>> obj.__dict__['over'] = 8 # 6
>>> vars(obj) # 7 
{'over': 8}
>>> obj.over # 8
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
  1. 创建Mnanged 对象,供测试使用
  2. obj.over 触发描述符的__get__方法,传入的第二个参数是托管实例obj
  3. Managed.over 触发描述符的__get__方法,传入的第二个参数(instance) 是None
  4. 为obj.over = 7 赋值,触发描述符的__set__方法,传入的最后一个参数是7
  5. 读取obj.over 仍会调用描述符的__get__方法、
  6. 绕过描述符,直接通过obj.__dict__设置属性
  7. 确认值在obj._dict_ 属性中,在over键名下
  8. 然而,即使是名为over 的实例属性,Managed.over描述符仍会覆盖obj.over 操作。也就是说obj.attr ,实例属性不会遮盖描述符行为
posted @ 2022-04-25 23:50  chuangzhou  阅读(93)  评论(0编辑  收藏  举报