Призначення вираження лямбда в 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 я міг ланцюжок тверджень про друк до виразу повернення, думав, що це може бути повторено в Python
Cat

Досить болісно бути в потоці розвитку ланцюгового pipeilne, тоді розумієш: "о, я хочу створити temp var, щоб зробити потік більш чітким" або "я хочу зафіксувати цей проміжний крок": і тоді вам доведеться стрибати де-небудь ще, щоб створити функцію для цього: і назвіть цю функцію та відслідковуйте її - навіть якщо вона використовується лише в одному місці.
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його не можна використовувати всередині lambdas.

Існує рішення (див. Нижче), але поки ми на тему ...


У деяких випадках ви можете використовувати це, щоб зробити все, що знаходиться всередині 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)

Останній приклад у цій відповіді не дає такого ж результату, як приклад, але мені здається, що вихідний приклад невірний.
Джеремі

коротше кажучи, це зводиться до: використання .setattr()і подібних ( словники також повинні робити, наприклад) для злому побічних ефектів у функціональний код так чи інакше, було показано прохолодний код від @JeremyBanks :)
jno

Thx для замітки на 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].
гелекс

Порядок елементів може бути важливим.
MAnyKey

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

Загалом, читабельність щоразу перетворює компактність.


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 )

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()не впливає на локальну область імен.


Або просто скористайтеся оригінальним Lisp : (let ((var 42)) (lambda () (setf var 43))).
Каз

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

-2

по-перше, вам не потрібно використовувати місцеві завдання для своєї роботи, просто перевірте вищевказану відповідь

по-друге, прості у використанні localals () та globals (), щоб отримати таблицю змінних, а потім змінити значення

перевірити цей зразок коду:

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

якщо вам потрібно змінити додати глобальну змінну до середовища, спробуйте замінити localals () на глобальні ()

Список комп’ютерів python класний, але більшість тридіальних проектів не приймають цього (як колба: [)

сподіваюся, що це може допомогти


2
Ви не можете використовувати locals(), в документації чітко зазначається, що його зміна насправді не змінює локальну область застосування (або, принаймні, не завжди). globals()з іншого боку працює як очікувалося.
JPvdMerwe

@JPvdMerwe просто спробуйте, не слідкуйте за документом наосліп. і призначення в лямбда є порушення правила вже
jyf1987

3
На жаль, він працює лише в глобальному просторі імен, і в цьому випадку ви дійсно повинні використовувати globals(). pastebin.com/5Bjz1mR4 (перевірено як в 2.6, так і в 3.2) це доводить.
JPvdMerwe
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.