Asignación dentro de la expresión lambda en Python

105

Tengo una lista de objetos y quiero eliminar todos los objetos que están vacíos excepto uno, usando filtery una lambdaexpresión.

Por ejemplo, si la entrada es:

[Object(name=""), Object(name="fake_name"), Object(name="")]

... entonces la salida debería ser:

[Object(name=""), Object(name="fake_name")]

¿Hay alguna forma de agregar una asignación a una lambdaexpresión? Por ejemplo:

flag = True 
input = [Object(name=""), Object(name="fake_name"), Object(name="")] 
output = filter(
    (lambda o: [flag or bool(o.name), flag = flag and bool(o.name)][0]),
    input
)
Gato
fuente
1
No. Pero no necesitas esto. En realidad, creo que sería una forma bastante oscura de lograr esto incluso si funcionara.
8
¿Por qué no pasar una función antigua normal al filtro?
dfb
5
Quería usar lambda solo para que fuera una solución realmente compacta. Recuerdo que en OCaml pude imprimir declaraciones en cadena antes de la expresión de retorno, pensé que esto podría replicarse en Python
Cat
Es bastante doloroso estar en el flujo de desarrollo de una tubería encadenada y luego darse cuenta: "oh, quiero crear una var temporal para que el flujo sea más claro" o "quiero registrar este paso intermedio": y luego tienes que saltar en otro lugar para crear una función para hacerlo: y nombrar esa función y realizar un seguimiento de ella, aunque se use en un solo lugar.
javadba

Respuestas:

215

El operador de expresión de asignación :=agregado en Python 3.8 admite la asignación dentro de expresiones lambda. Este operador solo puede aparecer dentro de una expresión (...)entre paréntesis , entre corchetes [...]o entre corchetes {...}por razones sintácticas. Por ejemplo, podremos escribir lo siguiente:

import sys
say_hello = lambda: (
    message := "Hello world",
    sys.stdout.write(message + "\n")
)[-1]
say_hello()

En Python 2, era posible realizar asignaciones locales como efecto secundario de las listas por comprensión.

import sys
say_hello = lambda: (
    [None for message in ["Hello world"]],
    sys.stdout.write(message + "\n")
)[-1]
say_hello()

Sin embargo, no es posible usar ninguno de estos en su ejemplo porque su variable flagestá en un alcance externo, no en el lambdaalcance de. Esto no tiene que ver con lambda, es el comportamiento general en Python 2. Python 3 le permite evitar esto con la nonlocalpalabra clave dentro de defs, pero nonlocalno puede usarse dentro de lambdas.

Hay una solución alternativa (ver más abajo), pero ya que estamos en el tema ...


En algunos casos, puede usar esto para hacer todo dentro de un lambda:

(lambda: [
    ['def'
        for sys in [__import__('sys')]
        for math in [__import__('math')]

        for sub in [lambda *vals: None]
        for fun in [lambda *vals: vals[-1]]

        for echo in [lambda *vals: sub(
            sys.stdout.write(u" ".join(map(unicode, vals)) + u"\n"))]

        for Cylinder in [type('Cylinder', (object,), dict(
            __init__ = lambda self, radius, height: sub(
                setattr(self, 'radius', radius),
                setattr(self, 'height', height)),

            volume = property(lambda self: fun(
                ['def' for top_area in [math.pi * self.radius ** 2]],

                self.height * top_area))))]

        for main in [lambda: sub(
            ['loop' for factor in [1, 2, 3] if sub(
                ['def'
                    for my_radius, my_height in [[10 * factor, 20 * factor]]
                    for my_cylinder in [Cylinder(my_radius, my_height)]],

                echo(u"A cylinder with a radius of %.1fcm and a height "
                     u"of %.1fcm has a volume of %.1fcm³."
                     % (my_radius, my_height, my_cylinder.volume)))])]],

    main()])()

Un cilindro con un radio de 10.0cm y una altura de 20.0cm tiene un volumen de 6283.2cm³.
Un cilindro con un radio de 20,0 cm y una altura de 40,0 cm tiene un volumen de 50265,5 cm³.
Un cilindro con un radio de 30.0cm y una altura de 60.0cm tiene un volumen de 169646.0cm³.

Por favor no lo hagas.


... volviendo a su ejemplo original: aunque no puede realizar asignaciones a la flagvariable en el alcance externo, puede usar funciones para modificar el valor asignado previamente.

Por ejemplo, flagpodría ser un objeto .valueque configuramos usando setattr:

flag = Object(value=True)
input = [Object(name=''), Object(name='fake_name'), Object(name='')] 
output = filter(lambda o: [
    flag.value or bool(o.name),
    setattr(flag, 'value', flag.value and bool(o.name))
][0], input)
[Object(name=''), Object(name='fake_name')]

Si quisiéramos encajar en el tema anterior, podríamos usar una lista de comprensión en lugar de setattr:

    [None for flag.value in [bool(o.name)]]

Pero realmente, en código serio, siempre debe usar una definición de función regular en lugar de una lambdasi va a realizar una asignación externa.

flag = Object(value=True)
def not_empty_except_first(o):
    result = flag.value or bool(o.name)
    flag.value = flag.value and bool(o.name)
    return result
input = [Object(name=""), Object(name="fake_name"), Object(name="")] 
output = filter(not_empty_except_first, input)
Jeremy
fuente
El último ejemplo en esta respuesta no produce el mismo resultado que el ejemplo, pero me parece que el resultado del ejemplo es incorrecto.
Jeremy
En resumen, esto se reduce a: use .setattr()y similares (los diccionarios también deberían funcionar, por ejemplo) para piratear los efectos secundarios en el código funcional de todos modos, se mostró un código genial de @JeremyBanks :)
jno
¡Gracias por la nota en el assignment operator!
javadba
37

Realmente no puede mantener el estado en una expresión filter/ lambda(a menos que abuse del espacio de nombres global). Sin embargo, puede lograr algo similar utilizando el resultado acumulado que se transmite en una reduce()expresión:

>>> f = lambda a, b: (a.append(b) or a) if (b not in a) else a
>>> input = ["foo", u"", "bar", "", "", "x"]
>>> reduce(f, input, [])
['foo', u'', 'bar', 'x']
>>> 

Por supuesto, puede modificar un poco la condición. En este caso, filtra los duplicados, pero también puede usar a.count(""), por ejemplo, para restringir solo las cadenas vacías.

No hace falta decir que puedes hacer esto, pero realmente no deberías. :)

Por último, puede hacer cualquier cosa en Python puro lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/

Ivo van der Wijk
fuente
17

No es necesario usar una lambda, cuando puede eliminar todas las nulas y volver a colocar una si cambia el tamaño de entrada:

input = [Object(name=""), Object(name="fake_name"), Object(name="")] 
output = [x for x in input if x.name]
if(len(input) != len(output)):
    output.append(Object(name=""))
Gabi Purcaru
fuente
1
Creo que tienes un pequeño error en tu código. La segunda línea debería ser output = [x for x in input if x.name].
halex
El orden de los elementos puede ser importante.
MAnyKey
15

La asignación normal ( =) no es posible dentro de una lambdaexpresión, aunque es posible realizar varios trucos con setattry amigos.

Sin embargo, resolver su problema es bastante simple:

input = [Object(name=""), Object(name="fake_name"), Object(name="")]
output = filter(
    lambda o, _seen=set():
        not (not o and o in _seen or _seen.add(o)),
    input
    )

que te dará

[Object(Object(name=''), name='fake_name')]

Como puede ver, mantiene la primera instancia en blanco en lugar de la última. Si necesita el último en su lugar, invierta la lista que va a filter, e invierta la lista que sale de filter:

output = filter(
    lambda o, _seen=set():
        not (not o and o in _seen or _seen.add(o)),
    input[::-1]
    )[::-1]

que te dará

[Object(name='fake_name'), Object(name='')]

Una cosa a tener en cuenta: para que esto funcione con objetos arbitrarios, esos objetos deben implementarse correctamente __eq__y __hash__como se explica aquí .

Ethan Furman
fuente
7

ACTUALIZAR :

[o for d in [{}] for o in lst if o.name != "" or d.setdefault("", o) == o]

o usando filtery lambda:

flag = {}
filter(lambda o: bool(o.name) or flag.setdefault("", o) == o, lst)

Respuesta anterior

OK, ¿estás atascado en el uso de filtro y lambda?

Parece que esto se serviría mejor con una comprensión de diccionario,

{o.name : o for o in input}.values()

Creo que la razón por la que Python no permite la asignación en una lambda es similar a la razón por la que no permite la asignación en una comprensión y eso tiene algo que ver con el hecho de que estas cosas se evalúan en un Clado y, por lo tanto, pueden darnos una aumento de velocidad. Al menos esa es mi impresión después de leer uno de los ensayos de Guido .

Supongo que esto también iría en contra de la filosofía de tener una forma correcta de hacer cualquier cosa en Python.

cartero lechoso
fuente
Entonces esto no es del todo correcto. No preservará el orden, ni preservará los duplicados de los objetos con cadenas no vacías.
JPvdMerwe
7

TL; DR: cuando se usan modismos funcionales, es mejor escribir código funcional

Como muchas personas han señalado, en Python no se permite la asignación de lambdas. En general, cuando se utilizan modismos funcionales, es mejor pensar de manera funcional, lo que significa que, siempre que sea posible, no habrá efectos secundarios ni asignaciones.

Aquí hay una solución funcional que usa un lambda. He asignado la lambda a fnpara mayor claridad (y porque se volvió un poco larga).

from operator import add
from itertools import ifilter, ifilterfalse
fn = lambda l, pred: add(list(ifilter(pred, iter(l))), [ifilterfalse(pred, iter(l)).next()])
objs = [Object(name=""), Object(name="fake_name"), Object(name="")]
fn(objs, lambda o: o.name != '')

También puede hacer este trato con iteradores en lugar de listas cambiando un poco las cosas. También tiene algunas importaciones diferentes.

from itertools import chain, islice, ifilter, ifilterfalse
fn = lambda l, pred: chain(ifilter(pred, iter(l)), islice(ifilterfalse(pred, iter(l)), 1))

Siempre puede reorganizar el código para reducir la longitud de las declaraciones.

dietbuddha
fuente
6

Si en lugar de flag = Truepodemos hacer una importación, entonces creo que esto cumple con los criterios:

>>> from itertools import count
>>> a = ['hello', '', 'world', '', '', '', 'bob']
>>> filter(lambda L, j=count(): L or not next(j), a)
['hello', '', 'world', 'bob']

O tal vez el filtro esté mejor escrito como:

>>> filter(lambda L, blank_count=count(1): L or next(blank_count) == 1, a)

O, solo para un booleano simple, sin ninguna importación:

filter(lambda L, use_blank=iter([True]): L or next(use_blank, False), a)
Jon Clements
fuente
6

La forma pitónica de rastrear el estado durante la iteración es con generadores. La forma de itertools es bastante difícil de entender en mi humilde opinión y tratar de hackear lambdas para hacer esto es una tontería. Intentaría:

def keep_last_empty(input):
    last = None
    for item in iter(input):
        if item.name: yield item
        else: last = item
    if last is not None: yield last

output = list(keep_last_empty(input))

En general, la legibilidad siempre triunfa sobre la compacidad.

usuario2735379
fuente
4

No, no puede poner una asignación dentro de una lambda debido a su propia definición. Si trabaja con programación funcional, debe asumir que sus valores no son mutables.

Una solución sería el siguiente código:

output = lambda l, name: [] if l==[] \
             else [ l[ 0 ] ] + output( l[1:], name ) if l[ 0 ].name == name \
             else output( l[1:], name ) if l[ 0 ].name == "" \
             else [ l[ 0 ] ] + output( l[1:], name )
Baltasarq
fuente
4

Si necesita una lambda para recordar el estado entre llamadas, recomendaría una función declarada en el espacio de nombres local o una clase con una sobrecarga __call__ . Ahora que todas mis advertencias contra lo que está tratando de hacer están fuera del camino, podemos obtener una respuesta real a su consulta.

Si realmente necesita tener su lambda para tener algo de memoria entre llamadas, puede definirlo como:

f = lambda o, ns = {"flag":True}: [ns["flag"] or o.name, ns.__setitem__("flag", ns["flag"] and o.name)][0]

Entonces solo necesitas pasar fa filter(). Si realmente lo necesita, puede recuperar el valor de flagcon lo siguiente:

f.__defaults__[0]["flag"]

Alternativamente, puede modificar el espacio de nombres global modificando el resultado de globals(). Desafortunadamente, no puede modificar el espacio de nombres local de la misma manera que modificar el resultado de locals()no afecta el espacio de nombres local.

JPvdMerwe
fuente
O simplemente utilizar el Lisp el original: (let ((var 42)) (lambda () (setf var 43))).
Kaz
4

Puede usar una función de vinculación para usar una lambda pseudo-múltiple. Luego, puede usar una clase contenedora para una bandera para habilitar la asignación.

bind = lambda x, f=(lambda y: y): f(x)

class Flag(object):
    def __init__(self, value):
        self.value = value

    def set(self, value):
        self.value = value
        return value

input = [Object(name=""), Object(name="fake_name"), Object(name="")]
flag = Flag(True)
output = filter(
            lambda o: (
                bind(flag.value, lambda orig_flag_value:
                bind(flag.set(flag.value and bool(o.name)), lambda _:
                bind(orig_flag_value or bool(o.name))))),
            input)
pyrospade
fuente
0

Una especie de solución desordenada, pero la asignación en lambdas es ilegal de todos modos, por lo que realmente no importa. Puede usar la exec()función incorporada para ejecutar la asignación desde dentro de la lambda, como este ejemplo:

>>> val
Traceback (most recent call last):
  File "<pyshell#31>", line 1, in <module>
    val
NameError: name 'val' is not defined
>>> d = lambda: exec('val=True', globals())
>>> d()
>>> val
True
Usuario 12692182
fuente
-2

Primero, no necesita usar una asignación local para su trabajo, solo verifique la respuesta anterior

segundo, es simple usar locals () y globals () para obtener la tabla de variables y luego cambiar el valor

verifique este código de muestra:

print [locals().__setitem__('x', 'Hillo :]'), x][-1]

si necesita cambiar el agregar una variable global a su entorno, intente reemplazar locals () con globals ()

La composición de la lista de Python es genial, pero la mayoría del proyecto tridicional no acepta esto (como frasco: [)

espero que pueda ayudar

jyf1987
fuente
2
No se puede usar locals(), dice explícitamente en la documentación que cambiarlo en realidad no cambia el alcance local (o al menos no siempre lo hará). globals()por otro lado funciona como se esperaba.
JPvdMerwe
@JPvdMerwe solo inténtalo, no sigas el documento a ciegas. y la asignación en lambda es la regla de ruptura ya
jyf1987
3
Desafortunadamente, solo funciona en el espacio de nombres global, en cuyo caso realmente debería usar globals(). pastebin.com/5Bjz1mR4 (probado en 2.6 y 3.2) lo demuestra.
JPvdMerwe