许多人认为Python中的元编程不必要地使代码复杂化,但是如果正确使用它,则可以快速而优雅地实现复杂的设计模式。 此外,众所周知的Python框架(例如Django,DRF和SQLAlchemy)使用元类来提供易于扩展的功能和易于重复使用的代码。

在本文中,我将告诉您为什么不应该害怕在项目中使用元编程并显示最适合的任务。 您可以在“ 高级Python”课程中了解有关元编程选项的更多信息。
首先,让我们回顾一下Python元编程的基础。 补充一点,下面编写的所有内容都适用于Python 3.5及更高版本。
快速浏览Python数据模型
因此,我们都知道Python中的所有内容都是一个对象,并且对于每个对象都有一个特定的类来生成它已经不是什么秘密了,例如:
>>> def f(): pass >>> type(f) <class 'function'>
可以使用内置的type函数来确定对象的类型或生成该对象的类,该函数具有相当有趣的调用签名(我们稍后再讨论)。 通过在任何对象上派生__class__
属性,可以实现相同的效果。
因此,要创建函数,请使用某个内置的类function
。 让我们看看我们能用它做什么。 为此,请从内置的类型模块中获取空白:
>>> from types import FunctionType >>> FunctionType <class 'function'> >>> help(FunctionType) class function(object) | function(code, globals[, name[, argdefs[, closure]]]) | | Create a function object from a code object and a dictionary. | The optional name string overrides the name from the code object. | The optional argdefs tuple specifies the default argument values. | The optional closure tuple supplies the bindings for free variables.
如我们所见,Python中的任何函数都是上述类的实例。 现在,让我们尝试创建一个新函数,而不用通过def
声明它。 为此,我们需要学习如何使用解释器中内置的compile函数创建代码对象:
太好了! 在元工具的帮助下,我们学习了如何动态创建函数,但实际上很少使用这种知识。 现在让我们看一下如何创建这些类的类对象和实例对象:
>>> class User: pass >>> user = User() >>> type(user) <class '__main__.User'> >>> type(User) <class 'type'>
很明显, User
类用于创建user
的实例,查看type
类(用于创建User
类本身)会更加有趣。 在这里,我们将转到调用内置type
函数的第二个选项,该函数组合为Python中任何类的元类。 根据定义,元类是其实例是另一个类的类。 元类使我们可以自定义创建类的过程,并部分控制创建类实例的过程。
根据文档,第二种签名type(name, bases, attrs)
-返回新的数据类型,或者,如果以简单的方式,则返回新的类,并且如果name
属性成为返回的类的__name__
属性,则返回__name__
父类的列表将作为__bases__
,好吧, attrs
包含类的所有属性和方法的类似dict的对象将进入__dict__
。 该函数的原理可以描述为Python中的简单伪代码:
type(name, bases, attrs) ~ class name(bases): attrs
让我们看看如何仅使用type
调用来构造一个全新的类:
>>> User = type('User', (), {}) >>> User <class '__main__.User'>
如您所见,我们不需要使用class
关键字来创建新类, type
函数不需要它,现在让我们来看一个更复杂的示例:
class User: def __init__(self, name): self.name = name class SuperUser(User): """Encapsulate domain logic to work with super users""" group_name = 'admin' @property def login(self): return f'{self.group_name}/{self.name}'.lower()
从上面的示例中可以看到,使用关键字class
和def
进行的类和函数的描述只是语法糖,任何类型的对象都可以通过对内置函数的常规调用来创建。 现在,最后,让我们谈谈如何在实际项目中使用动态创建类。
有时我们需要根据先前已知的数据方案来验证来自用户或其他外部来源的信息。 例如,我们要在管理面板中更改用户登录表单-删除和添加字段,更改其验证策略等。
为了说明这一点,让我们尝试动态创建一个Django表单,该表单的方案描述以以下json
格式存储:
{ "fist_name": { "type": "str", "max_length": 25 }, "last_name": { "type": "str", "max_length": 30 }, "age": { "type": "int", "min_value": 18, "max_value": 99 } }
现在,根据上面的描述,使用我们已经知道的type
函数创建一组字段和一个新表单:
import json from django import forms fields_type_map = { 'str': forms.CharField, 'int': forms.IntegerField, }
太好了! 现在,您可以将创建的表单转移到模板并为用户呈现。 相同的方法可以与其他框架一起用于数据验证和表示( DRF序列化器 , 棉花糖等)。
上面,我们看了已经“完成”的type
元类,但是最常见的是,在代码中,您将创建自己的元类,并使用它们配置新类及其实例的创建。 在一般情况下,元类的“空白”看起来像这样:
class MetaClass(type): """ : mcs – , <__main__.MetaClass> name – , , , "User" bases – -, (SomeMixin, AbstractUser) attrs – dict-like , cls – , <__main__.User> extra_kwargs – keyword- args kwargs – """ def __new__(mcs, name, bases, attrs, **extra_kwargs): return super().__new__(mcs, name, bases, attrs) def __init__(cls, name, bases, attrs, **extra_kwargs): super().__init__(cls) @classmethod def __prepare__(mcs, cls, bases, **extra_kwargs): return super().__prepare__(mcs, cls, bases, **kwargs) def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs)
若要使用此元类配置User
类,请使用以下语法:
class User(metaclass=MetaClass): def __new__(cls, name): return super().__new__(cls) def __init__(self, name): self.name = name
最有趣的是创建类本身时,Python解释器调用元类元方法的顺序:
- 解释器确定并找到当前类的父类(如果有)。
- 解释器定义一个元类(在本例中为
MetaClass
)。 MetaClass.__prepare__
方法-它应该返回一个类似dict的对象,该类的属性和方法将写入该对象。 之后,该对象将通过attrs
参数传递给MetaClass.__new__
。 在后面的示例中,我们将讨论该方法的实际使用。- 解释器读取
User
类的主体,并生成参数以将其传递给MetaClass
元类。 MetaClass.__new__
方法MetaClass.__new__
方法,返回创建的类对象。 将参数传递给type
函数时,我们已经遇到了参数name
, bases
和attrs
,稍后我们将讨论**extra_kwargs
参数。 如果使用__prepare__
更改了attrs
参数的类型,则必须在将其传递给super()
方法调用之前将其转换为dict
。MetaClass.__init__
方法MetaClass.__init__
初始化方法,您可以使用该方法将其他属性和方法添加到类中的类对象。 实际上,它用于从其他元类继承元类的情况,否则可以在__init__
中完成的所有事情最好在__new__
完成。 例如, 只能通过将__slots__
参数写入到attrs
对象中来设置__slots__
参数。- 在这一步,该类被视为已创建。
现在创建我们的User
类的实例,并查看调用链:
user = User(name='Alyosha')
- 在调用
User(...)
解释器将调用MetaClass.__call__(name='Alyosha')
方法,该方法将传递类对象并传递参数。 MetaClass.__call__
调用User.__new__(name='Alyosha')
-创建并返回User
类实例的构造方法- 接下来,
MetaClass.__call__
调用User.__init__(name='Alyosha')
-一种初始化程序方法,该方法将新属性添加到创建的实例中。 MetaClass.__call__
返回User
类的创建和初始化的实例。- 此时,该类的实例被视为已创建。
当然,此描述并不涵盖使用元类的所有细微差别,但是足以开始使用元编程来实现某些体系结构模式。 转发到示例!
抽象类
在标准库中可以找到第一个示例: ABCMeta-元类允许您声明我们的任何类抽象,并强制其所有后代实现预定义的方法,属性和属性,请看:
from abc import ABCMeta, abstractmethod class BasePlugin(metaclass=ABCMeta): """ supported_formats run """ @property @abstractmethod def supported_formats(self) -> list: pass @abstractmethod def run(self, input_data: dict): pass
如果继承人中未实现所有抽象方法和属性,那么当我们尝试创建继承人类的实例时,将得到TypeError
:
class VideoPlugin(BasePlugin): def run(self): print('Processing video...') plugin = VideoPlugin()
使用抽象类有助于立即修复基类接口,并避免将来发生继承错误,例如,重写方法名称中的拼写错误。
自动注册插件系统
通常,元编程用于实现各种设计模式。 几乎所有知名的框架都使用元类来创建注册表对象。 这些对象存储到其他对象的链接,并允许它们在程序中的任何位置快速接收。 考虑一个自动注册插件的简单示例,该插件可播放各种格式的媒体文件。
元类实现:
class RegistryMeta(ABCMeta): """ , . " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs)
这是插件本身,我们将从上一示例中获取BasePlugin
实现:
class BasePlugin(metaclass=RegistryMeta): ... class VideoPlugin(BasePlugin): supported_formats = ['mpg', 'mov'] def run(self): ... class AudioPlugin(BasePlugin): supported_formats = ['mp3', 'flac'] def run(self): ...
执行完此代码后,解释器将在我们的注册表中注册4种格式和2个可处理这些格式的插件:
>>> RegistryMeta.show_registry() {'flac': <class '__main__.AudioPlugin'>, 'mov': <class '__main__.VideoPlugin'>, 'mp3': <class '__main__.AudioPlugin'>, 'mpg': <class '__main__.VideoPlugin'>} >>> plugin_class = RegistryMeta.get_plugin('mov') >>> plugin_class <class '__main__.VideoPlugin'> >>> plugin_class().run() Processing video...
值得一提的是,使用元类还有一个有趣的细微差别,这show_registry
于非显而易见的方法解析顺序,我们不仅可以在RegistyMeta
类上调用show_registry
方法,还可以在它是元类的任何其他类上调用show_registry
方法:
>>> AudioPlugin.get_plugin('avi')
使用元类,可以将类属性名称用作其他对象的元数据。 不清楚吗? 但是我敢肯定,您已经多次看到这种方法,例如,在Django中对模型字段进行声明式声明:
class Book(models.Model): title = models.Charfield(max_length=250)
在上面的示例中, title
是Python标识符的名称,它也用于命名book
表中的列,尽管我们在任何地方都没有明确指出。 是的,这种“魔术”可以通过元编程来实现。 例如,让我们实现一个用于将应用程序错误传输到前端的系统,以便每个消息都具有可用于将消息翻译为另一种语言的可读代码。 因此,我们有一个可以转换为json
的消息对象:
class Message: def __init__(self, text, code=None): self.text = text self.code = code def to_json(self): return json.dumps({'text': self.text, 'code': self.code})
我们所有的错误消息都将存储在单独的“名称空间”中:
class Messages: not_found = Message('Resource not found') bad_request = Message('Request body is invalid') ... >>> Messages.not_found.to_json() {"text": "Resource not found", "code": null}
现在我们希望code
不是null
,而是not_found
,为此,我们编写以下元类:
class MetaMessage(type): def __new__(mcs, name, bases, attrs): for attr, value in attrs.items():
让我们看看我们的帖子现在看起来如何:
>>> Messages.not_found.to_json() {"text": "Resource not found", "code": "not_found"} >>> Messages.bad_request.to_json() {"text": "Request body is invalid", "code": "bad_request"}
你需要什么! 现在您知道该怎么做,以便通过数据格式可以轻松找到处理它们的代码。
另一个常见情况是在类创建阶段缓存任何静态数据,以免浪费时间在应用程序运行时对其进行计算。 此外,在创建类的新实例时可以更新某些数据,例如,创建对象数量的计数器。
如何使用? 假设您正在开发一个用于构建报告和表的框架,并且您有一个这样的对象:
class Row(metaclass=MetaRow): name: str age: int ... def __init__(self, **kwargs): self.counter = None for attr, value in kwargs.items(): setattr(self, attr, value) def __str__(self): out = [self.counter]
我们要在创建新行时保存并增加计数器,还希望提前生成结果表的标题。 元类抢救!
class MetaRow(type):
这里需要澄清两件事:
Row
类不具有名称为name
和age
类属性-这些是类型注释 ,因此它们不在attrs
字典键中,并且为了获取字段列表,我们使用__annotations__
class __annotations__
。- 操作
cls.row_count += 1
应该误导您:怎么回事? 毕竟, cls
是Row
类;它没有row_count
属性。 一切都是正确的,但是正如我在上面解释的那样-如果创建的类不具有他们尝试调用的属性或方法,则解释器将沿着基类链进一步发展-如果基类中没有基类,则在元类中进行搜索。 在这种情况下,为了不混淆任何人,最好使用另一条记录: MetaRow.row_count += 1
。
看看您现在可以多么优雅地显示整个表格:
rows = [ Row(name='Valentin', age=25), Row(name='Sergey', age=33), Row(name='Gosha'), ] print(' | '.join(Row.__header__)) for row in rows: print(row)
№ | age | name 1 | 25 | Valentin 2 | 33 | Sergey 3 | N/A | Gosha
顺便说一句,显示和使用表可以封装在单独的Sheet
类中。
待续...
在本文的下一部分中 ,我将描述如何使用元类来调试应用程序代码,如何对元类的创建进行参数化,并显示使用__prepare__
方法的基本示例。 敬请期待!
在Python中有关元类和描述符的更多详细信息,我将在Advanced Python框架中讲述。