Метаклассы Python и метапрограммирование

Представьте, если бы у вас были компьютерные программы, которые написали бы ваш код за вас. Это возможно, но машины не напишут за вас весь ваш код! Этот метод, называемый метапрограммированием [https://en.wikipedia.org/wiki/Metaprogramming], популярен среди разработчиков фреймворка кода. Так вы получаете генерацию кода и интеллектуальные функции во многих популярных фреймворках и библиотеках, таких как Ruby On Rails или TensorFlow [/ tag / tensorflow /]. Следует отметить функциональные языки программирования, такие как Elixir, Clojure и Ruby.

Представьте, если бы у вас были компьютерные программы, которые написали бы ваш код за вас. Это возможно, но машины не напишут за вас весь ваш код!

Этот метод, называемый метапрограммированием , популярен среди разработчиков фреймворка кода. Так вы получаете генерацию кода и интеллектуальные функции во многих популярных фреймворках и библиотеках, таких как 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.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus