Aprenda Sobre Decoradores de Classe

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Neste laboratório, você aprenderá sobre decoradores de classe em Python e revisitará e estenderá descritores Python. Ao combinar esses conceitos, você pode criar estruturas de código poderosas e limpas.

Neste laboratório, você se baseará em conceitos de descritores anteriores e os estenderá usando decoradores de classe. Essa combinação permite que você crie código mais limpo, mais fácil de manter e com capacidades de validação aprimoradas. Os arquivos a serem modificados são validate.py e structure.py.

Implementando Verificação de Tipo com Descritores

Nesta etapa, vamos criar uma classe Stock que utiliza descritores para verificação de tipo. Mas primeiro, vamos entender o que são descritores. Descritores são um recurso realmente poderoso em Python. Eles lhe dão controle sobre como os atributos são acessados nas classes.

Descritores são objetos que definem como os atributos são acessados em outros objetos. Eles fazem isso implementando métodos especiais como __get__, __set__ e __delete__. Esses métodos permitem que os descritores gerenciem como os atributos são recuperados, definidos e excluídos. Descritores são muito úteis para implementar validação, verificação de tipo e propriedades computadas. Por exemplo, você pode usar um descritor para garantir que um atributo seja sempre um número positivo ou uma string de um determinado formato.

O arquivo validate.py já possui classes validadoras (String, PositiveInteger, PositiveFloat). Podemos usar essas classes para validar os atributos da nossa classe Stock.

Agora, vamos criar nossa classe Stock com descritores.

  1. Primeiro, abra o arquivo stock.py no seu editor.

  2. Assim que o arquivo estiver aberto, substitua o conteúdo placeholder pelo seguinte código:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    _fields = ('name', 'shares', 'price')
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

## Create an __init__ method based on _fields
Stock.create_init()

Vamos analisar o que este código faz. A tupla _fields define os atributos da classe Stock. Estes são os nomes dos atributos que nossos objetos Stock terão.

Os atributos name, shares e price são definidos como objetos descritores. O descritor String() garante que o atributo name seja uma string. O descritor PositiveInteger() garante que o atributo shares seja um inteiro positivo. E o descritor PositiveFloat() garante que o atributo price seja um número de ponto flutuante positivo.

A propriedade cost é uma propriedade computada. Ela calcula o custo total do estoque com base no número de ações e no preço por ação.

O método sell é usado para reduzir o número de ações. Quando você chama este método com um número de ações para vender, ele subtrai esse número do atributo shares.

A linha Stock.create_init() cria dinamicamente um método __init__ para nossa classe. Este método nos permite criar objetos Stock passando os valores para os atributos name, shares e price.

  1. Após adicionar o código, salve o arquivo. Isso garantirá que suas alterações sejam salvas e possam ser usadas ao executar os testes.

  2. Agora, vamos executar os testes para verificar sua implementação. Primeiro, mude o diretório para o diretório ~/project executando o seguinte comando:

cd ~/project

Em seguida, execute os testes usando o seguinte comando:

python3 teststock.py

Se sua implementação estiver correta, você deverá ver uma saída semelhante a esta:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Esta saída significa que todos os testes estão passando. Os descritores estão validando com sucesso os tipos de cada atributo!

Vamos tentar criar um objeto Stock no interpretador Python. Primeiro, certifique-se de estar no diretório ~/project. Em seguida, execute o seguinte comando:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Você deverá ver a seguinte saída:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Você implementou com sucesso descritores para verificação de tipo! Agora, vamos melhorar ainda mais este código.

Criando um Decorador de Classe para Validação

Na etapa anterior, nossa implementação funcionou, mas havia uma redundância. Tivemos que especificar tanto a tupla _fields quanto os atributos descritores. Isso não é muito eficiente e podemos melhorar. Em Python, decoradores de classe são uma ferramenta poderosa que pode nos ajudar a simplificar esse processo. Um decorador de classe é uma função que recebe uma classe como argumento, a modifica de alguma forma e, em seguida, retorna a classe modificada. Ao usar um decorador de classe, podemos extrair automaticamente informações de campo dos descritores, o que tornará nosso código mais limpo e fácil de manter.

Vamos criar um decorador de classe para simplificar nosso código. Aqui estão os passos que você precisa seguir:

  1. Primeiro, abra o arquivo structure.py no seu editor.

  2. Em seguida, adicione o seguinte código no topo do arquivo structure.py, logo após quaisquer instruções de importação. Este código define nosso decorador de classe:

from validate import Validator

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields list automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

Vamos analisar o que este decorador faz:

  • Ele primeiro cria uma lista vazia chamada validators. Em seguida, itera sobre todos os atributos da classe usando vars(cls).items(). Se um atributo for uma instância da classe Validator, ele adiciona esse atributo à lista validators.
  • Depois disso, ele define o atributo _fields da classe. Ele cria uma lista de nomes a partir dos validadores na lista validators e a atribui a cls._fields.
  • Finalmente, ele chama o método create_init() da classe para gerar o método __init__ e, em seguida, retorna a classe modificada.
  1. Assim que você adicionar o código, salve o arquivo structure.py. Salvar o arquivo garante que suas alterações sejam preservadas.

  2. Agora, precisamos modificar nosso arquivo stock.py para usar este novo decorador. Abra o arquivo stock.py no seu editor.

  3. Atualize o arquivo stock.py para usar o decorador validate_attributes. Substitua o código existente pelo seguinte:

## stock.py

from structure import Structure, validate_attributes
from validate import String, PositiveInteger, PositiveFloat

@validate_attributes
class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Observe as mudanças que fizemos:

  • Adicionamos o decorador @validate_attributes logo acima da definição da classe Stock. Isso informa ao Python para aplicar o decorador validate_attributes à classe Stock.
  • Removemos a declaração explícita de _fields porque o decorador cuidará disso automaticamente.
  • Também removemos a chamada para Stock.create_init() porque o decorador cuida da criação do método __init__.

Como resultado, a classe agora está mais simples e limpa. O decorador cuida de todos os detalhes que costumávamos lidar manualmente.

  1. Após fazer essas alterações, precisamos verificar se tudo ainda funciona como esperado. Execute os testes novamente usando os seguintes comandos:
cd ~/project
python3 teststock.py

Se tudo estiver funcionando corretamente, você deverá ver a seguinte saída:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Esta saída indica que todos os testes foram concluídos com sucesso.

Vamos também testar nossa classe Stock interativamente. Execute o seguinte comando no terminal:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Você deverá ver a seguinte saída:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Ótimo! Você implementou com sucesso um decorador de classe que simplifica nosso código, cuidando automaticamente das declarações de campo e da inicialização. Isso torna nosso código mais eficiente e fácil de manter.

Aplicando Decoradores via Herança

Na Etapa 2, criamos um decorador de classe que simplifica nosso código. Um decorador de classe é um tipo especial de função que recebe uma classe como argumento e retorna uma classe modificada. É uma ferramenta útil em Python para adicionar funcionalidade às classes sem modificar seu código original. No entanto, ainda precisamos aplicar explicitamente o decorador @validate_attributes a cada classe. Isso significa que toda vez que criarmos uma nova classe que precise de validação, teremos que nos lembrar de adicionar este decorador, o que pode ser um pouco complicado.

Podemos melhorar isso ainda mais aplicando o decorador automaticamente através da herança. Herança é um conceito fundamental em programação orientada a objetos, onde uma subclasse pode herdar atributos e métodos de uma classe pai. O método __init_subclass__ do Python foi introduzido no Python 3.6 para permitir que as classes pai personalizem a inicialização das subclasses. Isso significa que, quando uma subclasse é criada, a classe pai pode realizar algumas ações sobre ela. Podemos usar esse recurso para aplicar automaticamente nosso decorador a qualquer classe que herde de Structure.

Vamos implementar isso:

  1. Abra o arquivo structure.py no seu editor. Este arquivo contém a definição da classe Structure, e vamos modificá-lo para usar o método __init_subclass__.

  2. Adicione o método __init_subclass__ à classe Structure:

class Structure:
    _fields = ()
    _types = ()

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f'{type(self).__name__}({values})'

    @classmethod
    def create_init(cls):
        '''
        Create an __init__ method from _fields
        '''
        body = 'def __init__(self, %s):\n' % ', '.join(cls._fields)
        for name in cls._fields:
            body += f'    self.{name} = {name}\n'

        ## Execute the function creation code
        namespace = {}
        exec(body, namespace)
        setattr(cls, '__init__', namespace['__init__'])

    @classmethod
    def __init_subclass__(cls):
        validate_attributes(cls)

O método __init_subclass__ é um método de classe, o que significa que ele pode ser chamado na própria classe, em vez de em uma instância da classe. Quando uma subclasse de Structure é criada, este método será chamado automaticamente. Dentro deste método, chamamos o decorador validate_attributes na subclasse cls. Dessa forma, toda subclasse de Structure terá automaticamente o comportamento de validação.

  1. Salve o arquivo.

Após fazer alterações no arquivo structure.py, precisamos salvá-lo para que as alterações sejam aplicadas.

  1. Agora, vamos atualizar nosso arquivo stock.py para aproveitar este novo recurso. Abra o arquivo stock.py no seu editor para modificá-lo. Este arquivo contém a definição da classe Stock, e vamos fazê-la herdar da classe Structure para usar a aplicação automática do decorador.

  2. Modifique o arquivo stock.py para remover o decorador explícito:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Note que nós:

  • Removemos a importação validate_attributes porque não precisamos mais importá-la explicitamente, já que o decorador é aplicado automaticamente através da herança.
  • Removemos o decorador @validate_attributes porque o método __init_subclass__ na classe Structure cuidará de aplicá-lo.
  • O código agora depende apenas da herança de Structure para obter o comportamento de validação.
  1. Execute os testes novamente para verificar se tudo ainda funciona:
cd ~/project
python3 teststock.py

Executar os testes é importante para garantir que nossas alterações não quebraram nada. Se todos os testes passarem, significa que a aplicação automática do decorador através da herança está funcionando corretamente.

Você deverá ver todos os testes passando:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Vamos testar nossa classe Stock novamente para garantir que ela funcione como esperado:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Este comando cria uma instância da classe Stock e imprime sua representação e o custo. Se a saída for a esperada, significa que a classe Stock está funcionando corretamente com a aplicação automática do decorador.

Saída:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Esta implementação é ainda mais limpa! Ao usar __init_subclass__, eliminamos a necessidade de aplicar decoradores explicitamente. Qualquer classe que herde de Structure obtém automaticamente o comportamento de validação.

Adicionando Funcionalidade de Conversão de Linha

Em programação, é frequentemente útil criar instâncias de uma classe a partir de linhas de dados, especialmente ao lidar com dados de fontes como arquivos CSV. Nesta seção, adicionaremos a capacidade de criar instâncias da classe Structure a partir de linhas de dados. Faremos isso implementando um método de classe from_row na classe Structure.

  1. Primeiro, abra o arquivo structure.py no seu editor. É aqui que faremos as alterações no código.

  2. Em seguida, modificaremos a função validate_attributes. Esta função é um decorador de classe que extrai instâncias de Validator e constrói as listas _fields e _types automaticamente. Vamos atualizá-la para coletar também informações de tipo.

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields and _types lists automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

Nesta função atualizada, estamos coletando o atributo expected_type de cada validador e armazenando-o na variável de classe _types. Isso será útil mais tarde, quando convertermos dados de linhas para os tipos corretos.

  1. Agora, adicionaremos o método de classe from_row à classe Structure. Este método nos permitirá criar uma instância da classe a partir de uma linha de dados, que pode ser uma lista ou uma tupla.
@classmethod
def from_row(cls, row):
    """
    Create an instance from a data row (list or tuple)
    """
    rowdata = [func(val) for func, val in zip(cls._types, row)]
    return cls(*rowdata)

Veja como este método funciona:

  • Ele recebe uma linha de dados, que pode estar na forma de uma lista ou tupla.
  • Ele converte cada valor na linha para o tipo esperado usando a função correspondente da lista _types.
  • Em seguida, ele cria e retorna uma nova instância da classe usando os valores convertidos.
  1. Após fazer essas alterações, salve o arquivo structure.py. Isso garante que as alterações do seu código sejam preservadas.

  2. Vamos testar nosso método from_row para garantir que ele funcione como esperado. Criaremos um teste simples usando a classe Stock. Execute o seguinte comando no seu terminal:

cd ~/project
python3 -c "from stock import Stock; s = Stock.from_row(['GOOG', '100', '490.1']); print(s); print(f'Cost: {s.cost}')"

Você deverá ver uma saída semelhante a esta:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Observe que os valores de string '100' e '490.1' foram automaticamente convertidos para os tipos corretos (inteiro e float). Isso mostra que nosso método from_row está funcionando corretamente.

  1. Finalmente, vamos tentar ler dados de um arquivo CSV usando nosso módulo reader.py. Execute o seguinte comando no seu terminal:
cd ~/project
python3 -c "from stock import Stock; import reader; portfolio = reader.read_csv_as_instances('portfolio.csv', Stock); print(portfolio); print(f'Total value: {sum(s.cost for s in portfolio)}')"

Você deverá ver uma saída mostrando as ações do arquivo CSV:

[Stock('GOOG', 100, 490.1), Stock('AAPL', 50, 545.75), Stock('MSFT', 200, 30.47)]
Total value: 82391.5

O método from_row nos permite converter facilmente dados CSV em instâncias da classe Stock. Quando combinado com a função read_csv_as_instances, temos uma maneira poderosa de carregar e trabalhar com dados estruturados.

Adicionando Validação de Argumentos de Método

Em Python, validar dados é uma parte importante da escrita de código robusto. Nesta seção, levaremos nossa validação um passo adiante, validando automaticamente os argumentos de métodos. O arquivo validate.py já inclui um decorador @validated. Um decorador em Python é uma função especial que pode modificar outra função. O decorador @validated aqui pode verificar os argumentos de funções em relação às suas anotações. Anotações em Python são uma maneira de adicionar metadados a parâmetros de função e valores de retorno.

Vamos modificar nosso código para aplicar este decorador a métodos com anotações:

  1. Primeiro, precisamos entender como funciona o decorador validated. Abra o arquivo validate.py no seu editor para revisá-lo.

O decorador validated usa anotações de função para validar argumentos. Antes de permitir que a função seja executada, ele cria uma instância da classe validadora para cada parâmetro anotado e chama o método validate para verificar o argumento. Por exemplo, se um argumento for anotado com PositiveInteger, o decorador criará uma instância de PositiveInteger e validará que o valor passado é de fato um inteiro positivo. Se a validação falhar, ele coleta todos os erros e levanta um TypeError com mensagens de erro detalhadas.

  1. Agora, modificaremos a função validate_attributes em structure.py para envolver métodos anotados com o decorador validated. Isso significa que qualquer método com anotações na classe terá seus argumentos automaticamente validados. Abra o arquivo structure.py no seu editor.

  2. Atualize a função validate_attributes:

def validate_attributes(cls):
    """
    Class decorator that:
    1. Extracts Validator instances and builds _fields and _types lists
    2. Applies @validated decorator to methods with annotations
    """
    ## Import the validated decorator
    from validate import validated

    ## Process validator descriptors
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Apply @validated decorator to methods with annotations
    for name, val in vars(cls).items():
        if callable(val) and hasattr(val, '__annotations__'):
            setattr(cls, name, validated(val))

    ## Create initialization method
    cls.create_init()

    return cls

Esta função atualizada agora faz o seguinte:

  1. Processa descritores de validadores como antes. Descritores de validadores são usados para definir regras de validação para atributos de classe.

  2. Encontra todos os métodos com anotações na classe. Anotações são adicionadas a parâmetros de método para especificar o tipo esperado do argumento.

  3. Aplica o decorador @validated a esses métodos. Isso garante que os argumentos passados para esses métodos sejam validados de acordo com suas anotações.

  4. Salve o arquivo após fazer essas alterações. Salvar o arquivo é importante porque garante que nossas modificações sejam armazenadas e possam ser usadas posteriormente.

  5. Agora, vamos atualizar o método sell na classe Stock para incluir uma anotação. Anotações ajudam a especificar o tipo esperado do argumento, que será usado pelo decorador @validated para validação. Abra o arquivo stock.py no seu editor.

  6. Modifique o método sell para incluir uma anotação de tipo:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

A mudança importante é adicionar : PositiveInteger ao parâmetro nshares. Isso informa ao Python (e ao nosso decorador @validated) para validar este argumento usando o validador PositiveInteger. Portanto, quando chamarmos o método sell, o argumento nshares deve ser um inteiro positivo.

  1. Execute os testes novamente para verificar se tudo ainda funciona. Executar testes é uma boa maneira de garantir que nossas alterações não quebraram nenhuma funcionalidade existente.
cd ~/project
python3 teststock.py

Você deverá ver todos os testes passando:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK
  1. Vamos testar nossa nova validação de argumentos. Tentaremos chamar o método sell com argumentos válidos e inválidos para ver se a validação funciona como esperado.
cd ~/project
python3 -c "
from stock import Stock
s = Stock('GOOG', 100, 490.1)
s.sell(25)
print(s)
try:
    s.sell(-25)
except Exception as e:
    print(f'Error: {e}')
"

Você deverá ver uma saída semelhante a:

Stock('GOOG', 75, 490.1)
Error: Bad Arguments
  nshares: nshares must be >= 0

Isso mostra que nossa validação de argumentos de método está funcionando! A primeira chamada para sell(25) é bem-sucedida porque 25 é um inteiro positivo. Mas a segunda chamada para sell(-25) falha porque -25 não é um inteiro positivo.

Você agora implementou um sistema completo para:

  1. Validar atributos de classe usando descritores. Descritores são usados para definir regras de validação para atributos de classe.
  2. Coletar informações de campo automaticamente usando decoradores de classe. Decoradores de classe podem modificar o comportamento de uma classe, como coletar informações de campo.
  3. Converter dados de linha em instâncias. Isso é útil ao trabalhar com dados de fontes externas.
  4. Validar argumentos de método usando anotações. Anotações ajudam a especificar o tipo esperado do argumento para validação.

Isso demonstra o poder de combinar descritores e decoradores em Python para criar classes expressivas e auto-validadoras.

Resumo

Neste laboratório, você aprendeu a combinar recursos poderosos do Python para criar código limpo e auto-validável. Você dominou conceitos-chave como o uso de descritores para validação de atributos, a criação de decoradores de classe para automação de geração de código e a aplicação automática de decoradores através de herança.

Essas técnicas são ferramentas poderosas para criar código Python robusto e de fácil manutenção. Elas permitem que você expresse claramente os requisitos de validação e os aplique em toda a sua base de código. Agora você pode aplicar esses padrões em seus próprios projetos Python para melhorar a qualidade do código e reduzir o código repetitivo.