Introdução
Neste laboratório, você aprenderá como retornar múltiplos valores de funções em Python. Você também entenderá valores de retorno opcionais e como lidar com erros de forma eficaz.
Além disso, você explorará o conceito de Futures para programação concorrente. Embora retornar um valor possa parecer simples, diferentes cenários de programação apresentam vários padrões e considerações.
Retornando Múltiplos Valores de Funções
Em Python, quando você precisa que uma função retorne mais de um valor, existe uma solução prática: retornar uma tupla. Uma tupla é um tipo de estrutura de dados em Python. É uma sequência imutável, o que significa que, uma vez criada uma tupla, você não pode alterar seus elementos. Tuplas são úteis porque podem conter múltiplos valores de diferentes tipos, todos em um só lugar.
Vamos criar uma função para analisar linhas de configuração no formato nome=valor. O objetivo desta função é receber uma linha nesse formato e retornar tanto o nome quanto o valor como itens separados.
- Primeiro, você precisa criar um novo arquivo Python. Este arquivo conterá o código para nossa função e o código de teste. No diretório do projeto, crie um arquivo chamado
return_values.py. Você pode usar o seguinte comando no terminal para criar este arquivo:
touch ~/project/return_values.py
- Agora, abra o arquivo
return_values.pyem seu editor de código. Dentro deste arquivo, escreveremos a funçãoparse_line. Esta função recebe uma linha como entrada, a divide no primeiro sinal '=', e retorna o nome e o valor como uma tupla.
def parse_line(line):
"""
Analisa uma linha no formato 'nome=valor' e retorna tanto o nome quanto o valor.
Args:
line (str): Linha de entrada para analisar no formato 'nome=valor'
Returns:
tuple: Uma tupla contendo (nome, valor)
"""
parts = line.split('=', 1) ## Divide no primeiro sinal de igual
if len(parts) == 2:
name = parts[0]
value = parts[1]
return (name, value) ## Retorna como uma tupla
Nesta função, o método split é usado para dividir a linha de entrada em duas partes no primeiro sinal '='. Se a linha estiver no formato nome=valor correto, extraímos o nome e o valor e os retornamos como uma tupla.
- Após definir a função, precisamos adicionar algum código de teste para ver se a função funciona como esperado. O código de teste chamará a função
parse_linecom uma entrada de amostra e imprimirá os resultados.
## Teste a função parse_line
if __name__ == "__main__":
result = parse_line('email=guido@python.org')
print(f"Result as tuple: {result}")
## Desempacotando a tupla em variáveis separadas
name, value = parse_line('email=guido@python.org')
print(f"Unpacked name: {name}")
print(f"Unpacked value: {value}")
No código de teste, primeiro chamamos a função parse_line e armazenamos a tupla retornada na variável result. Em seguida, imprimimos esta tupla. Depois, usamos o desempacotamento de tupla para atribuir diretamente os elementos da tupla às variáveis name e value e imprimimos-os separadamente.
- Depois de escrever a função e o código de teste, salve o arquivo
return_values.py. Em seguida, abra o terminal e execute o seguinte comando para executar o script Python:
python ~/project/return_values.py
Você deve ver uma saída semelhante a:
Result as tuple: ('email', 'guido@python.org')
Unpacked name: email
Unpacked value: guido@python.org
Explicação:
- A função
parse_linedivide a string de entrada no caractere '=' usando o métodosplit. Este método divide a string em partes com base no separador especificado. - Ela retorna ambas as partes como uma tupla usando a sintaxe
return (name, value). Uma tupla é uma maneira de agrupar múltiplos valores juntos. - Ao chamar a função, você tem duas opções. Você pode armazenar toda a tupla em uma variável, como fizemos com a variável
result. Ou você pode "desempacotar" a tupla diretamente em variáveis separadas usando a sintaxename, value = parse_line(...). Isso facilita o trabalho com os valores individuais.
Este padrão de retornar múltiplos valores como uma tupla é muito comum em Python. Ele torna as funções mais versáteis porque elas podem fornecer mais de uma informação ao código que as chama.
Retornando Valores Opcionais
Em programação, há momentos em que uma função pode não ser capaz de gerar um resultado válido. Por exemplo, quando uma função deve extrair informações específicas de uma entrada, mas a entrada não tem o formato esperado. Em Python, uma maneira comum de lidar com essas situações é retornar None. None é um valor especial em Python que indica a ausência de um valor de retorno válido.
Vamos dar uma olhada em como podemos modificar uma função para lidar com casos em que a entrada não atende aos critérios esperados. Trabalharemos na função parse_line, que foi projetada para analisar uma linha no formato 'nome=valor' e retornar tanto o nome quanto o valor.
- Atualize a função
parse_lineem seu arquivoreturn_values.py:
def parse_line(line):
"""
Analisa uma linha no formato 'nome=valor' e retorna tanto o nome quanto o valor.
Se a linha não estiver no formato correto, retorna None.
Args:
line (str): Linha de entrada para analisar no formato 'nome=valor'
Returns:
tuple or None: Uma tupla contendo (nome, valor) ou None se a análise falhar
"""
parts = line.split('=', 1) ## Divide no primeiro sinal de igual
if len(parts) == 2:
name = parts[0]
value = parts[1]
return (name, value) ## Retorna como uma tupla
else:
return None ## Retorna None para entrada inválida
Nesta função parse_line atualizada, primeiro dividimos a linha de entrada no primeiro sinal de igual usando o método split. Se a lista resultante tiver exatamente dois elementos, significa que a linha está no formato 'nome=valor' correto. Em seguida, extraímos o nome e o valor e os retornamos como uma tupla. Se a lista não tiver dois elementos, significa que a entrada é inválida e retornamos None.
- Adicione código de teste para demonstrar a função atualizada:
## Teste a função parse_line atualizada
if __name__ == "__main__":
## Entrada válida
result1 = parse_line('email=guido@python.org')
print(f"Valid input result: {result1}")
## Entrada inválida
result2 = parse_line('invalid_line_without_equals_sign')
print(f"Invalid input result: {result2}")
## Verificando None antes de usar o resultado
test_line = 'user_info'
result = parse_line(test_line)
if result is None:
print(f"Could not parse the line: '{test_line}'")
else:
name, value = result
print(f"Name: {name}, Value: {value}")
Este código de teste chama a função parse_line com entradas válidas e inválidas. Em seguida, ele imprime os resultados. Observe que, ao usar o resultado da função parse_line, primeiro verificamos se é None. Isso é importante porque, se tentarmos desempacotar um valor None como se fosse uma tupla, obteremos um erro.
- Salve o arquivo e execute-o:
python ~/project/return_values.py
Ao executar o script, você deve ver uma saída semelhante a:
Valid input result: ('email', 'guido@python.org')
Invalid input result: None
Could not parse the line: 'user_info'
Explicação:
- A função agora verifica se a linha contém um sinal de igual. Isso é feito dividindo a linha no sinal de igual e verificando o comprimento da lista resultante.
- Se a linha não contiver um sinal de igual, ela retorna
Nonepara indicar que a análise falhou. - Ao usar tal função, é importante verificar se o resultado é
Noneantes de tentar usá-lo. Caso contrário, você pode encontrar erros ao tentar acessar elementos de um valorNone.
Discussão de Design: Uma abordagem alternativa para lidar com entrada inválida é lançar uma exceção. Essa abordagem é adequada em certas situações:
- Entrada inválida é realmente excepcional e não um caso esperado. Por exemplo, se a entrada deve vir de uma fonte confiável e sempre deve estar no formato correto.
- Você deseja forçar o chamador a lidar com o erro. Ao lançar uma exceção, o fluxo normal do programa é interrompido e o chamador deve lidar com o erro explicitamente.
- Você precisa fornecer informações detalhadas sobre o erro. Exceções podem conter informações adicionais sobre o erro, o que pode ser útil para depuração.
Exemplo de uma abordagem baseada em exceção:
def parse_line_with_exception(line):
"""Analisa uma linha e lança uma exceção para entrada inválida."""
parts = line.split('=', 1)
if len(parts) != 2:
raise ValueError(f"Invalid format: '{line}' does not contain '='")
return (parts[0], parts[1])
A escolha entre retornar None e lançar exceções depende das necessidades do seu aplicativo:
- Retorne
Nonequando a ausência de um resultado for comum e esperada. Por exemplo, ao pesquisar um item em uma lista e ele pode não estar lá. - Lance exceções quando a falha for inesperada e deva interromper o fluxo normal. Por exemplo, ao tentar acessar um arquivo que sempre deve existir.
Trabalhando com Futures para Programação Concorrente
Em Python, quando você precisa executar funções ao mesmo tempo, ou concorrentemente, a linguagem oferece ferramentas úteis como threads e processos. Mas aqui está um problema comum que você enfrentará: como você pode obter o valor que uma função retorna quando ela está sendo executada em uma thread diferente? É aqui que o conceito de um Future se torna muito importante.
Um Future é como um espaço reservado para um resultado que estará disponível mais tarde. É uma maneira de representar um valor que uma função produzirá no futuro, mesmo antes que a função termine de ser executada. Vamos entender melhor esse conceito com um exemplo simples.
Passo 1: Crie um Novo Arquivo
Primeiro, você precisa criar um novo arquivo Python. Vamos chamá-lo de futures_demo.py. Você pode usar o seguinte comando em seu terminal para criar este arquivo:
touch ~/project/futures_demo.py
Passo 2: Adicione o Código da Função Básica
Agora, abra o arquivo futures_demo.py e adicione o seguinte código Python. Este código define uma função simples e mostra como uma chamada de função normal funciona.
import time
import threading
from concurrent.futures import Future, ThreadPoolExecutor
def worker(x, y):
"""A function that takes time to complete"""
print('Starting work...')
time.sleep(5) ## Simulate a time-consuming task
print('Work completed')
return x + y
## Part 1: Normal function call
print("--- Part 1: Normal function call ---")
result = worker(2, 3)
print(f"Result: {result}")
Neste código, a função worker recebe dois números, os soma, mas primeiro simula uma tarefa demorada, pausando por 5 segundos. Quando você chama esta função de maneira normal, o programa espera que a função termine e, em seguida, obtém o valor de retorno.
Passo 3: Execute o Código Básico
Salve o arquivo e execute-o usando o seguinte comando em seu terminal:
python ~/project/futures_demo.py
Você deve ver a saída assim:
--- Part 1: Normal function call ---
Starting work...
Work completed
Result: 5
Isso mostra que uma chamada de função normal espera que a função termine e, em seguida, retorna o resultado.
Passo 4: Execute a Função em uma Thread Separada
Em seguida, vamos ver o que acontece quando executamos a função worker em uma thread separada. Adicione o seguinte código ao arquivo futures_demo.py:
## Part 2: Running in a separate thread (problem: no way to get result)
print("\n--- Part 2: Running in a separate thread ---")
t = threading.Thread(target=worker, args=(2, 3))
t.start()
print("Main thread continues while worker runs...")
t.join() ## Wait for the thread to complete
print("Worker thread finished, but we don't have its return value!")
Aqui, estamos usando a classe threading.Thread para iniciar a função worker em uma nova thread. A thread principal não espera que a função worker termine e continua sua execução. No entanto, quando a thread worker termina, não temos uma maneira fácil de obter o valor de retorno.
Passo 5: Execute o Código Threaded
Salve o arquivo novamente e execute-o usando o mesmo comando:
python ~/project/futures_demo.py
Você notará que a thread principal continua, a thread worker é executada, mas não podemos acessar o valor de retorno da função worker.
Passo 6: Use um Future Manualmente
Para resolver o problema de obter o valor de retorno de uma thread, podemos usar um objeto Future. Adicione o seguinte código ao arquivo futures_demo.py:
## Part 3: Using a Future to get the result
print("\n--- Part 3: Using a Future manually ---")
def do_work_with_future(x, y, future):
"""Wrapper that sets the result in the Future"""
result = worker(x, y)
future.set_result(result)
## Create a Future object
fut = Future()
## Start a thread that will set the result in the Future
t = threading.Thread(target=do_work_with_future, args=(2, 3, fut))
t.start()
print("Main thread continues...")
print("Waiting for the result...")
## Block until the result is available
result = fut.result() ## This will wait until set_result is called
print(f"Got the result: {result}")
Neste código, criamos um objeto Future e o passamos para uma nova função do_work_with_future. Esta função chama a função worker e, em seguida, define o resultado no objeto Future. A thread principal pode então usar o método result() do objeto Future para obter o resultado quando ele estiver disponível.
Passo 7: Execute o Código com Future
Salve o arquivo e execute-o novamente:
python ~/project/futures_demo.py
Agora você verá que podemos obter com sucesso o valor de retorno da função em execução na thread.
Passo 8: Use ThreadPoolExecutor
A classe ThreadPoolExecutor em Python torna o trabalho com tarefas concorrentes ainda mais fácil. Adicione o seguinte código ao arquivo futures_demo.py:
## Part 4: Using ThreadPoolExecutor (easier way)
print("\n--- Part 4: Using ThreadPoolExecutor ---")
with ThreadPoolExecutor() as executor:
## Submit the work to the executor
future = executor.submit(worker, 2, 3)
print("Main thread continues after submitting work...")
print("Checking if the future is done:", future.done())
## Get the result (will wait if not ready)
result = future.result()
print("Now the future is done:", future.done())
print(f"Final result: {result}")
O ThreadPoolExecutor cuida de criar e gerenciar os objetos Future para você. Você só precisa enviar a função e seus argumentos, e ele retornará um objeto Future que você pode usar para obter o resultado.
Passo 9: Execute o Código Completo
Salve o arquivo pela última vez e execute-o:
python ~/project/futures_demo.py
Explicação
- Chamada de Função Normal: Quando você chama uma função da maneira normal, o programa espera que a função termine e obtém diretamente o valor de retorno.
- Problema da Thread: Executar uma função em uma thread separada tem uma desvantagem. Não há uma maneira integrada de obter o valor de retorno da função em execução nessa thread.
- Future Manual: Ao criar um objeto
Futuree passá-lo para a thread, podemos definir o resultado noFuturee, em seguida, obter o resultado da thread principal. - ThreadPoolExecutor: Esta classe simplifica a programação concorrente. Ele lida com a criação e o gerenciamento de objetos
Futurepara você, tornando mais fácil executar funções concorrentemente e obter seus valores de retorno.
Objetos Future têm vários métodos úteis:
result(): Este método é usado para obter o resultado da função. Se o resultado ainda não estiver pronto, ele esperará até que esteja.done(): Você pode usar este método para verificar se a computação da função foi concluída.add_done_callback(): Este método permite que você registre uma função que será chamada quando o resultado estiver pronto.
Este padrão é muito importante na programação concorrente, especialmente quando você precisa obter resultados de funções em execução em paralelo.
Resumo
Neste laboratório, você aprendeu vários padrões-chave para retornar valores de funções em Python. Em primeiro lugar, as funções Python podem retornar múltiplos valores, empacotando-os em uma tupla, permitindo um retorno e desempacotamento de valores limpos e legíveis. Em segundo lugar, para funções que nem sempre podem produzir resultados válidos, retornar None é uma maneira comum de indicar a ausência de um valor, e lançar exceções também foi apresentado como uma alternativa.
Por fim, na programação concorrente, um Future atua como um espaço reservado para um resultado futuro, permitindo que você obtenha valores de retorno de funções em execução em threads ou processos separados. Compreender esses padrões aprimorará a robustez e a flexibilidade do seu código Python. Para prática adicional, experimente diferentes estratégias de tratamento de erros, use Futures com outros tipos de execução concorrente e explore sua aplicação em programação assíncrona com async/await.