¿Cómo devuelvo múltiples valores de una función? [cerrado]

1067

La forma canónica de devolver múltiples valores en idiomas que lo admiten es a menudo tupling .

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 desea devolver cuatro o cinco valores? Claro, podrías seguir tuplándolos, pero es fácil olvidar qué valor es dónde. También es bastante feo desempacarlos 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 hacer esto es mediante a dict.

Considera lo siguiente:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0': y0, 'y1': y1 ,'y2': y2}

(Para ser claros, y0, y1 e y2 son solo identificadores abstractos. Como se señaló, en la práctica se usarían identificadores significativos).

Ahora, tenemos un mecanismo por 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 cambio, podríamos devolver una estructura especializada. Lo he enmarcado en el contexto de Python, pero estoy seguro de que también se aplica a otros lenguajes. De hecho, si estuvieras trabajando en C, esta podría ser tu ú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 fontanería, después de todo, { y0, y1, y2 }terminan siendo entradas en el interior __dict__del 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 mantener 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, mecanografía y otras herramientas útiles agregadas automáticamente:

@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 había pasado por alto proviene de Bill el Lagarto:

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 es de tipo no mixto, pero posiblemente podría serlo.

Una lista utilizada de esta manera realmente no gana nada con respecto a la tupla por lo que puedo decir. La única diferencia real entre listas y tuplas en Python es que las listas son mutables , mientras que las tuplas no lo son.

Personalmente, tiendo a transferir las convenciones de la programación funcional: use 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 inevitable pregunta. ¿Qué método (crees) es el mejor?

saffsd
fuente
10
En sus excelentes ejemplos, usted usa variable y3, pero a menos que y3 se declare global, ¿esto produciría NameError: global name 'y3' is not definedtal vez solo uso 3?
hetepeperfan
11
Muchas grandes preguntas con excelentes respuestas están cerradas porque surge la palabra clave 'opinión'. Se podría argumentar que todo el SO se basa en la opinión, pero es una opinión informada por hechos, referencias y experiencia específica. El hecho de que alguien pregunte "cuál crees que es el mejor" no significa que esté pidiendo opiniones personales extraídas de hechos del mundo real, referencias y experiencia específica. Es casi seguro que piden precisamente ese tipo de opinión, basada en los hechos, referencias y experiencia específica que la persona usó para formar la opinión, y documentada con ellos.
NeilG
@hetepeperfan no necesita cambiar 3, y tampoco está definiendo y3 en global, también podría usar un nombre local y3, que también hará el mismo trabajo.
okie

Respuestas:

637

Se agregaron tuplas con nombre en 2.6 para este propósito. También vea os.stat para un ejemplo incorporado 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 (3.6+, creo), la nueva typingbiblioteca consiguió que la NamedTupleclase hiciera las tuplas con nombre más fáciles de crear y más poderosas. Heredar de le typing.NamedTuplepermite usar cadenas de documentos, 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
A. coadyuvante
fuente
68
Esta es solo la respuesta correcta porque es la única estructura canónica que el OP no consideró y porque aborda su problema de administrar largas tuplas. Debe marcarse como aceptado.
ataque aéreo
77
Bueno, la lógica del diseño namedtuplees tener una huella de memoria más pequeña para resultados masivos (largas listas de tuplas, como los resultados de consultas DB). Para elementos individuales (si la función en cuestión no se llama con frecuencia), los diccionarios y las clases también están bien. Pero las tuplas con nombre también son una solución agradable / más agradable en este caso.
Lutz Prechelt
8
@wom: No hagas esto. Python no hace ningún esfuerzo para unificar las namedtupledefiniciones (cada llamada crea una nueva), la creación de la namedtupleclase es relativamente costosa tanto en la CPU como en la memoria, y todas las definiciones de clase implican intrínsecamente referencias cíclicas (por lo tanto, en CPython, está esperando una ejecución cíclica de GC para que sean liberados). También hace que sea imposible para picklela clase (y, por lo tanto, imposible usar instancias multiprocessingen la mayoría de los casos). Cada creación de la clase en mi 3.6.4 x64 consume ~ 0.337 ms, y ocupa poco menos de 1 KB de memoria, eliminando cualquier ahorro de instancia.
ShadowRanger
3
Notaré que Python 3.7 mejoró la velocidad de creación de nuevas namedtupleclases ; los costos de CPU caen aproximadamente un factor de 4x , pero siguen siendo aproximadamente 1000x más altos que el costo de crear una instancia, y el costo de memoria para cada clase sigue siendo alto (me equivoqué en mi último comentario sobre "menos de 1 KB" para la clase, _sourceen sí mismo suele ser de 1.5 KB; _sourcese elimina en 3.7, por lo que es probable que esté más cerca del reclamo original de poco menos de 1 KB por creación de clase).
ShadowRanger
44
@SergeStroobandt: esta es parte de la biblioteca estándar, solo que no está integrada. No debe preocuparse de que no se pueda instalar en otro sistema con Python> = 2.6. ¿O simplemente te opones a la línea extra de código?
Justin
234

Para proyectos pequeños, me resulta más fácil trabajar con tuplas. Cuando eso se vuelve demasiado difícil de administrar (y no antes) empiezo a agrupar cosas en estructuras lógicas, sin embargo, creo que su uso sugerido de diccionarios y ReturnValueobjetos es incorrecto (o demasiado simplista).

La devolución de un diccionario con las teclas "y0", "y1", "y2", etc no ofrece ninguna ventaja sobre tuplas. Devolviendo unReturnValue instancia con propiedades .y0, .y1, .y2, etc no ofrece ninguna ventaja sobre tuplas tampoco. Necesitas comenzar a nombrar cosas si quieres llegar a cualquier 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 obtienes de re.match()o open(file).

demasiado php
fuente
66
Pregunta: ¿hay alguna diferencia entre size, type, dimensions = getImageData(x)y (size, type, dimensions) = getImageData(x)? Es decir, ¿hace alguna diferencia envolver el lado izquierdo de una tarea tupulada?
Reb.Cabin
11
@ Reb.Cabin No hay diferencia. La tupla se identifica con una coma y el uso de paréntesis es solo para agrupar las cosas. Por ejemplo, (1)es un int while (1,)o 1,es una tupla.
phil
19
Re "devolver un diccionario con las claves y0, y1, y2, etc. no ofrece ninguna ventaja sobre las tuplas": el diccionario tiene la ventaja de que puede agregar campos al diccionario devuelto sin romper el código existente.
ostrokach
Re "devolver un diccionario con las teclas y0, y1, y2, etc. no ofrece ninguna ventaja sobre las tuplas": también es más legible y menos propenso a errores cuando accede a los datos en función de su nombre en lugar de su posición.
Denis Dollfus
204

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
Joseph Hansen
fuente
24
Todavía estás devolviendo una colección. Es una tupla. Prefiero paréntesis para hacerlo más explícito. Intenta esto: type(f())vuelve <class 'tuple'>.
Igor
20
@Igor: no hay ninguna razón para hacer tupleexplícito el aspecto; no es realmente importante que devuelva un tuple, este es el idioma para devolver el período de valores múltiples. La misma razón por la que omite los parens con el idioma de intercambio x, y = y, x, inicialización múltiple x, y = 0, 1, etc. claro, hace tuples bajo el capó, pero no hay razón para hacerlo explícito, ya que los tuples no son el punto en absoluto. El tutorial de Python introduce asignaciones múltiples mucho antes de que incluso toque tuples.
ShadowRanger
@ShadowRanger cualquier secuencia de valores separados por una coma en el lado derecho de =es una tupla en Python con o sin paréntesis a su alrededor. Entonces, en realidad no hay explícito o implícito aquí. a, b, c es tanto una tupla como (a, b, c). Tampoco se hace una tupla "debajo del capó" cuando se devuelven dichos valores, porque es simplemente una tupla simple. El OP ya mencionó las tuplas, por lo que en realidad no hay diferencia entre lo que mencionó y lo que muestra esta respuesta. Ninguno
Ken4scholars
2
Esta es literalmente la primera opción sugerida en la pregunta
endolito el
1
@endolith Los dos veces el chico hace una pregunta ( "¿Cómo puedo devolver múltiples valores?" y "¿Cómo se devuelve varios valores?") son contestadas por esta respuesta. El texto de la pregunta ha cambiado a veces. Y es una pregunta basada en la opinión.
Joseph Hansen
74

Yo voto por el diccionario.

Me parece que si hago una función que devuelve más de 2-3 variables, las doblaré en un diccionario. De lo contrario, tiendo a olvidar el orden y el contenido de lo que estoy devolviendo.

Además, la introducción de una estructura 'especial' hace que su código sea más difícil de seguir. (Alguien más tendrá que buscar a través del código para averiguar qué es)

Si le preocupa la búsqueda de tipos, use claves descriptivas del diccionario, 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 }
Monkut
fuente
55
Después de muchos años de programación, tiendo a lo que sea que se requiera la estructura de los datos y la función. Primero, siempre puede refactorizar según sea necesario.
monkut
¿Cómo obtendríamos los valores dentro del diccionario sin llamar a la función varias veces? Por ejemplo, si quiero usar y1 e y3 en una función diferente?
Matt
3
Asignar los resultados a una variable separada. result = g(x); other_function(result)
Monkut
1
@monkut sí. De esta manera también permite pasar el resultado a varias funciones, que toman diferentes argumentos del resultado, sin tener que hacer referencia específica a partes particulares del resultado cada vez.
Gnudiff
38

Otra opción sería usar 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 las tuplas de la OMI suelen ser mejores, excepto en los casos en que los valores que se devuelven son candidatos para la encapsulación en una clase.

rlms
fuente
1
Esta parece ser la solución más limpia y tiene una sintaxis limpia. ¿Hay alguna desventaja en esto? Si no utiliza todas las devoluciones, ¿hay rendimientos 'no gastados' esperando lastimarlo?
Jiminion
24
Esto puede ser "limpio", pero no parece intuitivo en absoluto. ¿Cómo podría alguien que nunca antes ha encontrado este patrón saber que hacer el desempaquetado automático de tuplas desencadenaría cada una yield?
coredumperror
1
@CoreDumpError, los generadores son solo eso ... generadores. No existe una diferencia externa entre def f(x): …; yield b; yield a; yield rvs. (g for g in [b, a, r]), y ambos se convertirán fácilmente en listas o tuplas, y como tal admitirán el desempaquetado de tuplas. La forma del generador de tuplas sigue un enfoque funcional, mientras que la forma de la función es imprescindible y permitirá el control del flujo y la asignación de variables.
sleblanc
30

Prefiero usar tuplas cuando una tupla se siente "natural"; Las coordenadas son un ejemplo típico, donde los objetos separados pueden sostenerse por sí mismos, por ejemplo, en cálculos de escala de un solo eje, y el orden es importante. Nota: si puedo ordenar o barajar los elementos sin un efecto adverso al significado del grupo, entonces probablemente no debería usar una tupla.

Utilizo los diccionarios como valor de retorno solo cuando los objetos agrupados no son siempre iguales. Piensa en encabezados de correo electrónico opcionales.

Para el resto de los casos, donde los objetos agrupados tienen un significado inherente dentro del grupo o se necesita un objeto completo con sus propios métodos, utilizo una clase.

tzot
fuente
29

Yo prefiero:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }

Parece que todo lo demás es solo código extra para hacer lo mismo.

UnkwnTech
fuente
22
Las tuplas son más fáciles de desempaquetar: y0, y1, y2 = g () con un dict que debes hacer: resultado = g () y0, y1, y2 = result.get ('y0'), result.get ('y1' ), result.get ('y2') que es un poco feo. Cada solución tiene sus "ventajas" y sus "desventajas".
Oli
27
>>> def func():
...    return [1,2,3]
...
>>> a,b,c = func()
>>> a
1
>>> b
2
>>> c
3
WebQube
fuente
@edouard No, no lo hace, devuelve una tupla, no una lista.
Simon Hibbs
1
desestructuración es el argumento para el retorno de las listas en mi opinión
semiomant
21

En general, la "estructura especializada" en realidad ES un estado actual sensible de un objeto, con sus propios métodos.

class Some3SpaceThing(object):
  def __init__(self,x):
    self.g(x)
  def g(self,x):
    self.y0 = x + 1
    self.y1 = x * 3
    self.y2 = y0 ** y3

r = Some3SpaceThing( x )
r.y0
r.y1
r.y2

Me gusta encontrar nombres para estructuras anónimas siempre que sea posible. Los nombres significativos aclaran las cosas.

S.Lott
fuente
20

Las tuplas, los dictados y los objetos de Python le ofrecen al programador una compensación suave entre formalidad y conveniencia para pequeñas estructuras de datos ("cosas"). Para mí, la elección de cómo representar una cosa está dictada principalmente por cómo voy a usar la estructura. En C ++, es una convención común usar structpara elementos solo de datos y classpara objetos con métodos, aunque legalmente puede poner métodos en un struct; mi hábito es similar en Python, con dicty tupleen lugar de struct.

Para conjuntos de coordenadas, usaré un en tuplelugar de un punto classo un dict(y tenga en cuenta que puede usar un tuplecomo clave de diccionario, por lo que dicts hacen grandes conjuntos dispersos multidimensionales).

Si voy a iterar sobre una lista de cosas, prefiero desempaquetar tuples en la iteración:

for score,id,name in scoreAllTheThings():
    if score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(score,id,name)

... ya que la versión del objeto está más abarrotada para leer:

for entry in scoreAllTheThings():
    if entry.score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry.score,entry.id,entry.name)

... y mucho menos el dict.

for entry in scoreAllTheThings():
    if entry['score'] > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry['score'],entry['id'],entry['name'])

Si la cosa se usa ampliamente y te encuentras realizando operaciones similares no triviales en varios lugares del código, entonces generalmente vale la pena convertirlo en un objeto de clase con los métodos adecuados.

Finalmente, si voy a intercambiar datos con componentes del sistema que no son de Python, con mayor frecuencia los guardaré en un dictporque es lo más adecuado para la serialización JSON.

Russell Borogove
fuente
19

+1 en la sugerencia de S.Lott de una clase de contenedor con nombre.

Para Python 2.6 y versiones posteriores, una tupla con nombre proporciona una forma útil de crear fácilmente estas clases de contenedor, y los resultados son "ligeros y no requieren más memoria que las tuplas normales".

John Fouhy
fuente
4

En lenguajes como Python, normalmente usaría un diccionario ya que implica menos sobrecarga que crear una nueva clase.

Sin embargo, si me encuentro constantemente devolviendo el mismo conjunto de variables, entonces eso probablemente implique una nueva clase que factorizaré.

pelusa
fuente
4

Usaría un dict para pasar y devolver valores de una función:

Use la forma variable como se define en la forma .

form = {
    'level': 0,
    'points': 0,
    'game': {
        'name': ''
    }
}


def test(form):
    form['game']['name'] = 'My game!'
    form['level'] = 2

    return form

>>> print(test(form))
{u'game': {u'name': u'My game!'}, u'points': 0, u'level': 2}

Esta es la forma más eficiente para mí y para la unidad de procesamiento.

Debe pasar solo un puntero y devolver solo un puntero.

No tiene que cambiar los argumentos de las funciones (miles de ellas) cada vez que realiza un cambio en su código.

Elis Byberi
fuente
Los dictados son mutables. Si pasa un dict a una función y esa función edita el dict, los cambios se reflejarán fuera del alcance de esa función. Hacer que la función devuelva el dict al final puede implicar que la función no tiene efectos secundarios, por lo tanto, el valor no debe devolverse, dejando en claro que testmodificará directamente el valor. Compare esto con dict.update, que no devuelve un valor.
sleblanc
@sleblanc "Hacer que la función devuelva el dict al final podría implicar que la función no tiene efectos secundarios". No implica eso porque, como dijiste, dict es mutable. Sin embargo, regresar formno perjudica la legibilidad ni el rendimiento. En los casos en que necesite formvolver a formatear , devolverlo [formulario] se asegura de que formse devuelva el último porque no realizará un seguimiento de los cambios en el formulario en ningún lugar.
Elis Byberi
3

"Lo mejor" es una decisión parcialmente subjetiva. Use tuplas para pequeños conjuntos de retorno en el caso general donde un inmutable es aceptable. Una tupla siempre es preferible a una lista cuando la mutabilidad no es un requisito.

Para valores de retorno más complejos, o para el caso donde la formalidad es valiosa (es decir, código de alto valor), una tupla nombrada es mejor. Para el caso más complejo, un objeto suele ser el mejor. Sin embargo, es realmente la situación lo que importa. Si tiene sentido devolver un objeto porque eso es lo que tiene naturalmente al final de la función (p. Ej., Patrón de fábrica), devuelva el objeto.

Como dijo el sabio:

La optimización prematura es la raíz de todo mal (o al menos la mayor parte) en la programación.

joel3000
fuente
1
Donald Knuth lo dijo (en 1974).
Peter Mortensen