美文网首页
Python Metaprogramming

Python Metaprogramming

作者: SkyDavid | 来源:发表于2016-03-24 19:16 被阅读0次

    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

    相关文章

      网友评论

          本文标题:Python Metaprogramming

          本文链接:https://www.haomeiwen.com/subject/efhrlttx.html