Назначение внутри лямбда-выражения в Python - PullRequest
88 голосов
/ 08 июня 2011

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

Ответы [ 12 ]

194 голосов
/ 31 января 2013

Оператор выражения присваивания :=, добавленный в 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 внутри def s, но nonlocal нельзя использовать внутриlambda с.

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


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

Но на самом деле, в серьезном коде вы всегда должны использовать обычныйопределение функции вместо 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)
33 голосов
/ 31 января 2013

Вы не можете реально поддерживать состояние в выражении 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/

16 голосов
/ 08 июня 2011

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

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=""))
11 голосов
/ 04 февраля 2013

Обычное назначение (=) невозможно в выражении 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__, как объяснено здесь .

6 голосов
/ 31 января 2013

ОБНОВЛЕНИЕ :

[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 и, таким образом, могут дать нам увеличение скорости.По крайней мере, это мое впечатление после прочтения одного из эссе Гвидо .

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

5 голосов
/ 31 августа 2013

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

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

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

5 голосов
/ 06 февраля 2013

Если вместо 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)
5 голосов
/ 05 февраля 2013

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

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

3 голосов
/ 04 февраля 2013

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

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)
3 голосов
/ 31 января 2013

Если вам нужна лямбда для запоминания состояния между вызовами, я бы порекомендовал либо функцию, объявленную в локальном пространстве имен, либо класс с перегруженным __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() не влияет на локальное пространство имен.

...