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



























4.5/5 — (2 голоса)

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

Эта методика, именуемая метапрограммированием, популярна среди разработчиков фреймворков. Так вы получаете генерацию кода и умные возможности во многих распространённых фреймворках и библиотеках, таких как Ruby On Rails или TensorFlow.

Языки фукционального программирования, такие как Elixir, Clojure и Ruby, известны своими возможностями метапрограммирования. В этом руководстве мы покажем, как вы можете использовать мощь метапрограммирования в Python.

Примеры кода написаны для Python 3, но будут работать на Python 2 с некоторыми исправлениями.

Что такое метакласс в Python?

Python – это объектно-оринтированный язык, который облегчает работу с классами.

Метапрограммирование в Python основано на новом особом типе классов, которые называются метаклассами. Этот тип класса, вкратце, содержит инструкции о закулисной генерации кода, который вы хотите получить, запуская другой код.

Википедия довольно хорошо описывает метаклассы:

Метакласс (англ. Metaclass) — в объектно-ориентированном программировании это класс, экземпляры которого в свою очередь являются классами.

Когда мы определяем класс, его объекты создаются, используя класс как пример. Но как насчёт самого класса? Что есть этот самый пример для класса?

Здесь на сцену выходит метакласс. Метакласс – это пример самого класса, как класс есть пример для его экземпляров. Метакласс – это класс, который определяет свойства других классов. С помощью метакласса мы можем определять свойства, которые следует добавить к новым классам, определяемымв нашем коде.

Например, метакласс в следующем образце кода добавляет свойство hello к каждому классу, использующему этот метакласс как шаблон. Это значит, что новые классы – экземпляры этого метакласса будут иметь свойство hello без необходимости определять его отдельно:

# hello_metaclass.py

# Простой метакласс

# Этот метакласс добавляет метод 'hello' к классам, использующим его значение,

# те классы получают метод 'hello' без лишних усилий

# метакласс заботится о генерации кода для нас

class HelloMeta(type):  

    # Метод hello

    def hello(cls):

        print("greetings from %s, a HelloMeta type class" % (type(cls())))



    # Вызываем метакласс

    def __call__(self, *args, **kwargs):

        # создаём новый класс как обычно

        cls = type.__call__(self, *args)



        # определяем новый метод hello для каждого из этих классов

        setattr(cls, "hello", self.hello)



        # возвращаем класс

        return cls



# Проверяем метакласс

class TryHello(object, metaclass=HelloMeta):  

    def greet(self):

        self.hello()



# Создаём экземпляр метакласса. Он должен автоматически содержать метод hello

# хотя он не объявлен в классе вручную

# иными словами, он объявлен за нас метаклассом

greeter = TryHello()  

greeter.greet()

В результате запуска этого кода новый класс TryHello способен напечатать приветствие:

greetings from <class '__main__.TryHello'>, a HelloMeta type class

Метод, ответственный за этот вывод, не объявлен в декларации класса. Вместо этого метакласс, в данном случае HelloMeta, порождает код во время запуска, что сразу связывает этот метод с классом.

Чтобы увидеть это в действии, смело копируйте код в консоль Python. Также прочтите комментарии, чтобы лучше понимать, что мы сделали в каждой части кода. У нас есть новый объект по имени greeter, который является сущностью класса TryHello. Впрочем, мы можем вызвать метод self.hello класса TryHello, хотя этот метод не определён в объявлении класса TryHello.

Вместо того, чтобы выдать ошибку вызова несуществующего метода, TryHello автоматически привязывает этот метод к классу, используя HelloMeta как его метакласс.

Метаклассы дают нам возможность писать код, который изменяет не только данные, но и другой код, то есть изменяет класс во время его создания. В примере выше наш метакласс автоматически добавляет новый метод к новым классам, которые мы определяем, чтобы использовать метакласс.

Это пример метапрограммирования. Метапрограммирование — это просто написание кода, который работает с метаклассами и схожие методики изменения кода на заднем плане.

Прекрасно в метапрограммировании то, что вместо вывода исходного кода, оно даёт нам лишь исполнение этого кода. Пользователь нашей программы не знает о “магии”, происходящей за кадром.

Подумайте о фреймворках, которые порождают код в фоновом режиме, чтобы убедиться, что вам как программисту нужно писать меньше кода для всего. Вот несколько очень хороших примеров:

Помимо языка Python, другие библиотеки вроде Ruby On Rails(Ruby) и Boost(C++) служат примерами того, как метапрограммирование используется авторами фреймворков, чтобы неявно порождать код и заботиться обо всем. В результате получаем упрощённые пользовательские API, которые автоматизируют много работы за программиста, пишущего код фреймворка.

Обеспечить, чтобы простота работала за кадром – как раз то, для чего метапрограммирование встроено в исходный код фреймворков.

Немного теории: разберёмся, как работают метаклассы

Чтобы понять, как работают метаклассы в 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?

>>> type(type)

<type 'type'>  


В результате снова получаем “type”. Так мы находим, что type есть не только метакласс для типов наподобие int, но и свой собственный метакласс!

Специальные методы, которые используются метаклассами

Здесь нам поможет немного теории. Вспомним, что метакласс — это класс, сущностями которого являются сами классы, а не просто обычные объекты. В Python 3 можно назначить метакласс при создании нового класса, передав главный класс в определение нового класса.

Тип type, как метакласс по умолчанию в Python, определяет особые методы, которые новый метакласс может переписать, чтобы создать поведение уникального кода. Вот краткий обзор этих “волшебных” методов, существующих в метаклассе:

  • __new__: этот метод вызывается в метаклассе до того, как создаётся сущность класса, на нём основанного
  • __init__: этот метод вызывается, чтобы установить переменные после создания сущности/объекта
  • __prepare__: определяет пространство имён класса в отображении, сохраняющием атрибуты
  • __call__: этот метод вызывается, когда конструктор нового класса нужно использовать для создания оъекта

Вот методы, переопределение которых в вашем метаклассе даст вашим классам поведение, отличное от типа typeметакласса по умолчанию.

Практика метапрограммирования 1: использование декораторов, чтобы изменить поведение функции

Сделаем шаг назад, прежде чем мы приступим к практике метапрограммирования. Обычное применение метапрограммирования на Python – это использоваине декораторов.

Декоратор есть функция, которая изменяет исполнение функции. Иными словами, он принимает функцию на вход и возвращает другую функцию.

Например, вот декоратор, который берёт функцию и печатает её имя перед обычным запуском функции. Это может быть полезно для логирования вызовов функции, например:

# decorators.py



from functools import wraps



# Создаём новый декоратор по имени notifyfunc

def notifyfunc(fn):  

    """печатает имя функции перед её исполнением"""

    @wraps(fn)

    def composite(*args, **kwargs):

        print("Executing '%s'" % fn.__name__)

        # Запускаем исходную функцию и возвращаем результат

        rt = fn(*args, **kwargs)

        return rt

    # Возвращаем нашу сложную функцию

    return composite



# Применяем наш декоратор к обычной функции, которая печатает произведение своих аргументов

@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, если он поддерживает форматирование кода.

# metaclassdecorator.py

import types



# Функция, которая возвращает имя поступающей на вход функции и возвращает новую функцию

# инкапсулирующию поведение исходной функции

def notify(fn, *args, **kwargs):



    def fncomposite(*args, **kwargs):

        # Обычная функциональность notify

        print("running %s" % fn.__name__)

        rt = fn(*args, **kwargs)

        return rt

    # Возвращаем сложную функцию

    return fncomposite



# Метакласс, меняющий поведение своих классов

# на новые методы, 'дополненные' поведением преобразователя сложной функции

class Notifies(type):



    def __new__(cls, name, bases, attr):

        # Заменим каждую функцию на выражение, которое печатает имя функции

        # перед запуском вычисления с предоставленными args и возвращает его результат

        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)



# Проверим метакласс

class Math(metaclass=Notifies):  

    def multiply(a, b):

        product = a * b

        print(product)

        return product



Math.multiply(5, 6)



# Запуск multiply():

# 30





class Shouter(metaclass=Notifies):  

    def intro(self):

        print("I shout!")



s = Shouter()  

s.intro()



# Запуск intro():

# I shout!


В классах, которые используют наш метакласс Notifies, например Shouter и Math, во время создания заменены методы на дополненные версии, которые сначала сообщают нам через выражение print имя запускаемого сейчас метода. Это идентично поведению, которое мы имплементировали перед использованием функции-декоратора.

Пример метаклассов 1: объявление класса, от которого нельзя унаследоваться

Обычные сценарии использования для метапрограммирования включают контроль сущностей классов. Например, одиночки (singletons) используются во многих библиотеках кода. Класс-одиночка контролирует создание сущностей так, что может существовать лишь один экземпляр класса в программе.

Финальный класс – другой пример того, как можно контролировать использование классов. Этот класс не позволяет создавать свои подклассы. Финальные классы используются в некоторых фреймворках для безопасности, чтобы убедиться, что класс сохраняет свои исходные атрибуты.

Ниже мы дадим объявление финального класса, использующего метакласс, который не позволяют классу наследоваться от другого.

# final.py



# финальный метакласс. Унаследоваться от класса, имеющего свои метаклассом Final, не получится

class Final(type):  

    def __new__(cls, name, bases, attr):

        # от финального класса нельзя унаследоваться

        # проверим, что класс Final не выступает в качестве базового

        # если так, укажем об ошибке, иначе создадим новый класс с атрибутами Final

        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)





# Тест: применим метакласс, чтобы создать финальный класс Cop



class Cop(metaclass=Final):  

    def exit():

        print("Exiting...")

        quit()



# Попытка создать класс Cop, в идеале следует возбудить исключение!

class FakeCop(Cop):  

    def scam():

        print("This is a hold up!")



cop1 = Cop()  

fakecop1 = FakeCop()



# Больше тестов, другой класс Final

class Goat(metaclass=Final):  

    location = "Goatland"



# Унаследоваться от финального класса не получится

class BillyGoat(Goat):  

    location = "Billyland"


В этом коде мы ввели объявления классов, чтобы попробовать унаследоваться от класса Final. Эти попытки провалились, в результате было выброшено исключение. Использование метаклассов, что ограничивают наследование, позволяет нам объявить финальные классы в нашей кодовой базе.

Пример метаклассов 2: создание класса, отслеживающего время исполнения

Профилировщики используются, чтобы проанализировать потребление ресурсов в вычислительной системе. Они могут определять занимаемую память, скорость процессора и прочие технические характеристики.

Мы можем использовать метакласс, чтобы следить за временем исполнения кода. Наш пример кода не совсем профилировщик, но он показывает, как можно метапрограммированием решать подобные задачи.

# timermetaclass.py	 	 

import types	 	 

# Класс функций времени	 	 

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()	 	 

# Далее мы создаём метакласс Timed, который считает время работы своих методов	 	 

# вместе с функциями установки, которые переписывают методы классa	 	 

# времена создания классов	 	 

# Функция, засекающая время исполнения встроенной функции, возвращает новую,	 	 

# инкапсулируя поведение исходной функции	 	 

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 fncomposite



# Метакласс 'Timed', который заменяет методы своих классов	

# с новым методами 'timed' на поведение сложной функции-преобразователя	 	 

class Timed(type):	 	 

 def __new__(cls, name, bases, attr):	 	 

 # меняет каждую функцию на новую, время которой замеряется	 	 

 # запускает вычисление с заданными args и возвращает результат	 	 

 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)	 	 

# Следующий пример кода проверяет метакласс	 	 

# Классы, применяющие метакласс Timed, следует замерить за нас автоматически	 	 

# проверьте результат в 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 , замеры делаются за нас сами, без нужды делать что-либо ещё.

Метапрограммирование — важная вещь, если вы пишете код и инструменты для применения другими разработчиками, в частности фреймворков или отладчиков. С генерацией кода и метапрограммированием вы можете облегчить жизнь программистов, которые пользуются вашими библиотеками кода.


2018-03-14T12:43:04
Программирование