¿Por qué progtwigr funcionalmente en Python?

En el trabajo, solíamos progtwigr nuestro Python de una manera bastante estándar OO. Últimamente, un par de tipos se subieron al carro funcional. Y su código ahora contiene muchas más lambdas, mapas y reducciones. Entiendo que los lenguajes funcionales son buenos para la concurrencia, pero ¿la progtwigción de Python realmente ayuda con la concurrencia? Solo estoy tratando de entender lo que obtengo si comienzo a usar más funciones funcionales de Python.

Edit : los fanáticos de FP en Python, pero no exclusivamente, me han tomado en cuenta en los comentarios (en parte, al parecer, por no proporcionar más explicaciones / ejemplos, por lo tanto, ampliando la respuesta para proporcionar algo).

lambda , más aún el map (y el filter ), y más especialmente la reduce , casi nunca son la herramienta adecuada para el trabajo en Python, que es un lenguaje fuertemente multi-paradigma.

lambda ventaja principal de lambda (?) en comparación con la def normal es que hace una función anónima , mientras que la def le da un nombre a la función, y por esa muy dudosa ventaja, usted paga un precio enorme (el cuerpo de la función está limitado a una expresión, el objeto de función resultante no es pickleable, la falta de un nombre a veces hace que sea mucho más difícil entender un seguimiento de la stack o, de lo contrario, depurar un problema, ¿debo continuar?! -).

Considere cuál es probablemente el idioma más idiotico que a veces se usa en “Python” (Python con “frases de miedo”, porque obviamente no es idiomático. Es una transliteración de un esquema idiomático o algo similar, como el uso excesivo más frecuente de OOP en Python es una mala transliteración de Java o similar):

 inc = lambda x: x + 1 

al asignar la lambda a un nombre, este enfoque desecha de inmediato la “ventaja” antes mencionada, ¡y no pierde ninguna de las desventajas! Por ejemplo, inc no sabe su nombre – inc.__name__ es la cadena inútil '' – buena suerte al entender una traza de stack con algunos de estos ;-). La forma correcta de Python para lograr la semántica deseada en este caso simple es, por supuesto:

 def inc(x): return x + 1 

Ahora inc.__name__ es la cadena 'inc' , como claramente debería ser, y el objeto es pickleable: la semántica es idéntica (en este simple caso donde la funcionalidad deseada cabe cómodamente en una expresión simple, def también la hace trivialmente fácil de refactorizar si necesita insertar declaraciones temporal o permanentemente como print o raise , por supuesto).

lambda es (parte de) una expresión, mientras que def es (parte de) una statement: ese es el bit de syntax que hace que la gente use lambda veces. A muchos entusiastas de la FP (al igual que a los fans de OOP y de procedimiento) no les gusta la distinción razonablemente fuerte de Python entre expresiones y declaraciones (parte de una postura general hacia la Separación de la Consulta de Comando ). Yo, creo que cuando usas un lenguaje es mejor usarlo “con el grano”, la forma en que fue diseñado para ser usado, en lugar de luchar contra él; así que programo Python de forma pythonica, Esquema de forma esquemática (;-), Fortran de manera Fortesque (?), y así sucesivamente :-).

Pasando a reduce : un comentario que reduce es la mejor manera de calcular el producto de una lista. ¿Oh enserio? Veamos…:

 $ python -mtimeit -s'L=range(12,52)' 'reduce(lambda x,y: x*y, L, 1)' 100000 loops, best of 3: 18.3 usec per loop $ python -mtimeit -s'L=range(12,52)' 'p=1' 'for x in L: p*=x' 100000 loops, best of 3: 10.5 usec per loop 

¿Entonces el bucle simple, elemental y trivial es aproximadamente el doble de rápido (y más conciso) que la “mejor manera” de realizar la tarea? -) Supongo que las ventajas de la velocidad y la concisión deben hacer que el bucle trivial sea el “mejor” “camino, ¿verdad? -)

Al sacrificar aún más la compacidad y la legibilidad …

 $ python -mtimeit -s'import operator; L=range(12,52)' 'reduce(operator.mul, L, 1)' 100000 loops, best of 3: 10.7 usec per loop 

casi podemos regresar al rendimiento fácilmente obtenido del enfoque más simple, obvio, compacto y legible (el bucle simple, elemental y trivial). Esto señala otro problema con lambda , en realidad: ¡rendimiento! Para operaciones suficientemente simples, como la multiplicación, la sobrecarga de una llamada de función es bastante significativa en comparación con la operación real que se está realizando. reduce (y map y filter ) a menudo lo obliga a insertar dicha llamada de función donde los bucles simples, enumera las comprensiones, y las expresiones del generador, permiten la legibilidad, la compacidad y la velocidad de las operaciones en línea.

Tal vez incluso peor que el antedicho “asignar un lambda a un nombre” anti-idiom es en realidad el siguiente anti-idiom, por ejemplo, para ordenar una lista de cadenas según su longitud:

 thelist.sort(key=lambda s: len(s)) 

En lugar de lo obvio, legible, compacto, más rápido.

 thelist.sort(key=len) 

Aquí, el uso de lambda no hace más que insertar un nivel de direccionamiento indirecto, sin ningún efecto positivo y muchos malos.

La motivación para usar lambda es a menudo permitir el uso de map y filter lugar de una comprensión de lista o bucle muy preferible que le permitiría hacer cálculos simples y normales en línea; Todavía pagas ese “nivel de direccionamiento indirecto”, por supuesto. No es Pythonic tener que preguntarse “¿debería usar una lista o un mapa aquí?”, Siempre use las “listcomps”, cuando ambas aparezcan aplicables y usted no sepa cuál elegir, sobre la base de “debería haber una, y Preferiblemente solo una, forma obvia de hacer algo “. A menudo, escribirá listas de comandos que no podrían traducirse de manera sensata a un mapa (bucles nesteds, cláusulas if , etc.), mientras que no hay llamadas a map que no puedan ser reescritas sensiblemente como una lista compacta.

Los enfoques funcionales perfectamente adecuados en Python a menudo incluyen listas de comprensión, expresiones generadoras, itertools , funciones de orden superior, funciones de primer orden en varias formas, cierres, generadores (y ocasionalmente otros tipos de iteradores).

itertools , como comentó un comentarista, incluye imap y ifilter : la diferencia es que, como todos los itertools, estos se basan en flujos (como el map y el filter incorporados en Python 3, pero de manera diferente a los incorporados en Python 2). itertools ofrece un conjunto de bloques de construcción que se componen bien entre sí, y un rendimiento espléndido: especialmente si se encuentra potencialmente lidiando con secuencias muy largas (o incluso ilimitadas!), debe familiarizarse con las itertools: su Todo el capítulo en la documentación es una buena lectura, y las recetas en particular son bastante instructivas.

Escribir sus propias funciones de orden superior suele ser útil, especialmente cuando son adecuadas para usarlas como decoradoras (ambas funciones, como se explica en esa parte de los documentos, y decoradores de clase, introducidas en Python 2.6). ¡Recuerde utilizar functools.wraps en sus decoradores de funciones (para mantener envueltos los metadatos de la función)!

Entonces, resumiendo …: cualquier cosa que pueda codificar con lambda , map y filter , puede codificar (más a menudo que no ventajosamente) con def (funciones nombradas) y listcomps – y, por lo general, subir una muesca a generadores, expresiones generadoras , o itertools , es aún mejor. reduce cumple con la definición legal de “molestia atractiva” …: casi nunca es la herramienta adecuada para el trabajo (¡por eso ya no se incluye en Python 3, por fin! -).

La FP es importante no solo para la concurrencia; de hecho, virtualmente no hay concurrencia en la implementación canónica de Python (¿tal vez 3.x cambia eso?). en cualquier caso, FP se presta a la concurrencia porque conduce a progtwigs con menos o menos estados (explícitos). Los estados son problemáticos por algunas razones. uno es que hacen difícil distribuir el cálculo (er) (ese es el argumento de la concurrencia), otro, mucho más importante en la mayoría de los casos, es la tendencia a infligir errores. La mayor fuente de errores en el software contemporáneo son las variables (existe una estrecha relación entre las variables y los estados). FP puede reducir el número de variables en un progtwig: ¡bugs aplastados!

vea cuántos errores puede introducir al mezclar las variables en estas versiones:

 def imperative(seq): p = 1 for x in seq: p *= x return p 

versus (advertencia, la lista de parámetros de my.reduce difiere de la de python reduce ; razón my.reduce más adelante)

 import operator as ops def functional(seq): return my.reduce(ops.mul, 1, seq) 

Como puede ver, es un hecho que FP le da menos oportunidades de dispararse en el pie con un error relacionado con las variables.

también, legibilidad: puede tomar un poco de entrenamiento, pero functional es mucho más fácil de leer que imperative : ves reduce (“ok, está reduciendo una secuencia a un solo valor”), mul (“por multiplicación”). Donde el imperative tiene la forma genérica de un ciclo, salpicado de variables y asignaciones. estos ciclos se ven igual, así que para tener una idea de lo que está sucediendo en el imperative , es necesario leer casi todo.

Luego hay sucintidad y flexibilidad. Me das un imperative y te digo que me gusta, pero también quieres algo para sumr secuencias. No hay problema, dices, y listo, copiando y pegando:

 def imperative(seq): p = 1 for x in seq: p *= x return p def imperative2(seq): p = 0 for x in seq: p += x return p 

¿Qué puedes hacer para reducir la duplicación? Bueno, si los operadores fueran valores, podrías hacer algo como

 def reduce(op, seq, init): rv = init for x in seq: rv = op(rv, x) return rv def imperative(seq): return reduce(*, 1, seq) def imperative2(seq): return reduce(+, 0, seq) 

¡Oh espera! operators proporcionan operadores que son valores! pero … Alex Martelli ya condenó a reduce … parece que si quieres permanecer dentro de los límites que sugiere, estás condenado a copiar y pegar el código de tuberías.

¿Es la versión de FP mejor? ¿Seguro que también necesitarías copiar y pegar?

 import operator as ops def functional(seq): return my.reduce(ops.mul, 1, seq) def functional2(seq): return my.reduce(ops.add, 0, seq) 

Bueno, eso es solo un artefacto del enfoque a medias. Al abandonar la def imperativa, puede contratar ambas versiones para

 import functools as func, operator as ops functional = func.partial(my.reduce, ops.mul, 1) functional2 = func.partial(my.reduce, ops.add, 0) 

o incluso

 import functools as func, operator as ops reducer = func.partial(func.partial, my.reduce) functional = reducer(ops.mul, 1) functional2 = reducer(ops.add, 0) 

( func.partial es la razón de my.reduce )

¿Qué pasa con la velocidad de ejecución? Sí, usar FP en un lenguaje como Python incurrirá en algunos gastos generales. Aquí solo repito lo que algunos profesores tienen que decir sobre esto:

  • La optimización prematura es la fuente de todos los males.
  • la mayoría de los progtwigs gastan el 80% de su tiempo de ejecución en el 20% de su código.
  • perfil, no especular!

No soy muy bueno explicando cosas. No me permita enturbiar demasiado el agua, lea la primera mitad del discurso que dio John Backus con motivo de recibir el Premio Turing en 1977. Cita:

5.1 Un progtwig de von Neumann para el producto interno

 c := 0 for i := I step 1 until n do c := c + a[i] * b[i] 

Destacan varias propiedades de este progtwig:

  1. Sus declaraciones operan en un “estado” invisible de acuerdo con reglas complejas.
  2. No es jerárquico. Excepto por el lado derecho de la instrucción de asignación, no construye entidades complejas a partir de entidades más simples. (Los progtwigs más grandes, sin embargo, a menudo lo hacen).
  3. Es dynamic y repetitivo. Uno debe ejecutarlo mentalmente para entenderlo.
  4. Calcula palabra por palabra por repetición (de la asignación) y por modificación (de la variable i).
  5. Parte de los datos, n , está en el progtwig; por lo tanto, carece de generalidad y funciona solo para vectores de longitud n .
  6. Nombra sus argumentos; solo se puede utilizar para los vectores a y b . Para hacerse general, se requiere una statement de procedimiento. Estos implican problemas complejos (por ejemplo, llamada por nombre frente a llamada por valor).
  7. Sus operaciones de “limpieza” están representadas por símbolos en lugares dispersos (en la statement for y los subíndices en la asignación). Esto hace que sea imposible consolidar las operaciones de limpieza, las más comunes de todas, en operadores únicos, potentes y de gran utilidad. Por lo tanto, en la progtwigción de esas operaciones, siempre se debe comenzar nuevamente en el punto uno, escribiendo ” for i := ... ” y ” for j := ... ” seguido de declaraciones de asignación esparcidas con i ‘s y j ‘ s.

Programo en Python todos los días, y tengo que decir que demasiado ‘arrasar’ hacia OO o funcional podría llevar a perder soluciones elegantes. Creo que ambos paradigmas tienen sus ventajas para ciertos problemas, y creo que es cuando se sabe qué enfoque utilizar. Use un enfoque funcional cuando le deje una solución limpia, legible y eficiente. Lo mismo ocurre con OO.

Y esa es una de las razones por las que amo Python: el hecho de que es multi-paradigma y le permite al desarrollador elegir cómo resolver su problema.

Esta respuesta está completamente reelaborada. Incorpora muchas observaciones de las otras respuestas.

Como puede ver, hay muchos sentimientos fuertes en torno al uso de construcciones de progtwigción funcional en Python. Hay tres grandes grupos de ideas aquí.

Primero, casi todos, excepto las personas que están más ligadas a la expresión más pura del paradigma funcional, están de acuerdo en que la comprensión de la lista y el generador son mejores y más claras que el uso de un map o un filter . Sus colegas deben evitar el uso de map y filter si está apuntando a una versión de Python lo suficientemente nueva como para admitir la comprensión de listas. Y deberías evitar itertools.imap e itertools.ifilter si tu versión de Python es lo suficientemente nueva como para comprender el generador.

En segundo lugar, hay mucha ambivalencia en la comunidad en general sobre lambda . Mucha gente está realmente molesta por la syntax además de la def de funciones, especialmente una que involucra una palabra clave como lambda que tiene un nombre bastante extraño. Y a la gente también le molesta que a estas pequeñas funciones anónimas les falte cualquiera de los metadatos que describen cualquier otro tipo de función. Hace la depuración más difícil. Por último, las funciones pequeñas declaradas por lambda menudo no son tan eficientes, ya que requieren la sobrecarga de una llamada a la función Python cada vez que se invocan, lo que suele estar en un bucle interno.

Por último, la mayoría de las personas (lo que significa> 50%, pero probablemente no el 90%) piensan que reduce es un poco extraño y oscuro. Yo mismo admito que tengo el print reduce.__doc__ cuando quiero usarlo, lo cual no es tan frecuente. Aunque cuando lo veo usado, la naturaleza de los argumentos (es decir, función, lista o iterador, escalar) hablan por sí mismos.

En cuanto a mí, caigo en el campamento de personas que piensan que el estilo funcional suele ser muy útil. Pero equilibrar ese pensamiento es el hecho de que Python no es un lenguaje funcional en el fondo. Y el uso excesivo de construcciones funcionales puede hacer que los progtwigs parezcan extrañamente retorcidos y difíciles de entender para las personas.

Para entender cuándo y dónde el estilo funcional es muy útil y mejora la legibilidad, considere esta función en C ++:

 unsigned int factorial(unsigned int x) { int fact = 1; for (int i = 2; i <= n; ++i) { fact *= i; } return fact } 

Este bucle parece muy simple y fácil de entender. Y en este caso lo es. Pero su aparente simplicidad es una trampa para los incautos. Considere este medio alternativo de escribir el bucle:

 unsigned int factorial(unsigned int n) { int fact = 1; for (int i = 2; i <= n; i += 2) { fact *= i--; } return fact; } 

De repente, la variable de control de bucle ya no varía de manera obvia. Está reducido a mirar el código y razonar cuidadosamente sobre lo que sucede con la variable de control de bucle. Ahora este ejemplo es un poco patológico, pero hay ejemplos del mundo real que no lo son. Y el problema está en el hecho de que la idea es la asignación repetida a una variable existente. No se puede confiar en que el valor de la variable sea el mismo en todo el cuerpo del bucle.

Este es un problema reconocido hace mucho tiempo, y en Python escribir un bucle como este es bastante poco natural. Tienes que usar un bucle while, y se ve mal. En cambio, en Python escribirías algo como esto:

 def factorial(n): fact = 1 for i in xrange(2, n): fact = fact * i; return fact 

Como puede ver, la forma en que habla acerca de la variable de control de bucle en Python no se puede engañar con ella dentro del bucle. Esto elimina muchos de los problemas con los bucles "inteligentes" en otros lenguajes imperativos. Desafortunadamente, es una idea que está semi-prestada de lenguajes funcionales.

Incluso esto se presta a un juguetón extraño. Por ejemplo, este bucle:

 c = 1 for i in xrange(0, min(len(a), len(b))): c = c * (a[i] + b[i]) if i < len(a): a[i + 1] = a[a + 1] + 1 

Vaya, otra vez tenemos un bucle que es difícil de entender. Superficialmente se asemeja a un bucle realmente simple y obvio, y debe leerlo cuidadosamente para darse cuenta de que una de las variables utilizadas en el cálculo del bucle se está confundiendo de una manera que afectará las ejecuciones futuras del bucle.

De nuevo, un acercamiento más funcional al rescate:

 from itertools import izip c = 1 for ai, bi in izip(a, b): c = c * (ai + bi) 

Ahora, al observar el código, tenemos una clara indicación (en parte por el hecho de que la persona está utilizando este estilo funcional) de que las listas a y b no se modifican durante la ejecución del bucle. Una cosa menos para pensar.

Lo último de lo que preocuparse es la modificación de formas extrañas. Quizás es una variable global y está siendo modificada por una llamada a la función de rotonda. Para rescatarnos de esta preocupación mental, aquí hay un enfoque puramente funcional:

 from itertools import izip c = reduce(lambda x, ab: x * (ab[0] + ab[1]), izip(a, b), 1) 

Muy conciso, y la estructura nos dice que x es puramente un acumulador. Es una variable local dondequiera que aparezca. El resultado final se asigna de forma inequívoca a c. Ahora hay mucho menos de qué preocuparse. La estructura del código elimina varias clases de posibles errores.

Es por eso que la gente puede elegir un estilo funcional. Es conciso y claro, al menos si entiendes lo que reduce y lambda hacen. Hay grandes clases de problemas que podrían afectar a un progtwig escrito en un estilo más imperativo que usted sabe que no afectará a su progtwig de estilo funcional.

En el caso de factorial, hay una forma muy simple y clara de escribir esta función en Python en un estilo funcional:

 import operator def factorial(n): return reduce(operator.mul, xrange(2, n+1), 1) 

La pregunta, que parece ser mayormente ignorada aquí:

¿La progtwigción funcional de Python realmente ayuda con la concurrencia?

No. El valor que FP aporta a la concurrencia es la eliminación del estado en el cómputo, que en última instancia es responsable de la maldad de los errores no intencionados en el cómputo concurrente. Pero depende de que los lenguajes de progtwigción concurrentes no sean ellos mismos con estado, algo que no se aplica a Twisted. Si hay lenguajes de concurrencia para Python que aprovechan la progtwigción sin estado, no los conozco.

Aquí hay un breve resumen de las respuestas positivas cuando / por qué progtwigr funcionalmente.

  • Las comprensiones de listas se importaron de Haskell, un lenguaje de FP. Ellos son pythonicos. Prefiero escribir
 y = [i*2 for i in k if i % 3 == 0] 

que utilizar un constructo imperativo (loop).

  • list.sort(key=lambda x: x.value.estimate()) lambda cuando diera una clave complicada para sort , como list.sort(key=lambda x: x.value.estimate())

  • Es más limpio usar funciones de orden superior que escribir código usando patrones de diseño de OOP como visitante o fábrica abstracta

  • La gente dice que debes progtwigr Python en Python, C ++ en C ++ etc. Eso es cierto, pero ciertamente deberías poder pensar de diferentes maneras en la misma cosa. Si mientras escribe un bucle sabe que realmente está reduciendo (doblando), entonces podrá pensar en un nivel superior. Eso limpia tu mente y te ayuda a organizar. Por supuesto, el pensamiento de nivel inferior también es importante.

NO debes usar esas funciones en exceso, hay muchas trampas, consulta la publicación de Alex Martelli. Subjetivamente, el peligro más grave es que el uso excesivo de esas funciones destruirá la legibilidad de su código, que es un atributo central de Python.

Las funciones estándar filter (), map () y reduce () se usan para varias operaciones en una lista y las tres funciones esperan dos argumentos: una función y una lista

Podríamos definir una función separada y usarla como un argumento para filtrar (), etc., y es probablemente una buena idea si esa función se usa varias veces, o si la función es demasiado compleja para escribirla en una sola línea. Sin embargo, si solo se necesita una vez y es bastante simple, es más conveniente usar una construcción lambda para generar una función anónima (temporal) y pasarla a filter ().

Esto ayuda en la readability and compact code.

El uso de esta función también resultaría efficient , ya que el bucle en los elementos de la lista se realiza en C, que es un poco más rápido que el bucle en python.

Y la forma orientada a objetos se necesita a la fuerza cuando se deben mantener los estados, aparte de la abstracción, agrupación, etc., si el requisito es bastante simple, me quedaría con funcional que para la progtwigción orientada a objetos.

Mapa y filtro tienen su lugar en la progtwigción OO. Justo al lado de la lista de comprensión y funciones del generador.

Reducir menos así. El algoritmo para reducir puede absorber rápidamente más tiempo del que merece; con un poco de pensamiento, un ciclo de reducción escrito manualmente será más eficiente que un botón de reducción que aplica una función de bucle mal pensada a una secuencia.

Lambda nunca. Lambda es inútil. Uno puede argumentar que realmente hace algo, por lo que no es completamente inútil . Primero: Lambda no es sintáctico “azúcar”; Hace las cosas más grandes y más feas. Segundo: la única vez en 10,000 líneas de código que cree que necesita una función “anónima” se convierte en dos veces en 20,000 líneas de código, lo que elimina el valor del anonimato, convirtiéndolo en una obligación de mantenimiento.

Sin embargo.

El estilo funcional de la progtwigción sin cambio de estado de objeto sigue siendo de naturaleza OO. Solo haces más creación de objetos y menos actualizaciones de objetos. Una vez que comience a usar las funciones del generador, la progtwigción de OO se desvía en una dirección funcional.

Cada cambio de estado parece traducirse en una función generadora que construye un nuevo objeto en el nuevo estado a partir de objetos antiguos. Es una visión del mundo interesante porque el razonamiento sobre el algoritmo es mucho, mucho más simple.

Pero eso no es una llamada para usar reduce o lambda.