Представьте, если бы у вас были компьютерные программы, которые написали бы ваш код за вас. Это возможно, но машины не напишут за вас весь ваш код!
Этот метод, называемый метапрограммированием , популярен среди разработчиков фреймворка кода. Так вы получаете генерацию кода и интеллектуальные функции во многих популярных фреймворках и библиотеках, таких как Ruby On Rails или TensorFlow .
Языки функционального программирования, такие как Elixir, Clojure и Ruby, известны своими возможностями метапрограммирования. В этом руководстве мы покажем вам, как можно использовать возможности метапрограммирования в Python. Примеры кода написаны для Python 3, но с некоторыми изменениями будут работать и для Python 2.
Что такое метакласс в Python?
Python - объектно-ориентированный язык, упрощающий работу с классами.
Метапрограммирование в Python полагается на специальный новый тип класса, который называется метаклассом . Короче говоря, этот тип класса содержит инструкции о скрытой генерации кода, которую вы хотите выполнять, когда выполняется другой фрагмент кода.
Википедия довольно хорошо резюмирует метаклассы:
В объектно-ориентированном программировании метакласс - это класс, экземпляры которого являются классами
Когда мы определяем класс, объекты этого класса создаются с использованием этого класса в качестве чертежа.
А как насчет самого класса? Каков план самого класса?
Здесь на помощь приходит метакласс. Метакласс - это проект самого класса, точно так же, как класс является планом экземпляров этого класса. Метакласс - это класс, который определяет свойства других классов.
С помощью метакласса мы можем определять свойства, которые следует добавлять в новые классы, определенные в нашем коде.
Например, следующий пример кода метакласса добавляет hello
к каждому
классу, который использует этот метакласс в качестве своего шаблона. Это
означает, что новые классы, являющиеся экземплярами этого метакласса,
будут иметь hello
без необходимости определять его сами.
# hello_metaclass.py
# A simple metaclass
# This metaclass adds a 'hello' method to classes that use the metaclass
# meaning, those classes get a 'hello' method with no extra effort
# the metaclass takes care of the code generation for us
class HelloMeta(type):
# A hello method
def hello(cls):
print("greetings from %s, a HelloMeta type class" % (type(cls())))
# Call the metaclass
def __call__(self, *args, **kwargs):
# create the new class as normal
cls = type.__call__(self, *args)
# define a new hello method for each of these classes
setattr(cls, "hello", self.hello)
# return the class
return cls
# Try out the metaclass
class TryHello(object, metaclass=HelloMeta):
def greet(self):
self.hello()
# Create an instance of the metaclass. It should automatically have a hello method
# even though one is not defined manually in the class
# in other words, it is added for us by the metaclass
greeter = TryHello()
greeter.greet()
Результатом выполнения этого кода является то, что новый TryHello
может распечатать приветствие, в котором говорится:
greetings from <class '__main__.TryHello'>, a HelloMeta type class
Метод, ответственный за эту распечатку, не объявлен в объявлении класса.
Скорее, метакласс, которым HelloMeta
, генерирует код во время
выполнения, который автоматически прикрепляет метод к классу.
Чтобы увидеть это в действии, скопируйте и вставьте код в консоль
Python. Также прочтите комментарии, чтобы лучше понять, что мы сделали в
каждой части кода. У нас есть новый объект с именем greeter
, который
является экземпляром класса TryHello
Однако мы можем вызвать
TryHello
self.hello
даже если такой метод не был определен в
TryHello
класса TryHello.
Вместо того, чтобы получать ошибку при вызове несуществующего TryHello
, TryHello получает такой метод, автоматически прикрепленный к нему
из-за использования HelloMeta
качестве своего метакласса.
Метаклассы дают нам возможность писать код, который преобразует не только данные, но и другой код, например, преобразует класс во время его создания. В приведенном выше примере наш метакласс автоматически добавляет новый метод к новым классам, которые мы определяем для использования нашего метакласса в качестве своего метакласса.
Это пример метапрограммирования. Метапрограммирование - это просто написание кода, который работает с метаклассами и связанными с ними методами для выполнения некоторой формы преобразования кода в фоновом режиме.
Самое прекрасное в метапрограммировании заключается в том, что вместо вывода исходного кода оно возвращает нам только выполнение этого кода. Конечный пользователь нашей программы не подозревает о «волшебстве», происходящем в фоновом режиме.
Подумайте о программных фреймворках, которые генерируют код в фоновом режиме, чтобы вам, как программисту, приходилось писать меньше кода для всего. Вот несколько отличных примеров:
Помимо Python, другие популярные библиотеки, такие как Ruby On Rails (Ruby) и Boost (C ++), являются примерами того, где метапрограммирование используется авторами фреймворка для генерации кода и заботы о вещах в фоновом режиме.
Результатом являются упрощенные API для конечных пользователей, которые автоматизируют большую часть работы программиста, который кодирует во фреймворке.
Забота о том, чтобы эта простота работала за кулисами, заключается в большом количестве метапрограммирования, встроенном в исходный код фреймворка.
Теоретический раздел: понимание того, как работают метаклассы
Чтобы понять, как работают метаклассы Python, вам нужно хорошо разбираться в понятиях типов в Python.
Тип - это просто номенклатура данных или объекта для объекта в Python.
Определение типа объекта
Используя Python REPL, давайте создадим простой строковый объект и проверим его тип следующим образом:
>>> day = "Sunday"
>>> print("The type of variable day is %s" % (type(day)))
The type of variable day is <type 'str'>
Как и следовало ожидать, мы получаем распечатку, что переменная day
имеет тип str
, который является строковым типом. Вы можете найти тип
любого объекта, просто используя встроенную type
с одним аргументом
объекта.
Определение типа класса
Итак, строка типа "Sunday"
или "hello"
имеет тип str
, но как
насчет самой str
Какой тип класса str
Снова введите в консоли Python:
>>> type(str)
<type 'type'>
На этот раз мы получаем распечатку, в которой str
имеет тип type
.
Тип и тип типа
А как насчет самого type
Что такое type
типа «s?
>>> type(type)
<type 'type'>
Результат снова «тип». Таким образом, мы обнаруживаем, что type
- это
не только метакласс таких классов, как int
, но и его собственный
метакласс!
Специальные методы, используемые метаклассами
На этом этапе это может помочь немного пересмотреть теорию. Помните, что метакласс - это класс, экземпляры которого сами являются классами, а не просто объектами.
В Python 3 вы можете назначить метакласс для создания нового класса, передав намеченный мастер-класс определению нового класса.
type
типа, как метакласс по умолчанию в Python, определяет специальные
методы, которые новые метаклассы могут переопределить для реализации
уникального поведения генерации кода. Вот краткий обзор этих «волшебных»
методов, существующих в метаклассе:
__new__
: этот метод вызывается в метаклассе перед созданием экземпляра класса на основе метакласса.__init__
: этот метод вызывается для установки значений после создания экземпляра / объекта.__prepare__
: определяет пространство имен класса в сопоставлении, в котором хранятся атрибуты__call__
: этот метод вызывается, когда конструктор нового класса должен использоваться для создания объекта.
Это методы, которые необходимо переопределить в вашем настраиваемом
метаклассе, чтобы дать вашим классам поведение, отличное от type
,
который является метаклассом по умолчанию.
Метапрограммирование, практика 1: использование декораторов для преобразования поведения функций
Давайте сделаем шаг назад, прежде чем приступить к использованию практики метапрограммирования метаклассов. Распространенное использование метапрограммирования в Python - использование декораторов.
Декоратор - это функция, которая преобразует выполнение функции. Другими словами, он принимает функцию в качестве входных данных и возвращает другую функцию.
Например, вот декоратор, который принимает любую функцию и распечатывает имя функции перед запуском исходной функции в обычном режиме. Это может быть полезно для регистрации вызовов функций, например:
# decorators.py
from functools import wraps
# Create a new decorator named notifyfunc
def notifyfunc(fn):
"""prints out the function name before executing it"""
@wraps(fn)
def composite(*args, **kwargs):
print("Executing '%s'" % fn.__name__)
# Run the original function and return the result, if any
rt = fn(*args, **kwargs)
return rt
# Return our composite function
return composite
# Apply our decorator to a normal function that prints out the result of multiplying its arguments
@notifyfunc
def multiply(a, b):
product = a * b
return product
Вы можете скопировать и вставить код в Python REPL. Самое интересное в использовании декоратора заключается в том, что составная функция выполняется вместо функции ввода. Результатом приведенного выше кода является то, что функция умножения объявляет о том, что она запущена, до выполнения ее вычисления:
>>> multiply(5, 6)
Executing 'multiply'
30
>>>
>>> multiply(89, 5)
Executing 'multiply'
445
Короче говоря, декораторы достигают того же поведения преобразования кода, что и метаклассы, но намного проще. Вы хотели бы использовать декораторы там, где вам нужно применить обычное метапрограммирование вокруг вашего кода. Например, вы можете написать декоратор, который регистрирует все вызовы базы данных.
Практика метапрограммирования 2. Использование метаклассов в качестве функции-декоратора
Метаклассы могут заменять или изменять атрибуты классов. У них есть возможность подключиться до создания нового объекта или после создания нового объекта. Результат - большая гибкость в отношении того, для чего вы можете их использовать.
Ниже мы создаем метакласс, который дает тот же результат, что и декоратор из предыдущего примера.
Чтобы сравнить эти два примера, вы должны запустить оба примера рядом, а затем следить за аннотированным исходным кодом. Обратите внимание, что вы можете скопировать код и вставить его прямо в ваш REPL, если ваш REPL сохраняет форматирование кода.
# metaclassdecorator.py
import types
# Function that prints the name of a passed in function, and returns a new function
# encapsulating the behavior of the original function
def notify(fn, *args, **kwargs):
def fncomposite(*args, **kwargs):
# Normal notify functionality
print("running %s" % fn.__name__)
rt = fn(*args, **kwargs)
return rt
# Return the composite function
return fncomposite
# A metaclass that replaces methods of its classes
# with new methods 'enhanced' by the behavior of the composite function transformer
class Notifies(type):
def __new__(cls, name, bases, attr):
# Replace each function with
# a print statement of the function name
# followed by running the computation with the provided args and returning the computation result
for name, value in attr.items():
if type(value) is types.FunctionType or type(value) is types.MethodType:
attr[name] = notify(value)
return super(Notifies, cls).__new__(cls, name, bases, attr)
# Test the metaclass
class Math(metaclass=Notifies):
def multiply(a, b):
product = a * b
print(product)
return product
Math.multiply(5, 6)
# Running multiply():
# 30
class Shouter(metaclass=Notifies):
def intro(self):
print("I shout!")
s = Shouter()
s.intro()
# Running intro():
# I shout!
Классы , которые используют наш Notifies
метакласс, например Shouter
и Math
, есть способы их замена во время создания, с
усовершенствованными версиями, первым уведомляют нас через print
заявление от имени методы в настоящее время работает. Это идентично
поведению, которое мы реализовали перед использованием функции
декоратора.
Метаклассы, пример 1: реализация класса, который не может быть разделен на подклассы
Общие варианты использования метапрограммирования включают в себя управление экземплярами классов.
Например, синглтоны используются во многих библиотеках кода. Одноэлементный класс управляет созданием экземпляра, поэтому в программе всегда может быть не более одного экземпляра класса.
Последний класс - еще один пример управления использованием класса. В случае последнего класса класс не позволяет создавать подклассы. Конечные классы используются в некоторых фреймворках для обеспечения безопасности, гарантируя, что класс сохраняет свои исходные атрибуты.
Ниже мы даем реализацию последнего класса с использованием метакласса, чтобы ограничить наследование класса другим.
# final.py
# a final metaclass. Subclassing a class that has the Final metaclass should fail
class Final(type):
def __new__(cls, name, bases, attr):
# Final cannot be subclassed
# check that a Final class has not been passed as a base
# if so, raise error, else, create the new class with Final attributes
type_arr = [type(x) for x in bases]
for i in type_arr:
if i is Final:
raise RuntimeError("You cannot subclass a Final class")
return super(Final, cls).__new__(cls, name, bases, attr)
# Test: use the metaclass to create a Cop class that is final
class Cop(metaclass=Final):
def exit():
print("Exiting...")
quit()
# Attempt to subclass the Cop class, this should idealy raise an exception!
class FakeCop(Cop):
def scam():
print("This is a hold up!")
cop1 = Cop()
fakecop1 = FakeCop()
# More tests, another Final class
class Goat(metaclass=Final):
location = "Goatland"
# Subclassing a final class should fail
class BillyGoat(Goat):
location = "Billyland"
В код мы включили объявления классов для попытки создать подкласс класса
Final
Эти объявления терпят неудачу, что приводит к возникновению
исключений. Использование метакласса, ограничивающего создание
подклассов для его классов, позволяет нам реализовать конечные классы в
нашей кодовой базе.
Пример метаклассов 2: Создание времени выполнения операции отслеживания класса
Профилировщики используются для оценки использования ресурсов в вычислительной системе. Профилировщик может отслеживать такие вещи, как использование памяти, скорость обработки и другие технические показатели.
Мы можем использовать метакласс, чтобы отслеживать время выполнения кода. Наш пример кода не является полным профилировщиком, но является доказательством концепции того, как вы можете выполнять метапрограммирование для функций, подобных профилировщику.
# timermetaclass.py
import types
# A timer utility class
import time
class Timer:
def __init__(self, func=time.perf_counter):
self.elapsed = 0.0
self._func = func
self._start = None
def start(self):
if self._start is not None:
raise RuntimeError('Already started')
self._start = self._func()
def stop(self):
if self._start is None:
raise RuntimeError('Not started')
end = self._func()
self.elapsed += end - self._start
self._start = None
def reset(self):
self.elapsed = 0.0
@property
def running(self):
return self._start is not None
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
# Below, we create the Timed metaclass that times its classes' methods
# along with the setup functions that rewrite the class methods at
# class creation times
# Function that times execution of a passed in function, returns a new function
# encapsulating the behavior of the original function
def timefunc(fn, *args, **kwargs):
def fncomposite(*args, **kwargs):
timer = Timer()
timer.start()
rt = fn(*args, **kwargs)
timer.stop()
print("Executing %s took %s seconds." % (fn.__name__, timer.elapsed))
return rt
# return the composite function
return fncomposite
# The 'Timed' metaclass that replaces methods of its classes
# with new methods 'timed' by the behavior of the composite function transformer
class Timed(type):
def __new__(cls, name, bases, attr):
# replace each function with
# a new function that is timed
# run the computation with the provided args and return the computation result
for name, value in attr.items():
if type(value) is types.FunctionType or type(value) is types.MethodType:
attr[name] = timefunc(value)
return super(Timed, cls).__new__(cls, name, bases, attr)
# The below code example test the metaclass
# Classes that use the Timed metaclass should be timed for us automatically
# check the result in the REPL
class Math(metaclass=Timed):
def multiply(a, b):
product = a * b
print(product)
return product
Math.multiply(5, 6)
class Shouter(metaclass=Timed):
def intro(self):
print("I shout!")
s = Shouter()
s.intro()
def divide(a, b):
result = a / b
print(result)
return result
div = timefunc(divide)
div(9, 3)
Как видите, мы смогли создать Timed
который на лету переписывает свои
классы. Каждый раз, когда объявляется новый класс, который использует
Timed
, его методы переписываются так, чтобы они были синхронизированы
нашим служебным классом таймера. Всякий раз, когда мы запускаем
вычисления с использованием Timed
, мы получаем отсчет времени
автоматически, без необходимости делать что-либо дополнительно.
Метапрограммирование - отличный инструмент, если вы пишете код и инструменты для использования другими разработчиками, например веб-фреймворки или отладчики. С помощью генерации кода и метапрограммирования вы можете облегчить жизнь программистам, использующим ваши библиотеки кода.
::: {style=“padding: 35px;margin: 25px;background-color: #f3f3f3;border-radius: 4px;”} Рекомендуемый курс: Освоение Python{.udemy-link} :::
Освоение возможностей метаклассов
Метаклассы и метапрограммирование обладают большой силой. Обратной стороной является то, что метапрограммирование может быть довольно сложным. Во многих случаях использование декораторов обеспечивает более простой способ получить элегантное решение. Метаклассы следует использовать, когда обстоятельства требуют общности, а не простоты.
Чтобы эффективно использовать метаклассы, мы рекомендуем прочитать официальную документацию по метаклассам Python 3.