Convenções de Passagem de Argumentos de Funções

Beginner

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

Introdução

Neste laboratório, você aprenderá sobre as convenções de passagem de argumentos de funções Python. Você também criará uma estrutura reutilizável para classes de dados e aplicará princípios de design orientado a objetos para simplificar seu código.

Este exercício visa reescrever o arquivo stock.py de uma maneira mais organizada. Antes de começar, copie seu trabalho existente em stock.py para um novo arquivo chamado orig_stock.py para referência. Os arquivos que você criará são structure.py e stock.py.

Compreendendo a Passagem de Argumentos de Funções

Em Python, as funções são um conceito fundamental que permite agrupar um conjunto de instruções para realizar uma tarefa específica. Ao chamar uma função, você frequentemente precisa fornecer alguns dados, que chamamos de argumentos. Python oferece diferentes maneiras de passar esses argumentos para as funções. Essa flexibilidade é incrivelmente útil, pois ajuda a escrever um código mais limpo e fácil de manter. Antes de começarmos a aplicar essas técnicas ao nosso projeto, vamos dar uma olhada mais de perto nessas convenções de passagem de argumentos.

Criando um Backup do Seu Trabalho

Antes de começarmos a fazer alterações no nosso arquivo stock.py, é uma boa prática criar um backup. Dessa forma, se algo der errado durante nossa experimentação, sempre podemos voltar à versão original. Para criar um backup, abra um terminal e execute o seguinte comando:

cp stock.py orig_stock.py

Este comando usa o comando cp (copy) no terminal. Ele pega o arquivo stock.py e cria uma cópia dele chamada orig_stock.py. Ao fazer isso, garantimos que nosso trabalho original seja preservado com segurança.

Explorando a Passagem de Argumentos de Funções

Em Python, existem várias maneiras de chamar funções com diferentes tipos de argumentos. Vamos explorar cada um desses métodos em detalhes.

1. Argumentos Posicionais

A maneira mais simples de passar argumentos para uma função é por posição. Ao definir uma função, você especifica uma lista de parâmetros. Ao chamar a função, você fornece valores para esses parâmetros na mesma ordem em que são definidos.

Aqui está um exemplo:

def calculate(x, y, z):
    return x + y + z

## Chamada com argumentos posicionais
result = calculate(1, 2, 3)
print(result)  ## Output: 6

Neste exemplo, a função calculate recebe três parâmetros: x, y e z. Quando chamamos a função com calculate(1, 2, 3), o valor 1 é atribuído a x, 2 é atribuído a y e 3 é atribuído a z. A função então soma esses valores e retorna o resultado.

2. Argumentos de Palavra-chave (Keyword Arguments)

Além dos argumentos posicionais, você também pode especificar argumentos por seus nomes. Isso é chamado de uso de argumentos de palavra-chave. Quando você usa argumentos de palavra-chave, não precisa se preocupar com a ordem dos argumentos.

Aqui está um exemplo:

## Chamada com uma mistura de argumentos posicionais e de palavra-chave
result = calculate(1, z=3, y=2)
print(result)  ## Output: 6

Neste exemplo, primeiro passamos o argumento posicional 1 para x. Em seguida, usamos argumentos de palavra-chave para especificar os valores para y e z. A ordem dos argumentos de palavra-chave não importa, desde que você forneça os nomes corretos.

3. Desempacotando Sequências e Dicionários

Python fornece uma maneira conveniente de passar sequências e dicionários como argumentos usando a sintaxe * e **. Isso é chamado de desempacotamento (unpacking).

Aqui está um exemplo de desempacotamento de uma tupla em argumentos posicionais:

## Desempacotando uma tupla em argumentos posicionais
args = (1, 2, 3)
result = calculate(*args)
print(result)  ## Output: 6

Neste exemplo, temos uma tupla args que contém os valores 1, 2 e 3. Quando usamos o operador * antes de args na chamada da função, Python desempacota a tupla e passa seus elementos como argumentos posicionais para a função calculate.

Aqui está um exemplo de desempacotamento de um dicionário em argumentos de palavra-chave:

## Desempacotando um dicionário em argumentos de palavra-chave
kwargs = {'y': 2, 'z': 3}
result = calculate(1, **kwargs)
print(result)  ## Output: 6

Neste exemplo, temos um dicionário kwargs que contém os pares chave-valor 'y': 2 e 'z': 3. Quando usamos o operador ** antes de kwargs na chamada da função, Python desempacota o dicionário e passa seus pares chave-valor como argumentos de palavra-chave para a função calculate.

4. Aceitando Argumentos Variáveis

Às vezes, você pode querer definir uma função que pode aceitar qualquer número de argumentos. Python permite que você faça isso usando a sintaxe * e ** na definição da função.

Aqui está um exemplo de uma função que aceita qualquer número de argumentos posicionais:

## Aceitar qualquer número de argumentos posicionais
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2))           ## Output: 3
print(sum_all(1, 2, 3, 4, 5))  ## Output: 15

Neste exemplo, a função sum_all usa o parâmetro *args para aceitar qualquer número de argumentos posicionais. O operador * coleta todos os argumentos posicionais em uma tupla chamada args. A função então usa a função sum embutida para somar todos os elementos na tupla.

Aqui está um exemplo de uma função que aceita qualquer número de argumentos de palavra-chave:

## Aceitar qualquer número de argumentos de palavra-chave
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Python", year=1991)
## Output:
## name: Python
## year: 1991

Neste exemplo, a função print_info usa o parâmetro **kwargs para aceitar qualquer número de argumentos de palavra-chave. O operador ** coleta todos os argumentos de palavra-chave em um dicionário chamado kwargs. A função então itera sobre os pares chave-valor no dicionário e os imprime.

Essas técnicas nos ajudarão a criar estruturas de código mais flexíveis e reutilizáveis nos próximos passos. Para se sentir mais confortável com esses conceitos, vamos abrir o interpretador Python e experimentar alguns desses exemplos.

python3

Depois de estar no interpretador Python, tente inserir os exemplos acima. Isso lhe dará experiência prática com essas técnicas de passagem de argumentos.

Criando uma Classe Base de Estrutura

Agora que temos uma boa compreensão da passagem de argumentos de funções, vamos criar uma classe base reutilizável para estruturas de dados. Esta etapa é crucial porque nos ajuda a evitar escrever o mesmo código repetidamente quando criamos classes simples que contêm dados. Ao usar uma classe base, podemos simplificar nosso código e torná-lo mais eficiente.

O Problema com Código Repetitivo

Nos exercícios anteriores, você definiu uma classe Stock como mostrado abaixo:

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

Dê uma olhada no método __init__. Você notará que ele é bastante repetitivo. Você tem que atribuir manualmente cada atributo um por um. Isso pode se tornar muito tedioso e demorado, especialmente quando você tem muitas classes com um grande número de atributos.

Criando uma Classe Base Flexível

Vamos criar uma classe base Structure que pode lidar automaticamente com a atribuição de atributos. Primeiro, abra o WebIDE e crie um novo arquivo chamado structure.py. Em seguida, adicione o seguinte código a este arquivo:

## structure.py

class Structure:
    """
    A base class for creating simple data structures.
    Automatically populates object attributes from _fields and constructor arguments.
    """
    _fields = ()

    def __init__(self, *args):
        ## Check that the number of arguments matches the number of fields
        if len(args) != len(self._fields):
            raise TypeError(f"Expected {len(self._fields)} arguments")

        ## Set the attributes
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

Esta classe base tem vários recursos importantes:

  1. Ela define uma variável de classe _fields. Por padrão, essa variável está vazia. Essa variável conterá os nomes dos atributos que a classe terá.
  2. Ela verifica se o número de argumentos passados para o construtor corresponde ao número de campos definidos em _fields. Se não corresponderem, ela levanta um TypeError. Isso nos ajuda a detectar erros no início.
  3. Ela define os atributos do objeto usando os nomes dos campos e os valores fornecidos como argumentos. A função setattr é usada para definir dinamicamente os atributos.

Testando Nossa Classe Base de Estrutura

Agora, vamos criar algumas classes de exemplo que herdam da classe base Structure. Adicione o seguinte código ao seu arquivo structure.py:

## Example classes using Structure
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

class Point(Structure):
    _fields = ('x', 'y')

class Date(Structure):
    _fields = ('year', 'month', 'day')

Para testar se nossa implementação funciona corretamente, criaremos um arquivo de teste chamado test_structure.py. Adicione o seguinte código a este arquivo:

## test_structure.py
from structure import Stock, Point, Date

## Test Stock class
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}, shares: {s.shares}, price: {s.price}")

## Test Point class
p = Point(3, 4)
print(f"Point coordinates: ({p.x}, {p.y})")

## Test Date class
d = Date(2023, 11, 9)
print(f"Date: {d.year}-{d.month}-{d.day}")

## Test error handling
try:
    s2 = Stock('AAPL', 50)  ## Missing price argument
    print("This should not print")
except TypeError as e:
    print(f"Error correctly caught: {e}")

Para executar o teste, abra seu terminal e execute o seguinte comando:

python3 test_structure.py

Você deve ver a seguinte saída:

Stock name: GOOG, shares: 100, price: 490.1
Point coordinates: (3, 4)
Date: 2023-11-9
Error correctly caught: Expected 3 arguments

Como você pode ver, nossa classe base está funcionando como esperado. Tornou muito mais fácil definir novas estruturas de dados sem ter que escrever o mesmo código boilerplate repetidamente.

Melhorando a Representação de Objetos

Nossa classe Structure é útil para criar e acessar objetos. No entanto, ela atualmente não tem uma boa maneira de se representar como uma string. Quando você imprime um objeto ou o visualiza no interpretador Python, você quer ver uma exibição clara e informativa. Isso ajuda você a entender o que o objeto é e quais são seus valores.

Compreendendo a Representação de Objetos em Python

Em Python, existem dois métodos especiais que são usados para representar objetos de diferentes maneiras. Esses métodos são importantes porque permitem que você controle como seus objetos são exibidos.

  • __str__ - Este método é usado pela função str() e pela função print(). Ele fornece uma representação legível por humanos do objeto. Por exemplo, se você tiver um objeto Stock, o método __str__ pode retornar algo como "Stock: GOOG, 100 shares at $490.1".
  • __repr__ - Este método é usado pelo interpretador Python e pela função repr(). Ele fornece uma representação mais técnica e inequívoca do objeto. O objetivo de __repr__ é fornecer uma string que possa ser usada para recriar o objeto. Por exemplo, para um objeto Stock, ele pode retornar "Stock('GOOG', 100, 490.1)".

Vamos adicionar um método __repr__ à nossa classe Structure. Isso tornará mais fácil depurar nosso código porque podemos ver claramente o estado de nossos objetos.

Implementando uma Boa Representação

Agora, você precisa atualizar seu arquivo structure.py. Você adicionará o método __repr__ à classe Structure. Este método criará uma string que representa o objeto de uma forma que pode ser usada para recriá-lo.

def __repr__(self):
    """
    Return a representation of the object that can be used to recreate it.
    Example: Stock('GOOG', 100, 490.1)
    """
    ## Get the class name
    cls_name = type(self).__name__

    ## Get all the field values
    values = [getattr(self, name) for name in self._fields]

    ## Format the fields and values
    args_str = ', '.join(repr(value) for value in values)

    ## Return the formatted string
    return f"{cls_name}({args_str})"

Aqui está o que este método faz passo a passo:

  1. Ele obtém o nome da classe usando type(self).__name__. Isso é importante porque informa que tipo de objeto você está lidando.
  2. Ele recupera todos os valores dos campos da instância. Isso fornece os dados que o objeto contém.
  3. Ele cria uma representação de string com o nome da classe e os valores. Essa string pode ser usada para recriar o objeto.

Testando a Representação Melhorada

Vamos testar nossa implementação aprimorada. Crie um novo arquivo chamado test_repr.py. Este arquivo criará algumas instâncias de nossas classes e imprimirá suas representações.

## test_repr.py
from structure import Stock, Point, Date

## Create instances
s = Stock('GOOG', 100, 490.1)
p = Point(3, 4)
d = Date(2023, 11, 9)

## Print the representations
print(repr(s))
print(repr(p))
print(repr(d))

## Direct printing also uses __repr__ in the interpreter
print(s)
print(p)
print(d)

Para executar o teste, abra seu terminal e digite o seguinte comando:

python3 test_repr.py

Você deve ver a seguinte saída:

Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)
Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)

Esta saída é muito mais informativa do que antes. Quando você vê Stock('GOOG', 100, 490.1), você sabe imediatamente o que o objeto representa. Você pode até copiar esta string e usá-la para recriar o objeto em seu código.

O Benefício de Boas Representações

Uma boa implementação de __repr__ é muito útil para depuração. Quando você está olhando para objetos no interpretador ou registrando-os durante a execução do programa, uma representação clara facilita a identificação de problemas rapidamente. Você pode ver o estado exato do objeto e entender o que pode estar dando errado.

Restringindo Nomes de Atributos

Atualmente, nossa classe Structure permite que qualquer atributo seja definido em suas instâncias. Para iniciantes, isso pode parecer conveniente a princípio, mas na verdade pode levar a muitos problemas. Quando você está trabalhando com uma classe, você espera que certos atributos estejam presentes e sejam usados de uma maneira específica. Se os usuários digitarem incorretamente os nomes dos atributos ou tentarem definir atributos que não faziam parte do design original, isso pode causar erros difíceis de encontrar.

A Necessidade de Restrição de Atributos

Vamos analisar um cenário simples para entender por que precisamos restringir os nomes dos atributos. Considere o seguinte código:

s = Stock('GOOG', 100, 490.1)
s.shares = 50      ## Correct attribute name
s.share = 60       ## Typo in attribute name - creates a new attribute instead of updating

Na segunda linha, há um erro de digitação. Em vez de shares, escrevemos share. Em Python, em vez de gerar um erro, ele simplesmente criará um novo atributo chamado share. Isso pode levar a bugs sutis porque você pode pensar que está atualizando o atributo shares, mas na verdade está criando um novo. Isso pode fazer com que seu código se comporte de forma inesperada e seja muito difícil de depurar.

Implementando a Restrição de Atributos

Para resolver esse problema, podemos substituir o método __setattr__. Este método é chamado toda vez que você tenta definir um atributo em um objeto. Ao substituí-lo, podemos controlar quais atributos podem ser definidos e quais não podem.

Atualize sua classe Structure em structure.py com o seguinte código:

def __setattr__(self, name, value):
    """
    Restrict attribute setting to only those defined in _fields
    or attributes starting with underscore (private attributes).
    """
    if name.startswith('_'):
        ## Allow setting private attributes (starting with '_')
        super().__setattr__(name, value)
    elif name in self._fields:
        ## Allow setting attributes defined in _fields
        super().__setattr__(name, value)
    else:
        ## Raise an error for other attributes
        raise AttributeError(f'No attribute {name}')

Veja como este método funciona:

  1. Se o nome do atributo começar com um sublinhado (_), ele é considerado um atributo privado. Atributos privados são frequentemente usados para fins internos em uma classe. Permitimos que esses atributos sejam definidos porque eles fazem parte da implementação interna da classe.
  2. Se o nome do atributo estiver na lista _fields, isso significa que é um dos atributos definidos no design da classe. Permitimos que esses atributos sejam definidos porque eles fazem parte do comportamento esperado da classe.
  3. Se o nome do atributo não atender a nenhuma dessas condições, geramos um AttributeError. Isso informa ao usuário que ele está tentando definir um atributo que não existe na classe.

Testando a Restrição de Atributos

Agora que implementamos a restrição de atributos, vamos testá-la para garantir que funcione conforme o esperado. Crie um arquivo chamado test_attributes.py com o seguinte código:

## test_attributes.py
from structure import Stock

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

## This should work - valid attribute
print("Setting shares to 50")
s.shares = 50
print(f"Shares is now: {s.shares}")

## This should work - private attribute
print("\nSetting _internal_data")
s._internal_data = "Some data"
print(f"_internal_data is: {s._internal_data}")

## This should fail - invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.share = 60  ## Typo in attribute name
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

Para executar o teste, abra seu terminal e digite o seguinte comando:

python3 test_attributes.py

Você deve ver a seguinte saída:

Setting shares to 50
Shares is now: 50

Setting _internal_data
_internal_data is: Some data

Trying to set an invalid attribute:
Error correctly caught: No attribute share

Esta saída mostra que nossa classe agora impede erros acidentais de atributos. Ele nos permite definir atributos válidos e atributos privados, mas gera um erro quando tentamos definir um atributo inválido.

O Valor da Restrição de Atributos

Restringir nomes de atributos é muito importante para escrever um código robusto e sustentável. Veja o porquê:

  1. Ajuda a detectar erros de digitação nos nomes dos atributos. Se você cometer um erro ao digitar um nome de atributo, o código gerará um erro em vez de criar um novo atributo. Isso facilita a localização e a correção de erros no início do processo de desenvolvimento.
  2. Impede tentativas de definir atributos que não existem no design da classe. Isso garante que a classe seja usada conforme o pretendido e que o código se comporte de forma previsível.
  3. Evita a criação acidental de novos atributos. A criação de novos atributos pode levar a um comportamento inesperado e tornar o código mais difícil de entender e manter.

Ao restringir os nomes dos atributos, tornamos nosso código mais confiável e mais fácil de trabalhar.

Reescrevendo a Classe Stock

Agora que temos uma classe base Structure bem definida, é hora de reescrever nossa classe Stock. Ao usar esta classe base, podemos simplificar nosso código e torná-lo mais organizado. A classe Structure fornece um conjunto de funcionalidades comuns que podemos reutilizar em nossa classe Stock, o que é uma grande vantagem para a manutenibilidade e legibilidade do código.

Criando a Nova Classe Stock

Vamos começar criando um novo arquivo chamado stock.py. Este arquivo conterá nossa classe Stock reescrita. Aqui está o código que você precisa colocar no arquivo stock.py:

## stock.py
from structure import Structure

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

    @property
    def cost(self):
        """
        Calculate the cost as shares * price
        """
        return self.shares * self.price

    def sell(self, nshares):
        """
        Sell a number of shares
        """
        self.shares -= nshares

Vamos detalhar o que esta nova classe Stock faz:

  1. Ela herda da classe Structure. Isso significa que a classe Stock pode usar todos os recursos fornecidos pela classe Structure. Um dos benefícios é que não precisamos escrever um método __init__ nós mesmos, porque a classe Structure cuida da atribuição de atributos automaticamente.
  2. Definimos _fields, que é uma tupla que especifica os atributos da classe Stock. Esses atributos são name, shares e price.
  3. A propriedade cost é definida para calcular o custo total da ação. Ela multiplica o número de shares pelo price.
  4. O método sell é usado para reduzir o número de ações. Quando você chama este método com um número de ações para vender, ele subtrai esse número do número atual de ações.

Testando a Nova Classe Stock

Para garantir que nossa nova classe Stock funcione conforme o esperado, precisamos criar um arquivo de teste. Vamos criar um arquivo chamado test_stock.py com o seguinte código:

## test_stock.py
from stock import Stock

## Create a stock
s = Stock('GOOG', 100, 490.1)

## Check the attributes
print(f"Stock: {s}")
print(f"Name: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost}")

## Sell some shares
print("\nSelling 20 shares...")
s.sell(20)
print(f"Shares after selling: {s.shares}")
print(f"Cost after selling: {s.cost}")

## Try to set an invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.prices = 500  ## Invalid attribute (should be 'price')
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

Neste arquivo de teste, primeiro importamos a classe Stock do arquivo stock.py. Em seguida, criamos uma instância da classe Stock com o nome 'GOOG', 100 ações e um preço de 490,1. Imprimimos os atributos da ação para verificar se eles estão definidos corretamente. Depois disso, vendemos 20 ações e imprimimos o novo número de ações e o novo custo. Finalmente, tentamos definir um atributo inválido prices (deveria ser price). Se nossa classe Stock estiver funcionando corretamente, ela deverá gerar um AttributeError.

Para executar o teste, abra seu terminal e digite o seguinte comando:

python3 test_stock.py

A saída esperada é a seguinte:

Stock: Stock('GOOG', 100, 490.1)
Name: GOOG
Shares: 100
Price: 490.1
Cost: 49010.0

Selling 20 shares...
Shares after selling: 80
Cost after selling: 39208.0

Trying to set an invalid attribute:
Error correctly caught: No attribute prices

Executando Testes de Unidade

Se você tiver testes de unidade de exercícios anteriores, poderá executá-los em sua nova implementação. Em seu terminal, digite o seguinte comando:

python3 teststock.py

Observe que alguns testes podem falhar. Isso pode ser porque eles esperam comportamentos ou métodos específicos que ainda não implementamos. Não se preocupe com isso! Continuaremos a construir sobre essa base em exercícios futuros.

Revisão de Nosso Progresso

Vamos dedicar um momento para revisar o que alcançamos até agora:

  1. Criamos uma classe base Structure reutilizável. Esta classe:

    • Lida automaticamente com a atribuição de atributos, o que nos poupa de escrever muito código repetitivo.
    • Fornece uma boa representação de string, tornando mais fácil imprimir e depurar nossos objetos.
    • Restringe os nomes dos atributos para evitar erros, o que torna nosso código mais robusto.
  2. Reescrevemos nossa classe Stock. Ela:

    • Hereda da classe Structure para reutilizar a funcionalidade comum.
    • Define apenas os campos e métodos específicos do domínio, o que mantém a classe focada e limpa.
    • Tem um design claro e simples, tornando-o fácil de entender e manter.

Essa abordagem tem vários benefícios para nosso código:

  • É mais sustentável porque temos menos repetição. Se precisarmos alterar algo na funcionalidade comum, só precisamos alterá-lo na classe Structure.
  • É mais robusto por causa da melhor verificação de erros fornecida pela classe Structure.
  • É mais legível porque as responsabilidades de cada classe são claras.

Em exercícios futuros, continuaremos a construir sobre essa base para criar um sistema de gerenciamento de portfólio de ações mais sofisticado.

Resumo

Neste laboratório, você aprendeu sobre as convenções de passagem de argumentos de função em Python e as aplicou para construir uma base de código mais organizada e sustentável. Você explorou os mecanismos flexíveis de passagem de argumentos do Python, criou uma classe base Structure reutilizável para objetos de dados e aprimorou a representação de objetos para melhor depuração.

Você também adicionou validação de atributos para evitar erros comuns e reescreveu a classe Stock usando a nova estrutura. Essas técnicas demonstram os principais princípios de design orientado a objetos, como herança para reutilização de código, encapsulamento para integridade de dados e polimorfismo por meio de interfaces comuns. Ao aplicar esses princípios, você pode desenvolver um código mais robusto e sustentável com menos repetição e menos erros.