Retornando Valores de Funções

Beginner

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

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.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 91%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

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.

  1. 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
  1. Agora, abra o arquivo return_values.py em seu editor de código. Dentro deste arquivo, escreveremos a função parse_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.

  1. 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_line com 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.

  1. 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_line divide a string de entrada no caractere '=' usando o método split. 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 sintaxe name, 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.

  1. Atualize a função parse_line em seu arquivo return_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.

  1. 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.

  1. 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 None para indicar que a análise falhou.
  • Ao usar tal função, é importante verificar se o resultado é None antes de tentar usá-lo. Caso contrário, você pode encontrar erros ao tentar acessar elementos de um valor None.

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:

  1. 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.
  2. 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.
  3. 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 None quando 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

  1. 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.
  2. 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.
  3. Future Manual: Ao criar um objeto Future e passá-lo para a thread, podemos definir o resultado no Future e, em seguida, obter o resultado da thread principal.
  4. ThreadPoolExecutor: Esta classe simplifica a programação concorrente. Ele lida com a criação e o gerenciamento de objetos Future para 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.