Python中的元编程

Database and Ruby, Python, History


习惯了Ruby,总希望能够在其他语言里面找到同样的身影。比如,通过method_missing去处理没有定义的方法,或者用define_methodclass_eval等去动态定义方法,抑或者用included, prepended, extended这些方法增加方法。

但是在Python中,我基本没有看到included这样的操作,甚至对于类这样的概念,用的都不是很频繁。我有时候都怀疑Python里面有没有元编程,虽然我知道django里面肯定有元编程,类似的ORM包里面肯定也有。

无论如何,的确需要看看Python里面的元编程是怎么样的。

动态属性

在Python中,你可以通过覆写__getattr__来实现虚拟属性。比如,访问某个json节点,可以通过__getattr__来访问json的内容。

特性

通过设置@property装饰器来实现属性的读取和写入。

class Class:
    data = 'the class data attr'

    @property
    def prop(self):
        return 'the prop value'
    

如果实例中没有对应的属性的时候,会试图去读取类属性。

# check attr

obj = Class()
print(vars(obj)) # => {}
print(obj.__dict__) # => {}
print(obj.data) # => the class data attr

obj.data = 'bar'
print(vars(obj)) # => {'data': 'bar'}
print(obj.__dict__) # => {'data': 'bar'}
print(obj.data) # => bar
print(Class.data)  # => the class data attr

而且,特性(类属性)会覆盖实例属性。所以obj.attr会先从obj.__class__中寻找名为attr的特性,找不到再从obj实例中寻找。

# check for property
print(Class.prop) # => <property object at 0x10bf71368>
print(obj.prop) # => the prop value
# obj.prop = 'foo' # error raise here

# 特性依旧覆盖实例属性
obj.__dict__['prop'] = 'foo'
print(obj.prop) # => the prop value

# remove property 则返回实例属性
Class.prop = 'abc'
print(obj.prop) # => foo

# add property back
# instance attr is overriden by Class property
print(obj.data) # => bar
Class.data = property(lambda self: 'the "data" prop value')
print(obj.data) # => the "data" prop value

# instance attr is back
del Class.data
print(obj.data) # => bar

特性工厂

可以通过定义一个quantity函数,返回property对象。这样也可以实现@property装饰器同样的效果。

def quantity(storage_name):

    def qty_getter(instance):
        return instance.__dict__[storage_name]

    def qty_setter(instance, value):
        if value > 0:
            instance.__dict__[storage_name] = value
        else:
            raise ValueError('value must be > 0')

    return property(qty_getter, qty_setter)

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

描述符

描述符是指实现了__get__, __set____delete__的类。上面的property类实现了完整的描述符协议。下面是描述符的实现,等效于特性工厂的实现。

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)

    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

  1. 覆盖性描述符拥有__set__方法
    • __get____set__的描述符,和特性一样,obj的读取和写入都会调用__get____set__方法。
    • 没有__get__方法,通过实例读取描述符就会返回描述符本身。如果实例有同名属性,则返回该属性。设置还是会有__set__接手。
  2. 非覆盖性描述符
    • 只有__get__方法,实例属性还是会覆盖__get__方法。

所有的方法都是非覆盖性描述符

元类

晃眼一看,还以为是Ruby中singleton_class,但实际上不一样。

Singleton类的基类是typeSpam中有metaclass=Singleton。这样,在导入的时候,就会触发Singleton__init__方法。这里会设置Spam._instance = None。当第一次调用Spam()的时候,就会调用__call__方法,这个时候self._instanceNone,就新生成一个object。第二次调用的时候,就返回上一次生成的实例。

class Singleton(type):
    def __init__(self, *args, **kwargs):
        self._instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = super().__call__(*args, **kwargs)
            return self._instance
        else:
            return self._instance


class Spam(metaclass=Singleton):
    def __init__(self):
        print("Spam!!!")

但是,书上并不建议使用元类,除非你在创建某个框架。

参考

  1. https://zhuanlan.zhihu.com/p/29849145
  2. https://blog.millionintegrals.com/metaclasses-in-python/