《Python 元编程的力量:全面掌控一切》

Python中的元编程探索

许多人对“元编程”这一概念并不熟悉,而且没有一个非常精确的定义。本文主要围绕Python中的元编程展开。然而,实际上,这里讨论的内容可能并不完全符合“元编程”的严格定义。只是我找不到一个更合适的术语来代表本文的主题,因此借用了这个词。

副标题是“控制你想要控制的一切”。本质上,这篇文章专注于一件事:利用Python提供的特性,使代码尽可能优雅和简洁。具体来说,通过编程技术,我们在更高的抽象层次上修改抽象的特性。

首先,众所周知,Python中的一切都是对象。此外,Python提供了许多“元编程”机制,例如特殊方法和 metaclass。动态地向对象添加属性和方法在Python中根本不被视为“元编程”。但在某些静态语言中,实现这一点需要一定的技巧。让我们讨论一些可能让Python程序员感到困惑的方面。

我们先从将对象分类为不同层次开始。通常,我们知道一个对象有其类型,而Python早已将类型实现为对象。因此,我们有实例对象和类对象,这两个层次。具备基本理解的读者会意识到metaclass的存在。简而言之,metaclass是“类”的“类”,这意味着它处于比类更高的层次。这又增加了一个层次。还有更多吗?

导入时间与运行时间

如果我们从不同的角度来看,不需要应用与前面三个级别相同的标准,我们可以区分两个概念:ImportTime 和 RunTime。它们之间的界限并不明显。顾名思义,它们指的是两个时刻:导入时间和执行时间。

当一个模块被导入时,会发生什么?全局作用域中的语句(非定义性语句)会被执行。那么函数定义呢?一个函数对象会被创建,但其中的代码不会被执行。对于类定义,会创建一个类对象,类定义作用域中的代码会被执行,而类方法中的代码自然不会被执行。

那么在执行期间呢?函数和方法中的代码会被执行。当然,你需要先调用它们。

元类

因此,我们可以说元类和类属于 ImportTime。在模块被导入后,它们会被创建。实例对象属于 RunTime。简单地导入一个模块不会创建实例对象。然而,我们不能过于教条,因为如果你在模块作用域内实例化一个类,实例对象也会被创建。只不过我们通常在函数内部编写实例化,因此有了这样的分类。

如果你想控制创建的实例对象的特性,该怎么做呢?其实很简单。在类定义中重写 __init__ 方法。那么,如果我们想控制类的一些属性呢?是否有这样的需求?当然有!

关于经典的单例模式,大家都知道有多种实现方式。其要求是一个类只能有一个实例。

最简单的实现如下:

class _Spam:
    def __init__(self):
        print("Spam!!!")

_spam_singleton = None

def Spam():
    global _spam_singleton
    if _spam_singleton is not None:
        return _spam_singleton

else:
        _spam_singleton = _Spam()
        return _spam_singleton

这种工厂模式并不是很优雅。让我们再一次回顾一下需求。我们希望一个类只有一个实例。我们在类中定义的方法是实例对象的行为。因此,如果我们想改变一个类的行为,我们需要一些更高层次的东西。这就是元类发挥作用的地方。如前所述,元类是类的类。也就是说,元类的 __init__ 方法是类的初始化方法。我们知道还有 __call__ 方法,它使得一个实例可以像函数一样被调用。那么,当一个类被实例化时,元类的这个方法就是被调用的。

代码可以这样编写:

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

这段代码定义了一个类的方法,其中使用了`super()`来调用父类的初始化方法,并在`__call__`方法中实现了单例模式的逻辑。具体来说,当`_instance`属性为`None`时,会调用父类的`__call__`方法并将返回值赋给`_instance`,否则直接返回已有的`_instance`。


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

与一般类定义相比,有两个主要区别。一个是 Singleton 的基类是 type,另一个是在 Spam 的定义中有 metaclass = Singleton。什么是 type?它是 object 的一个子类,而 object 是它的实例。也就是说, type 是所有类的类,最基本的 metaclass。它规定了所有类在创建时需要的一些操作。因此,我们自定义的 metaclass 需要继承自 type。同时, type 也是一个对象,因此它是 object 的一个子类。这一点有些难以理解,但只需大致了解即可。

装饰器

让我们来谈谈装饰器。大多数人认为装饰器是 Python 中最难理解的概念之一。实际上,它只是语法糖。一旦你理解了函数也是对象,你就可以轻松编写自己的装饰器。

from functools import wraps

def print_result(func):


@wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(result)
        return result

    return wrapper

@print_result
def add(x, y):
    return x + y

# 等价于:
# add = print_result(add)

add(1, 3)

在这里,我们还使用了一个装饰器 @wraps,它用于使返回的内部函数 wrapper 具有与原始函数相同的函数签名。基本上,在编写装饰器时我们应该添加它。

正如我在评论中所写的,@decorator 的形式相当于 func = decorator(func)。理解这一点可以让我们编写更多类型的装饰器。例如,类装饰器,以及将装饰器写成类的形式。

def attr_upper(cls):

for attrname, value in cls.__dict__.items():
        if isinstance(value, str):
            if not value.startswith('__'):
                setattr(cls, attrname, bytes.decode(str.encode(value).upper()))
    return cls

@attr_upper
class Person:

这段代码的功能是遍历类的属性字典,检查每个属性的值是否为字符串类型。如果是字符串且不以双下划线开头,则将其值转换为大写字节串并重新设置该属性。


sex ='man'

print(Person.sex)  # MAN

注意普通装饰器和类装饰器实现之间的区别。

数据抽象 – 描述符

如果我们希望某些类具有某些共同特征,或者能够在类定义中对它们进行控制,我们可以自定义一个元类,并使其成为这些类的元类。如果我们希望某些函数具有某些共同功能并避免代码重复,我们可以定义一个装饰器。那么,如果我们希望实例的属性具有某些共同特征呢?有些人可能会说我们可以使用 property,确实可以。但这个逻辑必须在每个类定义中编写。如果我们希望这些类的实例的某些属性具有相同的特征,我们可以自定义一个描述符类。

关于描述符,本文 https://docs.python.org/3/howto/descriptor.html 解释得非常清楚。同时,它还详细阐述了描述符如何隐藏在函数背后,以实现函数和方法之间的统一与差异。以下是一些示例。

class TypedField:
    def __init__(self, _type):


self._type = _type

def __get__(self, instance, cls):
    if instance is None:
        return self
    else:
        return getattr(instance, self.name)

def __set_name__(self, cls, name):
    self.name = name

def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError('预期类型为' + str(self._type))
        instance.__dict__[self.name] = value

class Person:
    age = TypedField(int)

name = TypedField(str)

def __init__(self, age, name):
    self.age = age
    self.name = name

jack = Person(15, 'Jack')
jack.age = '15'  # 将会引发错误
 

这里有几个角色。TypedField 是一个描述符类,而 Person 的属性是描述符类的实例。看起来描述符作为 Person 的一个属性存在,也就是说,它是一个类属性而不是实例属性。但实际上,一旦 Person 的一个实例访问了同名的属性,描述符就会生效。需要注意的是,在 Python 3.5 及之前的版本中,没有 __set_name__ 特殊方法。这意味着如果你想知道描述符在类定义中被赋予了什么名称,你需要在实例化时显式地将其传递给描述符,也就是说,你需要一个额外的参数。然而,在 Python 3.6 中,这个问题得到了解决。你只需在描述符类定义中重写 __set_name__ 方法。此外,还要注意 __get__ 的写法。基本上,instance 的判断是必要的,否则会引发错误。这个原因并不难理解,因此我就不详细说明了。

控制子类创建 – 一种替代元类的方法

在 Python 3.6 中,我们可以通过实现 __init_subclass__ 特殊方法来自定义子类的创建。通过这种方式,我们可以在某些情况下避免使用相对繁琐的 metaclass。

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

摘要

元编程技术,如元类,对于大多数人来说有些晦涩且难以理解,而且大多数情况下,我们并不需要使用它们。然而,大多数框架的实现利用了这些技术,使得用户编写的代码可以简洁易懂。如果你想深入了解这些技术,可以参考一些书籍,如流畅的PythonPython Cookbook(本文部分内容参考自这些书籍),或者阅读官方文档中的一些章节,例如上面提到的描述符如何使用,以及数据模型部分等。或者直接检查Python源代码,包括用Python编写的源代码和CPython源代码。

请记住,只有在充分理解这些技术后才使用它们,不要试图在任何地方都使用它们。

更多