¿Es pitónico que una función devuelva múltiples valores?

81

En Python, puede hacer que una función devuelva varios valores. Aquí hay un ejemplo artificial:

def divide(x, y):
    quotient = x/y
    remainder = x % y
    return quotient, remainder  

(q, r) = divide(22, 7)

Esto parece muy útil, pero parece que también se puede abusar de él ("Bueno ... la función X ya calcula lo que necesitamos como valor intermedio. Hagamos que X devuelva ese valor también").

¿Cuándo debería trazar la línea y definir un método diferente?

Solo lectura
fuente

Respuestas:

107

Absolutamente (para el ejemplo que proporcionó).

Las tuplas son ciudadanos de primera clase en Python

Hay una función incorporada divmod()que hace exactamente eso.

q, r = divmod(x, y) # ((x - x%y)/y, x%y) Invariant: div*y + mod == x

Hay otros ejemplos: zip, enumerate, dict.items.

for i, e in enumerate([1, 3, 3]):
    print "index=%d, element=%s" % (i, e)

# reverse keys and values in a dictionary
d = dict((v, k) for k, v in adict.items()) # or 
d = dict(zip(adict.values(), adict.keys()))

Por cierto, los paréntesis no son necesarios la mayor parte del tiempo. Cita de la referencia de la biblioteca de Python :

Las tuplas se pueden construir de varias formas:

  • Usando un par de paréntesis para denotar la tupla vacía: ()
  • Usando una coma final para una tupla singleton: a, o (a,)
  • Separar elementos con comas: a, b, c o (a, b, c)
  • Usando la tupla () incorporada: tupla () o tupla (iterable)

Las funciones deben tener un solo propósito

Por lo tanto, deben devolver un solo objeto. En su caso, este objeto es una tupla. Considere la tupla como una estructura de datos compuestos ad-hoc. Hay lenguajes en los que casi todas las funciones devuelven varios valores (lista en Lisp).

A veces es suficiente regresar en (x, y)lugar de Point(x, y).

Tuplas con nombre

Con la introducción de tuplas con nombre en Python 2.6, en muchos casos es preferible devolver tuplas con nombre en lugar de tuplas simples.

>>> import collections
>>> Point = collections.namedtuple('Point', 'x y')
>>> x, y = Point(0, 1)
>>> p = Point(x, y)
>>> x, y, p
(0, 1, Point(x=0, y=1))
>>> p.x, p.y, p[0], p[1]
(0, 1, 0, 1)
>>> for i in p:
...   print(i)
...
0
1
jfs
fuente
1
La capacidad de usar tuplas como esa es muy útil. Realmente extraño tener esa habilidad en Java algunas veces.
MBCook
3
Muchas gracias por mencionar las tuplas con nombre. He estado buscando algo como esto.
vobject
para valores devueltos limitados, dict () podría ser más simple / más rápido (especialmente si la función se usa para REST API y JSON). en mi sistema, def test_nt (): Point = namedtuple ('Point', ['x', 'y']) p = Point (10.5, 11.5) json.dumps (p._asdict ()) toma 819 μs / loop , def test_dict (): d = {'x': 10.5, 'y': 11.5} json.dumps (d) toma 15.9μs / loop .... entonces> 50 veces más rápido
comte
@comte: Sí, Point(10.5, 11.5)es 6 veces más lento que {'x': 10.5, 'y':11.5}. Los tiempos absolutos son 635 ns +- 26 nsvs.105 ns +- 4 ns . Es poco probable que cualquiera de los dos sea un cuello de botella en la aplicación; no optimice a menos que su generador de perfiles indique lo contrario. No cree clases a nivel de función a menos que sepa por qué lo necesita. Si su API requiere un dictuso dict, no tiene nada que ver con el rendimiento.
jfs
27

En primer lugar, tenga en cuenta que Python permite lo siguiente (no es necesario el paréntesis):

q, r = divide(22, 7)

Con respecto a su pregunta, no existe una regla estricta de ninguna manera. Para ejemplos simples (y generalmente artificiales), puede parecer que siempre es posible que una función determinada tenga un solo propósito, lo que resulta en un solo valor. Sin embargo, cuando usa Python para aplicaciones del mundo real, rápidamente se encuentra con muchos casos en los que es necesario devolver múltiples valores y da como resultado un código más limpio.

Entonces, diría que hagas lo que tenga sentido y no trates de ajustarte a una convención artificial. Python admite múltiples valores de retorno, así que utilícelo cuando sea apropiado.

Jason Etheridge
fuente
4
coma crea una tupla, por lo tanto, expresión: q, r es una tupla.
jfs
Vinko, un ejemplo es tempfile.mkstemp () de la biblioteca estándar, que devuelve una tupla que contiene un identificador de archivo y una ruta absoluta. JFS, sí, tienes razón.
Jason Etheridge
Pero ese ES un solo propósito ... Puede tener un solo propósito y múltiples valores de retorno
Vinko Vrsalovic
Si otros lenguajes pudieran tener múltiples tipos de retorno, no estaría atrapado con referencias y parámetros de 'salida'.
Orip el
¡Devolver múltiples valores es una de las mejores características de Python!
Martin Beckett
13

El ejemplo que da es en realidad una función incorporada de Python, llamada divmod. Entonces, alguien, en algún momento, pensó que era lo suficientemente pitónico como para incluirlo en la funcionalidad principal.

Para mí, si hace que el código sea más limpio, es pitónico. Compare estos dos bloques de código:

seconds = 1234
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)

seconds = 1234
minutes = seconds / 60
seconds = seconds % 60
hours = minutes / 60
minutes = minutes % 60
Nathan Jones
fuente
3

Sí, devolver múltiples valores (es decir, una tupla) es definitivamente pitónico. Como han señalado otros, hay muchos ejemplos en la biblioteca estándar de Python, así como en proyectos de Python muy respetados. Dos comentarios adicionales:

  1. Devolver varios valores a veces es muy, muy útil. Tomemos, por ejemplo, un método que opcionalmente maneja un evento (devolviendo algún valor al hacerlo) y también devuelve éxito o fracaso. Esto puede surgir en un patrón de cadena de responsabilidad. En otros casos, desea devolver múltiples datos estrechamente vinculados, como en el ejemplo dado. En esta configuración, devolver varios valores es similar a devolver una única instancia de una clase anónima con varias variables miembro.
  2. El manejo de Python de los argumentos del método requiere la capacidad de devolver directamente varios valores. En C ++, por ejemplo, los argumentos del método se pueden pasar por referencia, por lo que puede asignarles valores de salida, además del valor de retorno formal. En Python, los argumentos se pasan "por referencia" (pero en el sentido de Java, no de C ++). No puede asignar nuevos valores a los argumentos del método y hacer que se refleje fuera del alcance del método. Por ejemplo:

    // C++
    void test(int& arg)
    {
        arg = 1;
    }
    
    int foo = 0;
    test(foo); // foo is now 1!
    

    Comparar con:

    # Python
    def test(arg):
        arg = 1
    
    foo = 0
    test(foo) # foo is still 0
    
zweiterlinde
fuente
"un método que opcionalmente maneja un evento y también devuelve el éxito o el fracaso": para eso están las excepciones.
Nathan
Es ampliamente aceptado que las excepciones son sólo por condiciones "excepcionales", no por mero éxito o fracaso. Google "códigos de error frente a excepciones" para algunos de los debates.
zweiterlinde
1

Definitivamente es pitónico. El hecho de que puede devolver varios valores de una función, el modelo estándar que tendría en un lenguaje como C, donde necesita definir una estructura para cada combinación de tipos que devuelva en algún lugar.

Sin embargo, si llega al punto en el que está devolviendo algo loco como 10 valores de una sola función, debería considerar seriamente agruparlos en una clase porque en ese punto se vuelve difícil de manejar.

sangría
fuente
1

OT: Algol68 de RSRE tiene el curioso operador "/: =". p.ej.

INT quotient:=355, remainder;
remainder := (quotient /:= 113);

Dando un cociente de 3 y un resto de 16.

Nota: típicamente, el valor de "(x /: = y)" se descarta ya que el cociente "x" se asigna por referencia, pero en el caso de RSRE el valor devuelto es el resto.

cf Aritmética de enteros - Algol68

NevilleDNZ
fuente
0

Está bien devolver múltiples valores usando una tupla para funciones simples como divmod . Si hace que el código sea legible, es Pythonic.

Si el valor de retorno comienza a ser confuso, verifique si la función está haciendo demasiado y divídalo si es así. Si se utiliza una tupla grande como un objeto, conviértala en un objeto. Además, considere usar tuplas nombre , que serán parte de la biblioteca estándar en Python 2.6.

Will Harris
fuente
0

Soy bastante nuevo en Python, pero la técnica de tuplas me parece muy pitónica. Sin embargo, tuve otra idea que puede mejorar la legibilidad. El uso de un diccionario permite acceder a los diferentes valores por nombre en lugar de por posición. Por ejemplo:

def divide(x, y):
    return {'quotient': x/y, 'remainder':x%y }

answer = divide(22, 7)
print answer['quotient']
print answer['remainder']
Fred Larson
fuente
2
No hagas esto. No puede asignar el resultado en una sola línea, es muy torpe. (A menos que desee almacenar el resultado, en cuyo caso es mejor poner esto en un método). Si su intención con el dict es auto-documentar los valores de retorno, entonces a) dé a fn un nombre razonablemente explicativo (como " divmod ") yb) poner el resto de la explicación en una cadena de documentos (que es el lugar Pythonic para ponerlos); Si esa cadena de documentos requiere más de dos líneas, eso sugiere que la función está sobrecargada temáticamente y puede ser una mala idea.
smci