Inspecione os Detalhes Internos das Funções

Beginner

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

Introdução

Neste laboratório, você aprenderá como explorar os detalhes internos das funções Python. Funções em Python são objetos com seus próprios atributos e métodos, e a compreensão destes pode oferecer insights mais profundos sobre como as funções operam. Este conhecimento permitirá que você escreva um código mais poderoso e adaptável.

Você aprenderá a inspecionar atributos e propriedades de funções, usar o módulo inspect para examinar as assinaturas de funções e aplicar essas técnicas de inspeção para aprimorar a implementação de uma classe. O arquivo que você modificará é structure.py.

Explorando Atributos de Funções

Em Python, as funções são consideradas objetos de primeira classe (first-class objects). O que isso significa? Bem, é semelhante a como você tem diferentes tipos de objetos no mundo real, como um livro ou uma caneta. Em Python, as funções também são objetos e, assim como outros objetos, vêm com seu próprio conjunto de atributos. Esses atributos podem nos fornecer muitas informações úteis sobre a função, como seu nome, onde ela é definida e como ela é implementada.

Vamos começar nossa exploração abrindo um shell interativo Python. Este shell é como um playground onde podemos escrever e executar código Python imediatamente. Para fazer isso, primeiro navegaremos até o diretório do projeto e, em seguida, iniciaremos o interpretador Python. Aqui estão os comandos para executar em seu terminal:

cd ~/project
python3

Agora que estamos no shell interativo Python, vamos definir uma função simples. Esta função pegará dois números e os somará. Veja como podemos defini-la:

def add(x, y):
    'Adds two things'
    return x + y

Neste código, criamos uma função chamada add. Ela recebe dois parâmetros, x e y, e retorna sua soma. A string 'Adds two things' é chamada de docstring, que é usada para documentar o que a função faz.

Usando dir() para Inspecionar Atributos de Funções

Em Python, a função dir() é uma ferramenta útil. Ela pode ser usada para obter uma lista de todos os atributos e métodos que um objeto possui. Vamos usá-la para ver quais atributos nossa função add possui. Execute o seguinte código no shell interativo Python:

dir(add)

Quando você executar este código, verá uma longa lista de atributos. Aqui está um exemplo de como a saída pode ser:

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Esta lista mostra todos os atributos e métodos associados à função add.

Acessando Informações Básicas da Função

Agora, vamos dar uma olhada mais de perto em alguns dos atributos básicos da função. Esses atributos podem nos dizer informações importantes sobre a função. Execute o seguinte código no shell interativo Python:

print(add.__name__)
print(add.__module__)
print(add.__doc__)

Quando você executar este código, verá a seguinte saída:

add
__main__
Adds two things

Vamos entender o que cada um desses atributos significa:

  • __name__: Este atributo nos dá o nome da função. Em nosso caso, a função se chama add.
  • __module__: Ele nos diz o módulo onde a função é definida. Quando executamos código no shell interativo, o módulo geralmente é __main__.
  • __doc__: Esta é a string de documentação da função, ou docstring. Ela fornece uma breve descrição do que a função faz.

Examinando o Código da Função

O atributo __code__ de uma função é muito interessante. Ele contém informações sobre como a função é implementada, incluindo seu bytecode e outros detalhes. Vamos ver o que podemos aprender com ele. Execute o seguinte código no shell interativo Python:

print(add.__code__.co_varnames)
print(add.__code__.co_argcount)

A saída será:

('x', 'y')
2

Aqui está o que esses atributos nos dizem:

  • co_varnames: É uma tupla que contém os nomes de todas as variáveis locais usadas pela função. Em nossa função add, as variáveis locais são x e y.
  • co_argcount: Este atributo nos diz o número de argumentos que a função espera. Nossa função add espera dois argumentos, então o valor é 2.

Se você estiver curioso para explorar mais atributos do objeto __code__, pode usar a função dir() novamente. Execute o seguinte código:

dir(add.__code__)

Isso exibirá todos os atributos do objeto de código, que contêm detalhes de baixo nível sobre como a função é implementada.

Usando o Módulo inspect

Em Python, a biblioteca padrão vem com um módulo inspect muito útil. Este módulo é como uma ferramenta de detetive que nos ajuda a reunir informações sobre objetos ativos (live objects) em Python. Objetos ativos podem ser coisas como módulos, classes e funções. Em vez de cavar manualmente nos atributos de um objeto para encontrar informações, o módulo inspect fornece maneiras mais organizadas e de alto nível de entender as propriedades das funções.

Vamos continuar usando o mesmo shell interativo Python para explorar como este módulo funciona.

Assinaturas de Funções (Function Signatures)

A função inspect.signature() é uma ferramenta útil. Quando você passa uma função para ela, ela retorna um objeto Signature. Este objeto contém detalhes importantes sobre os parâmetros da função.

Aqui está um exemplo. Suponha que temos uma função chamada add. Podemos usar a função inspect.signature() para obter sua assinatura:

import inspect
sig = inspect.signature(add)
print(sig)

Quando você executar este código, a saída será:

(x, y)

Esta saída nos mostra a assinatura da função, que nos diz quais parâmetros a função pode aceitar.

Examinando Detalhes dos Parâmetros

Podemos ir um passo além e obter informações mais detalhadas sobre cada parâmetro da função.

print(sig.parameters)

A saída deste código será:

OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">)])

Os parâmetros da função são armazenados em um dicionário ordenado. Às vezes, podemos estar interessados apenas nos nomes dos parâmetros. Podemos converter este dicionário ordenado em uma tupla para extrair apenas os nomes dos parâmetros.

param_names = tuple(sig.parameters)
print(param_names)

A saída será:

('x', 'y')

Examinando Parâmetros Individuais

Também podemos dar uma olhada mais de perto em cada parâmetro individual. O código a seguir percorre cada parâmetro na função e imprime alguns detalhes importantes sobre ele.

for name, param in sig.parameters.items():
    print(f"Parameter: {name}")
    print(f"  Kind: {param.kind}")
    print(f"  Default: {param.default if param.default is not param.empty else 'No default'}")

Este código mostrará detalhes sobre cada parâmetro. Ele nos diz o tipo do parâmetro (se é um parâmetro posicional, um parâmetro de palavra-chave, etc.) e seu valor padrão, se tiver um.

O módulo inspect tem muitas outras funções úteis para introspecção de funções. Aqui estão alguns exemplos:

  • inspect.getdoc(obj): Esta função recupera a string de documentação para um objeto. Strings de documentação são como notas que os programadores escrevem para explicar o que um objeto faz.
  • inspect.getfile(obj): Ela nos ajuda a descobrir o arquivo onde um objeto é definido. Isso pode ser muito útil quando queremos localizar o código-fonte de um objeto.
  • inspect.getsource(obj): Esta função busca o código-fonte de um objeto. Ela nos permite ver exatamente como o objeto é implementado.

Aplicando a Inspeção de Funções em Classes

Agora, vamos pegar o que aprendemos sobre inspeção de funções e usá-lo para melhorar a implementação de uma classe. A inspeção de funções nos permite olhar para dentro das funções e entender sua estrutura, como os parâmetros que elas recebem. Neste caso, vamos usá-la para tornar nosso código de classe mais eficiente e menos propenso a erros. Vamos modificar uma classe Structure para que ela possa detectar automaticamente os nomes dos campos a partir da assinatura do método __init__.

Entendendo a Classe Structure

O arquivo structure.py contém uma classe Structure. Esta classe atua como uma classe base, o que significa que outras classes podem herdar dela para criar objetos de dados estruturados. Atualmente, para definir os atributos dos objetos criados a partir de classes que herdam de Structure, precisamos definir uma variável de classe _fields.

Vamos abrir o arquivo no editor. Usaremos o seguinte comando para navegar até o diretório do projeto:

cd ~/project

Depois de executar este comando, você pode encontrar e visualizar a classe Structure existente no arquivo structure.py dentro do WebIDE.

Criando uma Classe Stock

Vamos criar uma classe Stock que herda da classe Structure. Herança significa que a classe Stock obterá todos os recursos da classe Structure e também poderá adicionar os seus próprios. Adicionaremos o seguinte código ao final do arquivo structure.py:

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

    def __init__(self, name, shares, price):
        self._init()

No entanto, há um problema com essa abordagem. Precisamos definir tanto a tupla _fields quanto o método __init__ com os mesmos nomes de parâmetros. Isso é redundante porque estamos essencialmente escrevendo a mesma informação duas vezes. Se esquecermos de atualizar um quando alteramos o outro, isso pode levar a erros.

Adicionando um Método de Classe set_fields

Para corrigir esse problema, adicionaremos um método de classe set_fields à classe Structure. Este método detectará automaticamente os nomes dos campos a partir da assinatura __init__. Aqui está o código que precisamos adicionar à classe Structure:

@classmethod
def set_fields(cls):
    ## Get the signature of the __init__ method
    import inspect
    sig = inspect.signature(cls.__init__)

    ## Get parameter names, skipping 'self'
    params = list(sig.parameters.keys())[1:]

    ## Set _fields attribute on the class
    cls._fields = tuple(params)

Este método usa o módulo inspect, que é uma ferramenta poderosa em Python para obter informações sobre objetos como funções e classes. Primeiro, ele obtém a assinatura do método __init__. Em seguida, ele extrai os nomes dos parâmetros, mas ignora o parâmetro self porque self é um parâmetro especial em classes Python que se refere à própria instância. Finalmente, ele define a variável de classe _fields com esses nomes de parâmetros.

Modificando a Classe Stock

Agora que temos o método set_fields, podemos simplificar nossa classe Stock. Substitua o código anterior da classe Stock pelo seguinte:

class Stock(Structure):
    def __init__(self, name, shares, price):
        self._init()

## Call set_fields to automatically set _fields from __init__
Stock.set_fields()

Dessa forma, não precisamos definir manualmente a tupla _fields. O método set_fields cuidará disso para nós.

Testando a Classe Modificada

Para garantir que nossa classe modificada funcione corretamente, criaremos um script de teste simples. Crie um novo arquivo chamado test_structure.py e adicione o seguinte código:

from structure import Stock

def test_stock():
    ## Create a Stock object
    s = Stock(name='GOOG', shares=100, price=490.1)

    ## Test string representation
    print(f"Stock representation: {s}")

    ## Test attribute access
    print(f"Name: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price}")

    ## Test attribute modification
    s.shares = 50
    print(f"Updated shares: {s.shares}")

    ## Test attribute error
    try:
        s.share = 50  ## Misspelled attribute
        print("Error: Did not raise AttributeError")
    except AttributeError as e:
        print(f"Correctly raised: {e}")

if __name__ == "__main__":
    test_stock()

Este script de teste cria um objeto Stock, testa sua representação de string, acessa seus atributos, modifica um atributo e tenta acessar um atributo com erro de ortografia para verificar se ele gera o erro correto.

Para executar o script de teste, use o seguinte comando:

python3 test_structure.py

Você deve ver uma saída semelhante a esta:

Stock representation: Stock('GOOG',100,490.1)
Name: GOOG
Shares: 100
Price: 490.1
Updated shares: 50
Correctly raised: No attribute share

Como Funciona

  1. O método set_fields usa inspect.signature() para obter os nomes dos parâmetros do método __init__. Esta função nos dá informações detalhadas sobre os parâmetros do método __init__.
  2. Em seguida, ele define automaticamente a variável de classe _fields com base nesses nomes de parâmetros. Portanto, não precisamos escrever os mesmos nomes de parâmetros em dois lugares diferentes.
  3. Isso elimina a necessidade de definir manualmente tanto _fields quanto __init__ com nomes de parâmetros correspondentes. Isso torna nosso código mais fácil de manter, porque se alterarmos os parâmetros no método __init__, o _fields será atualizado automaticamente.

Essa abordagem usa a inspeção de funções para tornar nosso código mais fácil de manter e menos propenso a erros. É uma aplicação prática dos recursos de introspecção do Python, que nos permitem examinar e modificar objetos em tempo de execução.

Resumo

Neste laboratório, você aprendeu como inspecionar os detalhes internos das funções Python. Você examinou os atributos da função diretamente usando métodos como dir() e acessou atributos especiais como __name__, __doc__ e __code__. Você também usou o módulo inspect para obter informações estruturadas sobre assinaturas e parâmetros de funções.

A inspeção de funções é um recurso poderoso do Python que permite escrever um código mais dinâmico, flexível e fácil de manter. A capacidade de examinar e manipular as propriedades da função em tempo de execução oferece possibilidades para metaprogramação, criação de código autodocumentado e construção de frameworks avançados.