Python Metaprogramming

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

>>> 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
>
>>> Managed.spam

>>> Managed.spam.__get__(None, Managed)

>>> Managed.spam.__get__(obj)
>
>>> 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 已经变成了函数,这也是用这种方式不好的地方
    
    >>> 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)
    
    >>> 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

你可能感兴趣的:(Python Metaprogramming)