Python:生产中的元编程。 第一部分

许多人认为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函数创建代码对象:


 #   ,    "Hello, world!" >>> code = compile('print("Hello, world!")', '<repl>', 'eval') >>> code <code object <module> at 0xdeadbeef, file "<repl>", line 1> #  ,     , #      >>> func = FunctionType(code, globals(), 'greetings') >>> func <function <module> at 0xcafefeed> >>> func.__name__ 'greetings' >>> func() Hello, world! 

太好了! 在元工具的帮助下,我们学习了如何动态创建函数,但实际上很少使用这种知识。 现在让我们看一下如何创建这些类的类对象和实例对象:


 >>> 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() #     SuperUser "" CustomSuperUser = type( #   'SuperUser', #  ,      (User, ), #         { '__doc__': 'Encapsulate domain logic to work with super users', 'group_name': 'admin', 'login': property(lambda self: f'{self.group_name}/{self.name}'.lower()), } ) assert SuperUser.__doc__ == CustomSuperUser.__doc__ assert SuperUser('Vladimir').login == CustomSuperUser('Vladimir').login 

从上面的示例中可以看到,使用关键字classdef进行的类和函数的描述只是语法糖,任何类型的对象都可以通过对内置函数的常规调用来创建。 现在,最后,让我们谈谈如何在实际项目中使用动态创建类。


动态创建表单和验证器


有时我们需要根据先前已知的数据方案来验证来自用户或其他外部来源的信息。 例如,我们要在管理面板中更改用户登录表单-删除和添加字段,更改其验证策略等。


为了说明这一点,让我们尝试动态创建一个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, } # form_description –  json    deserialized_form_description: dict = json.loads(form_description) form_attrs = {} #            for field_name, field_description in deserialized_form_description.items(): field_class = fields_type_map[field_description.pop('type')] form_attrs[field_name] = field_class(**field_description) user_form_class = type('DynamicForm', (forms.Form, ), form_attrs) >>> form = user_form_class({'age': 101}) >>> form <DynamicForm bound=True, valid=Unknown, fields=(fist_name;last_name;age)> >>> form.is_valid() False >>> form.errors {'fist_name': ['This field is required.'], 'last_name': ['This field is required.'], 'age': ['Ensure this value is less than or equal to 99.']} 

太好了! 现在,您可以将创建的表单转移到模板并为用户呈现。 相同的方法可以与其他框架一起用于数据验证和表示( 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解释器调用元类元方法的顺序:


  1. 解释器确定并找到当前类的父类(如果有)。
  2. 解释器定义一个元类(在本例中为MetaClass )。
  3. MetaClass.__prepare__方法-它应该返回一个类似dict的对象,该类的属性和方法将写入该对象。 之后,该对象将通过attrs参数传递给MetaClass.__new__ 。 在后面的示例中,我们将讨论该方法的实际使用。
  4. 解释器读取User类的主体,并生成参数以将其传递给MetaClass元类。
  5. MetaClass.__new__方法MetaClass.__new__方法,返回创建的类对象。 将参数传递给type函数时,我们已经遇到了参数namebasesattrs ,稍后我们将讨论**extra_kwargs参数。 如果使用__prepare__更改了attrs参数的类型,则必须在将其传递给super()方法调用之前将其转换为dict
  6. MetaClass.__init__方法MetaClass.__init__初始化方法,您可以使用该方法将其他属性和方法添加到类中的类对象。 实际上,它用于从其他元类继承元类的情况,否则可以在__init__中完成的所有事情最好在__new__完成。 例如, 只能通过将__slots__参数写入到attrs对象中来设置__slots__参数。
  7. 在这一步,该类被视为已创建。

现在创建我们的User类的实例,并查看调用链:


 user = User(name='Alyosha') 

  1. 在调用User(...)解释器将调用MetaClass.__call__(name='Alyosha')方法,该方法将传递类对象并传递参数。
  2. MetaClass.__call__调用User.__new__(name='Alyosha') -创建并返回User类实例的构造方法
  3. 接下来, MetaClass.__call__调用User.__init__(name='Alyosha') -一种初始化程序方法,该方法将新属性添加到创建的实例中。
  4. MetaClass.__call__返回User类的创建和初始化的实例。
  5. 此时,该类的实例被视为已创建。

当然,此描述并不涵盖使用元类的所有细微差别,但是足以开始使用元编程来实现某些体系结构模式。 转发到示例!


抽象类


在标准库中可以找到第一个示例: 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() # TypeError: Can't instantiate abstract class VideoPlugin # with abstract methods supported_formats 

使用抽象类有助于立即修复基类接口,并避免将来发生继承错误,例如,重写方法名称中的拼写错误。


自动注册插件系统


通常,元编程用于实现各种设计模式。 几乎所有知名的框架都使用元类来创建注册表对象。 这些对象存储到其他对象的链接,并允许它们在程序中的任何位置快速接收。 考虑一个自动注册插件的简单示例,该插件可播放各种格式的媒体文件。


元类实现:


 class RegistryMeta(ABCMeta): """ ,      .     " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs) #     (BasePlugin) if inspect.isabstract(cls): return cls for media_format in cls.supported_formats: if media_format in mcs._registry_formats: raise ValueError(f'Format {media_format} is already registered') #       mcs._registry_formats[media_format] = cls return cls @classmethod def get_plugin(mcs, media_format: str): try: return mcs._registry_formats[media_format] except KeyError: raise RuntimeError(f'Plugin is not defined for {media_format}') @classmethod def show_registry(mcs): from pprint import pprint pprint(mcs._registry_formats) 

这是插件本身,我们将从上一示例中获取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') # RuntimeError: Plugin is not found for 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(): #          Message #    code    # ( code   ) if isinstance(value, Message) and value.code is None: value.code = attr return super().__new__(mcs, name, bases, attrs) class Messages(metaclass=MetaMessage): ... 

让我们看看我们的帖子现在看起来如何:


 >>> 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] #  __header__      for name in self.__header__[1:]: out.append(getattr(self, name, 'N/A')) return ' | '.join(map(str, out)) 

我们要在创建新行时保存并增加计数器,还希望提前生成结果表的标题。 元类抢救!


 class MetaRow(type): #      row_count = 0 def __new__(mcs, name, bases, attrs): cls = super().__new__(mcs, name, bases, attrs) #          cls.__header__ = ['№'] + sorted(attrs['__annotations__'].keys()) return cls def __call__(cls, *args, **kwargs): #      row: 'Row' = super().__call__(*args, **kwargs) #    cls.row_count += 1 #     row.counter = cls.row_count return row 

这里需要澄清两件事:


  • Row类不具有名称为nameage类属性-这些是类型注释 ,因此它们不在attrs字典键中,并且为了获取字段列表,我们使用__annotations__ class __annotations__
  • 操作cls.row_count += 1应该误导您:怎么回事? 毕竟, clsRow类;它没有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框架中讲述。

Source: https://habr.com/ru/post/zh-CN422409/


All Articles