การกำหนดภายในแลมบ์ดานิพจน์ใน 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 ที่ถูกล่ามโซ่จากนั้นให้ตระหนักว่า: "โอ้ฉันต้องการสร้างตัวแปรชั่วคราวเพื่อให้การไหลชัดเจนยิ่งขึ้น" หรือ "ฉันต้องการบันทึกขั้นตอนกลางนี้": จากนั้นคุณต้องกระโดด ที่อื่นเพื่อสร้างฟังก์ชันขึ้นมาและตั้งชื่อฟังก์ชันนั้นและติดตามฟังก์ชั่นนั้นแม้ว่าจะใช้ในที่เดียวก็ตาม
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 ได้

มีวิธีแก้ปัญหาชั่วคราว (ดูด้านล่าง) แต่ในขณะที่เราอยู่ในหัวข้อ ...


ในบางกรณีคุณสามารถใช้สิ่งนี้เพื่อทำทุกอย่างภายใน a 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)

ตัวอย่างสุดท้ายในคำตอบนี้ไม่ได้ให้ผลลัพธ์เหมือนกับตัวอย่าง แต่สำหรับฉันแล้วดูเหมือนว่าเอาต์พุตตัวอย่างไม่ถูกต้อง
Jeremy

ในระยะสั้นสิ่งนี้ลดลงไปที่: use .setattr()and alikes (เช่นพจนานุกรมควรทำเช่นกัน) เพื่อแฮ็กผลข้างเคียงเป็นรหัสการทำงานอยู่ดีรหัสเด็ดโดย @JeremyBanks แสดง :)
jno

ขอบคุณสำหรับหมายเหตุเกี่ยวกับassignment operator!
javadba

37

คุณไม่สามารถรักษาสถานะใน a filter/ lambdaexpression ได้ (เว้นแต่จะใช้เนมสเปซส่วนกลางในทางที่ผิด) อย่างไรก็ตามคุณสามารถบรรลุสิ่งที่คล้ายกันได้โดยใช้ผลลัพธ์สะสมที่ส่งผ่านไปใน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

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ด้านข้างและทำให้เราได้ เพิ่มความเร็ว อย่างน้อยนั่นก็เป็นความประทับใจของฉันหลังจากอ่านบทความหนึ่งของ Guidoหนึ่งของบทความของกุย

ฉันเดาว่านี่จะขัดกับปรัชญาของการมีวิธีหนึ่งที่ถูกต้องในการทำสิ่งใดสิ่งหนึ่งใน Python


นี่จึงไม่ถูกต้องทั้งหมด จะไม่รักษาคำสั่งซื้อและจะไม่เก็บรักษาซ้ำของอ็อบเจ็กต์สตริงที่ไม่ว่างเปล่า
JPvdMerwe

7

TL; DR: เมื่อใช้สำนวนการทำงานควรเขียนโค้ดที่ใช้งานได้ดีกว่า

อย่างที่หลาย ๆ คนชี้ให้เห็นว่าใน Python lambdas ไม่ได้รับอนุญาต โดยทั่วไปเมื่อใช้สำนวนการทำงานคุณจะดีกว่าการคิดในลักษณะการทำงานซึ่งหมายความว่าทุกที่ที่เป็นไปได้จะไม่มีผลข้างเคียงและไม่มีการมอบหมาย

นี่คือโซลูชันการทำงานที่ใช้แลมด้า ฉันได้มอบหมายแลมด้าให้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

วิธี pythonic ในการติดตามสถานะระหว่างการทำซ้ำคือการใช้เครื่องกำเนิดไฟฟ้า วิธี itertools นั้นค่อนข้างยากที่จะเข้าใจ IMHO และการพยายามแฮ็ก lambdas เพื่อทำสิ่งนี้เป็นเรื่องไร้สาระ ฉันจะลอง:

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

4

คุณสามารถใช้ฟังก์ชันการผูกเพื่อใช้แลมด้าหลายคำสั่งหลอก จากนั้นคุณสามารถใช้คลาส wrapper สำหรับแฟล็กเพื่อเปิดใช้งานการมอบหมาย

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

เป็นวิธีแก้ปัญหาที่ยุ่งเหยิง แต่การมอบหมายงานใน lambdas นั้นผิดกฎหมายอยู่แล้วดังนั้นจึงไม่สำคัญ คุณสามารถใช้exec()ฟังก์ชันbuiltin เพื่อเรียกใช้การมอบหมายจากภายในแลมบ์ดาเช่นตัวอย่างนี้:

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

ขั้นแรกคุณไม่จำเป็นต้องใช้การมอบหมายงานในท้องถิ่นสำหรับงานของคุณเพียงตรวจสอบคำตอบด้านบน

ประการที่สองมันง่ายต่อการใช้ local () และ globals () เพื่อรับตารางตัวแปรแล้วเปลี่ยนค่า

ตรวจสอบรหัสตัวอย่างนี้:

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

หากคุณต้องการเปลี่ยนการเพิ่มตัวแปรส่วนกลางให้กับสภาพแวดล้อมของคุณให้ลองแทนที่local ()ด้วยglobals ()

คอมพ์รายการของ python นั้นยอดเยี่ยม แต่โครงการสามมิติส่วนใหญ่ไม่ยอมรับสิ่งนี้ (เช่นขวด: [)

หวังว่ามันจะช่วยได้


2
คุณไม่สามารถใช้งานlocals()ได้มีการระบุไว้อย่างชัดเจนในเอกสารว่าการเปลี่ยนแปลงนั้นไม่ได้เปลี่ยนขอบเขตภายในเครื่อง (หรืออย่างน้อยก็ไม่เสมอไป) globals()ในทางกลับกันทำงานได้ตามที่คาดไว้
JPvdMerwe

@JPvdMer เราลองทำดูอย่าทำตามเอกสารสุ่มสี่สุ่มห้า และงานในแลมบ์ดากำลังทำลายกฎแล้ว
jyf1987

3
น่าเสียดายที่ใช้งานได้เฉพาะในเนมสเปซส่วนกลางซึ่งในกรณีนี้คุณควรใช้globals()จริงๆ pastebin.com/5Bjz1mR4 (ทดสอบทั้ง 2.6 และ 3.2) พิสูจน์ได้
JPvdMerwe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.