Шаг 172.
Python: сборник рецептов. Метапрограммирование. Принуждение к использованию соглашений о кодировании в классах

    На этом шаге мы рассмотрим особенности использования такого приема.

Задача

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

Решение

    Если вы хотите отслеживать определение классов, то часто можете сделать это путем определения метакласса. Базовый метакласс обычно определяется путем наследования от type и переопределения метода __new__() или __init__(). Например:

class MyMeta(type):
    def __new__(self, clsname, bases, clsdict):
        # clsname - имя определенного класса
        # bases - кортеж базового класса
        # clsdict - словарь класса
        return super().__new_(cls, clsname, bases, clsdict)

    Альтернативное решение, если определен __init__():

class MyMeta(type):
    def __init__(self, clsname, bases, clsdict):
        super().__init__(clsname, bases, clsdict)
        # clsname - имя определенного класса
        # bases - кортеж базового класса
        # clsdict - словарь класса

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

class Root(metaclass=MyMeta): 
    pass

class A(Root): 
    pass

class B(Root): 
    pass

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

    В качестве конкретного, хотя и вычурного примера приведем метакласс, который отвергает любые определения классов, содержащие методы с названиями, написанными в смешанных регистрах, т. е. в camelCase (например, чтобы позлить Java-программистов):

class NoMixedCaseMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        for name in clsdict:
            if name.lower() != name:
                raise TypeError('Bad attribute name: ' + name)
        return super().__new__(cls, clsname, bases, clsdict)

class Root(metaclass=NoMixedCaseMeta):
    pass

class A(Root):
    def foo_bar(self): # Ok
        pass

class B(Root):
    def fooBar(self): # TypeError
        pass

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

from inspect import signature 
import logging

class MatchSignaturesMeta(type):
    def __init__(self, clsname, bases, clsdict):
        super().__init__(clsname, bases, clsdict)
        sup = super(self, self)
        for name, value in clsdict.items():
            if name.startswith('_') or not callable(value):
                continue
        # Получить предыдущее определение
        # (если оно есть) и сравнение сигнатур
        prev_dfn = getattr(sup, name, None)
        if prev_dfn:
            prev_sig = signature(prev_dfn)
            val_sig = signature(value)
            if prev_sig != val_sig:
                logging.warning('Signature mismatch in %s. %s != %s',
                                          value.__qualname__, prev_sig, val_sig)

# Пример
class Root(metaclass=MatchSignaturesMeta):
    pass

class A(Root):
    def foo(self, x, y):
         pass

def spam(self, x, *, z):
    pass

# Класс с переопределенными методами,
# но со слегка различными сигнатурами
class B(A):
    def foo(self, a, b):
         pass

def spam(self,x,z):
    pass

    Если вы запустите этот код, то получите такой вывод:

WARNING:root:Signature mismatch in B.spam. (self, x, *, z) != (self, x, z) 
WARNING:root:Signature mismatch in B.foo. (self, x, y) != (self, a, b)

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

Обсуждение

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

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

    Выбор между переопределением __new__() или __init__() в метаклассе зависит от того, как вы хотите работать с получившимся классом. __new__() вызывается до создания класса и обычно используется, если метакласс хочет как-то изменить определение класса (путем изменения содержания словаря класса). Метод __init__() вызывается после создания класса, и он полезен, если вы хотите написать код, который работает с полностью сформированным объектом класса. В последнем примере это необходимо, поскольку он использует функцию super() для поиска вышестоящих в иерархии наследования определений. Это работает, только если объект класса создан и лежащий в основе порядок разрешения методов был установлен.

    Последний пример также иллюстрирует использование объекта сигнатуры функции Python. Метакласс принимает каждое определение вызываемого объекта в классе, ищет предыдущее определение в иерархии (если оно есть), а затем просто сравнивает их сигнатуры вызова, используя inspect.signature().

    И последнее: строка кода, которая использует super(self, self), не является опечаткой. При работе с метаклассом важно понимать, что self - это объект класса. Так что это объявление на самом деле используется для поиска определений, размещенных выше в классовой иерархии, которые создают родителей self.

    На следующем шаге мы рассмотрим программное определение классов .




Предыдущий шаг Содержание Следующий шаг