Метаклассы в Python — как работать на примерах

В этом руководстве мы рассмотрим пример метакласса Python, который создает классы с множеством функций.

Содержание

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

Ниже определяется класс Person с двумя атрибутами name и age:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        self._age = value

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __hash__(self):
        return hash(f'{self.name, self.age}')

    def __str__(self):
        return f'Person(name={self.name},age={self.age})'

    def __repr__(self):
        return f'Person(name={self.name},age={self.age})'

Обычно при определении нового класса вам необходимо:

  • Определить список свойств объекта.
  • Определить метод __init__ для инициализации атрибутов объекта.
  • Реализовать методы __str__ и __repr__ для представления объектов в удобочитаемом для человека и машиночитаемом форматах.
  • Метод __eq__ для сравнения объектов по значениям всех свойств.
  • Метод __hash__, чтобы использовать объекты класса в качестве ключей словаря или элементов набора.

Как видите, для этого требуется много кода.

Представьте, что вы хотите определить такой класс Person, который автоматически будет иметь все вышеперечисленные функции:

class Person:
    props = ['first_name', 'last_name', 'age']

Для этого вы и можете использовать метакласс.

Определение метакласса

  • Сначала определите метакласс Data, который наследуется от класса type:
class Data(type):
    pass
  • Во-вторых, переопределите метод __new__, чтобы он возвращал новый объект класса:
class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)
        return class_obj

Обратите внимание, что метод __new__ является статическим методом метакласса Data. И вам не нужно использовать декоратор @staticmethod, потому что Python обрабатывает его по-особенному.

Кроме того, метод __new__ создает новый класс, такой как класс Person, а не экземпляр класса Person.

Создание объектов свойств

Сначала определите класс Prop, который принимает имя атрибута и содержит три метода для создания объекта свойства (set, get и delete). Метакласс Data будет использовать этот класс Prop для добавления объектов свойств в класс.

class Prop:
    def __init__(self, attr):
        self._attr = attr

    def get(self, obj):
        return getattr(obj, self._attr)

    def set(self, obj, value):
        return setattr(obj, self._attr, value)

    def delete(self, obj):
        return delattr(obj, self._attr)

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

class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)
        Data.define_property(class_obj)

        return class_obj

    @staticmethod
    def define_property(class_obj):
        for prop in class_obj.props:
            attr = f'_{prop}'
            prop_obj = property(
                fget=Prop(attr).get,
                fset=Prop(attr).set,
                fdel=Prop(attr).delete
            )
            setattr(class_obj, prop, prop_obj)

        return class_obj

Ниже определяется класс Person, который использует метакласс Data:

class Person(metaclass=Data):
    props = ['name', 'age']

Класс Person имеет два свойства: name и age:

pprint(Person.__dict__)

Выход:

mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'Person' objects>,
              'age': <property object at 0x000002213CA92090>,
              'name': <property object at 0x000002213C772A90>,
              'props': ['name', 'age']})

Определить метод __init__

Следующий код определяет статический метод init и присваивает его атрибуту __init__ объекта класса:

class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)

        # create property
        Data.define_property(class_obj)

        # define __init__
        setattr(class_obj, '__init__', Data.init(class_obj))

        return class_obj

    @staticmethod
    def init(class_obj):
        def _init(self, *obj_args, **obj_kwargs):
            if obj_kwargs:
                for prop in class_obj.props:
                    if prop in obj_kwargs.keys():
                        setattr(self, prop, obj_kwargs[prop])

            if obj_args:
                for kv in zip(class_obj.props, obj_args):
                    setattr(self, kv[0], kv[1])

        return _init

    # more methods

Здесь создается новый экземпляр класса Person и инициализирует его атрибуты:

p = Person('John Doe', age=25)
print(p.__dict__)

Выход:

{'_age': 25, '_name': 'John Doe'}

p.__dict__ содержит два атрибута _name и _age, основанные на предопределенных именах в списке реквизитов.

Определить метод __repr__

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

class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)

        # create property
        Data.define_property(class_obj)

        # define __init__
        setattr(class_obj, '__init__', Data.init(class_obj))

        # define __repr__
        setattr(class_obj, '__repr__', Data.repr(class_obj))

        return class_obj

    @staticmethod
    def repr(class_obj):
        def _repr(self):
            prop_values =(getattr(self, prop) for prop in class_obj.props)
            prop_key_values =(f'{key}={value}' for key, value in zip(class_obj.props, prop_values))
            prop_key_values_str = ', '.join(prop_key_values)
            return f'{class_obj.__name__}({prop_key_values_str})'

        return _repr

Следующий  код создает новый экземпляр класса Person и отображает его:

p = Person('John Doe', age=25)
print(p)

Выход:

Person(name=John Doe, age=25)

Определение методов __eq__ и __hash__

Определяем методы eq и hash и присваиваем их __eq__ и __hash__ объекта класса метакласса:

class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)

        # create property
        Data.define_property(class_obj)

        # define __init__
        setattr(class_obj, '__init__', Data.init(class_obj))

        # define __repr__
        setattr(class_obj, '__repr__', Data.repr(class_obj))

        # define __eq__ & __hash__
        setattr(class_obj, '__eq__', Data.eq(class_obj))
        setattr(class_obj, '__hash__', Data.hash(class_obj))

        return class_obj

    @staticmethod
    def eq(class_obj):
        def _eq(self, other):
            if not isinstance(other, class_obj):
                return False

            self_values = [getattr(self, prop) for prop in class_obj.props]
            other_values = [getattr(other, prop) for prop in other.props]

            return self_values == other_values

        return _eq

    @staticmethod
    def hash(class_obj):
        def _hash(self):
            values =(getattr(self, prop) for prop in class_obj.props)
            return hash(tuple(values))

        return _hash

Следующее создает два экземпляра Person и сравнивает их. Если значения всех свойств одинаковы, они будут равны. В противном случае — не равны:

p1 = Person('John Doe', age=25)
p2 = Person('Jane Doe', age=25)

print(p1 == p2)  # False

p2.name = 'John Doe'
print(p1 == p2)  # True

Пример полного кода

from pprint import pprint


class Prop:
    def __init__(self, attr):
        self._attr = attr

    def get(self, obj):
        return getattr(obj, self._attr)

    def set(self, obj, value):
        return setattr(obj, self._attr, value)

    def delete(self, obj):
        return delattr(obj, self._attr)


class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)

        # create property
        Data.define_property(class_obj)

        # define __init__
        setattr(class_obj, '__init__', Data.init(class_obj))

        # define __repr__
        setattr(class_obj, '__repr__', Data.repr(class_obj))

        # define __eq__ & __hash__
        setattr(class_obj, '__eq__', Data.eq(class_obj))
        setattr(class_obj, '__hash__', Data.hash(class_obj))

        return class_obj

    @staticmethod
    def eq(class_obj):
        def _eq(self, other):
            if not isinstance(other, class_obj):
                return False

            self_values = [getattr(self, prop) for prop in class_obj.props]
            other_values = [getattr(other, prop) for prop in other.props]

            return self_values == other_values

        return _eq

    @staticmethod
    def hash(class_obj):
        def _hash(self):
            values =(getattr(self, prop) for prop in class_obj.props)
            return hash(tuple(values))

        return _hash

    @staticmethod
    def repr(class_obj):
        def _repr(self):
            prop_values =(getattr(self, prop) for prop in class_obj.props)
            prop_key_values =(f'{key}={value}' for key, value in zip(class_obj.props, prop_values))
            prop_key_values_str = ', '.join(prop_key_values)
            return f'{class_obj.__name__}({prop_key_values_str})'

        return _repr

    @staticmethod
    def init(class_obj):
        def _init(self, *obj_args, **obj_kwargs):
            if obj_kwargs:
                for prop in class_obj.props:
                    if prop in obj_kwargs.keys():
                        setattr(self, prop, obj_kwargs[prop])

            if obj_args:
                for kv in zip(class_obj.props, obj_args):
                    setattr(self, kv[0], kv[1])

        return _init

    @staticmethod
    def define_property(class_obj):
        for prop in class_obj.props:
            attr = f'_{prop}'
            prop_obj = property(
                fget=Prop(attr).get,
                fset=Prop(attr).set,
                fdel=Prop(attr).delete
            )
            setattr(class_obj, prop, prop_obj)

        return class_obj


class Person(metaclass=Data):
    props = ['name', 'age']


if __name__ == '__main__':
    pprint(Person.__dict__)

    p1 = Person('John Doe', age=25)
    p2 = Person('Jane Doe', age=25)

    print(p1 == p2)  # False

    p2.name = 'John Doe'
    print(p1 == p2)  # True

Декоратор

Ниже определяется класс Employee, который использует метакласс Data:

class Employee(metaclass=Data):
    props = ['name', 'job_title']


if __name__ == '__main__':
    e = Employee(name='John Doe', job_title='Python Developer')
    print(e)

Выход:

Employee(name=John Doe, job_title=Python Developer)

Код работает так, как ожидалось. Однако указание метакласса довольно подробное. Чтобы улучшить это, вы можете использовать декоратор функции.

Сначала определите декоратор функции, который возвращает новый класс, который является экземпляром метакласса Data:

def data(cls):
    return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))

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

@data
class Employee:
    props = ['name', 'job_title']

Ниже показан полный код:

class Prop:
    def __init__(self, attr):
        self._attr = attr

    def get(self, obj):
        return getattr(obj, self._attr)

    def set(self, obj, value):
        return setattr(obj, self._attr, value)

    def delete(self, obj):
        return delattr(obj, self._attr)


class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)

        # create property
        Data.define_property(class_obj)

        # define __init__
        setattr(class_obj, '__init__', Data.init(class_obj))

        # define __repr__
        setattr(class_obj, '__repr__', Data.repr(class_obj))

        # define __eq__ & __hash__
        setattr(class_obj, '__eq__', Data.eq(class_obj))
        setattr(class_obj, '__hash__', Data.hash(class_obj))

        return class_obj

    @staticmethod
    def eq(class_obj):
        def _eq(self, other):
            if not isinstance(other, class_obj):
                return False

            self_values = [getattr(self, prop) for prop in class_obj.props]
            other_values = [getattr(other, prop) for prop in other.props]

            return self_values == other_values

        return _eq

    @staticmethod
    def hash(class_obj):
        def _hash(self):
            values =(getattr(self, prop) for prop in class_obj.props)
            return hash(tuple(values))

        return _hash

    @staticmethod
    def repr(class_obj):
        def _repr(self):
            prop_values =(getattr(self, prop) for prop in class_obj.props)
            prop_key_values =(f'{key}={value}' for key, value in zip(class_obj.props, prop_values))
            prop_key_values_str = ', '.join(prop_key_values)
            return f'{class_obj.__name__}({prop_key_values_str})'

        return _repr

    @staticmethod
    def init(class_obj):
        def _init(self, *obj_args, **obj_kwargs):
            if obj_kwargs:
                for prop in class_obj.props:
                    if prop in obj_kwargs.keys():
                        setattr(self, prop, obj_kwargs[prop])

            if obj_args:
                for kv in zip(class_obj.props, obj_args):
                    setattr(self, kv[0], kv[1])

        return _init

    @staticmethod
    def define_property(class_obj):
        for prop in class_obj.props:
            attr = f'_{prop}'
            prop_obj = property(
                fget=Prop(attr).get,
                fset=Prop(attr).set,
                fdel=Prop(attr).delete
            )
            setattr(class_obj, prop, prop_obj)

        return class_obj


class Person(metaclass=Data):
    props = ['name', 'age']


def data(cls):
    return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))


@data
class Employee:
    props = ['name', 'job_title']

В Python 3.7 предусмотрен декоратор @dataclass, указанный в PEP 557, который имеет некоторые функции, такие как метакласс Data. Кроме того, класс Data предлагает больше возможностей, которые помогут вам сэкономить время при работе с классами.

Похожие посты
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *