Resumo : neste tutorial, você aprenderá sobre os descritores Python, como funcionam os descritores e como aplicá-los de forma mais eficaz.
Introdução aos descritores Python
Suponha que você tenha uma classe Person
com dois atributos de instância first_name
e last_name
:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
Linguagem de código: Python ( python )
E você deseja que os atributos first_name
e last_name
sejam strings não vazias. Esses atributos simples não podem garantir isso.
Para impor a validade dos dados, você pode usar a propriedade com métodos getter e setter, como este:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise ValueError('The first name must a string')
if len(value) == 0:
raise ValueError('The first name cannot be empty')
self._first_name = value
@property
def last_name(self):
return self._last_name
@last_name.setter
def last_name(self, value):
if not isinstance(value, str):
raise ValueError('The last name must a string')
if len(value) == 0:
raise ValueError('The last name cannot be empty')
self._last_name = value
Linguagem de código: Python ( python )
Nesta Person
classe, o getter retorna o valor do atributo enquanto o setter o valida antes de atribuí-lo ao atributo.
Este código funciona perfeitamente bem. No entanto, é redundante porque a lógica de validação valida que o nome e o sobrenome são iguais.
Além disso, se a classe tiver mais atributos que exijam uma string não vazia, será necessário duplicar essa lógica em outras propriedades. Em outras palavras, esta lógica de validação não é reutilizável.
Para evitar a duplicação da lógica, você pode ter um método que valide os dados e reutilize esse método em outras propriedades. Esta abordagem permitirá a reutilização. No entanto, Python tem uma maneira melhor de resolver isso usando descritores.
Primeiro, defina uma classe descritora que implemente três métodos __set_name__
, __get__
e __set__
:
class RequiredString:
def __set_name__(self, owner, name):
self.property_name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.property_name] or None
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError(f'The {self.property_name} must be a string')
if len(value) == 0:
raise ValueError(f'The {self.property_name} cannot be empty')
instance.__dict__[self.property_name] = value
Linguagem de código: Python ( python )
Em segundo lugar, use a RequiredString
classe na Person
classe:
class Person:
first_name = RequiredString()
last_name = RequiredString()
Linguagem de código: Python ( python )
Se você atribuir uma string vazia ou um valor sem string ao atributo first_name
ou last_name
da Person
classe, você receberá um erro.
Por exemplo, as seguintes tentativas de atribuir uma string vazia ao first_name
atributo:
try:
person = Person()
person.first_name = ''
except ValueError as e:
print(e)
Linguagem de código: Python ( python )
Erro:
The first_name must be a string
Linguagem de código: Python ( python )
Além disso, você pode usar a RequiredString
classe em qualquer classe com atributos que exijam um valor de string não vazio.
Além de RequiredString
, você pode definir outros descritores para impor outros tipos de dados, como idade, email e telefone. E esta é apenas uma aplicação simples dos descritores.
Vamos entender como funcionam os descritores.
Protocolo descritor
Em Python, o protocolo descritor consiste em três métodos:
__get__
obtém um valor de atributo__set__
define um valor de atributo__delete__
exclui um atributo
Opcionalmente, um descritor pode ter o __set_name__
método que define um atributo em uma instância de uma classe para um novo valor.
O que é um descritor
Um descritor é um objeto de uma classe que implementa um dos métodos especificados no protocolo descritor.
Os descritores têm dois tipos: descritor de dados e descritor de não dados.
- Um descritor de dados é um objeto de uma classe que implementa o método
__set__
e/ou__delete__
. - Um descritor que não é de dados é um objeto que implementa
__get__
apenas o método.
O tipo de descritor especifica a resolução de pesquisa da propriedade que abordaremos no próximo tutorial .
Como funcionam os descritores
O seguinte modifica a RequiredString
classe para incluir as print
instruções que imprimem os argumentos.
class RequiredString:
def __set_name__(self, owner, name):
print(f'__set_name__ was called with owner={owner} and name={name}')
self.property_name = name
def __get__(self, instance, owner):
print(f'__get__ was called with instance={instance} and owner={owner}')
if instance is None:
return self
return instance.__dict__[self.property_name] or None
def __set__(self, instance, value):
print(f'__set__ was called with instance={instance} and value={value}')
if not isinstance(value, str):
raise ValueError(f'The {self.property_name} must a string')
if len(value) == 0:
raise ValueError(f'The {self.property_name} cannot be empty')
instance.__dict__[self.property_name] = value
class Person:
first_name = RequiredString()
last_name = RequiredString()
Linguagem de código: Python ( python )
O __set_name__
método
Ao compilar o código, você verá que o Python cria os objetos descritores first_name
e last_name
chama automaticamente o __set_name__
método desses objetos. Aqui está o resultado:
__set_name__ was called with owner=<class '__main__.Person'> and name=first_name
__set_name__ was called with owner=<class '__main__.Person'> and name=last_name
Linguagem de código: texto simples ( texto simples )
Neste exemplo, o argumento proprietário de __set_name__
é definido para a Person
classe no __main__
módulo e o name
argumento é definido para o atributo first_name
and last_name
de acordo.
Isso significa que o Python chama automaticamente __set_name__
quando a classe proprietária Person
é criada. As seguintes declarações são equivalentes:
first_name = RequiredString()
Linguagem de código: Python ( python )
e
first_name.__set_name__(Person, 'first_name')
Linguagem de código: Python ( python )
Dentro do __set_name__
método, atribuímos o name
argumento ao property_name
atributo de instância do descriptor
objeto para que possamos acessá-lo posteriormente no método __get__
and :__set__
self.property_name = name
Linguagem de código: Python ( python )
As first_name
e last_name
são as variáveis de classe da Person
classe. Se você observar o Person.__dict__
atributo class, verá dois objetos descritores first_name
e last_name
:
from pprint import pprint
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>,
'first_name': <__main__.RequiredString object at 0x0000019D6AB947F0>,
'last_name': <__main__.RequiredString object at 0x0000019D6ACFBE80>})
Linguagem de código: Python ( python )
O __set__
método
Aqui está o __set__
método da RequiredString
classe:
def __set__(self, instance, value):
print(f'__set__ was called with instance={instance} and value={value}')
if not isinstance(value, str):
raise ValueError(f'The {self.property_name} must be a string')
if len(value) == 0:
raise ValueError(f'The {self.property_name} cannot be empty')
instance.__dict__[self.property_name] = value
Linguagem de código: Python ( python )
Quando você atribui o novo valor a um descritor, Python chama o __set__
método para definir o atributo em uma instância da classe proprietária para o novo valor. Por exemplo:
person = Person()
person.first_name = 'John'
Linguagem de código: Python ( python )
Saída:
__set__ was called with instance=<__main__.Person object at 0x000001F85F7167F0> and value=John
Linguagem de código: Python ( python )
Neste exemplo, o instance
argumento é person
object e o valor é string 'John'
. Dentro do __set__
método, levantamos a ValueError
se o novo valor não for uma string ou se for uma string vazia.
Caso contrário, atribuímos o valor ao atributo de instância first_name
do person
objeto:
instance.__dict__[self.property_name] = value
Linguagem de código: Python ( python )
Observe que Python usa instance.__dict__
dicionário para armazenar atributos de instância do instance
objeto.
Depois de definir o first_name
e last_name
de uma instância do Person
objeto, você verá os atributos da instância com os mesmos nomes no arquivo __dict__
. Por exemplo:
person = Person()
print(person.__dict__) # {}
person.first_name = 'John'
person.last_name = 'Doe'
print(person.__dict__) # {'first_name': 'John', 'last_name': 'Doe'}
Linguagem de código: Python ( python )
Saída:
{}
{'first_name': 'John', 'last_name': 'Doe'}
Linguagem de código: texto simples ( texto simples )
O __get__
método
O seguinte mostra o __get__
método da RequiredString
classe:
def __get__(self, instance, owner):
print(f'__get__ was called with instance={instance} and owner={owner}')
if instance is None:
return self
return instance.__dict__[self.property_name] or None
Linguagem de código: Python ( python )
Python chama o __get__
método do Person
objeto quando você acessa o first_name
atributo. Por exemplo:
person = Person()
person.first_name = 'John'
print(person.first_name)
Linguagem de código: Python ( python )
Saída:
__set__ was called with instance=<__main__.Person object at 0x000001F85F7167F0> and value=John
__get__ was called with instance=<__main__.Person object at 0x000001F85F7167F0> and owner=<class '__main__.Person'>
Linguagem de código: texto simples ( texto simples )
O __get__
método retorna o descritor se instance
for None
. Por exemplo, se você acessar first_name
ou last_name
da Person
classe, verá o objeto descritor:
print(Person.first_name)
Linguagem de código: Python ( python )
Saída:
<__main__.RequiredString object at 0x000001AF1DA147F0>
Linguagem de código: texto simples ( texto simples )
Se instance
não for None
, o __get__()
método retorna o valor do atributo com o nome property_name
do instance
objeto.
Resumo
- Descritores são objetos de classe que implementam um dos métodos no protocolo descritor, incluindo
__set__
,,__get__
__del__