Метаклассы в Python — как работать на примерах
В этом руководстве мы рассмотрим пример метакласса Python, который создает классы с множеством функций.
- Что такое метакласс в Python?
- Определение метакласса
- Создание объектов свойств
- Определить метод __init__
- Определить метод __repr__
- Определение методов __eq__ и __hash__
- Пример полного кода
- Декоратор
Что такое метакласс в 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 предлагает больше возможностей, которые помогут вам сэкономить время при работе с классами.