【Python】【元编程】【二】【描述符】

"""


#描述符实例是托管类的类属性;此外,托管类还有自己实例的同名属性

#20.1.1 LineItem类第三版:一个简单的描述符
#栗子20-1 dulkfood_v3.py 使用 Quantity 描述符管理 LineItem 的属性
class Quantity:# 描述符基于协议实现,无需创建子类。
def __init__(self,storage_name):
self.storage_name = storage_name
def __set__(self, instance, value): # instance是托管类实例,不用self是为了不和描述符实例冲突
if value > 0 :
instance.__dict__[self.storage_name] = value #这里,必须直接处理托管实例的 __dict__ 属性;如果使用内置的 setattr 函数,会再次触发 __set__ 方法,导致无限递归。
else:
raise ValueError('value must be > 0')
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


truffle = LineItem('White truffle',100,0) #ValueError: value must be > 0


#20.1.2 LineItem第四版:自动获取储存属性的名称
#栗子20-2 bulkfood_v4.py:每个 Quantity 描述符都有独一无二的 storage_name
'''为了生成 storage_name,我们以 '_Quantity#' 为前缀,然后在后面拼接一个整数:
Quantity.__counter 类属性的当前值,每次把一个新的 Quantity 描述符实例依附到
类上,都会递增这个值。在前缀中使用井号能避免 storage_name 与用户使用点号创建
的属性冲突,因为 nutmeg._Quantity#0 是无效的 Python 句法。但是,内置的 getattr
和 setattr 函数可以使用这种“无效的”标识符获取和设置属性,此外也可以直接处理实
例属性 __dict__
'''
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):
return getattr(instance,self.storage_name) #这里可以使用内置的高阶函数 getattr 和 setattr 存取值,无需使用instance.__dict__,因为托管属性和储存属性的名称不同,所以把储存属性传给getattr 函数不会触发描述符,不会像示例 20-1 那样出现无限递归
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

cocounts = LineItem('Brazilian cocount',20,17.95)
print(getattr(cocounts,'_Quantity#0'),getattr(cocounts,'_Quantity#1')) #20 17.95
print(getattr(cocounts,'weight'),getattr(cocounts,'price')) #20 17.95
print(cocounts.weight,cocounts.price) #20 17.95
#print(cocounts._Quantity#0) #SyntaxError: unexpected EOF while parsing
''', __get__ 方法有三个参数: self、 instance 和 owner。 owner 参数是托管类(如
LineItem)的引用,通过描述符从托管类中获取属性时用得到。如果使用
LineItem.weight 从类中获取托管属性(以 weight 为例),描述符的 __get__ 方法接
本文档由Linux公社 www.linuxidc.com 整理收到的 instance 参数值是 None。因此,下述控制台会话才会抛出 AttributeError 异

抛出 AttributeError 异常是实现 __get__ 方法的方式之一,如果选择这么做,应该修
改错误消息,去掉令人困惑的 NoneType 和 _Quantity#0,这是实现细节。把错误消息
改成"'LineItem' class has no such attribute" 更好。最好能给出缺少的属性
名,但是在这个示例中,描述符不知道托管属性的名称,因此目前只能做到这样
'''
#print(LineItem.weight) #AttributeError: 'NoneType' object has no attribute '_Quantity#0'




#示例 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, owner):
if instance is None:
return self #如果不是通过实例调用,返回描述符自身
else:
return getattr(instance, self.storage_name)
#这里可以使用内置的高阶函数 getattr 和 setattr 存取值,无需使用instance.__dict__,因为托管属性和储存属性的名称不同,所以把储存属性传给getattr 函数不会触发描述符,不会像示例 20-1 那样出现无限递归
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


print(LineItem.weight) #<__main__.Quantity object at 0x0000000001ECA710>
br_nuts = LineItem('Brazil nuts',10,34.95)
print(br_nuts.price) #34.95




'''
特性工厂函数与描述符类比较
特性工厂函数若想实现示例 20-2 中增强的描述符类并不难,只需在示例 19-24 的基
础上添加几行代码。 __counter 变量的实现方式是个难点,不过我们可以把它定义
本文档由Linux公社 www.linuxidc.com 整理成工厂函数对象的属性,以便在多次调用之间持续存在,如示例 20-5 所示。
示例 20-5 bulkfood_v4prop.py:使用特性工厂函数实现与示例 20-2 中的描述符
类相同的功能
def quantity(): ➊
try:
quantity.counter += 1 ➋
except AttributeError:
quantity.counter = 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)
❶ 没有 storage_name 参数。
❷ 不能依靠类属性在多次调用之间共享 counter,因此把它定义为 quantity 函数
自身的属性。
❸ 如果 quantity.counter 属性未定义,把值设为 0。
❹ 我们也没有实例变量,因此创建一个局部变量 storage_name,借助闭包保持它
的值,供后面的 qty_getter 和 qty_setter 函数使用。
❺ 余下的代码与示例 19-24 一样,不过这里可以使用内置的 getattr 和 setattr 函
数,而不用处理 instance.__dict__ 属性。
那么,你喜欢哪个?示例 20-2 还是示例 20-5 ?
我喜欢描述符类那种方式,主要有下列两个原因。
描述符类可以使用子类扩展;若想重用工厂函数中的代码,除了复制粘贴,很难
有其他方法。
与示例 20-5 中使用函数属性和闭包保持状态相比,在类属性和实例属性中保持
状态更易于理解。
此外,解说示例 20-5 时,我没有画机器和小怪兽的动力。特性工厂函数的代码不依
赖奇怪的对象关系,而描述符的方法中有名为 self 和 instance 的参数,表明里面
涉及奇怪的对象关系。
本文档由Linux公社 www.linuxidc.com 整理总之,从某种程度上来讲,特性工厂函数模式较简单,可是描述符类方式更易扩展,
而且应用也更广泛。
'''


#20.1.3 LineItem类第5版:一种新型描述符 [避免商品信息为空,导致无法下单]
#几个描述符类的层次结构。 AutoStorage 基类负责自动存储属性; Validated 类做验证,把职责委托给抽象方法 validate; Quantity 和NonBlank 是 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):
'''return validated value or raise ValueError'''

class Quantity(Validated):
'''a number greater than zero'''
def validate(self,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(self,instance,value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
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




#20.2 覆盖型与非覆盖型描述符对比
'''
#Python 贡献者和作者讨论这些概念时会使用不同的术语。覆盖型描述符也叫数据描述符或强制描述符。非覆盖型描述符也叫非数据描述符或遮盖型描述符
#依附在类上的描述符无法控制为类属性赋值的操作。其实,这意味着为类属性赋值能覆盖描述符属性
'''

### 辅助函数,仅用于显示 ###
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:
'''也称数据描述符或强制描述符'''

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 NonOverriding:
'''也称非数据描述符或遮盖型描述符'''

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


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

def spam(self):
print('-> Managed.spam({})'.format(display(self)))
#覆盖型描述符
obj = Managed()
print(obj.over) #-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
print(Managed.over) #-> Overriding.__get__(<Overriding object>, None, <class Managed>) 【解析】因为没有实例
obj.over = 7 #-> Overriding.__set__(<Overriding object>, <Managed object>, 7)
print(obj.over) #-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
obj.__dict__['over'] = 8 #跳过描述符,直接通过 obj.__dict__ 属性设值,所以不打印任何内容
print(vars(obj)) #{'over': 8} 【解析】确认值在 obj.__dict__ 属性中,在 over 键名下
print(obj.over) #-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>) 【解析】然而,即使是名为 over 的实例属性, Managed.over 描述符仍会覆盖读取 obj.over这个操作


#没有 __get__ 方法的覆盖型描述符
print(obj.over_no_get) #<__main__.OverridingNoGet object at 0x000000000385F860> 【解析】这个覆盖型描述符没有 __get__ 方法,因此, obj.over_no_get 从类中获取描述符实例
print(Managed.over_no_get) #<__main__.OverridingNoGet object at 0x0000000001EE3A58> 【解析】直接从托管类中读取描述符实例也是如此
obj.over_no_get = 7 #-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
print(obj.over_no_get) #<__main__.OverridingNoGet object at 0x0000000002203A58> 【解析】因为 __set__ 方法没有修改属性,所以在此读取 obj.over_no_get 获取的仍是托管类中的描述符实例
obj.__dict__['over_no_get'] = 9
print(obj.over_no_get) #9 【解析】现在, over_no_get 实例属性会遮盖描述符,但是只有读操作是如此
obj.over_no_get = 7 #-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
print(obj.over_no_get) #9 【解析】但是读取时,只要有同名的实例属性,描述符就会被遮盖

# 非覆盖型描述符
obj = Managed()
print(obj.non_over) #-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)
obj.non_over = 7
print(obj.non_over) #7
print(Managed.non_over) #-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)
del obj.non_over
print(obj.non_over) #None

#通过类可以覆盖任何描述符
obj = Managed()
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
print(Managed.over,Managed.over_no_get,Managed.non_over) #1 2 3 【解析】揭示了读写属性的另一种不对等:读类属性的操作可以由依附在托管类上定义有 __get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有__set__ 方法的描述符处理
#...若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。默认情况下,对用户定义的类来说,其元类是 type,而我们不能为 type 添加属性。不过在第 21 章,我们会自己创建元类







#方法是描述符
#方法是非覆盖性描述符
obj = Managed()
print(obj.spam) #<bound method Managed.spam of <__main__.Managed object at 0x000000000385F860>> 【解析】 obj.spam 获取的是绑定方法对象
print(Managed.spam) #<function Managed.spam at 0x00000000038D7B70> 【解析】但是 Managed.spam 获取的是函数
obj.spam = 7
print(obj.spam) #7 【解析】 函数没有实现 __set__ 方法,因此是非覆盖型描述符
'''
obj.spam 和 Managed.spam 获取的是不同的
对象。与描述符一样,通过托管类访问时,函数的 __get__ 方法会返回自身的引用。但
是,通过实例访问时,函数的 __get__ 方法返回的是绑定方法对象:一种可调用的对
象,里面包装着函数,并把托管实例(例如 obj)绑定给函数的第一个参数(即
self),这与 functools.partial 函数的行为一致
'''

#20.3 方法是描述符

import collections
class Text(collections.UserString):
def __str__(self):
return 'Text({!r})'.format(self.data)

def reverse(self):
return self[::-1]

word = Text('forward')
print(word) #Text('forward')
print(word.reverse()) #Text('drawrof')
print(Text.reverse(word))#Text('drawrof') 【解析】在类上调用方法相当于调用函数
print(type(Text.reverse),type(word.reverse)) #<class 'function'> <class 'method'>
print(list(map(Text.reverse,['repaid',(10,20,30),Text('stressed')]))) #['diaper', (30, 20, 10), 'desserts']
print(Text.reverse.__get__(word)) #<bound method Text.reverse of 'forward'> 【解析】 函数都是非覆盖型描述符。在函数上调用 __get__ 方法时传入实例,得到的是绑定到那个实例上的方法
print(Text.reverse.__get__(None,word)) #<function Text.reverse at 0x0000000001E8CD08> 【解析】调用函数的 __get__ 方法时,如果 instance 参数的值是 None,那么得到的是函数本身。
print(word.reverse) #<bound method Text.reverse of 'forward'>
print(word.reverse.__self__) #Text('forward')
print(word.reverse.__func__ is Text.reverse) #True
'''绑定方法的 __func__ 属性是依附在托管类上那个原始函数的引用。
绑定方法对象还有个 __call__ 方法,用于处理真正的调用过程。这个方法会调用
__func__ 属性引用的原始函数,把函数的第一个参数设为绑定方法的 __self__ 属性。
这就是形参 self 的隐式绑定方式。
函数会变成绑定方法,这是 Python 语言底层使用描述符的最好例证。
'''



#20.4 描述符用法建议
'''
下面根据刚刚论述的描述符特征给出一些实用的结论。
使用特性以保持简单
  内置的 property 类创建的其实是覆盖型描述符, __set__ 方法和 __get__ 方法都
实现了,即便不定义设值方法也是如此。特性的 __set__ 方法默认抛出
AttributeError: can't set attribute,因此创建只读属性最简单的方式是使用特
性,这能避免下一条所述的问题。
只读描述符必须有 __set__ 方法
  如果使用描述符类实现只读属性,要记住, __get__ 和 __set__ 两个方法必须都定
义,否则,实例的同名属性会遮盖描述符。只读属性的 __set__ 方法只需抛出
AttributeError 异常,并提供合适的错误消息。
Python 为此类异常提供的错误消息不一致。如果试图修改 complex 的 c.real 属性,那么得到的错误消息是
AttributeError: read-only attribute;但是,如果试图修改 c.conjugat(e complex 对象的方法),那么得到
的错误消息是 AttributeError: 'complex' object attribute 'conjugate' is read-only。
用于验证的描述符可以只有 __set__ 方法
  对仅用于验证的描述符来说, __set__ 方法应该检查 value 参数获得的值,如果有
效,使用描述符实例的名称为键,直接在实例的 __dict__ 属性中设置。这样,从实例中
读取同名属性的速度很快,因为不用经过 __get__ 方法处理。参见示例 20-1 中的代码。
仅有 __get__ 方法的描述符可以实现高效缓存
  如果只编写了 __get__ 方法,那么创建的是非覆盖型描述符。这种描述符可用于执
行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。同名实例属性会遮盖描述
符,因此后续访问会直接从实例的 __dict__ 属性中获取值,而不会再触发描述符的
__get__ 方法。
非特殊的方法可以被实例属性遮盖
  由于函数和方法只实现了 __get__ 方法,它们不会处理同名实例属性的赋值操作。
因此,像 my_obj.the_method = 7 这样简单赋值之后,后续通过该实例访问
the_method 得到的是数字 7——但是不影响类或其他实例。然而,特殊方法不受这个问
题的影响。解释器只会在类中寻找特殊的方法,也就是说, repr(x) 执行的其实是
x.__class__.__repr__(x),因此 x 的 __repr__ 属性对 repr(x) 方法调用没有影响。
出于同样的原因,实例的 __getattr__ 属性不会破坏常规的属性访问规则。
实例的非特殊方法可以被轻松地覆盖,这听起来不可靠且容易出错,可是在我使用 Python
的 15 年中从未受此困扰。然而,如果要创建大量动态属性,属性名称从不受自己控制的
数据中获取(像本章前面那样),那么你应该知道这种行为;或许你还可以实现某种机
制,过滤或转义动态属性的名称,以维持数据的健全性。
 示例 19-6 中的 FrozenJSON 类不会出现实例属性遮盖方法的问题,因为那个
类只有几个特殊方法和一个 build 类方法。只要通过类访问,类方法就是安全的,
在示例 19-6 中我就是这么调用 FrozenJSON.build 方法的——在示例 19-7 中替换成
__new__ 方法了。 Record 类(见示例 19-9 和示例 19-11)及其子类也是安全的,因
为只用到了特殊的方法、类方法、静态方法和特性。特性是数据描述符,因此不能被
实例属性覆盖。
讨论特性时讲了两个功能,这里讨论的描述符还未涉及,结束本章之前我们来讲讲:文档
和对删除托管属性的处理
'''


"""




































posted @ 2017-11-08 18:06  素人渔芙2017  阅读(247)  评论(0编辑  收藏  举报