Descritores Python

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

E você deseja que os atributos first_namee last_namesejam 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 = valueLinguagem de código:  Python  ( python )

Nesta Personclasse, 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] = valueLinguagem de código:  Python  ( python )

Em segundo lugar, use a RequiredStringclasse na Personclasse:

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_nameou last_nameda Personclasse, você receberá um erro.

Por exemplo, as seguintes tentativas de atribuir uma string vazia ao first_nameatributo:

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

Além disso, você pode usar a RequiredStringclasse 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.

  1. Um descritor de dados é um objeto de uma classe que implementa o método __set__e/ou __delete__.
  2. 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 RequiredStringclasse para incluir as printinstruçõ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_namee last_namechama 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_nameLinguagem de código:  texto simples  ( texto simples )

Neste exemplo, o argumento proprietário de __set_name__é definido para a Personclasse no __main__módulo e o nameargumento é definido para o atributo first_nameand last_namede 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 nameargumento ao property_nameatributo de instância do descriptorobjeto para que possamos acessá-lo posteriormente no método __get__and :__set__

self.property_name = nameLinguagem de código:  Python  ( python )

As first_namee last_namesão as variáveis ​​de classe da Personclasse. Se você observar o Person.__dict__atributo class, verá dois objetos descritores first_namee 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 RequiredStringclasse:

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] = valueLinguagem 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=JohnLinguagem de código:  Python  ( python )

Neste exemplo, o instanceargumento é personobject e o valor é string 'John'. Dentro do __set__método, levantamos a ValueErrorse 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_namedo personobjeto:

instance.__dict__[self.property_name] = valueLinguagem de código:  Python  ( python )

Observe que Python usa instance.__dict__dicionário para armazenar atributos de instância do instanceobjeto.

Depois de definir o first_namee last_namede uma instância do Personobjeto, 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 RequiredStringclasse:

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

Python chama o __get__método do Personobjeto quando você acessa o first_nameatributo. 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 instancefor None. Por exemplo, se você acessar first_nameou last_nameda Personclasse, 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 instancenão for None, o __get__()método retorna o valor do atributo com o nome property_namedo instanceobjeto.

Resumo

  • Descritores são objetos de classe que implementam um dos métodos no protocolo descritor, incluindo __set__,,__get____del__

Deixe um comentário

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