Присваивание внутри лямбда-выражения в Python

105

У меня есть список объектов, и я хочу удалить все пустые объекты, кроме одного, с использованием filterи lambdaвыражения.

Например, если ввод:

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

... тогда вывод должен быть:

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

Есть ли способ добавить присвоение lambdaвыражению? Например:

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
)
Кошка
источник
1
Нет, но тебе это не нужно. На самом деле я думаю, что это был бы довольно непонятный способ добиться этого, даже если бы он работал.
8
Почему бы просто не передать в фильтр обычную старую функцию?
dfb
5
Я хотел использовать лямбда, чтобы это было действительно компактное решение. Я помню, что в OCaml я мог связать операторы печати перед выражением return, думал, что это может быть воспроизведено в Python
Cat
Довольно болезненно находиться в процессе разработки цепочки конвейеров, а затем осознавать: «о, я хочу создать временную переменную, чтобы сделать поток более понятным» или «я хочу записать этот промежуточный шаг»: а затем вам нужно прыгнуть где-нибудь еще, чтобы создать функцию для этого: и назвать эту функцию, и отслеживать ее, даже если она используется только в одном месте.
javadba

Ответы:

215

Оператор выражения присваивания, :=добавленный в Python 3.8, поддерживает присваивание внутри лямбда-выражений. Этот оператор может появляться только в скобках (...), скобках [...]или {...}выражениях в скобках по синтаксическим причинам. Например, мы сможем написать следующее:

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

В Python 2 было возможно выполнять локальные назначения как побочный эффект понимания списков.

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

Однако в вашем примере невозможно использовать ни один из них, потому что ваша переменная flagнаходится во внешней области видимости, а не в lambdaобласти видимости. Это не имеет отношения к этому lambda, это общее поведение в Python 2. Python 3 позволяет обойти это с помощью nonlocalключевого слова внутри defs, но nonlocalне может использоваться внутриlambda s.

Есть обходной путь (см. Ниже), но пока мы обсуждаем эту тему ...


В некоторых случаях вы можете использовать это, чтобы делать все внутри 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()])()

Цилиндр с радиусом 10,0 см и высотой 20,0 см имеет объем 6283,2 см³.
Цилиндр с радиусом 20,0 см и высотой 40,0 см имеет объем 50265,5 см³.
Цилиндр с радиусом 30,0 см и высотой 60,0 см имеет объем 169646,0 см³.

Пожалуйста, не надо.


... вернемся к исходному примеру: хотя вы не можете выполнять назначения для flag переменной во внешней области видимости, вы можете использовать функции для изменения ранее присвоенного значения.

Например, это flagможет быть объект, который .valueмы установили с помощью 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')]

Если бы мы хотели соответствовать указанной выше теме, мы могли бы использовать понимание списка вместо setattr:

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

Но на самом деле в серьезном коде вы всегда должны использовать обычное определение функции вместо a, lambdaесли вы собираетесь выполнять внешнее присвоение.

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)
Джереми
источник
Последний пример в этом ответе не дает того же результата, что и пример, но мне кажется, что результат примера неверен.
Джереми
Короче говоря, это сводится к следующему: использовать .setattr()и похожие (например, словари тоже должны), чтобы в любом случае взламывать побочные эффекты в функциональный код, был показан классный код от @JeremyBanks :)
jno
Спасибо за заметку о assignment operator!
javadba
37

Вы не можете поддерживать состояние в выражении filter/ lambda(если не злоупотребляете глобальным пространством имен). Однако вы можете добиться чего-то подобного, используя накопленный результат, передаваемый в reduce()выражении:

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

Конечно, вы можете немного изменить это условие. В этом случае он отфильтровывает дубликаты, но вы также можете использоватьa.count("") , например, только для ограничения пустых строк.

Излишне говорить, что вы можете это сделать, но на самом деле не должны. :)

Наконец, вы можете делать все на чистом Python lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/

Иво ван дер Вейк
источник
17

Нет необходимости использовать лямбда, когда вы можете удалить все нулевые значения и вернуть один, если размер ввода изменится:

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=""))
Габи Пуркару
источник
1
Я думаю, в вашем коде есть небольшая ошибка. Вторая строка должна быть output = [x for x in input if x.name].
halex
Порядок элементов может иметь значение.
MAnyKey 09
15

Обычное присвоение ( =) невозможно внутри lambdaвыражения, хотя можно выполнять различные трюки с setattrи друзьями.

Однако решить вашу проблему на самом деле довольно просто:

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
    )

что даст вам

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

Как видите, он сохраняет первый пустой экземпляр вместо последнего. Если вместо этого вам нужен последний, переверните filterсписок, входящий в , и переверните список, выходящий из filter:

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

что даст вам

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

Одна вещь, о которой нужно знать: для того, чтобы это работало с произвольными объектами, эти объекты должны быть правильно реализованы __eq__и, __hash__как описано здесь .

Итан Фурман
источник
7

ОБНОВЛЕНИЕ :

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

или используя filterи lambda:

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

Предыдущий ответ

Хорошо, вы застряли на использовании фильтра и лямбды?

Похоже, это было бы лучше подавать с пониманием словаря,

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

Я думаю, что причина того, что Python не разрешает присваивание в лямбде, аналогична тому, почему он не допускает присваивания в понимании, и это как-то связано с тем фактом, что эти вещи оцениваются на Cстороне и, таким образом, могут дать нам увеличение скорости. По крайней мере, таково мое впечатление после прочтения одного из эссе Гвидо. .

Я предполагаю, что это также противоречит философии наличия одного правильного способа сделать что-либо в Python.

молочный почтальон
источник
Так что это не совсем так. Он не сохраняет порядок и не сохраняет дубликаты объектов с непустой строкой.
JPvdMerwe
7

TL; DR: при использовании функциональных идиом лучше писать функциональный код

Как отмечали многие люди, в Python присвоение лямбда-выражений не допускается. В общем, при использовании функциональных идиом лучше думать функционально, что означает отсутствие побочных эффектов и назначений везде, где это возможно.

Вот функциональное решение, в котором используется лямбда. Я назначил лямбду fnдля ясности (и потому, что она немного затянута).

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

Вы также можете решить эту проблему с помощью итераторов, а не списков, немного изменив ситуацию. У вас также есть другой импорт.

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

Вы всегда можете реорганизовать код, чтобы уменьшить длину операторов.

диетбуддха
источник
6

Если вместо этого flag = Trueмы можем выполнить импорт, то я думаю, что это соответствует критериям:

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

Или, может быть, фильтр лучше написать так:

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

Или просто для простого логического значения без импорта:

filter(lambda L, use_blank=iter([True]): L or next(use_blank, False), a)
Джон Клементс
источник
6

Питонический способ отслеживать состояние во время итерации - с помощью генераторов. Способ itertools довольно сложен для понимания IMHO, и пытаться взломать лямбды для этого просто глупо. Я бы попробовал:

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

В целом, удобочитаемость всегда важнее компактности.

user2735379
источник
4

Нет, вы не можете поместить присвоение внутри лямбды из-за его собственного определения. Если вы работаете с использованием функционального программирования, вы должны предполагать, что ваши значения неизменяемы.

Одним из решений может быть следующий код:

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
источник
4

Если вам нужна лямбда для запоминания состояния между вызовами, я бы порекомендовал либо функцию, объявленную в локальном пространстве имен, либо класс с перегруженным __call__. Теперь, когда все мои предостережения против того, что вы пытаетесь сделать, исчезли, мы можем перейти к фактическому ответу на ваш вопрос.

Если вам действительно нужно, чтобы ваша лямбда имела некоторую память между вызовами, вы можете определить ее следующим образом:

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

Тогда вам просто нужно перейти fна filter(). Если вам действительно нужно, вы можете вернуть значение flagс помощью следующего:

f.__defaults__[0]["flag"]

Кроме того, вы можете изменить глобальное пространство имен, изменив результат globals(). К сожалению, вы не можете изменить локальное пространство имен таким же образом, как изменение результата locals()не влияет на локальное пространство имен.

JPvdMerwe
источник
Или просто использовать оригинальный Lisp: (let ((var 42)) (lambda () (setf var 43))).
Kaz
4

Вы можете использовать функцию привязки, чтобы использовать псевдо-многопозиционную лямбду. Затем вы можете использовать класс-оболочку для флага, чтобы разрешить назначение.

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)
пироспад
источник
0

Это своего рода грязный обходной путь, но назначение в лямбдах в любом случае незаконно, поэтому на самом деле это не имеет значения. Вы можете использовать встроенную exec()функцию для выполнения присваивания из лямбда-выражения, например, в этом примере:

>>> 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
Пользователь 12692182
источник
-2

Во-первых, вам не нужно использовать местное задание для вашей работы, просто проверьте ответ выше

во-вторых, просто использовать locals () и globals (), чтобы получить таблицу переменных, а затем изменить значение

проверьте этот пример кода:

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

если вам нужно изменить добавление глобальной переменной в среду, попробуйте заменить locals () на globals ()

Составление списка в python - это круто, но большая часть трехкомпонентного проекта этого не принимает (например, flask: [)

надеюсь, это может помочь

jyf1987
источник
2
Вы не можете использовать locals(), в документации прямо сказано, что его изменение на самом деле не меняет локальную область (или, по крайней мере, не всегда). globals()с другой стороны работает как ожидалось.
JPvdMerwe
@JPvdMerwe просто попробуйте, не следуйте документу вслепую. а присваивание в
лямбде
3
К сожалению, он работает только в глобальном пространстве имен, и в этом случае вам действительно стоит использовать globals(). pastebin.com/5Bjz1mR4 (проверено как в 2.6, так и в 3.2) доказывает это.
JPvdMerwe 05