Função retornar vários valores

Publicado em 2010-10-19 por Vinicius Assef

Por definição, funções só retornam um valor. Não seria bom se uma função retornasse vários valores? Em Python há algumas maneiras de conseguir isso.

De fato, uma função só retorna um valor mesmo. Mas podemos usar algumas estratégias e recursos da linguagem para parecer que estamos recebendo vários retornos.

Vejamos um exemplo:

>>> def coordenadas():
...     return (170, 36)

A função está retornando apenas um conteúdo, uma tupla, mas os itens dela são os vários valores que nossa função precisa retornar.

Até aqui, nenhuma novidade.

Vamos ver as diferentes formas de receber esse resultado, o que pode fazer toda a diferença para o modo como programamos.

Recebendo uma tupla simples

O jeito comum de receber esse retorno é:

>>> xy = coordenadas()
>>> xy[0]
170
>>> xy[1]
36

Essa é a maneira clássica de recebermos uma tupla como retorno. Depois, usamos o índice dos seus elementos para fazermos referência a eles.

Simples, mas nada intuitivo.

Entretanto, Python nos dá uma boa alternativa.

Nomear os itens da tupla

Essa forma deixa o programa mais expressivo:

>>> (x, y) = coordenadas()
>>> x
170
>>> y
36

Bem melhor. Agora parece que estamos recebendo vários valores da nossa função. Note que não mexemos nela. coordenadas() continua retornando a mesma tupla.

O que fizemos? Ao invés de dar um nome para a tupla de retorno, demos nome para os itens dela, usando a syntaxe (x, y).

Ainda podemos melhorar um pouco:

>>> x, y = coordenadas()

Sim, o Python permite que eliminemos os parênteses, nesse caso. Aí realmente parece que coordenadas() está retornando mais de um valor. Na verdade, ela continua retornando a mesma tupla.

Desempacotando itens

Essa forma de "desempacotar" uma tupla pode ser feita também com uma lista. Vejamos:

>>> dados = ["Maria", 23, "o+"]
>>> nome, idade, sangue = dados

>>> nome
'Maria'
>>> idade
23
>>> sangue
'o+'

Retornando mais dados

Nosso exemplo foi bem simples, retornando apenas 2 valores: x e y.

Vamos complicar um pouquinho. Imagine, que precisamos retornar mais valores:

>>> def dados():
...     return ("Maria", 23, "o+", "Paulo", "Joana")

É possível pegar apenas alguns dados e ignorar outros:

>>> nome, idade, _* = dados()

>>> nome
'Maria'
>>> idade
23

Bem útil, mas só funciona quando queremos pegar os itens iniciais e ignorar os finais. Se quisermos ignorar os itens do meio, por exemplo, já não funciona.

Mas ainda temos um jeito para isso.

Quando nossa função precisa mesmo retornar mais dados, podemos usar uma classe, um dicionário ou outro tipo de dado que o Python nos fornece.

Retornando um dicionário

A partir de agora, vamos precisar mexer na função que retorna os dados, pois vamos usar outros tipos de dados, diferentes de tupla.

Em Python é bem comum vermos funções retornando dicionários:

>>> def dados():
...     return dict(
...         nome="Maria",
...         idade=23,
...         sangue="o+",
...         pai="Paulo",
...         mae="Joana")

Nesse caso, recebemos o retorno assim:

>>> pessoa = dados()
>>> pessoa["nome"]
'Maria'

Uso simples de dicionário, sem mistério, mas que também pode resolver a situação de retornar vários valores.

Retornando uma classe

Quando precisamos retornar estruturas mais complexas, a escolha comum em linguagens com recurso de orientação a objetos, é retornar uma classe.

O inconveniente é termos que criar uma classe específica para cada situação e ser usada apenas como ValueObject. Python traz alguns tipos de dados que nos ajudam nesse sentido.

Podemos usar um SimpleNamespace para criar uma instância de classe dinamicamente, sem precisarmos criar uma classe específica só para esse caso:

>>> from types import SimpleNamespace
>>> def dados():
...     return SimpleNamespace(
...         nome="Maria",
...         idade=23,
...         sangue="o+",
...         pai="Paulo",
...         mae="Joana")

Então, o retorno seria:

>>> pessoa = dados()
>>> pessoa.nome
'Maria'
>>> pessoa.pai
'Paulo'

O SimpleNamespace é muito simples. Tudo o que você passar como argumento nomeado no construtor, vira atributo da instância. E só.

Ele cria uma instância como qualquer outra, mas já vem com o atalho do construtor com argumentos dinâmicos nomeados e arbitrários.

Vale observar que ele não é imutável, como seria um ValueObject. Podemos modificar o conteúdo do objeto:

>>> pessoa.nome = "Fernanda"

Retornando uma namedtuple

Para funcionar como um ValueObject imutável, temos a namedtuple. Vejamos:

>>> from collections import namedtuple
>>> def dados():
...     return namedtuple("Pessoa",
...         "nome idade sangue pai mae")(
...         nome="Maria",
...         idade=23,
...         sangue="o+",
...         pai="Paulo",
...         mae="Joana")

Pegamos o retorno da mesma forma que o SimpleNamespace:

>>> pessoa = dados()
>>> pessoa.nome
'Maria'
>>> pessoa.idade
23

A diferença é que não podemos modificar uma namedtuple:

>>> pessoa.nome = "xxxxx"
...
AttributeError: can't set attribute

De fato, nesse caso, pessoa é descendente de tuple. Por isso não podemos modificá-la:

>>> isinstance(pessoa, tuple)
True

Conclusão

Acabamos de ver algumas formas de conseguir o efeito de uma função retornar vários valores, em Python. As mesmas técnicas podem ser usadas em métodos de classe, que, afinal de contas, também são funções.

Para casos simples, tuplas e dicionários funcionam bem.

Para estruturas mais complexas, SimpleNamespace e namedtuple ajudam bastante, já que fazem parte da biblioteca padrão (baterias incluídas) do Python.

Vinicius Assef

Eu sou apaixonado por Python e shell script.

Aprenda com seus erros e dê nome certo às coisas.