Alternativas para devolver múltiples valores desde una función de Python [cerrado]
La forma canónica de devolver múltiples valores en idiomas que lo admiten suele ser tuplar .
Opción: usar una tupla
Considere este ejemplo trivial:
def f(x):
y0 = x + 1
y1 = x * 3
y2 = y0 ** y3
return (y0, y1, y2)
Sin embargo, esto rápidamente se vuelve problemático a medida que aumenta el número de valores devueltos. ¿Qué pasa si quieres devolver cuatro o cinco valores? Claro, puedes seguir tuplicándolos, pero es fácil olvidar qué valor está dónde. También es bastante feo desembalarlos donde quieras recibirlos.
Opción: usar un diccionario
El siguiente paso lógico parece ser introducir algún tipo de "notación de registro". En Python, la forma obvia de hacerlo es mediante un archivo dict
.
Considera lo siguiente:
def g(x):
y0 = x + 1
y1 = x * 3
y2 = y0 ** y3
return {'y0': y0, 'y1': y1 ,'y2': y2}
(Para que quede claro, y0, y1 e y2 solo son identificadores abstractos. Como se señaló, en la práctica se usarían identificadores significativos).
Ahora tenemos un mecanismo mediante el cual podemos proyectar un miembro particular del objeto devuelto. Por ejemplo,
result['y0']
Opción: usar una clase
Sin embargo, hay otra opción. En su lugar, podríamos devolver una estructura especializada. He enmarcado esto en el contexto de Python, pero estoy seguro de que también se aplica a otros lenguajes. De hecho, si estuviera trabajando en C, esta podría ser su única opción. Aquí va:
class ReturnValue:
def __init__(self, y0, y1, y2):
self.y0 = y0
self.y1 = y1
self.y2 = y2
def g(x):
y0 = x + 1
y1 = x * 3
y2 = y0 ** y3
return ReturnValue(y0, y1, y2)
En Python, los dos anteriores son quizás muy similares en términos de plomería; después de todo, { y0, y1, y2 }
simplemente terminan siendo entradas en el interior __dict__
del archivo ReturnValue
.
Python proporciona una característica adicional para objetos pequeños: el __slots__
atributo. La clase podría expresarse como:
class ReturnValue(object):
__slots__ = ["y0", "y1", "y2"]
def __init__(self, y0, y1, y2):
self.y0 = y0
self.y1 = y1
self.y2 = y2
Del Manual de referencia de Python :
La
__slots__
declaración toma una secuencia de variables de instancia y reserva suficiente espacio en cada instancia para contener un valor para cada variable. Se ahorra espacio porque__dict__
no se crea para cada instancia.
Opción: usar una clase de datos (Python 3.7+)
Usando las nuevas clases de datos de Python 3.7, devuelve una clase con métodos especiales agregados automáticamente, escritura y otras herramientas útiles:
@dataclass
class Returnvalue:
y0: int
y1: float
y3: int
def total_cost(x):
y0 = x + 1
y1 = x * 3
y2 = y0 ** y3
return ReturnValue(y0, y1, y2)
Opción: usar una lista
Otra sugerencia que pasé por alto proviene de Bill the Lizard:
def h(x):
result = [x + 1]
result.append(x * 3)
result.append(y0 ** y3)
return result
Sin embargo, este es mi método menos favorito. Supongo que estoy contaminado por la exposición a Haskell, pero la idea de listas de tipos mixtos siempre me ha resultado incómoda. En este ejemplo particular, la lista no es de tipo mixto, pero posiblemente podría serlo.
Hasta donde yo sé, una lista utilizada de esta manera realmente no gana nada con respecto a la tupla. La única diferencia real entre listas y tuplas en Python es que las listas son mutables , mientras que las tuplas no.
Personalmente tiendo a trasladar las convenciones de la programación funcional: uso listas para cualquier número de elementos del mismo tipo y tuplas para un número fijo de elementos de tipos predeterminados.
Pregunta
Después del largo preámbulo, viene la pregunta inevitable. ¿Qué método (crees) es mejor?
Se agregaron tuplas con nombre en 2.6 para este propósito. Consulte también os.stat para ver un ejemplo integrado similar.
>>> import collections
>>> Point = collections.namedtuple('Point', ['x', 'y'])
>>> p = Point(1, y=2)
>>> p.x, p.y
1 2
>>> p[0], p[1]
1 2
En versiones recientes de Python 3 (creo que 3.6+), la nueva typing
biblioteca consiguió que la NamedTuple
clase hiciera que las tuplas con nombre fueran más fáciles de crear y más potentes. Heredar de typing.NamedTuple
le permite usar cadenas de documentación, valores predeterminados y anotaciones de tipo.
Ejemplo (de los documentos):
class Employee(NamedTuple): # inherit from typing.NamedTuple
name: str
id: int = 3 # default value
employee = Employee('Guido')
assert employee.id == 3
Para proyectos pequeños, me resulta más fácil trabajar con tuplas. Cuando eso se vuelve demasiado difícil de manejar (y no antes), empiezo a agrupar cosas en estructuras lógicas; sin embargo, creo que el uso sugerido de diccionarios y ReturnValue
objetos es incorrecto (o demasiado simplista).
Devolver un diccionario con claves "y0"
, "y1"
, "y2"
, etc. no ofrece ninguna ventaja sobre las tuplas. Devolver una ReturnValue
instancia con propiedades .y0
, .y1
, .y2
, etc. tampoco ofrece ninguna ventaja sobre las tuplas. Necesitas comenzar a nombrar cosas si quieres llegar a alguna parte, y puedes hacerlo usando tuplas de todos modos:
def get_image_data(filename):
[snip]
return size, (format, version, compression), (width,height)
size, type, dimensions = get_image_data(x)
En mi humilde opinión, la única buena técnica más allá de las tuplas es devolver objetos reales con métodos y propiedades adecuados, como los que se obtienen con re.match()
o open(file)
.
Muchas de las respuestas sugieren que necesita devolver una colección de algún tipo, como un diccionario o una lista. Puede omitir la sintaxis adicional y simplemente escribir los valores de retorno, separados por comas. Nota: esto técnicamente devuelve una tupla.
def f():
return True, False
x, y = f()
print(x)
print(y)
da:
True
False
Yo voto por el diccionario.
Encuentro que si creo una función que devuelve más de 2 o 3 variables, las agruparé en un diccionario. De lo contrario tiendo a olvidar el orden y el contenido de lo que devuelvo.
Además, introducir una estructura "especial" hace que el código sea más difícil de seguir. (Alguien más tendrá que buscar en el código para saber qué es)
Si le preocupa la búsqueda de tipos, utilice claves de diccionario descriptivas, por ejemplo, 'lista de valores x'.
def g(x):
y0 = x + 1
y1 = x * 3
y2 = y0 ** y3
return {'y0':y0, 'y1':y1 ,'y2':y2 }
Otra opción sería utilizar generadores:
>>> def f(x):
y0 = x + 1
yield y0
yield x * 3
yield y0 ** 4
>>> a, b, c = f(5)
>>> a
6
>>> b
15
>>> c
1296
Aunque en mi humilde opinión las tuplas suelen ser las mejores, excepto en los casos en que los valores que se devuelven son candidatos para encapsularse en una clase.