Рубрики
Без рубрики

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

Автор оригинала: Tendai Mutunhire.

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

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

Функциональные языки программирования, такие как Elixir, Clojure и Ruby, известны своими возможностями метапрограммирования. В этом руководстве мы покажем вам, как вы можете использовать возможности метапрограммирования в Python. Примеры кода написаны для Python 3, но будут работать для Python 2 с некоторыми корректировками.

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

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

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

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

В объектно-ориентированном программировании метакласс-это класс, экземпляры которого являются классами

Когда мы определяем класс, объекты этого класса создаются с использованием класса в качестве чертежа.

Но как насчет самого класса? Каков план самого класса?

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

С помощью метакласса мы можем определить свойства, которые должны быть добавлены к новым классам, определенным в нашем коде.

Например, следующий пример кода метакласса добавляет свойство 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()

Результатом выполнения этого кода является то, что новый класс Try Hello способен распечатать приветствие, в котором говорится::

greetings from , a HelloMeta type class

Метод, ответственный за эту распечатку, не объявлен в объявлении класса. Скорее, метакласс, который в данном случае является Hello Meta , генерирует код во время выполнения, который автоматически прикрепляет метод к классу.

Чтобы увидеть его в действии, не стесняйтесь копировать и вставлять код в консоль Python. Кроме того, прочтите комментарии, чтобы лучше понять, что мы сделали в каждой части кода. У нас есть новый объект с именем greeter , который является экземпляром класса Try Hello . Однако мы можем вызвать метод Try Hello ‘s self.hello , даже если такой метод не был определен в объявлении класса Try Hello .

Вместо того чтобы получить ошибку при вызове несуществующего метода, Try Hello получает такой метод автоматически прикрепленным к нему из-за использования класса 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 

Как и следовало ожидать, мы получаем распечатку , что переменная day имеет тип str , который является строковым типом. Вы можете найти тип любого объекта, просто используя встроенную функцию type с одним аргументом объекта.

Поиск типа класса

Итак, строка типа "Воскресенье" или "привет" имеет тип str , но как насчет самого str ? Каков тип класса str ?

Снова введите в консоль Python:

>>> type(str)

На этот раз мы получаем распечатку, которая str имеет тип type .

Тип и Тип типа

Но как насчет типа самого себя? Что такое type ‘s 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 объявляет о своем запуске до выполнения вычислений:

>>> multiply(5, 6)
Executing 'multiply'
30
>>>
>>> multiply(89, 5)
Executing 'multiply'
445

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

Метапрограммирование Практика 2: Использование метаклассов как функции декоратора

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

Ниже мы создаем метакласс, который достигает того же результата, что и декоратор из предыдущего примера.

Чтобы сравнить их, вы должны запустить оба примера бок о бок, а затем следовать вместе с аннотированным исходным кодом. Обратите внимание, что вы можете скопировать код и вставить его прямо в свой ОТВЕТ, если ваш ОТВЕТ сохраняет форматирование кода.

# 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 , его методы переписываются для синхронизации нашим служебным классом timer. Всякий раз, когда мы выполняем вычисления с помощью Timed class, мы получаем время, сделанное для нас автоматически, без необходимости делать что-либо дополнительное.

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

Овладение силой Метаклассов

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

Чтобы эффективно использовать метаклассы, мы предлагаем прочитать в официальной Python 3 metaclasses документации.