Aprenda Mais Sobre Closures

Beginner

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

Introdução

Neste laboratório, você aprenderá mais sobre closures (fechamentos) em Python. Closures são um conceito de programação poderoso que permite que funções se lembrem e acessem variáveis de seu escopo envolvente, mesmo após a função externa ter completado a execução.

Você também entenderá closures como uma estrutura de dados, explorará-os como um gerador de código e descobrirá como implementar a verificação de tipos com closures. Este laboratório o ajudará a descobrir alguns dos aspectos mais incomuns e poderosos dos closures em Python.

Closures como uma Estrutura de Dados

Em Python, closures oferecem uma maneira poderosa de encapsular dados. Encapsulamento significa manter os dados privados e controlar o acesso a eles. Com closures, você pode criar funções que gerenciam e modificam dados privados sem ter que usar classes ou variáveis globais. Variáveis globais podem ser acessadas e modificadas de qualquer lugar no seu código, o que pode levar a comportamentos inesperados. Classes, por outro lado, exigem uma estrutura mais complexa. Closures fornecem uma alternativa mais simples para encapsulamento de dados.

Vamos criar um arquivo chamado counter.py para demonstrar este conceito:

  1. Abra o WebIDE e crie um novo arquivo chamado counter.py no diretório /home/labex/project. É aqui que escreveremos o código que define nosso contador baseado em closure.

  2. Adicione o seguinte código ao arquivo:

def counter(value):
    """
    Cria um contador com funções de incremento e decremento.

    Args:
        value: Valor inicial do contador

    Returns:
        Duas funções: uma para incrementar o contador, outra para decrementá-lo
    """
    def incr():
        nonlocal value
        value += 1
        return value

    def decr():
        nonlocal value
        value -= 1
        return value

    return incr, decr

Neste código, definimos uma função chamada counter(). Esta função recebe um value inicial como argumento. Dentro da função counter(), definimos duas funções internas: incr() e decr(). Essas funções internas compartilham o acesso à mesma variável value. A palavra-chave nonlocal é usada para dizer ao Python que queremos modificar a variável value do escopo envolvente (a função counter()). Sem a palavra-chave nonlocal, o Python criaria uma nova variável local dentro das funções internas em vez de modificar o value do escopo externo.

  1. Agora, vamos criar um arquivo de teste para ver isso em ação. Crie um novo arquivo chamado test_counter.py com o seguinte conteúdo:
from counter import counter

## Cria um contador começando em 0
up, down = counter(0)

## Incrementa o contador várias vezes
print("Incrementando o contador:")
print(up())  ## Deve imprimir 1
print(up())  ## Deve imprimir 2
print(up())  ## Deve imprimir 3

## Decrementa o contador
print("\nDecrementando o contador:")
print(down())  ## Deve imprimir 2
print(down())  ## Deve imprimir 1

Neste arquivo de teste, primeiro importamos a função counter() do arquivo counter.py. Em seguida, criamos um contador começando em 0 chamando counter(0) e desempacotando as funções retornadas em up e down. Em seguida, chamamos a função up() várias vezes para incrementar o contador e imprimir os resultados. Depois disso, chamamos a função down() para decrementar o contador e imprimir os resultados.

  1. Execute o arquivo de teste executando o seguinte comando no terminal:
python3 test_counter.py

Você deve ver a seguinte saída:

Incrementando o contador:
1
2
3

Decrementando o contador:
2
1

Observe como não há nenhuma definição de classe envolvida aqui. As funções up() e down() estão manipulando um valor compartilhado que não é nem uma variável global nem um atributo de instância. Este valor é armazenado no closure, tornando-o acessível apenas às funções retornadas por counter().

Este é um exemplo de como closures podem ser usados como uma estrutura de dados. A variável encapsulada value é mantida entre as chamadas de função, e é privada para as funções que a acessam. Isso significa que nenhuma outra parte do seu código pode acessar ou modificar diretamente esta variável value, fornecendo um nível de proteção de dados.

Closures como um Gerador de Código

Nesta etapa, aprenderemos como closures podem ser usados para gerar código dinamicamente. Especificamente, construiremos um sistema de verificação de tipos para atributos de classe usando closures.

Primeiro, vamos entender o que são closures. Um closure é um objeto de função que lembra valores no escopo envolvente, mesmo que eles não estejam presentes na memória. Em Python, closures são criados quando uma função aninhada referencia um valor de sua função envolvente.

Agora, começaremos a implementar nosso sistema de verificação de tipos.

  1. Crie um novo arquivo chamado typedproperty.py no diretório /home/labex/project com o seguinte código:
## typedproperty.py

def typedproperty(name, expected_type):
    """
    Cria uma propriedade com verificação de tipo.

    Args:
        name: O nome da propriedade
        expected_type: O tipo esperado do valor da propriedade

    Returns:
        Um objeto de propriedade que realiza a verificação de tipo
    """
    private_name = '_' + name

    @property
    def value(self):
        return getattr(self, private_name)

    @value.setter
    def value(self, val):
        if not isinstance(val, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, val)

    return value

Neste código, a função typedproperty é um closure. Ela recebe dois argumentos: name e expected_type. O decorador @property é usado para criar um método getter para a propriedade, que recupera o valor do atributo privado. O decorador @value.setter cria um método setter que verifica se o valor que está sendo definido é do tipo esperado. Caso contrário, ele levanta um TypeError.

  1. Agora, vamos criar uma classe que usa essas propriedades tipadas. Crie um arquivo chamado stock.py com o seguinte código:
from typedproperty import typedproperty

class Stock:
    """Uma classe representando uma ação com atributos com verificação de tipo."""

    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

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

Na classe Stock, usamos a função typedproperty para criar atributos com verificação de tipo para name, shares e price. Quando criamos uma instância da classe Stock, a verificação de tipo será aplicada automaticamente.

  1. Vamos criar um arquivo de teste para ver isso em ação. Crie um arquivo chamado test_stock.py com o seguinte código:
from stock import Stock

## Cria uma ação com tipos corretos
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Tenta definir um atributo com o tipo errado
try:
    s.shares = "hundred"  ## Isso deve levantar um TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Neste arquivo de teste, primeiro criamos um objeto Stock com os tipos corretos. Em seguida, tentamos definir o atributo shares como uma string, o que deve levantar um TypeError porque o tipo esperado é um inteiro.

  1. Execute o arquivo de teste:
python3 test_stock.py

Você deve ver uma saída semelhante a:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>

Esta saída mostra que a verificação de tipo está funcionando corretamente.

  1. Agora, vamos aprimorar typedproperty.py adicionando funções de conveniência para tipos comuns. Adicione o seguinte código ao final do arquivo:
def String(name):
    """Cria uma propriedade string com verificação de tipo."""
    return typedproperty(name, str)

def Integer(name):
    """Cria uma propriedade inteira com verificação de tipo."""
    return typedproperty(name, int)

def Float(name):
    """Cria uma propriedade float com verificação de tipo."""
    return typedproperty(name, float)

Essas funções são apenas wrappers (invólucros) em torno da função typedproperty, tornando mais fácil criar propriedades de tipos comuns.

  1. Crie um novo arquivo chamado stock_enhanced.py que usa essas funções de conveniência:
from typedproperty import String, Integer, Float

class Stock:
    """Uma classe representando uma ação com atributos com verificação de tipo."""

    name = String('name')
    shares = Integer('shares')
    price = Float('price')

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

Esta classe Stock usa as funções de conveniência para criar atributos com verificação de tipo, o que torna o código mais legível.

  1. Crie um arquivo de teste test_stock_enhanced.py para testar a versão aprimorada:
from stock_enhanced import Stock

## Cria uma ação com tipos corretos
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Tenta definir um atributo com o tipo errado
try:
    s.price = "490.1"  ## Isso deve levantar um TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Este arquivo de teste é semelhante ao anterior, mas testa a classe Stock aprimorada.

  1. Execute o teste:
python3 test_stock_enhanced.py

Você deve ver uma saída semelhante a:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>

Nesta etapa, demonstramos como closures podem ser usados para gerar código. A função typedproperty cria objetos de propriedade que realizam a verificação de tipo, e as funções String, Integer e Float criam propriedades especializadas para tipos comuns.

Eliminando Nomes de Propriedades com Descriptors

Na etapa anterior, ao criar propriedades tipadas, tivemos que declarar explicitamente os nomes das propriedades. Isso é redundante porque os nomes das propriedades já estão especificados na definição da classe. Nesta etapa, usaremos descriptors para nos livrarmos dessa redundância.

Um descriptor em Python é um objeto especial que controla como o acesso a atributos funciona. Quando você implementa o método __set_name__ em um descriptor, ele pode obter automaticamente o nome do atributo da definição da classe.

Vamos começar criando um novo arquivo.

  1. Crie um novo arquivo chamado improved_typedproperty.py com o seguinte código:
## improved_typedproperty.py

class TypedProperty:
    """
    Um descriptor que realiza a verificação de tipo.

    Este descriptor captura automaticamente o nome do atributo da definição da classe.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None

    def __set_name__(self, owner, name):
        ## Este método é chamado quando o descriptor é atribuído a um atributo de classe
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Expected {self.expected_type}')
        instance.__dict__[self.name] = value

## Funções de conveniência
def String():
    """Cria uma propriedade string com verificação de tipo."""
    return TypedProperty(str)

def Integer():
    """Cria uma propriedade inteira com verificação de tipo."""
    return TypedProperty(int)

def Float():
    """Cria uma propriedade float com verificação de tipo."""
    return TypedProperty(float)

Este código define uma classe descriptor chamada TypedProperty que verifica o tipo de valores atribuídos aos atributos. O método __set_name__ é chamado automaticamente quando o descriptor é atribuído a um atributo de classe. Isso permite que o descriptor capture o nome do atributo sem que precisemos especificá-lo manualmente.

Em seguida, criaremos uma classe que usa essas propriedades tipadas aprimoradas.

  1. Crie um novo arquivo chamado stock_improved.py que usa as propriedades tipadas aprimoradas:
from improved_typedproperty import String, Integer, Float

class Stock:
    """Uma classe representando uma ação com atributos com verificação de tipo."""

    ## Não há necessidade de especificar nomes de propriedades
    name = String()
    shares = Integer()
    price = Float()

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

Observe que não precisamos especificar os nomes das propriedades ao criar as propriedades tipadas. O descriptor obterá automaticamente o nome do atributo da definição da classe.

Agora, vamos testar nossa classe aprimorada.

  1. Crie um arquivo de teste test_stock_improved.py para testar a versão aprimorada:
from stock_improved import Stock

## Cria uma ação com tipos corretos
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Tenta definir atributos com tipos errados
try:
    s.name = 123  ## Deve levantar TypeError
    print("Name type check failed")
except TypeError as e:
    print(f"Name type check succeeded: {e}")

try:
    s.shares = "hundred"  ## Deve levantar TypeError
    print("Shares type check failed")
except TypeError as e:
    print(f"Shares type check succeeded: {e}")

try:
    s.price = "490.1"  ## Deve levantar TypeError
    print("Price type check failed")
except TypeError as e:
    print(f"Price type check succeeded: {e}")

Finalmente, executaremos o teste para ver se tudo funciona como esperado.

  1. Execute o teste:
python3 test_stock_improved.py

Você deve ver uma saída semelhante a:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>

Nesta etapa, tornamos nosso sistema de verificação de tipos melhor usando descriptors e o método __set_name__. Isso elimina a especificação redundante do nome da propriedade, tornando o código mais curto e menos propenso a erros.

O método __set_name__ é um recurso muito útil dos descriptors. Ele permite que eles coletem automaticamente informações sobre como são usados em uma definição de classe. Isso pode ser usado para criar APIs que são mais fáceis de entender e usar.

Resumo

Neste laboratório, você aprendeu sobre aspectos avançados de closures em Python. Primeiro, você explorou o uso de closures como uma estrutura de dados, que pode encapsular dados e permitir que funções mantenham o estado entre chamadas sem depender de classes ou variáveis globais. Em segundo lugar, você viu como os closures podem atuar como um gerador de código, gerando objetos de propriedade com verificação de tipo para uma abordagem mais funcional à validação de atributos.

Você também descobriu como usar o protocolo descriptor e o método __set_name__ para criar atributos elegantes de verificação de tipo que capturam automaticamente seus nomes das definições de classe. Essas técnicas demonstram o poder e a flexibilidade dos closures, permitindo que você implemente comportamentos complexos de forma concisa. Compreender closures e descriptors oferece mais ferramentas para criar código Python sustentável e robusto.