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 Person
classe com dois atributos name
e 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 Data
metaclasse que herda da type
classe:
class Data(type):
pass
Linguagem 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_obj
Linguagem de código: Python ( python )
Observe que o __new__
método é um método estático da Data
metaclasse. E você não precisa usar o @staticmethod
decorador porque o Python o trata de maneira especial.
Além disso, o __new__
método cria uma nova classe como a Person
classe, não a instância da Person
classe.
Criar objetos de propriedade
Primeiro, defina uma Prop
classe que aceite um nome de atributo e contenha três métodos para criar um objeto de propriedade ( set
,, get
e delete
). A Data
metaclasse usará esta Prop
classe 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 props
lista:
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
Linguagem de código: Python ( python )
O seguinte define a Person
classe que usa a Data
metaclasse:
class Person(metaclass=Data):
props = ['name', 'age']
Linguagem de código: Python ( python )
A Person
classe tem duas propriedades name
e 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 init
mé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 methods
Linguagem de código: Python ( python )
O seguinte cria uma nova instância da Person
classe 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 _name
e _age
é baseado nos nomes predefinidos na props
lista.
Definir método __repr__
O seguinte define o repr
mé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 _repr
Linguagem de código: Python ( python )
O seguinte cria uma nova instância da Person
classe 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 eq
e hash
e 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 _hash
Linguagem 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) # True
Linguagem 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) # True
Linguagem de código: Python ( python )
Decorador
O seguinte define uma classe chamada Employee
que usa a Data
metaclasse:
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 Data
metaclasse:
def data(cls):
return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))
Linguagem de código: Python ( python )
Segundo, use o @data
decorador para qualquer classe que use the Data
como 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 @dataclass
decorador especificado no PEP 557 que possui alguns recursos como a Data
metaclasse. Além disso, a classe de dados oferece mais recursos que ajudam você a economizar tempo ao trabalhar com classes.