Regras de Escopo e Truques

Beginner

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

Introdução

Neste laboratório, você aprenderá sobre as regras de escopo do Python e explorará técnicas avançadas para trabalhar com escopo. Compreender o escopo em Python é crucial para escrever código limpo e de fácil manutenção, além de ajudar a evitar comportamentos inesperados.

Os objetivos deste laboratório incluem a compreensão detalhada das regras de escopo do Python, o aprendizado de técnicas práticas de escopo para inicialização de classes, a implementação de um sistema flexível de inicialização de objetos e a aplicação de técnicas de inspeção de frames para simplificar o código. Você trabalhará com os arquivos structure.py e stock.py.

Compreendendo o Problema com a Inicialização de Classes

No mundo da programação, as classes são um conceito fundamental que permite criar tipos de dados personalizados. Em exercícios anteriores, você pode ter criado uma classe Structure. Essa classe serve como uma ferramenta útil para definir facilmente estruturas de dados. Uma estrutura de dados é uma maneira de organizar e armazenar dados para que possam ser acessados e usados eficientemente. A classe Structure, como uma classe base, cuida da inicialização de atributos com base em uma lista predefinida de nomes de campos. Atributos são variáveis que pertencem a um objeto, e nomes de campos são os nomes que damos a esses atributos.

Vamos dar uma olhada mais de perto na implementação atual da classe Structure. Para fazer isso, precisamos abrir o arquivo structure.py no editor de código. Este arquivo contém o código para a classe Structure. Aqui estão os comandos para navegar até o diretório do projeto e abrir o arquivo:

cd ~/project
code structure.py

A classe Structure fornece uma estrutura básica para definir estruturas de dados simples. Quando criamos uma subclasse, como a classe Stock, podemos definir os campos específicos que queremos para essa subclasse. Uma subclasse herda as propriedades e métodos de sua classe base, neste caso, a classe Structure. Por exemplo, na classe Stock, definimos os campos name, shares e price:

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

Agora, vamos abrir o arquivo stock.py para ver como a classe Stock é implementada no contexto do código geral. Este arquivo provavelmente contém o código que usa a classe Stock e interage com ela. Use o seguinte comando para abrir o arquivo:

code stock.py

Embora essa abordagem de usar a classe Structure e suas subclasses funcione, ela tem várias limitações. Para identificar esses problemas, executaremos o interpretador Python e exploraremos como a classe Stock se comporta. O seguinte comando importará a classe Stock e exibirá suas informações de ajuda:

python3 -c "from stock import Stock; help(Stock)"

Ao executar este comando, você notará que a assinatura mostrada na saída de ajuda não é muito útil. Em vez de mostrar os nomes reais dos parâmetros, como name, shares e price, ela mostra apenas *args. Essa falta de nomes de parâmetros claros dificulta para os usuários entender como criar corretamente uma instância da classe Stock.

Vamos também tentar criar uma instância Stock usando argumentos de palavra-chave (keyword arguments). Argumentos de palavra-chave permitem que você especifique os valores dos parâmetros por seus nomes, o que pode tornar o código mais legível. Execute o seguinte comando:

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Você deve obter uma mensagem de erro como esta:

TypeError: __init__() got an unexpected keyword argument 'name'

Este erro ocorre porque nosso método __init__ atual, que é responsável por inicializar objetos da classe Stock, não lida com argumentos de palavra-chave. Ele aceita apenas argumentos posicionais, o que significa que você deve fornecer os valores em uma ordem específica sem usar os nomes dos parâmetros. Esta é uma limitação que queremos corrigir neste laboratório.

Neste laboratório, exploraremos diferentes abordagens para tornar nossa classe Structure mais flexível e amigável para o usuário. Ao fazer isso, podemos melhorar a usabilidade da classe Stock e outras subclasses de Structure.

Usando locals() para Acessar Argumentos de Função

Em Python, entender os escopos de variáveis é crucial. O escopo de uma variável determina onde no código ela pode ser acessada. Python fornece uma função embutida chamada locals() que é muito útil para iniciantes entenderem o escopo. A função locals() retorna um dicionário contendo todas as variáveis locais no escopo atual. Isso pode ser extremamente útil quando você deseja inspecionar argumentos de função, pois fornece uma visão clara de quais variáveis estão disponíveis dentro de uma parte específica do seu código.

Vamos criar um experimento simples no interpretador Python para ver como isso funciona. Primeiro, precisamos navegar até o diretório do projeto e iniciar o interpretador Python. Você pode fazer isso executando os seguintes comandos no seu terminal:

cd ~/project
python3

Depois de entrar no shell interativo do Python, definiremos uma classe Stock. Uma classe em Python é como um modelo para criar objetos. Nesta classe, usaremos o método especial __init__. O método __init__ é um construtor em Python, o que significa que ele é chamado automaticamente quando um objeto da classe é criado. Dentro deste método __init__, usaremos a função locals() para imprimir todas as variáveis locais.

class Stock:
    def __init__(self, name, shares, price):
        print(locals())

Agora, vamos criar uma instância desta classe Stock. Uma instância é um objeto real criado a partir do modelo da classe. Passaremos alguns valores para os parâmetros name, shares e price.

s = Stock('GOOG', 100, 490.1)

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

{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}

Esta saída mostra que locals() nos dá um dicionário contendo todas as variáveis locais no método __init__. A referência self é uma variável especial em classes Python que se refere à instância da própria classe. As outras variáveis são os valores dos parâmetros que passamos ao criar o objeto Stock.

Podemos usar essa funcionalidade locals() para inicializar automaticamente atributos de objeto. Atributos são variáveis associadas a um objeto. Vamos definir uma função auxiliar e modificar nossa classe Stock.

def _init(locs):
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

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

A função _init recebe o dicionário de variáveis locais obtido de locals(). Primeiro, ela remove a referência self do dicionário usando o método pop. Em seguida, ela itera sobre os pares chave-valor restantes no dicionário e usa a função setattr para definir cada variável como um atributo no objeto.

Agora, vamos testar esta implementação com argumentos posicionais e de palavra-chave. Argumentos posicionais são passados na ordem em que são definidos na assinatura da função, enquanto argumentos de palavra-chave são passados com os nomes dos parâmetros especificados.

## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)

## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)

Ambas as abordagens devem funcionar agora! A função _init nos permite lidar com argumentos posicionais e de palavra-chave perfeitamente. Ela também preserva os nomes dos parâmetros na assinatura da função, o que torna a saída de help() mais útil. A função help() em Python fornece informações sobre funções, classes e módulos, e ter os nomes dos parâmetros intactos torna essa informação mais significativa.

Quando terminar de experimentar, você pode sair do interpretador Python executando o seguinte comando:

exit()

Explorando a Inspeção do Frame da Pilha

A abordagem _init(locals()) que temos usado é funcional, mas tem uma desvantagem. Toda vez que definimos um método __init__, temos que chamar explicitamente locals(). Isso pode se tornar um pouco complicado, especialmente ao lidar com várias classes. Felizmente, podemos tornar nosso código mais limpo e eficiente usando a inspeção do frame da pilha (stack frame inspection). Essa técnica nos permite acessar automaticamente as variáveis locais do chamador sem ter que chamar locals() explicitamente.

Vamos começar a explorar essa técnica no interpretador Python. Primeiro, abra seu terminal e navegue até o diretório do projeto. Em seguida, inicie o interpretador Python. Você pode fazer isso executando os seguintes comandos:

cd ~/project
python3

Agora que estamos no interpretador Python, precisamos importar o módulo sys. O módulo sys fornece acesso a algumas variáveis usadas ou mantidas pelo interpretador Python. Usaremos isso para acessar as informações do frame da pilha.

import sys

Em seguida, definiremos uma versão aprimorada de nossa função _init(). Esta nova versão acessará o frame do chamador diretamente, eliminando a necessidade de passar locals() explicitamente.

def _init():
    ## Get the caller's frame (1 level up in the call stack)
    frame = sys._getframe(1)

    ## Get the local variables from that frame
    locs = frame.f_locals

    ## Extract self and set other variables as attributes
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

Neste código, sys._getframe(1) recupera o objeto frame da função chamadora. O 1 como argumento significa que estamos olhando um nível acima na pilha de chamadas (call stack). Depois de termos o objeto frame, podemos acessar suas variáveis locais usando frame.f_locals. Isso nos dá um dicionário de todas as variáveis locais no escopo do chamador. Em seguida, extraímos a variável self e definimos as variáveis restantes como atributos do objeto self.

Agora, vamos testar esta nova função _init() com uma nova versão de nossa classe Stock.

class Stock:
    def __init__(self, name, shares, price):
        _init()  ## No need to pass locals() anymore!

## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)

## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)

Como você pode ver, o método __init__ não precisa mais passar locals() explicitamente. Isso torna nosso código mais limpo e fácil de ler da perspectiva do chamador.

Como a Inspeção do Frame da Pilha Funciona

Quando você chama sys._getframe(1), o Python retorna o objeto frame que representa o frame de execução do chamador. O argumento 1 significa "um nível acima do frame atual" (a função chamadora).

Um objeto frame contém informações importantes sobre o contexto de execução. Isso inclui a função atual sendo executada, as variáveis locais nessa função e o número da linha atualmente sendo executada.

Ao acessar frame.f_locals, obtemos um dicionário de todas as variáveis locais no escopo do chamador. Isso é semelhante ao que locals() retornaria se fosse chamado diretamente desse escopo.

Essa técnica é bastante poderosa, mas deve ser usada com cautela. É geralmente considerada um recurso avançado do Python e pode parecer um pouco "mágica" porque atinge fora dos limites normais de escopo do Python.

Depois de terminar de experimentar com a inspeção do frame da pilha, você pode sair do interpretador Python executando o seguinte comando:

exit()

Implementando a Inicialização Avançada na Estrutura

Acabamos de aprender duas técnicas poderosas para acessar argumentos de função. Agora, usaremos essas técnicas para atualizar nossa classe Structure. Primeiro, vamos entender por que estamos fazendo isso. Essas técnicas tornarão nossa classe mais flexível e fácil de usar, especialmente ao lidar com diferentes tipos de argumentos.

Abra o arquivo structure.py no editor de código. Você pode fazer isso executando os seguintes comandos no terminal. O comando cd altera o diretório para a pasta do projeto e o comando code abre o arquivo structure.py no editor de código.

cd ~/project
code structure.py

Substitua o conteúdo do arquivo pelo seguinte código. Este código define uma classe Structure com vários métodos. Vamos analisar cada parte para entender o que ela faz.

import sys

class Structure:
    _fields = ()

    @staticmethod
    def _init():
        ## Get the caller's frame (the __init__ method that called this)
        frame = sys._getframe(1)

        ## Get the local variables from that frame
        locs = frame.f_locals

        ## Extract self and set other variables as attributes
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)

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

    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')

Aqui está o que fizemos no código:

  1. Removemos o antigo método __init__(). Como as subclasses definirão seus próprios métodos __init__, não precisamos mais do antigo.
  2. Adicionamos um novo método estático _init(). Este método usa a inspeção do frame para capturar e definir automaticamente todos os parâmetros como atributos. A inspeção do frame nos permite acessar as variáveis locais do método chamador.
  3. Mantivemos o método __repr__(). Este método fornece uma boa representação de string do objeto, o que é útil para depuração e impressão.
  4. Adicionamos um método __setattr__(). Este método impõe a validação de atributos, garantindo que apenas atributos válidos possam ser definidos no objeto.

Agora, vamos atualizar a classe Stock. Abra o arquivo stock.py usando o seguinte comando:

code stock.py

Substitua seu conteúdo pelo seguinte código:

from structure import Structure

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

    def __init__(self, name, shares, price):
        self._init()  ## This magically captures and sets all parameters!

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

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

A principal mudança aqui é que nosso método __init__ agora chama self._init() em vez de definir manualmente cada atributo. O método _init() usa a inspeção do frame para capturar e definir automaticamente todos os parâmetros como atributos. Isso torna o código mais conciso e fácil de manter.

Vamos testar nossa implementação executando os testes unitários. Os testes unitários nos ajudarão a garantir que nosso código funcione conforme o esperado. Execute os seguintes comandos no terminal:

cd ~/project
python3 teststock.py

Você deve ver que todos os testes passam, incluindo o teste para argumentos de palavra-chave que falhou antes. Isso significa que nossa implementação está funcionando corretamente.

Vamos também verificar a documentação de ajuda para nossa classe Stock. A documentação de ajuda fornece informações sobre a classe e seus métodos. Execute o seguinte comando no terminal:

python3 -c "from stock import Stock; help(Stock)"

Agora você deve ver uma assinatura adequada para o método __init__, mostrando todos os nomes dos parâmetros. Isso torna mais fácil para outros desenvolvedores entenderem como usar a classe.

Finalmente, vamos testar interativamente se os argumentos de palavra-chave funcionam conforme o esperado. Execute o seguinte comando no terminal:

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Você deve ver o objeto Stock criado corretamente com os atributos especificados. Isso confirma que nosso sistema de inicialização de classe suporta argumentos de palavra-chave.

Com esta implementação, alcançamos um sistema de inicialização de classe muito mais flexível e fácil de usar que:

  1. Preserva as assinaturas de função adequadas na documentação, tornando mais fácil para os desenvolvedores entenderem como usar a classe.
  2. Suporta argumentos posicionais e de palavra-chave, proporcionando mais flexibilidade ao criar objetos.
  3. Requer um código boilerplate mínimo em subclasses, reduzindo a quantidade de código que você precisa escrever.

Resumo

Neste laboratório, você aprendeu sobre as regras de escopo do Python e algumas técnicas poderosas para lidar com escopo. Primeiro, você explorou como usar a função locals() para acessar todas as variáveis locais dentro de uma função. Em segundo lugar, você aprendeu a inspecionar frames da pilha (stack frames) usando sys._getframe() para acessar as variáveis locais do chamador.

Você também aplicou essas técnicas para criar um sistema de inicialização de classe flexível. Este sistema captura e define automaticamente os parâmetros da função como atributos de objeto, mantém as assinaturas de função adequadas na documentação e suporta argumentos posicionais e de palavra-chave. Essas técnicas demonstram a flexibilidade e as capacidades de introspecção do Python. Embora a inspeção do frame seja uma técnica avançada que deve ser usada com cuidado, ela pode reduzir efetivamente o código boilerplate quando usada apropriadamente. Compreender as regras de escopo e essas técnicas avançadas equipa você com mais ferramentas para escrever um código Python mais limpo e mais fácil de manter.