อะไรคือ 'pythonic' เทียบเท่ากับฟังก์ชัน 'fold' จากการเขียนโปรแกรมเชิงฟังก์ชัน


118

อะไรคือวิธีที่เป็นสำนวนที่สุดในการบรรลุสิ่งต่อไปนี้ใน Haskell:

foldl (+) 0 [1,2,3,4,5]
--> 15

หรือเทียบเท่าใน Ruby:

[1,2,3,4,5].inject(0) {|m,x| m + x}
#> 15

เห็นได้ชัดว่า Python มีreduceฟังก์ชันซึ่งเป็นการใช้งานแบบพับตามที่กล่าวข้างต้นอย่างไรก็ตามฉันได้รับแจ้งว่าวิธีการเขียนโปรแกรมแบบ 'pythonic' คือการหลีกเลี่ยงlambdaคำศัพท์และฟังก์ชันลำดับที่สูงกว่าโดยเลือกใช้ความเข้าใจในรายการหากเป็นไปได้ ดังนั้นจึงมีวิธีที่ดีกว่าในการพับรายการหรือโครงสร้างแบบรายการใน Python ที่ไม่ใช่reduceฟังก์ชันหรือเป็นreduceวิธีที่เป็นสำนวนในการบรรลุสิ่งนี้หรือไม่?


2
sumยังไม่ดีพอ?
JBernardo

3
ไม่แน่ใจว่านี่เป็นตัวอย่างที่ดีสำหรับคำถามของคุณหรือไม่ สามารถทำได้อย่างง่ายดายโดยsumคุณอาจต้องการให้ตัวอย่างประเภทต่างๆ
jamylak

14
เฮ้ JBernardo - การสรุปรายการตัวเลขมีความหมายว่าเป็นตัวอย่างที่ค่อนข้างแย่ฉันสนใจแนวคิดทั่วไปเกี่ยวกับการสะสมองค์ประกอบของรายการโดยใช้การดำเนินการไบนารีและค่าเริ่มต้นไม่ใช่การรวมจำนวนเต็มโดยเฉพาะ
mistertim

1
@mistertim: sum()มีฟังก์ชั่นที่ จำกัด ในเรื่องนี้ sum([[a], [b, c, d], [e, f]], [])ผลตอบแทน[a, b, c, d, e, f]เช่น
Joel Cornett

แม้ว่ากรณีของการใช้รายการจะเป็นการสาธิตสิ่งที่ควรระวังด้วยเทคนิคนี้ แต่+ในรายการเป็นการดำเนินการเวลาเชิงเส้นทั้งในด้านเวลาและหน่วยความจำทำให้การเรียกกำลังสองทั้งหมด การใช้งานlist(itertools.chain.from_iterable([a], [b,c,d],[e,f],[]])เป็นแบบเชิงเส้นโดยรวม - และหากคุณต้องการย้ำซ้ำเพียงครั้งเดียวคุณสามารถวางสายlistเพื่อทำให้มันคงที่ในแง่ของหน่วยความจำ
lvc

คำตอบ:


116

วิธี Pythonic sumของข้อสรุปอาร์เรย์จะใช้ สำหรับวัตถุประสงค์อื่นบางครั้งคุณสามารถใช้reduce(จากfunctoolsโมดูล) และoperatorโมดูลร่วมกันได้เช่น:

def product(xs):
    return reduce(operator.mul, xs, 1)

โปรดทราบว่าreduceจริงๆแล้วfoldlในแง่ของ Haskell ไม่มีไวยากรณ์พิเศษในการพับไม่มีในตัวfoldrและการใช้งานจริงreduceกับตัวดำเนินการที่ไม่เชื่อมโยงถือเป็นรูปแบบที่ไม่ดี

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


4
@JBernardo: คุณกำลังบอกว่าสิ่งที่ไม่อยู่ในโมดูล builtins ไม่ใช่ pythonic?
Fred Foo

4
ไม่น่าจะโง่ที่จะพูด แต่ขอเหตุผลเดียวว่าทำไมคุณถึงคิดว่าGvR จะเกลียดฟังก์ชั่นลดขนาดมากจนถึงขั้นลบออกจากบิวด์อิน
JBernardo

6
@JBernardo: เพราะคนพยายามเล่นกลเม็ดที่ชาญฉลาดเกินไป หากต้องการอ้างอิงจากบล็อกโพสต์นั้น "การบังคับใช้reduce()ค่อนข้าง จำกัด เฉพาะตัวดำเนินการเชื่อมโยงเท่านั้นและในกรณีอื่น ๆ ทั้งหมดควรเขียนห่วงการสะสมอย่างชัดเจน" ดังนั้นการใช้งานจึงมี จำกัด แต่ GvR ก็ต้องยอมรับว่ามีประโยชน์เพียงพอที่จะเก็บไว้ในไลบรารีมาตรฐาน
Fred Foo

13
@JBernardo นั่นหมายความว่าการใช้พับทุกครั้งใน Haskell และ Scheme นั้นไม่ดีเท่ากันหรือไม่? มันเป็นเพียงรูปแบบการเขียนโปรแกรมที่แตกต่างกันโดยไม่สนใจและเอานิ้วอุดหูและพูดว่ามันไม่ชัดเจนไม่ได้ทำให้เป็นเช่นนั้น เช่นเดียวกับสิ่งที่มากที่สุดที่มีรูปแบบที่แตกต่างกันก็จะใช้เวลาการปฏิบัติที่จะได้ใช้มัน แนวคิดคือการจัดสิ่งต่างๆเป็นหมวดหมู่ทั่วไปเพื่อให้เหตุผลเกี่ยวกับโปรแกรมได้ง่ายขึ้น "โอ้ฉันต้องการทำสิ่งนี้อืมดูเหมือนพับ" (หรือแผนที่หรือแฉหรือคลี่แล้วพับทับ)
Wes

3
Lambda ใน Python ไม่สามารถมีได้มากกว่าหนึ่งนิพจน์ คุณไม่สามารถทำให้มันซับซ้อนได้แม้ว่าคุณจะพยายามอย่างเต็มที่ก็ตาม ดังนั้น Pythonistas ที่ไม่ชอบอาจจะไม่คุ้นเคยและด้วยเหตุนี้จึงไม่ชอบสไตล์การเขียนโปรแกรมเชิงฟังก์ชัน
golem

17

Haskell

foldl (+) 0 [1,2,3,4,5]

หลาม

reduce(lambda a,b: a+b, [1,2,3,4,5], 0)

เห็นได้ชัดว่านั่นเป็นตัวอย่างที่ไม่สำคัญในการอธิบายประเด็น ในหลามคุณเพียงแค่จะทำsum([1,2,3,4,5])และแม้กระทั่งครูสอน Haskell sum [1,2,3,4,5]ทั่วไปต้องการ

สำหรับสถานการณ์ที่ไม่สำคัญเมื่อไม่มีฟังก์ชั่นอำนวยความสะดวกที่ชัดเจนวิธีการ pythonic สำนวนคือการเขียน for loop อย่างชัดเจนและใช้การกำหนดตัวแปรที่ไม่แน่นอนแทนการใช้reduceหรือ a fold.

นั่นไม่ใช่รูปแบบการใช้งาน แต่เป็นวิธี "ไพโธนิก" Python ไม่ได้ออกแบบมาสำหรับผู้ที่มีความเชี่ยวชาญในการทำงาน ดูว่า Python สนับสนุนข้อยกเว้นสำหรับการควบคุมโฟลว์อย่างไรเพื่อดูว่า python สำนวนที่ไม่ทำงานเป็นอย่างไร


12
การพับมีประโยชน์มากกว่า "คนเจ้าระเบียบ" ที่ใช้งานได้ เป็นนามธรรมที่มีวัตถุประสงค์ทั่วไป ปัญหาซ้ำซากมีอยู่ทั่วไปในคอมพิวเตอร์ Folds นำเสนอวิธีการลบต้นแบบและวิธีทำให้โซลูชันการเรียกซ้ำปลอดภัยในภาษาที่ไม่รองรับการเรียกซ้ำโดยกำเนิด ดังนั้นสิ่งที่ใช้ได้จริง อคติของ GvR ในพื้นที่นี้เป็นเรื่องที่โชคร้าย
itsbruce

12

ในหลาม 3 reduceได้ถูกลบออก: บันทึกประจำรุ่น อย่างไรก็ตามคุณสามารถใช้โมดูล functools ได้

import operator, functools
def product(xs):
    return functools.reduce(operator.mul, xs, 1)

ในทางกลับกันเอกสารแสดงความพึงพอใจต่อfor-loop แทนreduceด้วยเหตุนี้:

def product(xs):
    result = 1
    for i in xs:
        result *= i
    return result

9
reduceไม่ได้ถูกลบออกจากไลบรารีมาตรฐาน Python 3 reduceย้ายไปที่functoolsโมดูลตามที่คุณแสดง
ดิน

1
@clay ฉันเพิ่งใช้วลีจากบันทึกประจำรุ่นของ Guido แต่คุณอาจจะพูดถูก :)
Kyr

7

เริ่มต้นPython 3.8และการแนะนำนิพจน์การกำหนด (PEP 572) ( :=ตัวดำเนินการ) ซึ่งให้ความเป็นไปได้ในการตั้งชื่อผลลัพธ์ของนิพจน์เราสามารถใช้ความเข้าใจในรายการเพื่อจำลองสิ่งที่ภาษาอื่น ๆ เรียกว่าการดำเนินการพับ / พับด้านซ้าย / ลด:

ระบุรายการฟังก์ชันการลดและตัวสะสม:

items = [1, 2, 3, 4, 5]
f = lambda acc, x: acc * x
accumulator = 1

เราสามารถพับitemsด้วยfเพื่อให้ได้ผลลัพธ์accumulation:

[accumulator := f(accumulator, x) for x in items]
# accumulator = 120

หรือในรูปแบบควบแน่น:

acc = 1; [acc := acc * x for x in [1, 2, 3, 4, 5]]
# acc = 120

โปรดทราบว่านี่เป็นการดำเนินการ "scanleft" ด้วยเนื่องจากผลของการทำความเข้าใจรายการแสดงถึงสถานะของการสะสมในแต่ละขั้นตอน:

acc = 1
scanned = [acc := acc * x for x in [1, 2, 3, 4, 5]]
# scanned = [1, 2, 6, 24, 120]
# acc = 120

5

คุณสามารถสร้างล้อใหม่ได้เช่นกัน:

def fold(f, l, a):
    """
    f: the function to apply
    l: the list to fold
    a: the accumulator, who is also the 'zero' on the first call
    """ 
    return a if(len(l) == 0) else fold(f, l[1:], f(a, l[0]))

print "Sum:", fold(lambda x, y : x+y, [1,2,3,4,5], 0)

print "Any:", fold(lambda x, y : x or y, [False, True, False], False)

print "All:", fold(lambda x, y : x and y, [False, True, False], True)

# Prove that result can be of a different type of the list's elements
print "Count(x==True):", 
print fold(lambda x, y : x+1 if(y) else x, [False, True, True], 0)

คุณสลับอาร์กิวเมนต์ไปfรอบ ๆ ในกรณีที่เกิดซ้ำของคุณ
KayEss

7
เนื่องจาก Python ไม่มีการเรียกซ้ำหางสิ่งนี้จะทำลายรายการที่ยาวขึ้นและเป็นการสิ้นเปลือง นอกจากนี้ไม่ได้อย่างแท้จริง "พับ" ฟังก์ชั่น แต่เพียงเท่าซ้ายคือ foldl ที่เป็นว่าสิ่งที่reduceมีอยู่แล้วข้อเสนอ (หมายเหตุที่ช่วยลดลายเซ็นของฟังก์ชั่นคือreduce(function, sequence[, initial]) -> value- มันเกินไปรวมถึงฟังก์ชั่นของการให้ค่าเริ่มต้นสำหรับการ สะสม)
cemper93

5

ไม่ใช่คำตอบสำหรับคำถามจริงๆ แต่เป็นหนึ่งสมุทรสำหรับพับและพับ:

a = [8,3,4]

## Foldl
reduce(lambda x,y: x**y, a)
#68719476736

## Foldr
reduce(lambda x,y: y**x, a[::-1])
#14134776518227074636666380005943348126619871175004951664972849610340958208L

3
ผมคิดว่านี่เป็นวิธีที่ดีกว่าที่จะเขียน foldr reduce(lambda y, x: x**y, reversed(a))ของคุณ: ตอนนี้มีการใช้งานที่เป็นธรรมชาติมากขึ้นทำงานร่วมกับตัวทำซ้ำและใช้หน่วยความจำน้อยลง
Mateen Ulhaq

2

คำตอบที่แท้จริงของปัญหา (ลด) นี้คือใช้การวนซ้ำ!

initial_value = 0
for x in the_list:
    initial_value += x #or any function.

สิ่งนี้จะเร็วกว่าการลดขนาดและสิ่งต่างๆเช่น PyPy สามารถเพิ่มประสิทธิภาพลูปเช่นนั้นได้

BTW กรณีผลรวมควรได้รับการแก้ไขด้วยsumฟังก์ชัน


5
นี่จะไม่ถือเป็น pythonic สำหรับตัวอย่างเช่นนี้
jamylak

7
ลูป Python ช้าอย่างฉาวโฉ่ การใช้ (หรือใช้ในทางที่ผิด) reduceเป็นวิธีทั่วไปในการเพิ่มประสิทธิภาพโปรแกรม Python
Fred Foo

2
@larsmans ได้โปรดอย่ามาพูดว่าการลดเร็วกว่าลูปธรรมดา ... มันจะมีค่าใช้จ่ายในการเรียกฟังก์ชันเสมอ นอกจากนี้อีกครั้ง Pypy สามารถเพิ่มประสิทธิภาพลูปให้เป็นความเร็ว C ได้
JBernardo

1
@JBernardo: ใช่นั่นคือสิ่งที่ฉันอ้าง ฉันเพิ่งทำโปรไฟล์ของฉันproductเทียบกับรุ่นหนึ่งในสไตล์ของคุณและมันเร็วกว่า (เล็กน้อย)
Fred Foo

1
@JBernardo สมมติว่าฟังก์ชัน builtin (เช่นoperator.add) เป็นอาร์กิวเมนต์เพื่อลด: การโทรพิเศษนั้นคือการโทร C (ซึ่งถูกกว่าการโทร Python มาก) และจะช่วยประหยัดการจัดส่งและตีความคำสั่ง bytecode สองสามคำซึ่งอาจทำให้เกิดหลายสิบได้อย่างง่ายดาย การเรียกใช้ฟังก์ชัน

1

ฉันเชื่อว่าผู้ตอบคำถามนี้บางคนพลาดความหมายที่กว้างขึ้นของfoldฟังก์ชันในฐานะเครื่องมือนามธรรม ใช่sumสามารถทำสิ่งเดียวกันสำหรับรายการจำนวนเต็ม แต่นี่เป็นกรณีเล็กน้อย foldเป็นเรื่องทั่วไปมากขึ้น จะมีประโยชน์เมื่อคุณมีลำดับโครงสร้างข้อมูลที่มีรูปร่างแตกต่างกันและต้องการแสดงการรวมอย่างหมดจด ดังนั้นแทนที่จะต้องสร้างforลูปด้วยตัวแปรรวมและคำนวณซ้ำด้วยตนเองทุกครั้งfoldฟังก์ชัน (หรือเวอร์ชัน Python ซึ่งreduceดูเหมือนจะสอดคล้องกัน) ช่วยให้โปรแกรมเมอร์สามารถแสดงเจตนาของการรวมได้อย่างชัดเจนมากขึ้นโดยเพียงแค่ระบุ สองสิ่ง:

  • ค่าเริ่มต้นเริ่มต้นหรือ "เมล็ดพันธุ์" สำหรับการรวม
  • ฟังก์ชันที่รับค่าปัจจุบันของการรวม (เริ่มต้นด้วย "seed") และองค์ประกอบถัดไปในรายการและส่งกลับค่าการรวมถัดไป

สวัสดี rq_! ฉันคิดว่าคำตอบของคุณจะได้รับการปรับปรุงและเพิ่มข้อเสนอมากมายหากคุณให้ตัวอย่างที่ไม่สำคัญfoldว่าเป็นเรื่องยากที่จะทำใน Python ให้หมดจดแล้ว " fold" ใน Python :-)
Scott Skiles

0

ฉันอาจจะไปปาร์ตี้ค่อนข้างช้า แต่เราสามารถสร้างแบบกำหนดเองได้foldrโดยใช้แคลคูลัสแลมบ์ดาอย่างง่ายและฟังก์ชัน curried นี่คือการใช้งาน foldr ใน python ของฉัน

def foldr(func):
    def accumulator(acc):
        def listFunc(l):
            if l:
                x = l[0]
                xs = l[1:]
                return func(x)(foldr(func)(acc)(xs))
            else:
                return acc
        return listFunc
    return accumulator  


def curried_add(x):
    def inner(y):
        return x + y
    return inner

def curried_mult(x):
    def inner(y):
        return x * y
    return inner

print foldr(curried_add)(0)(range(1, 6))
print foldr(curried_mult)(1)(range(1, 6))

แม้ว่าการดำเนินการเป็น recursive (อาจจะช้า) ก็จะพิมพ์ค่า15และ120ตามลำดับ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.