Alternativas para devolver múltiples valores desde una función de Python [closed]

13 minutos de lectura

avatar de usuario
saffsd

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

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 la cantidad de valores devueltos. ¿Qué sucede si desea devolver cuatro o cinco valores? Claro, podrías seguir entupándolos, pero es fácil olvidar qué valor está 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 por medio de un 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 son solo identificadores abstractos. Como se señaló, en la práctica usaría 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 idiomas. 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 } solo terminan siendo entradas en el interior __dict__ del ReturnValue.

Hay una característica adicional provista por Python, aunque para objetos diminutos, 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

Desde el Manual de referencia de Python:

los __slots__ La declaración toma una secuencia de variables de instancia y reserva el espacio suficiente en cada instancia para contener un valor para cada variable. Se ahorra espacio porque __dict__ no se crea para cada instancia.

Opción: Usar un clase de datos (Python 3.7+)

Con las nuevas clases de datos de Python 3.7, devuelva 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 había pasado 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 parecido incómoda. En este ejemplo particular, la lista no es de tipo mixto, pero posiblemente podría serlo.

Una lista usada 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 mudablemientras que las tuplas no lo son.

Personalmente tiendo a trasladar las convenciones de la programación funcional: usar 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 que) es el mejor?

  • En tus excelentes ejemplos usas variable y3pero a menos que y3 se declare global, esto produciría NameError: global name 'y3' is not defined tal vez solo use 3?

    – hetepeperfan

    19 de diciembre de 2013 a las 14:49

  • @hetepeperfan no es necesario cambiar 3, y tampoco definir y3 en global, también podría usar un nombre local y3eso también hará el mismo trabajo.

    – bien

    27 oct 2019 a las 23:31

avatar de usuario
A. Coady

tuplas con nombre se agregaron en 2.6 para este propósito. Ver también 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), el nuevo typing la biblioteca tiene el NamedTuple class para hacer que las tuplas con nombre sean 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

  • Bueno, la razón de diseño para namedtuple está teniendo una huella de memoria más pequeña para masa resultados (largas listas de tuplas, como resultados de consultas de base de datos). 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 nombradas también son una solución agradable/más agradable en este caso.

    – Lutz Prechelt

    23 de febrero de 2016 a las 17:07

  • La mejor respuesta, creo. Una cosa de la que no me di cuenta de inmediato: no necesita declarar la tupla con nombre en el ámbito externo; su función en sí misma puede definir el contenedor y devolverlo.

    – mujer

    15 de diciembre de 2016 a las 14:05

  • @wom: No hagas esto. Python no hace ningún esfuerzo por unificar namedtuple definiciones (cada llamada crea una nueva), creando el namedtuple La clase es relativamente costosa tanto en CPU como en memoria, y todas las definiciones de clase involucran intrínsecamente referencias cíclicas (por lo que en CPython, está esperando una ejecución cíclica de GC para que se publiquen). También hace que sea imposible pickle la clase (y por lo tanto, imposible de usar instancias con multiprocessing en 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

    20 abr 2018 a las 19:20


  • Tomaré nota, Python 3.7 mejorado la velocidad de creación de nuevos namedtuple clases; los costos de la CPU se reducen aproximadamente en un factor de 4xpero aún son aproximadamente 1000 veces más altos que el costo de crear una instancia, y el costo de la memoria para cada clase sigue siendo alto (me equivoqué en mi último comentario sobre “menos de 1 KB” para la clase, _source por sí mismo suele ser de 1,5 KB; _source se 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

    20 de abril de 2018 a las 19:31


  • @endolith porque puede agregar valores después de crearlo, lo que significa que puede agregar los resultados al espacio de nombres retval tan pronto como los obtenga y no tiene que esperar hasta que pueda ponerlos en una tupla con nombre todos a la vez. Puede reducir mucho el desorden en funciones más grandes a veces.

    -jaaq

    27 oct 2019 a las 12:45

avatar de usuario
demasiado php

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 las cosas en estructuras lógicas, sin embargo, creo que su uso sugerido de diccionarios y ReturnValue los objetos es incorrecto (o demasiado simplista).

Devolver un diccionario con claves "y0", "y1", "y2", etc. no ofrece ninguna ventaja sobre las tuplas. Devolviendo un ReturnValue instancia con propiedades .y0, .y1, .y2, etc. tampoco ofrece ninguna ventaja sobre las tuplas. Debe comenzar a nombrar las cosas si quiere llegar a alguna parte, y puede 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 se obtiene de re.match() o open(file).

  • 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

    28/08/2016 a las 21:10

  • Re “devolver un diccionario con las claves y0, y1, y2, etc. no ofrece ninguna ventaja sobre las tuplas”: también es más legible y menos propenso a errores al acceder a los datos en función de su nombre en lugar de su posición.

    – Denis Dollfus

    16 de enero de 2020 a las 13:09

avatar de usuario
jose hansen

Muchas de las respuestas sugieren que debe 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

  • Todavía estás devolviendo una colección. es una tupla. Prefiero los paréntesis para hacerlo más explícito. Prueba esto:type(f()) devoluciones <class 'tuple'>.

    – Igor

    17 mayo 2016 a las 18:52


  • @Igor: No hay razón para hacer el tuple aspecto explícito; no es realmente importante que estés devolviendo un tuple, este es el modismo para devolver un período de valores múltiples. La misma razón por la que omites los paréntesis con el idioma de intercambio, x, y = y, xinicialización múltiple x, y = 0, 1, etc.; seguro, hace tuples debajo del capó, pero no hay razón para hacerlo explícito, ya que el tuples no son el punto en absoluto. El tutorial de Python introduce la asignación múltiple mucho antes de que toque tuples.

    – ShadowRanger

    20 de abril de 2018 a las 19:39


  • @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. Así que en realidad no hay nada explícito o implícito aquí. a,b,c es una tupla tanto como (a,b,c). Tampoco se crea una tupla “bajo el capó” cuando devuelve tales valores, porque es solo una tupla simple. El OP ya mencionó tuplas, por lo que en realidad no hay diferencia entre lo que mencionó y lo que muestra esta respuesta. Ninguna

    – Ken4scholars

    23 de enero de 2019 a las 19:25

  • Esta es literalmente la primera opción sugerida en la pregunta.

    – endolito

    26 oct 2019 a las 15:43

  • @endolith Las dos veces que el chico hace una pregunta (“¿Cómo devuelvo múltiples valores?” y “¿Cómo devolver valores múltiples?”) se responden con esta respuesta. El texto de la pregunta ha cambiado a veces. Y es una pregunta basada en una opinión.

    –Joe Hansen

    19 de febrero de 2020 a las 14:22

Voto por el diccionario.

Encuentro 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 devuelvo.

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 en el código para averiguar qué es)

Si le preocupa la búsqueda de tipos, use 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 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 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 la encapsulación en una clase.

  • Esto puede ser “limpio”, pero no parece nada intuitivo. ¿Cómo sabría alguien que nunca antes se había encontrado con este patrón que hacer el desempaquetado automático de tuplas desencadenaría cada yield?

    – error de volcado de núcleo

    06/01/2016 a las 21:38

  • @CoreDumpError, los generadores son solo eso… generadores. No hay diferencia externa entre def f(x): …; yield b; yield a; yield r contra (g for g in [b, a, r]), y ambos se convertirán fácilmente en listas o tuplas y, como tales, admitirán el desempaquetado de tuplas. La forma del generador de tuplas sigue un enfoque funcional, mientras que la forma de función es imperativa y permitirá el control de flujo y la asignación de variables.

    – sleblanc

    28 de enero de 2019 a las 7:14


avatar de usuario
Pedro Mortensen

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 un código adicional para hacer lo mismo.

  • Esto puede ser “limpio”, pero no parece nada intuitivo. ¿Cómo sabría alguien que nunca antes se había encontrado con este patrón que hacer el desempaquetado automático de tuplas desencadenaría cada yield?

    – error de volcado de núcleo

    06/01/2016 a las 21:38

  • @CoreDumpError, los generadores son solo eso… generadores. No hay diferencia externa entre def f(x): …; yield b; yield a; yield r contra (g for g in [b, a, r]), y ambos se convertirán fácilmente en listas o tuplas y, como tales, admitirán el desempaquetado de tuplas. La forma del generador de tuplas sigue un enfoque funcional, mientras que la forma de función es imperativa y permitirá el control de flujo y la asignación de variables.

    – sleblanc

    28 de enero de 2019 a las 7:14


Prefiero usar tuplas cuando una tupla se siente “natural”; las coordenadas son un ejemplo típico, donde los objetos separados pueden valerse 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 en el significado del grupo, entonces probablemente no debería usar una tupla.

Uso diccionarios como valor de retorno solo cuando los objetos agrupados no siempre son los mismos. Piense 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, uso una clase.

¿Ha sido útil esta solución?