Criar Código com Exec

Beginner

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

Introdução

Neste laboratório, você aprenderá sobre a função exec() em Python. Esta função permite que você execute código Python representado como uma string dinamicamente. É um recurso poderoso que permite gerar e executar código em tempo de execução, tornando seus programas mais flexíveis e adaptáveis.

Os objetivos deste laboratório são aprender o uso básico da função exec(), usá-la para criar dinamicamente métodos de classe e examinar como a biblioteca padrão do Python usa exec() nos bastidores.

Compreendendo os Fundamentos de exec()

Em Python, a função exec() é uma ferramenta poderosa que permite executar código criado dinamicamente em tempo de execução. Isso significa que você pode gerar código em tempo real com base em uma determinada entrada ou configuração, o que é extremamente útil em muitos cenários de programação.

Vamos começar explorando o uso básico da função exec(). Para fazer isso, abriremos um shell Python. Abra seu terminal e digite python3. Este comando iniciará o interpretador Python interativo, onde você pode executar diretamente o código Python.

python3

Agora, vamos definir um trecho de código Python como uma string e, em seguida, usar a função exec() para executá-lo. Veja como funciona:

>>> code = '''
for i in range(n):
    print(i, end=' ')
'''
>>> n = 10
>>> exec(code)
0 1 2 3 4 5 6 7 8 9

Neste exemplo:

  1. Primeiro, definimos uma string chamada code. Esta string contém um loop for em Python. O loop foi projetado para iterar n vezes e imprimir cada número de iteração.
  2. Em seguida, definimos uma variável n e atribuímos a ela o valor 10. Esta variável é usada como o limite superior para a função range() em nosso loop.
  3. Depois disso, chamamos a função exec() com a string code como argumento. A função exec() pega a string e a executa como código Python.
  4. Finalmente, o loop foi executado e imprimiu os números de 0 a 9.

O verdadeiro poder da função exec() torna-se mais óbvio quando a usamos para criar estruturas de código mais complexas, como funções ou métodos. Vamos tentar um exemplo mais avançado onde criaremos dinamicamente um método __init__() para uma classe.

>>> class Stock:
...     _fields = ('name', 'shares', 'price')
...
>>> argstr = ','.join(Stock._fields)
>>> code = f'def __init__(self, {argstr}):\n'
>>> for name in Stock._fields:
...     code += f'    self.{name} = {name}\n'
...
>>> print(code)
def __init__(self, name,shares,price):
    self.name = name
    self.shares = shares
    self.price = price

>>> locs = { }
>>> exec(code, locs)
>>> Stock.__init__ = locs['__init__']

>>> ## Now try the class
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s.price
490.1

Neste exemplo mais complexo:

  1. Primeiro, definimos uma classe Stock com um atributo _fields. Este atributo é uma tupla que contém os nomes dos atributos da classe.
  2. Em seguida, criamos uma string que representa o código Python para um método __init__. Este método é usado para inicializar os atributos do objeto.
  3. Em seguida, usamos a função exec() para executar a string de código. Também passamos um dicionário vazio locs para exec(). A função resultante da execução é armazenada neste dicionário.
  4. Depois disso, atribuímos a função armazenada no dicionário como o método __init__ da nossa classe Stock.
  5. Finalmente, criamos uma instância da classe Stock e verificamos se o método __init__ funciona corretamente, acessando os atributos do objeto.

Este exemplo demonstra como a função exec() pode ser usada para criar dinamicamente métodos com base em dados disponíveis em tempo de execução.

Criando um Método init() Dinâmico

Agora, vamos aplicar o que aprendemos sobre a função exec() a um cenário de programação do mundo real. Em Python, a função exec() permite que você execute código Python armazenado em uma string. Nesta etapa, modificaremos a classe Structure para criar dinamicamente um método __init__(). O método __init__() é um método especial nas classes Python, que é chamado quando um objeto da classe é instanciado. Basearemos a criação deste método na variável de classe _fields, que contém uma lista de nomes de campos para a classe.

Primeiro, vamos dar uma olhada no arquivo structure.py existente. Este arquivo contém a implementação atual da classe Structure e uma classe Stock que herda dela. Para visualizar o conteúdo do arquivo, abra-o no WebIDE usando o seguinte comando:

cat /home/labex/project/structure.py

Na saída, você verá que a implementação atual usa uma abordagem manual para lidar com a inicialização de objetos. Isso significa que o código para inicializar os atributos do objeto é escrito explicitamente, em vez de ser gerado dinamicamente.

Agora, vamos modificar a classe Structure. Adicionaremos um método de classe create_init() que gerará dinamicamente o método __init__(). Para fazer essas alterações, abra o arquivo structure.py no editor WebIDE e siga estas etapas:

  1. Remova os métodos _init() e set_fields() existentes da classe Structure. Esses métodos fazem parte da abordagem de inicialização manual, e não precisaremos mais deles, pois usaremos uma abordagem dinâmica.

  2. Adicione o método de classe create_init() à classe Structure. Aqui está o código para o método:

@classmethod
def create_init(cls):
    """Dynamically create an __init__ method based on _fields."""
    ## Create argument string from field names
    argstr = ','.join(cls._fields)

    ## Create the function code as a string
    code = f'def __init__(self, {argstr}):\n'
    for name in cls._fields:
        code += f'    self.{name} = {name}\n'

    ## Execute the code and get the generated function
    locs = {}
    exec(code, locs)

    ## Set the function as the __init__ method of the class
    setattr(cls, '__init__', locs['__init__'])

Neste método, primeiro criamos uma string argstr que contém todos os nomes de campos separados por vírgulas. Esta string será usada como a lista de argumentos para o método __init__(). Em seguida, criamos o código para o método __init__() como uma string. Iteramos pelos nomes dos campos e adicionamos linhas ao código que atribuem cada argumento ao atributo de objeto correspondente. Depois disso, usamos a função exec() para executar o código e armazenar a função gerada no dicionário locs. Finalmente, usamos a função setattr() para definir a função gerada como o método __init__() da classe.

  1. Modifique a classe Stock para usar esta nova abordagem:
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

## Create the __init__ method for Stock
Stock.create_init()

Aqui, definimos o _fields para a classe Stock e, em seguida, chamamos o método create_init() para gerar o método __init__() para a classe Stock.

Seu arquivo structure.py completo agora deve ter uma aparência semelhante a esta:

class Structure:
    ## Restrict attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"No attribute {name}")

    ## String representation for debugging
    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f"{type(self).__name__}({args})"

    @classmethod
    def create_init(cls):
        """Dynamically create an __init__ method based on _fields."""
        ## Create argument string from field names
        argstr = ','.join(cls._fields)

        ## Create the function code as a string
        code = f'def __init__(self, {argstr}):\n'
        for name in cls._fields:
            code += f'    self.{name} = {name}\n'

        ## Execute the code and get the generated function
        locs = {}
        exec(code, locs)

        ## Set the function as the __init__ method of the class
        setattr(cls, '__init__', locs['__init__'])

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

## Create the __init__ method for Stock
Stock.create_init()

Agora, vamos testar nossa implementação para garantir que ela funcione corretamente. Executaremos o arquivo de teste unitário para verificar se todos os testes são aprovados. Use os seguintes comandos:

cd /home/labex/project
python3 -m unittest test_structure.py

Se sua implementação estiver correta, você deverá ver que todos os testes são aprovados. Isso significa que o método __init__() gerado dinamicamente está funcionando conforme o esperado.

Você também pode testar a classe manualmente no shell Python. Veja como você pode fazer isso:

>>> from structure import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s
Stock('GOOG', 100, 490.1)
>>> s.shares = 50
>>> s.share = 50  ## This should raise an AttributeError
Traceback (most recent call last):
  ...
AttributeError: No attribute share

No shell Python, primeiro importamos a classe Stock do arquivo structure.py. Em seguida, criamos uma instância da classe Stock e a imprimimos. Também podemos modificar o atributo shares do objeto. No entanto, quando tentamos definir um atributo que não existe na lista _fields, devemos obter um AttributeError.

Parabéns! Você usou com sucesso a função exec() para criar dinamicamente um método __init__() com base nos atributos da classe. Essa abordagem pode tornar seu código mais flexível e fácil de manter, especialmente ao lidar com classes que têm um número variável de atributos.

Examinando Como a Biblioteca Padrão do Python Usa exec()

Em Python, a biblioteca padrão é uma coleção poderosa de código pré-escrito que oferece várias funções e módulos úteis. Uma dessas funções é exec(), que pode ser usada para gerar e executar dinamicamente código Python. Gerar código dinamicamente significa criar código em tempo real durante a execução do programa, em vez de tê-lo codificado.

A função namedtuple do módulo collections é um exemplo bem conhecido na biblioteca padrão que usa exec(). Um namedtuple é um tipo especial de tupla que permite acessar seus elementos por nomes de atributos e índices. É uma ferramenta útil para criar classes simples de retenção de dados sem ter que escrever uma definição de classe completa.

Vamos explorar como namedtuple funciona e como ele usa exec() nos bastidores. Primeiro, abra seu shell Python. Você pode fazer isso executando o seguinte comando em seu terminal. Este comando inicia um interpretador Python onde você pode executar diretamente o código Python:

python3

Agora, vamos ver como usar a função namedtuple. O código a seguir demonstra como criar um namedtuple e acessar seus elementos:

>>> from collections import namedtuple
>>> Stock = namedtuple('Stock', ['name', 'shares', 'price'])
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s[1]  ## namedtuples also support indexing
100

No código acima, primeiro importamos a função namedtuple do módulo collections. Em seguida, criamos um novo tipo namedtuple chamado Stock com os campos name, shares e price. Criamos uma instância s do namedtuple Stock e acessamos seus elementos tanto por nomes de atributos (s.name, s.shares) quanto por índice (s[1]).

Agora, vamos dar uma olhada em como namedtuple é implementado. Podemos usar o módulo inspect para visualizar seu código-fonte. O módulo inspect fornece várias funções úteis para obter informações sobre objetos ativos, como módulos, classes, métodos, etc.

>>> import inspect
>>> from collections import namedtuple
>>> print(inspect.getsource(namedtuple))

Quando você executa este código, verá uma grande quantidade de código impressa. Se você olhar de perto, descobrirá que namedtuple usa a função exec() para criar dinamicamente uma classe. O que ele faz é construir uma string que contém o código Python para uma definição de classe. Em seguida, ele usa exec() para executar esta string como código Python.

Essa abordagem é muito poderosa porque permite que namedtuple crie classes com nomes de campos personalizados em tempo de execução. Os nomes dos campos são determinados pelos argumentos que você passa para a função namedtuple. Este é um exemplo do mundo real de como exec() pode ser usado para gerar código dinamicamente.

Aqui estão alguns pontos-chave a serem observados sobre a implementação de namedtuple:

  1. Ele usa formatação de string para construir uma definição de classe. A formatação de string é uma maneira de inserir valores em um modelo de string. No caso de namedtuple, ele usa isso para criar uma definição de classe com os nomes de campo corretos.
  2. Ele lida com a validação de nomes de campos. Isso significa que ele verifica se os nomes de campo que você fornece são identificadores Python válidos. Caso contrário, ele gerará um erro apropriado.
  3. Ele fornece recursos adicionais como docstrings e métodos. Docstrings são strings que documentam o propósito e o uso de uma classe ou função. namedtuple adiciona docstrings e métodos úteis às classes que ele cria.
  4. Ele executa o código gerado usando exec(). Esta é a etapa principal que transforma a string contendo a definição da classe em uma classe Python real.

Este padrão é semelhante ao que implementamos em nosso método create_init(), mas em um nível mais sofisticado. A implementação de namedtuple precisa lidar com cenários mais complexos e casos extremos para fornecer uma interface robusta e amigável.

Resumo

Neste laboratório, você aprendeu a usar a função exec() do Python para criar e executar dinamicamente código em tempo de execução. Os pontos-chave incluem o uso básico de exec() para executar fragmentos de código baseados em strings, o uso avançado para criar dinamicamente métodos de classe com base em atributos e sua aplicação no mundo real na biblioteca padrão do Python com namedtuple.

A capacidade de gerar código dinamicamente é um recurso poderoso que permite programas mais flexíveis e adaptáveis. Embora deva ser usado com cautela devido a preocupações de segurança e legibilidade, é uma ferramenta valiosa para programadores Python em cenários específicos, como a criação de APIs, a implementação de decoradores ou a construção de linguagens específicas de domínio. Você pode aplicar essas técnicas ao criar código que se adapta às condições de tempo de execução ou ao construir frameworks que geram código com base na configuração.