create - Listar la comprensión como sustituto de reduce() en Python
python list comprehension if (4)
Al principio me sorprendió descubrir que Guido van Rossum, creador de Python, estaba en contra del reduce
. Su razonamiento fue que, más allá de sumar, multiplicar, y -ing y oringing, el uso de reduce
produce una solución ilegible que se adapta mejor a una función que itera y actualiza un acumulador. Su artículo al respecto está here . Así que no, no hay una alternativa de comprensión de lista para reduce
, en lugar de eso, la forma "pythonic" es implementar una función de acumulación de la manera antigua:
En lugar de:
out = reduce((lambda x,y: x*y),[1,2,3])
Utilizar:
def prod(myList):
out = 1
for el in myList:
out *= el
return out
Por supuesto, nada le impide continuar usando reduce
(python 2) o functools.reduce
(python 3)
El siguiente tutorial de Python dice que:
La comprensión de lista es un sustituto completo de la función lambda, así como de las funciones
map()
,filter()
yreduce()
.
Sin embargo, no menciona un ejemplo de cómo una comprensión de lista puede sustituir a reduce()
y no puedo pensar en un ejemplo de cómo debería ser posible.
¿Alguien puede explicar cómo lograr una funcionalidad similar a la reducción con una lista de comprensión o confirmar que no es posible?
Idealmente, la comprensión de la lista es crear una nueva lista. Citando documentación oficial ,
Las comprensiones de listas proporcionan una forma concisa de crear listas. Las aplicaciones comunes son hacer nuevas listas donde cada elemento es el resultado de algunas operaciones aplicadas a cada miembro de otra secuencia o iterable, o crear una subsecuencia de aquellos elementos que satisfacen una determinada condición.
Mientras que reduce
se utiliza para reducir un valor iterable a un solo valor. Citando functools.reduce
,
Aplique la función de dos argumentos de forma acumulativa a los elementos de la secuencia, de izquierda a derecha, para reducir la secuencia a un solo valor .
Por lo tanto, la lista de comprensión no se puede utilizar como un reemplazo directo para reduce
.
Podría lograr algo como una reducción con una comprensión utilizando un par de funciones de ayuda que he nombrado last
y cofold
:
>>> last(r(a+b) for a, b, r in cofold(range(10)))
45
Esto es funcionalmente equivalente a
>>> reduce(lambda a, b: a+b, range(10))
45
Tenga en cuenta que a diferencia de reduce()
la comprensión no usó un lambda
.
El truco es usar un generador con una devolución de llamada para "devolver" el resultado del operador. cofold
es el doble corecursivo de la función reducir (o plegar).
_sentinel = object()
def cofold(it, initial=_sentinel):
if initial is _sentinel:
it = iter(it)
accumulator = next(it)
else:
accumulator = initial
def callback(result):
nonlocal accumulator
accumulator = result
return result
for element in it:
yield accumulator, element, callback
Aquí está cofold
en una lista de comprensión.
>>> [r(a+b) for a, b, r in cofold(range(10))]
[1, 3, 6, 10, 15, 21, 28, 36, 45]
Los elementos representan cada paso en la reducción dual. La última es nuestra respuesta. La last
función es trivial.
def last(it):
for e in it:
pass
return e
A diferencia de reduce
, cofold
es un generador perezoso, por lo que puede actuar con seguridad en infinitos iterables cuando se usa en una expresión de generador.
>>> from itertools import islice, count
>>> lazy_results = (r(a+b) for a, b, r in cofold(count()))
>>> [*islice(lazy_results, 0, 9)]
[1, 3, 6, 10, 15, 21, 28, 36, 45]
>>> next(lazy_results)
55
>>> next(lazy_results)
66
Se supone que las comprensiones de listas devuelven listas . Si se supone que su reducción debe devolver una lista, entonces sí, puede reemplazarla con una lista de comprensión.
Pero esto no es un obstáculo para proporcionar una "funcionalidad similar a una reducción". Las listas de Python pueden contener cualquier objeto. Si acepta el resultado contenido en una lista de un solo elemento, entonces hay un formulario de [...][0]
lista de comprensión de [...][0]
que puede reemplazar cualquier reduce()
absoluto.
Esto debería ser obvio, pero esa forma es
[x for x in [reduce(function, sequence, initial)]][0]
para alguna function
binaria y y alguna sequence
iterable y algún valor initial
. O, si quieres la initial
del primero de lo iterable,
[x for x in [reduce(function, sequence)]][0]
Podría decirse que lo anterior es trampa, y también sin sentido, ya que simplemente podría usar reduce
sin la comprensión. Así que intentémoslo sin reduce
.
[stack.append(function(stack.pop(), e)) or stack[0]
for stack in ([initial],)
for e in sequence][-1]
Esto produce una lista de todos los valores intermedios, y queremos el último. [-1]
es tan fácil como [0]
. Necesitamos un acumulador para reducir, pero no podemos usar declaraciones de asignación en una comprensión, de ahí la stack
(que es solo una lista), pero podríamos haber usado muchas otras estructuras de datos aquí. El .append()
siempre devuelve None
, así que usamos or stack[0]
para poner el valor hasta ahora en la lista resultante.
Es un poco más difícil sin initial
,
[stack.append(function(stack.pop(), e)) or stack[0]
for it in [iter(sequence)]
for stack in [[next(it)]]
for e in it][-1]
Realmente, también podrías usar una declaración for
en este punto.
Pero esto ocupa memoria para la lista de valores intermedios. Para una secuencia muy larga, eso podría ser un problema. Pero también podemos evitar eso usando expresiones generadoras.
Hacer esto es complicado, así que comencemos con un ejemplo más fácil y trabajemos para ello.
stack = [initial]
[stack.append(function(stack.pop(), e)) for e in sequence]
stack.pop() # returns the answer
Calcula la respuesta, pero también crea una lista inútil de None
. Podemos evitarlo convirtiéndolo en una expresión generadora dentro de una lista de comprensión.
stack = [initial]
[_ for _s in (stack.append(function(stack.pop(), e)) or ()
for e in sequence)
for _ in _s]
stack.pop()
La comprensión de la lista agota el generador que actualiza la pila, pero devuelve una lista vacía. Esto es posible porque el bucle interno siempre tiene cero iteraciones, porque _s
siempre es una tupla vacía.
Podemos mover el stack.pop()
dentro si el último _s
tiene un elemento. Sin embargo, no importa cuál sea ese elemento. Así que encadenamos a un [None]
como _s
final.
from itertools import chain
stack = [initial]
[stack.pop()
for _s in chain((stack.append(function(stack.pop(), e)) or ()
for e in sequence),
[[None]])
for _ in _s][0]
De nuevo, tenemos una lista de comprensión de un solo ítem. También podemos implementar la chain
como expresión generadora. Y ya has visto cómo mover la variable de stack
dentro de una lista de un solo elemento.
[stack.pop()
for stack in [[initial]]
for _s in (
x
for xs in [
(stack.append(function(stack.pop(), e)) or ()
for e in sequence),
[[None]],
]
for x in xs)
for _ in _s][0]
Y también podemos obtener la inicial de la secuencia para la reduce
dos argumentos.
[stack.pop()
for it in [iter(sequence)]
for stack in [[next(it)]]
for _s in (
x
for xs in [
(stack.append(function(stack.pop(), e)) or ()
for e in it),
[[None]],
]
for x in xs)
for _ in _s][0]
Esto es una locura. Pero funciona. Así que sí, es posible obtener una "funcionalidad similar a la reducción" con las comprensiones. Eso no significa que debas . ¡Siete for
s es demasiado difícil!