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