Hàm 'pythonic' tương đương với hàm 'gấp' trong lập trình hàm là gì?


116

Cách thành ngữ nhất để đạt được điều gì đó như sau, trong Haskell:

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

Hoặc tương đương của nó trong Ruby:

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

Rõ ràng, Python cung cấp reducehàm, là một triển khai của gấp, chính xác như ở trên, tuy nhiên, tôi được biết rằng cách lập trình 'pythonic' là tránh lambdacác thuật ngữ và các hàm bậc cao hơn, ưu tiên danh sách hiểu rõ hơn nếu có thể. Do đó, có cách nào được ưu tiên để gấp một danh sách hoặc cấu trúc giống như danh sách trong Python mà không phải là reducehàm, hay là reducecách thành ngữ để đạt được điều này?


2
sumkhông đủ tốt?
JBernardo

3
không chắc đây có phải là một ví dụ tốt cho câu hỏi của bạn hay không. Nó có thể dễ dàng đạt được sum, bạn có thể muốn cung cấp một số loại ví dụ khác nhau.
jamylak

14
Này JBernardo - Tính tổng trên một danh sách các số được coi là một ví dụ khá thoái hóa, tôi quan tâm hơn đến ý tưởng chung về việc tích lũy các phần tử của một danh sách bằng cách sử dụng một số phép toán nhị phân và một giá trị bắt đầu, chứ không phải tổng các số nguyên cụ thể.
mistertim

1
@mistertim: sum()thực sự cung cấp chức năng hạn chế với điều này. sum([[a], [b, c, d], [e, f]], [])trả về [a, b, c, d, e, f]chẳng hạn.
Joel Cornett

Mặc dù trường hợp thực hiện với danh sách là một minh chứng tốt về những điều cần theo dõi với kỹ thuật này - +trên danh sách là một phép toán thời gian tuyến tính cả về thời gian và bộ nhớ, làm cho toàn bộ cuộc gọi là bậc hai. Việc sử dụng list(itertools.chain.from_iterable([a], [b,c,d],[e,f],[]])là tuyến tính tổng thể - và nếu bạn chỉ cần lặp lại nó một lần, bạn có thể bỏ lệnh gọi đến listđể làm cho nó không đổi về mặt bộ nhớ.
lvc

Câu trả lời:


115

Cách Pythonic để tính tổng một mảng đang được sử dụng sum. Đối với các mục đích khác, đôi khi bạn có thể sử dụng một số kết hợp của reduce(từ functoolsmô-đun) và operatormô-đun, ví dụ:

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

Cần biết rằng đó reducethực sự là một foldl, theo thuật ngữ Haskell. Không có cú pháp đặc biệt nào để thực hiện các nếp gấp, không có nội trang foldrvà thực sự sử dụng reducevới các toán tử không liên kết được coi là kiểu xấu.

Sử dụng các hàm bậc cao là khá khó; nó sử dụng tốt nguyên tắc của Python rằng mọi thứ đều là một đối tượng, bao gồm các hàm và lớp. Bạn nói đúng rằng lambdas bị một số Pythonistas ghét bỏ, nhưng chủ yếu là vì chúng có xu hướng không dễ đọc khi chúng trở nên phức tạp.


4
@JBernardo: bạn đang nói rằng bất cứ thứ gì không có trong mô-đun nội trang đều không phải là pythonic?
Fred Foo

4
Không, điều đó thật ngu ngốc khi nói. Nhưng hãy cho tôi một lý do duy nhất tại sao bạn nghĩ rằng GvR sẽ ghét chức năng giảm đến mức xóa nó khỏi nội trang?
JBernardo

6
@JBernardo: bởi vì mọi người cố chơi những mánh khóe quá thông minh với nó. Trích dẫn từ bài đăng trên blog đó, "khả năng áp dụng của reduce()bị hạn chế khá nhiều đối với các toán tử liên kết và trong tất cả các trường hợp khác, tốt hơn nên viết ra vòng lặp tích lũy một cách rõ ràng." Vì vậy, việc sử dụng nó bị hạn chế, nhưng ngay cả GvR dường như cũng phải thừa nhận rằng nó đủ hữu ích để giữ nó trong thư viện chuẩn.
Fred Foo

13
@JBernardo, vậy điều đó có nghĩa là mọi cách sử dụng nếp gấp trong Haskell và Scheme đều tệ như nhau? Đó chỉ là một phong cách lập trình khác, phớt lờ nó và đưa ngón tay vào tai và nói rằng nó không rõ ràng không làm cho nó trở nên như vậy. Giống như hầu hết những thứ có phong cách khác nhau , cần luyện tập để làm quen với nó . Ý tưởng là đưa mọi thứ vào các danh mục chung để dễ dàng lập luận về các chương trình hơn. "Ồ, tôi muốn làm điều này, hmm, trông giống như một màn hình gấp" (hoặc một bản đồ, hoặc một màn hình mở ra, hoặc một màn hình mở ra sau đó là một màn hình gập lại)
Wes

3
Lambda trong Python không được chứa nhiều hơn một biểu thức. Bạn không thể làm cho nó trở nên phức tạp ngay cả khi bạn cố gắng rất nhiều. Vì vậy, những người không thích Pythonistas có lẽ chỉ là không quen và do đó không thích phong cách lập trình hàm.
golem

16

Haskell

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

Python

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

Rõ ràng, đó là một ví dụ tầm thường để minh họa một điểm. Trong Python, bạn chỉ cần làm sum([1,2,3,4,5])và ngay cả những người theo chủ nghĩa thuần túy Haskell thường thích hơn sum [1,2,3,4,5].

Đối với các tình huống không tầm thường khi không có chức năng tiện lợi rõ ràng, cách tiếp cận quan trọng thành ngữ là viết rõ ràng vòng lặp for và sử dụng phép gán biến có thể thay đổi thay vì sử dụng reducehoặc a fold.

Đó hoàn toàn không phải là phong cách chức năng, mà đó là cách "pythonic". Python không được thiết kế cho những người theo chủ nghĩa thuần túy chức năng. Xem cách Python ủng hộ các ngoại lệ cho điều khiển luồng để xem python thành ngữ phi chức năng như thế nào.


12
các nếp gấp hữu ích cho nhiều hơn những "người theo chủ nghĩa thuần túy" chức năng. Chúng là những trừu tượng có mục đích chung. Các vấn đề đệ quy phổ biến trong máy tính. Folds cung cấp một cách để loại bỏ bảng soạn sẵn và một cách để làm cho các giải pháp đệ quy an toàn bằng các ngôn ngữ không hỗ trợ đệ quy. Vì vậy, một điều rất thiết thực. Định kiến ​​của GvR trong lĩnh vực này là không may.
itbruce

12

Trong Python 3, reduceđã bị loại bỏ: Ghi chú phát hành . Tuy nhiên, bạn có thể sử dụng mô-đun functools

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

Mặt khác, tài liệu thể hiện ưu tiên đối với for-loop thay vì reduce, do đó:

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

8
reducekhông bị xóa khỏi thư viện chuẩn Python 3. reduceđã chuyển đến functoolsmô-đun như bạn hiển thị.
đất sét

@clay, tôi chỉ mất cụm từ từ release notes Guido, nhưng bạn có thể đúng :)
Kyr

6

Bắt đầu Python 3.8và sự ra đời của các biểu thức gán (PEP 572) ( :=toán tử), mang lại khả năng đặt tên cho kết quả của một biểu thức, chúng ta có thể sử dụng khả năng hiểu danh sách để sao chép những gì các ngôn ngữ khác gọi là hoạt động gấp / lật / giảm:

Cho một danh sách, một hàm giảm và một bộ tích lũy:

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

chúng ta có thể gấp itemsvới fđể có được kết quả accumulation:

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

hoặc ở dạng cô đặc:

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

Lưu ý rằng đây thực sự cũng là một hoạt động "scanleft" vì kết quả của việc hiểu danh sách đại diện cho trạng thái tích lũy ở mỗi bước:

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

5

Bạn cũng có thể phát minh lại bánh xe:

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)

Bạn hoán đổi các đối số ftrong trường hợp đệ quy của mình.
KayEss

7
Bởi vì Python thiếu đệ quy đuôi, điều này sẽ phá vỡ trên các danh sách dài hơn và lãng phí. Hơn nữa, đây không thực sự là chức năng "gấp", mà chỉ đơn thuần là một nếp gấp bên trái, tức là nếp gấp, chính xác những gì reduceđã cung cấp (lưu ý rằng chữ ký hàm của Reduce là reduce(function, sequence[, initial]) -> value- nó cũng bao gồm chức năng cung cấp giá trị ban đầu cho tích lũy).
cemper93

5

Không thực sự trả lời cho câu hỏi, nhưng một lớp lót cho giấy gấp và giấy gấp:

a = [8,3,4]

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

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

2
Tôi nghĩ rằng đây là một cách tốt hơn để viết foldr của bạn: reduce(lambda y, x: x**y, reversed(a)). Bây giờ nó có cách sử dụng tự nhiên hơn, hoạt động với các trình vòng lặp và tiêu tốn ít bộ nhớ hơn.
Mateen Ulhaq,

2

Câu trả lời thực tế cho vấn đề (giảm) này là: Chỉ cần sử dụng một vòng lặp!

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

Điều này sẽ nhanh hơn so với giảm và những thứ như PyPy có thể tối ưu hóa các vòng lặp như vậy.

BTW, trường hợp tổng phải được giải quyết bằng sumhàm


5
Ví dụ như ví dụ này sẽ không được coi là pythonic.
jamylak

7
Các vòng lặp của Python nổi tiếng là chậm. Sử dụng (hoặc lạm dụng) reducelà một cách phổ biến để tối ưu hóa một chương trình Python.
Fred Foo

2
@larsmans Làm ơn, đừng nói rằng giảm nhanh hơn một vòng lặp đơn giản ... Nó sẽ luôn có phí gọi hàm cho mỗi lần lặp. Ngoài ra, một lần nữa, Pypy có thể tối ưu hóa các vòng lặp đến tốc độ C
JBernardo

1
@JBernardo: vâng, đó là những gì tôi đang tuyên bố. Tôi vừa mô tả phiên bản của tôi productso với một phiên bản theo phong cách của bạn và nó nhanh hơn (mặc dù vậy).
Fred Foo

1
@JBernardo Giả sử một hàm nội trang (như operator.add) làm đối số để giảm: Lệnh gọi bổ sung đó là lệnh gọi C (rẻ hơn nhiều so với lệnh gọi Python) và nó tiết kiệm việc gửi và diễn giải một vài lệnh bytecode, điều này có thể dễ dàng gây ra hàng tá các cuộc gọi chức năng.

1

Tôi tin rằng một số người trả lời câu hỏi này đã bỏ lỡ hàm ý rộng hơn của foldchức năng như một công cụ trừu tượng. Có, sumcó thể làm điều tương tự đối với danh sách các số nguyên, nhưng đây là một trường hợp nhỏ. foldchung chung hơn. Nó hữu ích khi bạn có một chuỗi cấu trúc dữ liệu có hình dạng khác nhau và muốn thể hiện rõ ràng một tập hợp. Vì vậy, thay vì phải tạo một forvòng lặp với một biến tổng hợp và tính toán lại nó theo cách thủ công mỗi lần, một foldhàm (hoặc phiên bản Python, có reducevẻ như tương ứng với) cho phép lập trình viên thể hiện ý định của biến tổng hợp một cách rõ ràng hơn nhiều bằng cách chỉ cần cung cấp hai điều:

  • Giá trị bắt đầu hoặc giá trị "gốc" mặc định cho tập hợp.
  • Một hàm nhận giá trị hiện tại của tổng hợp (bắt đầu bằng "hạt giống") và phần tử tiếp theo trong danh sách và trả về giá trị tổng hợp tiếp theo.

Chào bạn rq_! Tôi nghĩ rằng câu trả lời của bạn sẽ được cải thiện và thêm một thỏa thuận tuyệt vời nếu bạn đã đưa ra một ví dụ không tầm thường của foldđiều đó là khó khăn để làm sạch bằng Python, và sau đó " fold" đó bằng Python :-)
Scott Skiles

0

Tôi có thể đến bữa tiệc khá muộn, nhưng chúng ta có thể tạo tùy chỉnh foldrbằng cách sử dụng tính toán lambda đơn giản và hàm cà ri. Đây là cách thực hiện của tôi về trình gấp trong 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))

Mặc dù việc triển khai là đệ quy (có thể chậm), nó sẽ in ra các giá trị 15120tương ứng

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.