Що таке "пітонічний" еквівалент функції "складання" від функціонального програмування?


117

Який самий ідіоматичний спосіб досягти чогось подібного в Haskell:

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

Або його еквівалент у Ruby:

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

Очевидно, що Python забезпечує reduceфункцію, яка є реалізацією складки, точно так само, як вище, однак мені сказали, що "пітонічним" способом програмування було уникати 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]наприклад.
Джоел Корнетт

Незважаючи на те, що це робиться зі списками, це хороша демонстрація того, на що слід дивитися за допомогою цієї методики - +у списках є лінійна операція часу як у часі, так і в пам'яті, що робить весь виклик квадратичним. Використання list(itertools.chain.from_iterable([a], [b,c,d],[e,f],[]])лінійне в цілому - і якщо вам потрібно повторити його лише один раз, ви можете кинути виклик, listщоб зробити його постійним з точки зору пам'яті.
lvc

Відповіді:


116

Використовується пітонічний спосіб підсумовування масиву sum. Для інших цілей іноді можна використовувати комбінацію reducefunctoolsмодуля) та operatorмодуля, наприклад:

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

Будьте в курсі, що reduceнасправді є foldl, з точки зору Haskell. Немає спеціального синтаксису для виконання складок, немає вбудованого foldr, і фактично використання reduceз неасоціативними операторами вважається поганим стилем.

Використання функцій вищого порядку досить пітонічне; він добре використовує принцип Python, що все є об'єктом, включаючи функції та класи. Ви маєте рацію, що лямбди нахмурюються деякими пітоністами, але в основному тому, що вони, як правило, не дуже читабельні, коли вони складні.


4
@JBernardo: ти кажеш, що нічого, що є у вбудованому модулі, не є пітонічним?
Фред Фоо

4
Ні, це було б дурно сказати. Але дайте мені єдину причину, чому ви вважаєте, що GvR так сильно ненавидить функцію зменшення в момент видалення її з вбудованих?
JBernardo

6
@JBernardo: тому що люди намагаються грати з ним занадто розумні хитрощі. Цитуючи з цього допису в блозі, "застосовність reduce()значно обмежена асоціативними операторами, і в усіх інших випадках краще виписати цикл накопичення чітко". Отже, його використання обмежене, але навіть GvR, мабуть, повинен був визнати його достатньо корисним, щоб зберігати його в стандартній бібліотеці.
Фред Фоо

13
@JBernardo, значить, це означає, що кожне використання складок у Haskell та Scheme однаково погано? Це просто інший стиль програмування, ігноруючи його і кладучи пальці у вуха і кажучи, що це незрозуміло, це не так. Як і більшість речей, які мають інший стиль, потрібно звикнути до цього . Ідея полягає в тому, щоб поділити речі на загальні категорії, щоб було легше міркувати про програми. "О, я хочу це зробити, хм, виглядає як складка" (або карта, або розгортання, або розгортання, а потім складка над цим)
Уес,

3
Лямбда в Python не може містити більше одного виразу. Ви не можете зробити це складним, навіть якщо дуже стараєтесь. Тож Pythonistas, які їм не подобаються, напевно, просто не звикли і, отже, не люблять функціональний стиль програмування.
голем

16

Хаскелл

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

Пітон

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

Очевидно, що це тривіальний приклад для ілюстрації точки. У Python ви просто зробили б, sum([1,2,3,4,5])і навіть пуристи Haskell, як правило, віддають перевагу sum [1,2,3,4,5].

Для нетривіальних сценаріїв, коли немає явної функції зручності, ідіоматичний пітонічний підхід полягає в тому, щоб явно виписати цикл for і використовувати призначення змінних змінних замість використання reduceабо a fold.

Це зовсім не функціональний стиль, але це "пітонічний" спосіб. Python не розроблений для функціональних пуристів. Подивіться, як Python надає перевагу виняткам для управління потоком, щоб побачити, як нефункціональний ідіоматичний пітон.


12
складки корисні більш ніж функціональним "пуристам". Вони є абстракціями загального призначення. Рекурсивні проблеми є поширеними в обчислювальних технологіях. Складки пропонують спосіб зняти котельну плиту та спосіб зробити рекурсивні рішення безпечними мовами, які не підтримують рекурсію. Тож дуже практична річ. Упередження GvR в цій галузі прикро.
йогобрюс

12

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

8
reduceне видалено зі стандартної бібліотеки Python 3. reduceпереходимо до functoolsмодуля під час показу.
глина

@clay, я щойно взяв фразу з нотаток про випуск Guido, але ви можете мати рацію :)
Kyr

6

Початок Python 3.8та введення виразів призначення (PEP 572) (:= оператор), що дає можливість назвати результат виразу, ми можемо використовувати розуміння списку, щоб повторити те, що інші мови називають операціями fold / foldleft / reduction:

Дано список, функцію відновлення та акумулятор:

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

Зауважте, що це насправді також операція "сканування", оскільки результат розуміння списку представляє стан накопичення на кожному кроці:

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

Не справді відповідь на питання, але одноразові для foldl та foldr:

a = [8,3,4]

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

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

2
Я думаю , що це найкращий спосіб , щоб написати foldr: reduce(lambda y, x: x**y, reversed(a)). Тепер він має більш природне використання, працює з ітераторами і витрачає менше пам’яті.
Матін Ульхак

2

Фактична відповідь на цю (зменшити) проблему: Просто використовуйте цикл!

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

Це буде швидше, ніж зменшення, і такі речі, як PyPy, можуть оптимізувати подібні петлі.

До речі, випадок суми повинен бути вирішений sumфункцією


5
Це не вважатиметься пітонічним для такого прикладу.
jamylak

7
Петлі Python, як відомо, повільно. Використання (або зловживання) reduce- поширений спосіб оптимізації програми Python.
Фред Фоо

2
@larsmans Будь ласка, не кажіть, що зменшити швидше, ніж простий цикл ... Він завжди матиме накладні виклики функцій для кожної ітерації. Також знову, Pypy може оптимізувати петлі до швидкості C
JBernardo

1
@JBernardo: так, це я стверджую. Я просто профілював свою версію productпроти однієї у вашому стилі, і це швидше (хоча незначно).
Фред Фоо

1
@JBernardo Припускаючи вбудовану функцію (подібну operator.add) як аргумент для зменшення: Цей додатковий виклик - це виклик C (що набагато дешевше, ніж виклик Python), і це економить диспетчеризацію та інтерпретацію пари інструкцій байтового коду, що може легко викликати десятки функціональні дзвінки.

1

Я вважаю, що деякі респонденти цього питання пропустили більш широке значення foldфункції як абстрактного інструменту. Так, sumможна зробити те ж саме для списку цілих чисел, але це тривіальний випадок. foldє більш загальним. Це корисно, коли у вас є послідовність структур даних різної форми і ви хочете чітко виразити агрегацію. Таким чином, замість того, щоб створювати forцикл із сукупною змінною та щоразу вручну перераховувати її, foldфункція (або версія Python, яка, як reduceвидається, відповідає) дозволяє програмісту виражати наміри агрегації набагато простіше, просто надаючи дві речі:

  • За замовчуванням початкове або "насінне" значення для агрегації.
  • Функція, яка приймає поточне значення агрегації (починаючи з "насіння") і наступний елемент у списку, і повертає наступне значення агрегації.

Привіт rq_! Я думаю, що ваша відповідь буде покращена, і ви додасте багато чого, якби ви дали нетривіальний приклад, foldщо важко зробити чисто в Python, а потім " fold" - що в Python :-)
Скотт

0

Я, можливо, запізнився на вечірку, але ми можемо створити на замовлення, foldrвикористовуючи просте обчислення лямбда та викривлену функцію. Ось моя реалізація 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))

Незважаючи на те, що реалізація є рекурсивною (може бути повільною), вона буде друкувати значення 15і 120відповідно

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.