Fluent Python Metaprogramming 部分的笔记, 在加上了其他杂七杂八的东西
Dynamic Attribute and Property
首先要先明白方法也是属性(attribute),只不过是能调用的属性
还有一种比较特殊的是 property(定义了 getter/setter)
Attribute
属性的获取设置有四种方式: obj.attr
, hasattr
, getattr
, setattr
有时候会直接通过设置对象 __dict__
绕过上面四种方法
假设有一个 Class 类, 一个 obj 实例
obj.attr
, hasattr(obj, 'attr')
, getattr(obj,'attr')
先会调用 __getattribute__
. 该方法是先会去寻找类中的描述器(type(obj).__dict__['attr'].__get__(obj, type(obj))
),然后查找实例属性,然后查找类属性。
如果没有找到相应属性, 则 __getattribute__
会抛出 AttributeError
. 然后会调用 Class.__getattr__(obj, 'attr')
. 一般自定义 __getattr__
实现对属性的控制。
obj.attr = val
, setattr(obj, 'attr', val)
调用 Class.__setattr__(obj, 'attr', val)
方法
__setattr__
使用不慎会造成无限循环
一个 __getattr__
例子:JSON 对象能够采用 dot 方式访问
# pseudo-code for object construction
# 可以发现只能 构造出的对象为 the_class 实例时,才会接着调用 __init__
# 非常有用的特性,参考下面那段代码
def object_marker(the_class, some_arg):
new_object = the_class.__new__(some_arg)
if isinstance(new_object, the_class):
the_class.__init__(new_object, sone_arg)
return new_object
from collections import abc
from keyword import iskeyword
class FrozenJSON:
"""A read-only facade for navigating a JSON-like object
using attribute notation
>>> f1= FrozenJSON(1)
>>> f1
1
>>> f2 = FrozenJSON({'a':1})
>>> f2.a
1
>>> f3 = FrozenJSON([1, {'a':1}])
>>> f3[0]
1
>>> f3[1].a
1
"""
# 自定义 __new__,其中对象构造特性参考上面伪代码
def __new__(cls, arg):
"""后面两种情况不会调用 __init__"""
if isinstance(arg, abc.Mapping):
# 获得 object 的 __new__
return super().__new__(cls)
elif isinstance(arg, abc.MutableSequence):
# 创建一数组的 FrozenJSON object
return [cls(item) for item in arg]
else:
return arg
def __init__(self, mapping):
self._data = {}
for key, value in mapping.items():
if iskeyword(key):
key += '_'
self._data[key] = value
def __getattr__(self, name):
if hasattr(self._data, name):
return getattr(self._data, name)
else:
return FrozenJSON(self._data[name])
Property
有两种方式来定义 property,使用 property 工厂函数 或描述器(事实上 property 是一个描述器)
虽然我们常常使用 @property, 但事实上 property 是个类(事实上在 Python 中类和函数常常互换使用,因为都能直接调用)
property(fget=None, fset=None, fdel=None, doc=None)
property 是类属性,但是控制着实例属性的访问
obj.attr
先从 obj.__class__
中查找是否有 property(注意不是 attribute),然后在 obj.__dict__
中查找,然后在类及父类中的 dict 中查找
class C:
data = 'the class data attr'
@property
def prop(self):
return 'the prop value'
>>> obj = C()
>>> C.prop
<property object at 0x04122810>
>>> obj.__dict__ # 实例没有 prop 属性
{}
>>> obj.prop # 调用的是类中 property 的 getter
the prop value
>>> obj.prop = 'foo' # 没有 setter 不能设置
AttributeError ...
>>> obj.__dict__['prop'] = 'foo' # 绕过 __setattr__ 给实例加上 prop 属性
>>> obj.__dict__
{'prop': 'foo'}
>>> obj.prop # 发现调用的还是 property 的 getter,实例属性不会遮盖
'the prop value'
>>> C.prop = 'baz' # 类的 property 被遮盖
>>> obj.prop # 现在能够访问到实例属性
'foo'
>>> C.data
'the class data attr'
>>> obj.data = 'bar'
>>> C.data = property(lambda self: "the 'data' prop value")
>>> obj.data # 实例属性被遮盖
"the 'data' prop value"
>>> del C.data
>>> obj.data
'bar'
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
@property
def weight(self):
return self._weight
@weight.setter
def weight(self, value):
if value > 0:
self._weight = value
else:
raise ValueError('value must be > 0')
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self):
return self._weight
def set_weight(self, value):
if value > 0:
self._weight = value
else:
raise ValueError('value must be > 0')
weight = property(get_weight, set_weight)
# 采用 Property 工厂函数, 与上面那个例子比较
class LineItem:
def quantity(storage_name):
def qty_getter(instance):
# return getattr(instance, storage_name)
# 这里必须用 __dict__ 获取/设置实例属性,绕过 property
return instance.__dict__[storage_name]
def qty_setter(instance, value):
if value > 0:
# setattr(instance, storage_name, value)
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
# 注意这两个 weight 的不同,一个会作为装饰器,一个会作为实例属性
weight = quantity('weight')
price = quantity('price')
def __init__(self, description, weight, price):
self.description = description
# 这里设定 weight, price 就用到 setter 了
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
Descriptor 描述器
先来看看描述器是用来解决什么问题。
描述器是用来处理属性获取逻辑。当然我们可以通过类似 Java getter,setter 的方法,不过 Python 作为动态语言,可能会有用户 obj.attr 这样的方式去获取设置属性,这样就绕过了我们的 getter,setter(当然我们可以自定义 __set__
, __get__
来防止,但这样就更麻烦了), 而且采用 getter, setter 代码不 Pythonic
我们想要有一种类似普通属性访问的形式,但可以实现属性获取逻辑。当然前面的 property 也是实现这个功能(property 就是一个描述器)。
我认为描述器是在 property 的基础上进一步抽象复用。
想象这么一个情形:我有 weight, price,都要求他们 > 0,当然我们可以对他们都用 property,但我们发现他们的逻辑是一样,我们的代码重复了。所以这时候就可以使用描述器。
所以,描述器就是用来抽象出属性获取设置逻辑,并加以复用 (e.g. Django 中的 Field 就是描述器)
描述器是实现了描述器协议的类(实现了 __get__
, __set__
, __delete__
中的一个或多个)。
property
实现了所有了描述器协议, 其他实现了描述器协议的还有 method,classmethod
装饰器,staticmethod
装饰器
前面的 property factory 是函数式编程的方式复用属性获取逻辑,如果换用面向对象的方式,则是使用描述器. 描述器的使用方式是将一个描述器实例作为类属性(跟类形式的 property 一样, 因为 property 就是个描述器)
描述器实例存在于类属性中,控制着实例属性的访问
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name
# 理解 self 与 instance 的区别
# self 代表的是描述器实例
# instance 代表的是 LineItem 实例,self 用来控制该实例的属性
# 可能你有几千个 LineItem 实例,但只有 weight, price 两个描述器实例
def __set__(self, instance, value):
if value > 0:
# 绕过 setattr 设置属性, 不然会导致无限循环
# 原因是实例属性名与描述器名相同
# 而 Python 会先去获取描述器(同前面 property)
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0')
class LineItem:
# 注意两个 weight 的不同,
# 一个为描述器实例作为类属性, 该描述器实例在导入模块时就存在了
# 另一个则作为实例属性存放在 obj.__dict__ 中
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
要打两次 weight 很麻烦,可以用如下技巧
class Quantity:
__counter = 0
def __init__(self):
# 保证每个描述器实例 storage_name 都不同
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1
def __get__(self, instance, owner):
# 因为实例名称与描述器名称不重合,我们需要定义 __get__
# 也是因为这点,这里我们可以用 getattr
# 如果是 LineItem 即类调用描述器,instance 为 None,这里我们返回描述器实例
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:
"""
>>> LineItem.weight
<__main__.Quantity at 0x37ede90>
>>> l = LineItem('hello', 1, 1)
>>> l.weight
1
>>> l.__dict__ # 注意属性名称
{'_Quantity#0': 1, '_Quantity#1': 1, 'description': 'hello'}
"""
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
这技巧用 property factory 也可以实现
# 采用 property FP 形式
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)
return property(qty_getter, qty_setter)
描述器与 property factory 各有利弊: 后者比较方便, 但前者可以通过类继承进一步复用
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):
""""""
class Quantity(Validated):
"""a number greate 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 canot be empty')
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
Overriding vs Nonoverriding Descriptor
定义了 __set__
的描述器称为 overriding descriptor. 因为虽然描述器是类属性, 但其能控制实例属性的访问.(前面的描述器都是)
property
也是 overriding descriptor, 如果你没有提供 setter 函数给 property, property 默认会抛出 AttributeError
class Overriding:
"""an overriding descriptor"""
def __get__(self, instance, owner):
print('__get__')
def __set__(self, instance, value):
print('__set__')
class OverridingNoGet:
"""an overriding descriptor without get"""
def __set__(self, instance, value):
print('__set__')
class NonOverriding:
"""a nonoverriding descriptor"""
def __get__(self, instance, owner):
print('__get__')
class Managed:
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverriding()
def spam(self):
pass
-----------------------
>>> obj = Managed()
# Overriding descriptor with get
>>> obj.over = 1
__set__
>>> obj.over
__get__
>>> obj.__dict__
{}
# Overriding descriptor without get
>>> obj.over_no_get # 由于没有定义 __get__, 且实例中没有该属性所以返回类中的 over_no_get 属性,这是个描述器实例
<__main__.OverridingNoGet at 0x38327f0>
>>> Managed.over_no_get
<__main__.OverridingNoGet at 0x38327f0>
>>> obj.over_no_get = 1 # 设置属性还是经由描述器
__set__
>>> obj.over_no_get
<__main__.OverridingNoGet at 0x38327f0>
>>> obj.__dict__['over_no_get'] = 1 # 给实例绑定上 over_no_get 属性
>>> obj.over_no_get # 由于没有定义 __get__, 所以能访问到实例属性
1
>>> obj.over_no_get = 2 # 但是设置属性还是必须经由描述器
__set__
>>> obj.over_no_get
1
# Nonoverriding descriptor
>>> obj.non_over
__get__
>>> obj.non_over = 1 # non_over 被覆盖
>>> obj.non_over
1
>>> Managed.non_over
__get__
>>> del obj.non_over
>>> obj.non_over
__get__
# 类中的 descriptor 实例可以被覆盖
# 说描述器只能控制实例的属性访问,如果你要控制类的属性访问,则必须在元类中定义描述器
>>> obj = Managed()
>>> Managed.over = 1
>>> Managed.over_no_get = 2
>>> Managed.non_over = 3
>>> obj.over, obj.over_no_get, obj.non_over
(1, 2, 3)
Methods Are Descriptors
定义在类中的函数是描述器(实现了 __get__
方法)
类中函数描述器的 __get__
是这样的:
__get__
可以根据 instance 是否为 None 判断是类调用还是实例调用
如果是类调用就直接返回自己
如果是实例调用,先绑定实例到第一个参数,然后返回 bound method (柯里化)
>>> obj = Managed()
>>> obj.spam
<bound method Managed.spam of <__main__.Managed object at 0x03742C30>>
>>> Managed.spam
<function __main__.Managed.spam>
>>> Managed.spam.__get__(None, Managed)
<function __main__.Managed.spam>
>>> Managed.spam.__get__(obj)
<bound method Managed.spam of <__main__.Managed object at 0x03D00C50>>
>>> obj.spam.__func__ is Managed.spam
True
>>> obj.spam.__self__
<__main__.Managed at 0x3d00c50>
>>> obj.spam = 1 # 没有定义 __setter__, 所以能覆盖
>>> obj.spam
1
MetaClass
type 用来创建类, 接受三个参数 name, bases, dict
def record_factory(cls_name, *fields_names):
"""
>>> Dog = record_factory('Dog', 'name', 'owner') # 创建了一个 Dog 类, 类似 namedtuple
"""
def __init__(self, *args, **kwargs):
attrs = dict(zip(self.__slots__, args))
attrs.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
def __iter__(self):
for name in self.__slots__:
yield getattr(self, name)
cls_attrs = dict(__slots__ = fields_names,
__init__ = __init__,
__iter__ = __iter__)
return type(cls_name, (object,), cls_attrs)
回忆前面我们的 LineItem 类, 用描述器控制实例访问时, 实例属性的名称是这样子的 _Quantity#0
.
但是针对这样一个描述器, weight = Quantity()
, 我们想让实例属性名称为 _Quantity#weight
这个问题的难点在哪里呢?首先我们先调用 Quantity(),这样就产生了一个 Quantity 实例,然后我们才把这个实例绑定到 weight 变量上。也就说产生 Quantity 时我们并不知道有 weight 。
所以我们要在类(这里是 LineItem)创建时进行操作, 动态修改类的行为(修改LineItem的描述器实例): 可以通过 class decorator 或 metaclass 实现
Class Decorator
类装饰器跟函数装饰器很像, 对类进行一些操作,然后返回一个类(可能是不同类)
但如果有子类继承该类, 并不能继承装饰器
import abc
class AutoStorage:
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):
""""""
class Quantity(Validated):
"""a number greate 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 canot be empty')
return value
def entity(cls):
for key, attr in cls.__dict__.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
# 注意是给描述器实例绑定 storage_name
# 描述器实例又会用 storage_name, 绑定 LineItem 实例属性
attr.storage_name = '_{}#{}'.format(type_name, key)
return cls
@entity
class LineItem:
"""
>>> l = LineItem('hello', 1, 1)
>>> l.__dict__
{'_NonBlank#description': 'hello', '_Quantity#price': 1, '_Quantity#weight': 1}
"""
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
MetaClass
元类中 __init__
和 __new__
区别
跟实例对象创建过程中 __init__
和 __new__
的行为一样(记住 类是元类的实例)
在下面这个例子中我只是想在装饰器中动态加上属性, __init__
就足够了
ps: 参考上面对象生成伪代码, 如果想返回其他类型对象, 则用 __new__
import abc
class AutoStorage:
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):
""""""
class Quantity(Validated):
"""a number greate 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 canot be empty')
return value
class EntityMeta(type):
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict)
for key, attr in attr_dict.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
class Entity(metaclass=EntityMeta):
"""定义这样一个类的好处在于用户只要继承该类就好, 不需要设置元类"""
class LineItem(Entity):
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
PS:单例模式
** class decorator **
def singleton(cls):
instances = {}
def get_instance():
if cls not in instances:
instances[cls] = cls()
return instances[cls]
return get_instance
@singleton
class Foo(object):
"""
>>> print(Foo) # Foo 已经变成了函数,这也是用这种方式不好的地方
<function get_instance at 0x032335F0>
>>> print(id(Foo()))
52575152
>>> print(id(Foo()))
52575152
"""
pass
** metaclass **
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Foo(object):
"""
>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo())
52891984
>>> print(Foo())
52891984
"""
__metaclass__ = Singleton
推荐阅读
Fluent Python 关于元编程部分的章节
Python Encapsulation with Descriptors Presentation
Intermediate Pythonista Descriptors
Descriptor HowTo Guide
Intermediate Pythonista Metaclass
Creating a singleton in Python
网友评论