Exemplo de metaclasse Python

Resumo : neste tutorial, você aprenderá sobre um exemplo de metaclasse Python que cria classes com muitos recursos.

Introdução ao exemplo de metaclasse Python

O seguinte define uma Personclasse com dois atributos namee 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})'Linguagem de código:  Python  ( python )

Normalmente, ao definir uma nova classe, você precisa:

  • Defina uma lista de propriedades do objeto.
  • Defina um __init__método para inicializar os atributos do objeto.
  • Implemente os métodos __str__e __repr__para representar os objetos em formatos legíveis por humanos e por máquinas.
  • Implemente o __eq__método para comparar objetos por valores de todas as propriedades.
  • Implemente o __hash__método para usar os objetos da classe como chaves de um dicionário ou elementos de um conjunto .

Como você pode ver, requer muito código.

Imagine que você deseja definir uma classe Person como esta e possuir automaticamente todas as funções acima:

class Person:
    props = ['first_name', 'last_name', 'age']Linguagem de código:  Python  ( python )

Para fazer isso, você pode usar uma metaclasse.

Defina uma metaclasse

Primeiro, defina a Datametaclasse que herda da typeclasse:

class Data(type):
    passLinguagem de código:  Python  ( python )

Segundo, substitua o __new__método para retornar um novo objeto de classe:

class Data(type):
    def __new__(mcs, name, bases, class_dict):
        class_obj = super().__new__(mcs, name, bases, class_dict)
        return class_objLinguagem de código:  Python  ( python )

Observe que o __new__método é um método estático da Datametaclasse. E você não precisa usar o @staticmethoddecorador porque o Python o trata de maneira especial.

Além disso, o __new__método cria uma nova classe como a Personclasse, não a instância da Personclasse.

Criar objetos de propriedade

Primeiro, defina uma Propclasse que aceite um nome de atributo e contenha três métodos para criar um objeto de propriedade ( set,, gete delete). A Datametaclasse usará esta Propclasse para adicionar objetos de propriedade à classe.

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)Linguagem de código:  Python  ( python )

Segundo, crie um novo método estático define_property()que crie um objeto de propriedade para cada atributo da propslista:

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_objLinguagem de código:  Python  ( python )

O seguinte define a Personclasse que usa a Datametaclasse:

class Person(metaclass=Data):
    props = ['name', 'age']Linguagem de código:  Python  ( python )

A Personclasse tem duas propriedades namee age:

pprint(Person.__dict__)Linguagem de código:  Python  ( python )

Saída:

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']})Linguagem de código:  Python  ( python )

Definir método __init__

O seguinte define um initmétodo estático e o atribui ao __init__atributo do objeto de classe:

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 methodsLinguagem de código:  Python  ( python )

O seguinte cria uma nova instância da Personclasse e inicializa seus atributos:

p = Person('John Doe', age=25)
print(p.__dict__)Linguagem de código:  Python  ( python )

Saída:

{'_age': 25, '_name': 'John Doe'}Linguagem de código:  Python  ( python )

Contém p.__dict__dois atributos _namee _ageé baseado nos nomes predefinidos na propslista.

Definir método __repr__

O seguinte define o reprmétodo estático que retorna uma função e a utiliza para o __repr__atributo do objeto de classe:

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 _reprLinguagem de código:  Python  ( python )

O seguinte cria uma nova instância da Personclasse e a exibe:

p = Person('John Doe', age=25)
print(p)Linguagem de código:  Python  ( python )

Saída:

Person(name=John Doe, age=25)Linguagem de código:  Python  ( python )

Defina os métodos __eq__ e __hash__

O seguinte define os métodos eqe hashe os atribui ao __eq__e __hash__do objeto de classe da metaclasse:

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 _hashLinguagem de código:  Python  ( python )

O seguinte cria duas instâncias de Person e as compara. Se os valores de todas as propriedades forem iguais, elas serão iguais. Caso contrário, eles não serão iguais:

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

print(p1 == p2)  # False

p2.name = 'John Doe'
print(p1 == p2)  # TrueLinguagem de código:  Python  ( python )

Junte tudo

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)  # TrueLinguagem de código:  Python  ( python )

Decorador

O seguinte define uma classe chamada Employeeque usa a Datametaclasse:

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


if __name__ == '__main__':
    e = Employee(name='John Doe', job_title='Python Developer')
    print(e)Linguagem de código:  Python  ( python )

Saída:

Employee(name=John Doe, job_title=Python Developer)Linguagem de código:  Python  ( python )

Funciona como esperado. No entanto, especificar a metaclasse é bastante detalhado. Para melhorar isso, você pode usar um decorador de funções .

Primeiro, defina um decorador de função que retorne uma nova classe que é uma instância da Datametaclasse:

def data(cls):
    return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))Linguagem de código:  Python  ( python )

Segundo, use o @datadecorador para qualquer classe que use the Datacomo metaclasse:

@data
class Employee:
    props = ['name', 'job_title']Linguagem de código:  Python  ( python )

O seguinte mostra o código completo:

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']Linguagem de código:  Python  ( python )

Python 3.7 forneceu um @dataclassdecorador especificado no PEP 557 que possui alguns recursos como a Datametaclasse. Além disso, a classe de dados oferece mais recursos que ajudam você a economizar tempo ao trabalhar com classes.

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *