Decoradores Python: Guia Completo com Exemplos

Os decoradores em Python representam um recurso extremamente valioso. Através de seu uso, é possível modificar o comportamento de uma função, envolvendo-a em outra. Essa abordagem promove a criação de um código mais conciso e facilita o compartilhamento de funcionalidades. Este guia prático não apenas demonstra como utilizar decoradores, mas também ensina a criá-los.

Conhecimentos Essenciais

O estudo de decoradores em Python requer um entendimento prévio de alguns conceitos. Apresento a seguir uma lista de tópicos com os quais você deve estar familiarizado para acompanhar este tutorial. Também disponibilizei links para materiais de apoio, caso necessite revisar algum conceito.

Fundamentos de Python

Este é um tópico de nível intermediário/avançado. Portanto, antes de prosseguir, é fundamental que você domine os conceitos básicos de Python, tais como tipos de dados, funções, objetos e classes.

Além disso, familiarize-se com princípios da programação orientada a objetos, como getters, setters e construtores. Se você não tem experiência com a linguagem Python, sugiro que explore os seguintes recursos introdutórios.

Funções como Cidadãos de Primeira Classe

Além do conhecimento básico de Python, é essencial compreender esse conceito avançado da linguagem. Em Python, as funções, assim como quase tudo mais, são tratadas como objetos, à semelhança de inteiros ou strings. Essa característica permite realizar diversas operações com elas:

  • Funções podem ser passadas como argumentos para outras funções, tal como strings ou inteiros.
  • Funções também podem ser retornadas por outras funções, da mesma forma que strings ou valores inteiros.
  • Funções podem ser armazenadas em variáveis.

A distinção fundamental entre objetos funcionais e outros tipos de objetos reside no fato de que os objetos funcionais possuem o método mágico __call__().

Neste ponto, espera-se que você esteja familiarizado com os conhecimentos prévios. Podemos então avançar para o tema central.

O que são Decoradores em Python?

Em essência, um decorador em Python é uma função que recebe outra função como entrada e gera uma versão modificada desta função. Em outras palavras, uma função ‘foo’ é considerada um decorador se ela aceita uma função ‘bar’ como argumento e produz uma nova função ‘baz’.

A função ‘baz’ representa uma alteração de ‘bar’, pois em seu corpo existe uma chamada para ‘bar’. No entanto, ‘baz’ tem a capacidade de executar ações adicionais antes e depois da execução de ‘bar’. Para ilustrar essa situação, observe o seguinte exemplo de código:

# 'foo' é um decorador que aceita outra função, 'bar', como argumento
def foo(bar):

    # Aqui criamos 'baz', uma versão modificada de 'bar'
    # 'baz' chamará 'bar', mas pode realizar ações antes e depois da chamada
    def baz():

        # Antes de chamar 'bar', exibimos algo
        print("Algo")

        # Em seguida, executamos 'bar' através de uma chamada de função
        bar()

        # Depois de executar 'bar', exibimos outra mensagem
        print("Outra coisa")

    # Por fim, 'foo' retorna 'baz', a versão modificada de 'bar'
    return baz
  

Como Construir um Decorador em Python?

Para demonstrar como os decoradores são construídos e utilizados em Python, apresentarei um exemplo simples. Neste caso, criaremos um decorador chamado ‘logger’ que registrará o nome da função decorada sempre que esta for executada.

Começaremos pela criação da função decoradora, que recebe ‘func’ como argumento. ‘func’ representa a função que será decorada.

def criar_logger(func):
    # O corpo da função será inserido aqui
  

Dentro da função decoradora, definiremos a função modificada que registrará o nome de ‘func’ antes de sua execução.

# Dentro de 'criar_logger'
def funcao_modificada():
    print("Chamando: ", func.__name__)
    func()
  

Em seguida, a função ‘criar_logger’ retornará a função modificada. Portanto, a função ‘criar_logger’ completa terá a seguinte estrutura:

def criar_logger(func):
    def funcao_modificada():
        print("Chamando: ", func.__name__)
        func()

    return funcao_modificada
  

Com isso, concluímos a criação do decorador. A função ‘criar_logger’ é um exemplo simples de um decorador. Ela recebe ‘func’, que é a função a ser decorada, e retorna outra função, ‘funcao_modificada’. Esta última registra o nome de ‘func’ antes de sua execução.

Como Aplicar Decoradores em Python

Para empregar nosso decorador, utilizamos a sintaxe ‘@’, conforme demonstrado a seguir:

@criar_logger
def dizer_ola():
    print("Olá, Mundo!")
  

Ao executar ‘dizer_ola()’ em nosso script, o resultado esperado será o seguinte:

Chamando:  dizer_ola
"Olá, Mundo!"
  

Mas qual o papel de ‘@criar_logger’? Ele aplica o decorador à nossa função ‘dizer_ola’. Para melhor compreensão, o código abaixo produz o mesmo resultado de adicionar ‘@criar_logger’ antes de ‘dizer_ola’.

def dizer_ola():
    print("Olá, Mundo!")

dizer_ola = criar_logger(dizer_ola)
  

Em outras palavras, uma forma de usar decoradores em Python é invocar o decorador explicitamente, passando a função como argumento, como ilustrado no código acima. A outra opção, mais concisa, é empregar a sintaxe ‘@’.

Nesta seção, abordamos a criação de decoradores em Python.

Exemplos Mais Complexos

O exemplo anterior era bastante simples. Existem situações um pouco mais complexas, como quando a função a ser decorada recebe argumentos. Outro cenário mais avançado ocorre quando se deseja decorar uma classe inteira. Abordarei essas duas situações a seguir.

Funções com Argumentos

Quando a função que você está decorando recebe argumentos, a função modificada também deve receber esses argumentos e repassá-los na chamada para a função original. Para esclarecer, vamos utilizar a nomenclatura foo-bar.

Lembre-se que ‘foo’ representa a função decoradora, ‘bar’ é a função a ser decorada, e ‘baz’ é a versão decorada de ‘bar’. Neste caso, ‘bar’ receberá os argumentos e os repassará para ‘baz’ durante a chamada. Observe o seguinte exemplo de código:

def foo(bar):
    def baz(*args, **kwargs):
        # É possível realizar ações aqui
        ___
        # Em seguida, chamamos 'bar', repassando os argumentos
        bar(*args, **kwargs)
        # Também é possível realizar ações após a chamada
        ___

    return baz
  

Os símbolos *args e **kwargs referem-se, respectivamente, aos argumentos posicionais e nomeados.

É importante observar que ‘baz’ tem acesso aos argumentos e pode, portanto, validar os argumentos antes de chamar ‘bar’.

Por exemplo, poderíamos criar uma função decoradora, ‘garantir_string’, que assegura que o argumento passado para uma função decorada seja uma string. A implementação seria a seguinte:

def garantir_string(func):
    def funcao_decorada(texto):
        if type(texto) is not str:
             raise TypeError('O argumento para ' + func.__name__ + ' deve ser uma string.')
        else:
             func(texto)

    return funcao_decorada
  

Poderíamos decorar a função ‘dizer_ola’ da seguinte maneira:

@garantir_string
def dizer_ola(nome):
    print('Olá', nome)
  

Para testar o código, podemos usar o seguinte:

dizer_ola('João') # Deve executar normalmente
dizer_ola(3) # Deve gerar uma exceção
  

A saída resultante seria:

Olá João
Traceback (mais recente chamada mais recente):
   Arquivo "/home/anesu/Documentos/python-tutorial/./decoradores.py", linha 20, em <módulo> dizer_ola(3) # deve gerar uma exceção
   Arquivo "/home/anesu/Documentos/python-tu$ ./decoradores.pytorial/./decoradores.py", linha 7, em funcao_decorada raise TypeError('O argumento para + func._nome_ + deve ser uma string.')
TypeError: O argumento para dizer ola deve ser uma string. $0
  

Como esperado, o script conseguiu exibir ‘Olá João’ porque ‘João’ é uma string. Ele lançou uma exceção ao tentar exibir ‘Olá 3’, pois ‘3’ não é uma string. O decorador ‘garantir_string’ pode ser usado para validar os argumentos de qualquer função que exija uma string.

Decorando Classes

Além de funções, também é possível decorar classes. Ao aplicar um decorador a uma classe, o método decorado substitui o método construtor/inicializador da classe (__init__).

Voltando à analogia foo-bar, se ‘foo’ for nosso decorador e ‘Bar’ for a classe a ser decorada, então ‘foo’ irá decorar ‘Bar.__init__’. Isso pode ser útil quando desejamos executar ações antes que objetos do tipo ‘Bar’ sejam instanciados.

Portanto, o seguinte código

def foo(func):
    def new_func(*args, **kwargs):
        print('Realizando ações antes da instanciação')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("No inicializador")
  

É equivalente a

def foo(func):
    def new_func(*args, **kwargs):
        print('Realizando ações antes da instanciação')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("No inicializador")


Bar.__init__ = foo(Bar.__init__)
  

Na prática, a instanciação de um objeto da classe ‘Bar’, utilizando qualquer um dos dois métodos, produzirá a mesma saída:

Realizando ações antes da instanciação
No inicializador
  

Decoradores de Exemplo em Python

Embora você possa definir seus próprios decoradores, o Python já oferece alguns embutidos. A seguir, apresento alguns decoradores comuns que você poderá encontrar:

@staticmethod

O decorador ‘@staticmethod’ é usado para indicar que o método que está sendo decorado é um método estático. Métodos estáticos são métodos que podem ser executados sem a necessidade de instanciar a classe. No exemplo abaixo, criamos uma classe ‘Cachorro’ com um método estático ‘latir’.

class Cachorro:
    @staticmethod
    def latir():
        print('Au, au!')
  

Agora, o método ‘latir’ pode ser acessado da seguinte forma:

Cachorro.latir()
  

A execução do código produzirá a seguinte saída:

Au, au!
  

Conforme mencionado na seção “Como usar decoradores”, eles podem ser utilizados de duas maneiras, sendo a sintaxe ‘@’ a mais concisa. A outra abordagem consiste em chamar a função decoradora, passando a função que desejamos decorar como argumento. Isso significa que o código acima obtém o mesmo resultado do seguinte código:

class Cachorro:
    def latir():
        print('Au, au!')

Cachorro.latir = staticmethod(Cachorro.latir)
  

E ainda podemos usar o método ‘latir’ da mesma maneira

Cachorro.latir()
  

O que também irá produzir a mesma saída

Au, au!
  

Como pode ser observado, o primeiro método é mais limpo e deixa mais evidente que a função é estática antes mesmo de começar a ler o código. Nos exemplos restantes, utilizarei o primeiro método. Entretanto, lembre-se de que o segundo método é uma alternativa válida.

@classmethod

Este decorador indica que o método que está sendo decorado é um método de classe. Métodos de classe são semelhantes aos métodos estáticos, pois ambos não exigem que a classe seja instanciada para serem chamados.

No entanto, a principal diferença reside no fato de que métodos de classe têm acesso aos atributos da classe, enquanto métodos estáticos não. Isso ocorre porque o Python automaticamente passa a classe como o primeiro argumento para um método de classe sempre que ele é chamado. Para criar um método de classe em Python, podemos usar o decorador ‘@classmethod’.

class Cachorro:
    @classmethod
    def qual_e_voce(cls):
        print("Eu sou um " + cls.__name__ + "!")
  

Para executar o código, basta chamar o método sem instanciar a classe:

Cachorro.qual_e_voce()
  

E a saída é:

Eu sou um Cachorro!
  

@property

O decorador ‘@property’ é utilizado para designar um método como um acessador de propriedade. Retornando ao nosso exemplo do ‘Cachorro’, criaremos um método para recuperar o nome do cachorro.

class Cachorro:
    # Criando um método construtor que recebe o nome do cachorro
    def __init__(self, nome):

         # Criando uma propriedade privada 'nome'
         # Os sublinhados duplos tornam o atributo privado
         self.__nome = nome

    
    @property
    def nome(self):
        return self.__nome
  

Agora, podemos acessar o nome do cachorro como uma propriedade normal,

# Criando uma instância da classe
foo = Cachorro('foo')

# Acessando a propriedade 'nome'
print("O nome do cachorro é:", foo.nome)
  

A execução do código resultaria em:

O nome do cachorro é: foo
  

@property.setter

O decorador ‘@property.setter’ é usado para criar um método setter para nossas propriedades. Para empregar o decorador ‘@property.setter’, substitua ‘property’ pelo nome da propriedade para a qual você está criando um setter. Por exemplo, se você estiver criando um setter para o método de propriedade ‘foo’, seu decorador será ‘@foo.setter’. A seguir, um exemplo com a classe ‘Cachorro’ para ilustrar:

class Cachorro:
    # Criando um método construtor que recebe o nome do cachorro
    def __init__(self, nome):

         # Criando uma propriedade privada 'nome'
         # Os sublinhados duplos tornam o atributo privado
         self.__nome = nome

    
    @property
    def nome(self):
        return self.__nome

    # Criando um setter para nossa propriedade 'nome'
    @nome.setter
    def nome(self, novo_nome):
        self.__nome = novo_nome
  

Para testar o setter, podemos usar o seguinte código:

# Criando um novo cachorro
foo = Cachorro('foo')

# Alterando o nome do cachorro
foo.nome="bar"

# Imprimindo o nome do cachorro na tela
print("O novo nome do cachorro é:", foo.nome)
  

A execução do código gerará a seguinte saída:

O novo nome do cachorro é: bar
  

A Importância dos Decoradores em Python

Agora que cobrimos o que são decoradores e você viu alguns exemplos práticos, podemos discutir a importância dos decoradores em Python. Eles são importantes por várias razões, algumas das quais listadas abaixo:

  • Permitem a reutilização de código: No exemplo de log, podemos usar ‘@criar_logger’ em qualquer função que desejarmos. Isso possibilita adicionar a funcionalidade de registro a todas as nossas funções sem reescrevê-la manualmente para cada uma delas.
  • Facilitam a criação de código modular: Retornando ao exemplo do log, com decoradores, é possível separar a função principal, no caso ‘dizer_ola’, da funcionalidade auxiliar, o registro.
  • Aprimoram estruturas e bibliotecas: Decoradores são amplamente usados em estruturas e bibliotecas Python para adicionar funcionalidades adicionais. Por exemplo, em estruturas web como Flask ou Django, eles são usados para definir rotas, manipular autenticação ou aplicar middleware a visualizações específicas.

Considerações Finais

Decoradores são ferramentas incrivelmente úteis; você pode utilizá-los para expandir a funcionalidade das funções sem alterar sua estrutura original. Isso se torna valioso quando você deseja mensurar o desempenho de funções, registrar cada chamada de uma função, validar argumentos ou verificar permissões antes da execução de uma função. Dominando o uso de decoradores, você poderá escrever um código mais limpo e eficiente.

Em seguida, sugiro que você leia nossos artigos sobre tuplas e como usar cURL em Python.