¿Por qué las funciones siempre devuelven el mismo tipo?

Leí en alguna parte que las funciones siempre deberían devolver solo un tipo, por lo que el siguiente código se considera un código incorrecto:

def x(foo): if 'bar' in foo: return (foo, 'bar') return None 

Supongo que la mejor solución sería

 def x(foo): if 'bar' in foo: return (foo, 'bar') return () 

¿No sería más barato que la memoria devolviera una Ninguna para crear una nueva tupla vacía o es esta diferencia de tiempo demasiado pequeña para notarla incluso en proyectos más grandes?

¿Por qué deberían las funciones devolver valores de un tipo consistente? Para cumplir con las siguientes dos reglas.

Regla 1: una función tiene un “tipo”: entradas asignadas a las salidas. Debe devolver un tipo de resultado consistente, o no es una función. Es un desastre.

Matemáticamente, decimos que alguna función, F, es un mapeo del dominio, D, al rango, R. F: D -> R El dominio y el rango forman el “tipo” de la función. Los tipos de entrada y el tipo de resultado son tan esenciales para la definición de la función como el nombre o el cuerpo.

Regla 2: cuando tiene un “problema” o no puede devolver un resultado adecuado, genere una excepción.

 def x(foo): if 'bar' in foo: return (foo, 'bar') raise Exception( "oh, dear me." ) 

Puede romper las reglas anteriores, pero el costo de mantenimiento y comprensibilidad a largo plazo es astronómico.

“¿No sería más barato en cuanto a memoria devolver un Ninguno?” Pregunta equivocada.

El punto no es optimizar la memoria a costa de un código claro, legible y obvio.

No está tan claro que una función siempre debe devolver objetos de un tipo limitado, o que devolver Ninguno es incorrecto. Por ejemplo, re.search puede devolver un objeto _sre.SRE_Match o un objeto NoneType :

 import re match=re.search('a','a') type(match) #  match=re.search('a','b') type(match) #  

Diseñado de esta manera, puedes probar la coincidencia con el idioma

 if match: # do xyz 

Si los desarrolladores hubieran requerido re.search para devolver un objeto _sre.SRE_Match , entonces el idioma tendría que cambiar a

 if match.group(1) is None: # do xyz 

No habría ninguna ganancia importante al requerir que re.search devuelva siempre un objeto _sre.SRE_Match .

Así que creo que la forma en que se diseña la función debe depender de la situación y, en particular, la forma en que planea usarla.

También tenga en cuenta que tanto _sre.SRE_Match como NoneType son instancias de objeto, por lo que en un sentido amplio son del mismo tipo. Así que la regla de que “las funciones siempre deben devolver solo un tipo” no tiene sentido.

Dicho esto, hay una hermosa simplicidad para las funciones que devuelven objetos que comparten todas las mismas propiedades. (¡La tipografía de pato, no la estática, es la manera de los pitones!) Puede permitirle encadenar funciones: foo (bar (baz))) y saber con certeza el tipo de objeto que recibirá en el otro extremo.

Esto puede ayudarte a verificar la corrección de tu código. Al exigir que una función devuelva solo objetos de un cierto tipo limitado, hay menos casos para verificar. “foo siempre devuelve un entero, por lo tanto, mientras se espere un entero en todas partes, foo, soy golden …”

Las mejores prácticas en lo que una función debería devolver varían mucho de un idioma a otro, e incluso entre diferentes proyectos de Python.

Para Python en general, estoy de acuerdo con la premisa de que devolver Ninguno es malo si su función generalmente devuelve un iterable, porque la iteración sin pruebas es imposible. Simplemente devuelva un iterable vacío en este caso, todavía probará False si usa la prueba de verdad estándar de Python:

 ret_val = x() if ret_val: do_stuff(ret_val) 

y aún así te permiten recorrerlo sin probar:

 for child in x(): do_other_stuff(child) 

Para las funciones que probablemente devuelvan un solo valor, creo que devolver Ninguno es perfectamente aceptable, solo documente que esto podría suceder en su cadena de documentación.

Aquí están mis pensamientos sobre todo eso e intentaré explicar también por qué creo que la respuesta aceptada es en su mayoría incorrecta.

Primero de todas programming functions != mathematical functions . Lo más cercano que puede llegar a las funciones matemáticas es si realiza una progtwigción funcional, pero incluso entonces hay muchos ejemplos que dicen lo contrario.

  • Las funciones no tienen que tener entrada.
  • Las funciones no tienen que tener salida.
  • Las funciones no tienen que asignar la entrada a la salida (debido a los dos puntos anteriores anteriores)

Una función en términos de progtwigción debe verse simplemente como un bloque de memoria con un inicio (el punto de entrada de la función), un cuerpo (vacío o de otro tipo) y un punto de salida (uno o varios, según la implementación), todos los cuales están ahí con el propósito de reutilizar el código que has escrito. Incluso si no lo ves, una función siempre “devuelve” algo. Este algo es en realidad la dirección de la siguiente statement justo después de la llamada a la función. Esto es algo que verás en todo su esplendor si haces alguna progtwigción de muy bajo nivel con un lenguaje ensamblador (te animo a hacer un esfuerzo adicional y hacer un código de máquina a mano, como Linus Torvalds, que siempre lo menciona durante mucho tiempo). Sus seminarios y entrevistas: D). Además, también puede tomar alguna entrada y también escupir una salida. Es por eso que

 def foo(): pass 

Es una pieza de código perfectamente correcta.

Entonces, ¿por qué sería malo devolver varios tipos? Bueno … No lo es en absoluto a menos que lo abuses. Esto es, por supuesto, una cuestión de habilidades de progtwigción deficientes y / o no saber qué puede hacer el lenguaje que está usando.

¿No sería más barato que la memoria devolviera una Ninguna para crear una nueva tupla vacía o es esta diferencia de tiempo demasiado pequeña para notarla incluso en proyectos más grandes?

Por lo que sé, sí, devolver un objeto NoneType sería mucho más barato en cuanto a memoria. Aquí hay un pequeño experimento (los valores devueltos son bytes):

 >> sys.getsizeof(None) 16 >> sys.getsizeof(()) 48 

Según el tipo de objeto que está utilizando como su valor de retorno (tipo numérico, lista, diccionario, tupla, etc.), Python administra la memoria de diferentes maneras, incluido el almacenamiento inicialmente reservado.

Sin embargo, también debe considerar el código que está alrededor de la llamada a la función y cómo maneja lo que devuelve su función. ¿Comprobar si no es NoneType ? ¿O simplemente verifica si la tupla devuelta tiene una longitud de 0? Esta propagación del valor devuelto y su tipo ( NoneType tipo frente a tupla vacía en su caso) podría ser más tedioso de manejar y explotar en su cara. No lo olvide: el código en sí está cargado en la memoria, por lo que si el manejo del NoneType requiere demasiado código (incluso pequeños fragmentos de código, pero en gran cantidad) es mejor dejar la tupla vacía, lo que también evitará la confusión en la mente de las personas que usan Su función y olvidando que en realidad devuelve 2 tipos de valores.

Hablando de devolver múltiples tipos de valor, esta es la parte en la que estoy de acuerdo con la respuesta aceptada (pero solo parcialmente): devolver un solo tipo hace que el código sea más fácil de mantener, sin lugar a dudas. Es mucho más fácil verificar solo el tipo A y luego el A, B, C, … etc.

Sin embargo, Python es un lenguaje orientado a objetos y, como tal, herencia, clases abstractas, etc., y todo lo que forma parte de la totalidad de los chanchullos OOP entra en juego. Puede llegar incluso a generar clases sobre la marcha, que descubrí hace unos meses y que quedé atónito (nunca vi eso en C / C ++).

Nota al margen: puede leer un poco acerca de las metaclases y las clases dinámicas en este artículo de descripción general con muchos ejemplos.

De hecho, existen múltiples patrones de diseño y técnicas que ni siquiera existirían sin las llamadas funciones polimórficas. A continuación, les presento dos temas muy populares (no puedo encontrar una mejor manera de resumir ambos en un solo término):

  • Tipografía de pato : a menudo es parte de los lenguajes de escritura dinámica de los que Python es un representante
  • Patrón de diseño del método de fábrica : básicamente es una función que devuelve varios objetos según la entrada que recibe.

Finalmente, si su función devuelve uno o varios tipos se basa totalmente en el problema que tiene que resolver. ¿Se puede abusar de este comportamiento polimórfico? Claro, como todo lo demás.

Personalmente creo que está perfectamente bien que una función devuelva una tupla o Ninguna. Sin embargo, una función debe devolver como máximo 2 tipos diferentes y el segundo debe ser Ninguno. Una función nunca debe devolver una cadena y una lista, por ejemplo.

Si x se llama así

 foo, bar = x(foo) 

devolviendo None resultaría en una

 TypeError: 'NoneType' object is not iterable 

si 'bar' no está en foo .

Ejemplo

 def x(foo): if 'bar' in foo: return (foo, 'bar') return None foo, bar = x(["foo", "bar", "baz"]) print foo, bar foo, bar = x(["foo", "NOT THERE", "baz"]) print foo, bar 

Esto resulta en:

 ['foo', 'bar', 'baz'] bar Traceback (most recent call last): File "f.py", line 9, in  foo, bar = x(["foo", "NOT THERE", "baz"]) TypeError: 'NoneType' object is not iterable 

La optimización prematura es la fuente de todos los males. Los aumentos de eficiencia minúsculos pueden ser importantes, pero no hasta que haya probado que los necesita.

Cualquiera que sea su idioma: una función se define una vez, pero tiende a usarse en cualquier número de lugares. Tener un tipo de retorno consistente (sin mencionar las condiciones previas y posteriores documentadas) significa que tiene que hacer un mayor esfuerzo para definir la función, pero simplifica enormemente el uso de la función. ¿Adivina si los costos de una sola vez tienden a superar los ahorros repetidos …?