Atribuição dentro da expressão lambda em Python

105

Eu tenho uma lista de objetos e quero remover todos os objetos que estão vazios, exceto um, usando filtere umlambda expressão.

Por exemplo, se a entrada for:

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

... então a saída deve ser:

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

Existe uma maneira de adicionar uma atribuição a uma lambdaexpressão? Por exemplo:

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
fonte
1
Não. Mas você não precisa disso. Na verdade, acho que seria uma maneira bem obscura de conseguir isso, mesmo que funcionasse.
8
Por que não apenas passar uma função antiga regular para o filtro?
dfb
5
Eu queria usar lambda apenas para que fosse uma solução realmente compacta. Lembro que no OCaml eu poderia encadear declarações de impressão antes da expressão de retorno, mas isso poderia ser replicado em Python
Cat
É muito doloroso estar no fluxo do desenvolvimento de um pipeilne encadeado, então perceber: "oh, eu quero criar uma var temp para tornar o fluxo mais claro" ou "quero registrar esta etapa intermediária": e então você tem que pular em outro lugar para criar uma função para fazê-lo: e nomear essa função e mantê-la sob controle - mesmo que seja usada em apenas um lugar.
javadba

Respostas:

215

O operador de expressão de atribuição :=adicionado no Python 3.8 suporta atribuição dentro de expressões lambda. Este operador só pode aparecer entre parênteses (...), colchetes [...]ou colchetes{...} expressão por motivos sintáticos. Por exemplo, seremos capazes de escrever o seguinte:

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

No Python 2, era possível realizar atribuições locais como um efeito colateral das compreensões de lista.

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

No entanto, não é possível usar nenhum desses em seu exemplo porque sua variável flagestá em um escopo externo, não o lambdaescopo de. Isso não tem a ver com lambda, é o comportamento geral em Python 2. Python 3 permite que você contorne isso com a nonlocalpalavra - chave dentro dedef s, mas nonlocalnão pode ser usada dentro de lambdas.

Há uma solução alternativa (veja abaixo), mas já que estamos no assunto ...


Em alguns casos, você pode usar isso para fazer tudo dentro de 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()])()

Um cilindro com raio de 10,0cm e altura de 20,0cm tem um volume de 6283,2cm³.
Um cilindro com raio de 20,0cm e altura de 40,0cm tem um volume de 50265,5cm³.
Um cilindro com raio de 30,0cm e altura de 60,0cm tem um volume de 169646,0cm³.

Por favor, não.


... de volta ao seu exemplo original: embora você não possa realizar atribuições à flagvariável no escopo externo, você pode usar funções para modificar o valor atribuído anteriormente.

Por exemplo, flagpoderia ser um objeto cujo .valuedefinimos 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')]

Se quiséssemos nos ajustar ao tema acima, poderíamos usar uma compreensão de lista em vez de setattr:

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

Mas, realmente, em código sério, você sempre deve usar uma definição de função regular em vez de uma lambdase for fazer atribuições externas.

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
fonte
O último exemplo nesta resposta não produz a mesma saída que o exemplo, mas me parece que a saída do exemplo está incorreta.
Jeremy
em suma, isso se resume a: use .setattr()and alikes ( dicionários devem servir também, por exemplo) para hackear efeitos colaterais em código funcional de qualquer maneira, código legal de @JeremyBanks foi mostrado :)
jno
Obrigado pela nota no assignment operator!
javadba
37

Você não pode realmente manter o estado em uma expressão filter/ lambda(a menos que esteja abusando do namespace global). No entanto, você pode conseguir algo semelhante usando o resultado acumulado transmitido em uma reduce()expressão:

>>> 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']
>>> 

Você pode, é claro, ajustar um pouco a condição. Nesse caso, ele filtra duplicatas, mas você também pode usar a.count(""), por exemplo, para restringir apenas strings vazias.

Desnecessário dizer que você pode fazer isso, mas realmente não deveria. :)

Por último, você pode fazer qualquer coisa em Python puro lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/

Ivo van der Wijk
fonte
17

Não há necessidade de usar um lambda, quando você pode remover todos os nulos e colocar um de volta se o tamanho de entrada mudar:

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
fonte
1
Acho que você tem um pequeno erro em seu código. A segunda linha deve ser output = [x for x in input if x.name].
halex
A ordem dos elementos pode ser importante.
MAnyKey
15

A atribuição normal ( =) não é possível dentro de uma lambdaexpressão, embora seja possível realizar vários truques com setattre amigos.

Resolver seu problema, no entanto, é na verdade muito simples:

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
    )

o que vai te dar

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

Como você pode ver, ele mantém a primeira instância em branco em vez da última. Se você precisar do último em vez disso, inverta a lista indo para filtere inverta a lista saindo de filter:

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

o que vai te dar

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

Uma coisa a estar ciente: para que isso funcione com objetos arbitrários, esses objetos devem ser implementados corretamente __eq__e __hash__conforme explicado aqui .

Ethan Furman
fonte
7

ATUALIZAÇÃO :

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

ou usando filtere lambda:

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

Resposta Anterior

OK, você está preso em usar filtro e lambda?

Parece que isso seria melhor servido com uma compreensão de dicionário,

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

Acho que o motivo pelo qual Python não permite atribuição em um lambda é semelhante ao motivo pelo qual ele não permite atribuição em uma compreensão e isso tem a ver com o fato de que essas coisas são avaliadas Clateralmente e, portanto, podem nos dar uma aumento na velocidade. Pelo menos essa é minha impressão depois de ler um dos ensaios de Guido .

Meu palpite é que isso também iria contra a filosofia de ter uma maneira certa de fazer qualquer coisa em Python.

carteiro leitoso
fonte
Portanto, isso não está totalmente certo. Não preservará a ordem, nem preservará duplicatas de objetos de string não vazia.
JPvdMerwe
7

TL; DR: ao usar expressões funcionais, é melhor escrever código funcional

Como muitas pessoas apontaram, em Python a atribuição de lambdas não é permitida. Em geral, ao usar expressões funcionais, é melhor pensar de maneira funcional, o que significa, sempre que possível, nenhum efeito colateral e nenhuma atribuição.

Aqui está uma solução funcional que usa um lambda. Atribuí lambda para fnpara maior clareza (e porque ficou um pouco longo).

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 != '')

Você também pode fazer isso com iteradores em vez de listas, mudando um pouco as coisas. Você também tem algumas importações diferentes.

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

Você sempre pode reorganizar o código para reduzir o comprimento das instruções.

dietbuddha
fonte
6

Se, em vez de flag = True, pudermos fazer uma importação, acho que isso atende aos critérios:

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

Ou talvez o filtro seja melhor escrito como:

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

Ou, apenas para um booleano simples, sem nenhuma importação:

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

A maneira pythônica de rastrear o estado durante a iteração é com geradores. A forma de itertools é bastante difícil de entender IMHO e tentar hackear lambdas para fazer isso é simplesmente bobo. Eu tentaria:

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))

No geral, a legibilidade sempre supera a compactação.

user2735379
fonte
4

Não, você não pode colocar uma atribuição dentro de um lambda por causa de sua própria definição. Se você trabalha com programação funcional, deve assumir que seus valores não são mutáveis.

Uma solução seria o seguinte 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
fonte
4

Se você precisar de um lambda para lembrar o estado entre as chamadas, eu recomendaria uma função declarada no namespace local ou uma classe com um __call__ . Agora que todas as minhas precauções contra o que você está tentando fazer estão fora do caminho, podemos obter uma resposta real à sua consulta.

Se você realmente precisa ter seu lambda para ter alguma memória entre as chamadas, pode defini-lo como:

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

Então você só precisa passar fpara filter(). Se realmente precisar, você pode recuperar o valor de flagcom o seguinte:

f.__defaults__[0]["flag"]

Como alternativa, você pode modificar o namespace global modificando o resultado de globals(). Infelizmente, você não pode modificar o namespace local da mesma maneira que modificar o resultado de locals()não afeta o namespace local.

JPvdMerwe
fonte
Ou simplesmente usar o Lisp original: (let ((var 42)) (lambda () (setf var 43))).
Kaz
4

Você pode usar uma função de ligação para usar um lambda pseudo-multi-instrução. Em seguida, você pode usar uma classe de wrapper para um Sinalizador para habilitar a atribuição.

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
fonte
0

Uma espécie de solução alternativa complicada, mas a atribuição em lambdas é ilegal de qualquer maneira, então isso realmente não importa. Você pode usar a exec()função integrada para executar a atribuição de dentro do lambda, como neste exemplo:

>>> 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
Usuário 12692182
fonte
-2

primeiro, você não precisa usar uma atribuição local para o seu trabalho, basta verificar a resposta acima

segundo, é simples usar locals () e globals () para obter a tabela de variáveis ​​e, em seguida, alterar o valor

verifique este código de amostra:

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

se você precisar alterar a adição de uma variável global ao seu ambiente, tente substituir locals () por globais ()

A lista de comp do python é legal, mas a maior parte do projeto tridicional não aceita isso (como o flask: [)

espero que possa ajudar

jyf1987
fonte
2
Você não pode usar locals(), ele diz explicitamente na documentação que alterá-lo não altera realmente o escopo local (ou pelo menos nem sempre). globals()por outro lado, funciona conforme o esperado.
JPvdMerwe
@JPvdMerwe apenas tente, não siga o documento cegamente. e a atribuição em lambda já está quebrando a regra
jyf1987
3
Infelizmente, ele só funciona no namespace global, caso em que você realmente deveria estar usando globals(). pastebin.com/5Bjz1mR4 (testado em 2.6 e 3.2) prova isso.
JPvdMerwe