Qual è l'equivalente "pitonico" della funzione "piega" dalla programmazione funzionale?


116

Qual è il modo più idiomatico per ottenere qualcosa di simile, in Haskell:

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

O il suo equivalente in Ruby:

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

Ovviamente, Python fornisce la reducefunzione, che è un'implementazione di fold, esattamente come sopra, tuttavia, mi è stato detto che il modo "pitonico" di programmazione era quello di evitare lambdatermini e funzioni di ordine superiore, preferendo la comprensione di elenchi ove possibile. Pertanto, esiste un modo preferito per piegare una lista, o una struttura simile a una lista in Python che non è la reducefunzione, o è reduceil modo idiomatico per ottenere ciò?


2
sumnon è abbastanza buono?
JBernardo

3
non sono sicuro che questo sia un buon esempio per la tua domanda. Può essere facilmente ottenuto con sum, potresti voler fornire alcuni diversi tipi di esempi.
jamylak

14
Ehi JBernardo - La somma su una lista di numeri era intesa come un esempio piuttosto degenerato, sono più interessato all'idea generale di accumulare gli elementi di una lista usando qualche operazione binaria e un valore iniziale, non sommando specificamente interi.
mistertim

1
@mistertim: sum()fornisce effettivamente funzionalità limitate con questo. sum([[a], [b, c, d], [e, f]], [])ritorna [a, b, c, d, e, f]per esempio.
Joel Cornett

Sebbene il caso di farlo con le liste sia una buona dimostrazione di cose da tenere d'occhio con questa tecnica, +sulle liste è un'operazione di tempo lineare sia nel tempo che nella memoria, rendendo l'intera chiamata quadratica. L'utilizzo list(itertools.chain.from_iterable([a], [b,c,d],[e,f],[]])è nel complesso lineare e se è necessario iterarlo solo una volta, è possibile eliminare la chiamata a listper renderlo costante in termini di memoria.
lvc

Risposte:


115

Il modo pitonico di sommare un array sta usando sum. Per altri scopi, a volte puoi usare una combinazione di reduce(dal functoolsmodulo) e il operatormodulo, ad esempio:

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

Tieni presente che in reducerealtà è un foldl, in termini Haskell. Non esiste una sintassi speciale per eseguire le pieghe, non esiste un builtin foldre in realtà l'utilizzo reducecon operatori non associativi è considerato un cattivo stile.

Usare le funzioni di ordine superiore è piuttosto pitonico; fa buon uso del principio di Python secondo cui tutto è un oggetto, comprese le funzioni e le classi. Hai ragione sul fatto che i lambda sono disapprovati da alcuni Pythonistas, ma soprattutto perché tendono a non essere molto leggibili quando diventano complessi.


4
@ JBernardo: stai dicendo che tutto ciò che non è nel modulo integrato non è pitonico?
Fred Foo

4
No, sarebbe stupido da dire. Ma dammi una sola ragione per cui pensi che GvR odierebbe così tanto la funzione di riduzione al punto di rimuoverla dai builtin?
JBernardo

6
@ JBernardo: perché le persone cercano di giocare brutti scherzi con esso. Per citare da quel post sul blog, "l'applicabilità di reduce()è praticamente limitata agli operatori associativi, e in tutti gli altri casi è meglio scrivere esplicitamente il ciclo di accumulazione". Quindi, il suo utilizzo è limitato, ma anche GvR a quanto pare ha dovuto ammettere che era abbastanza utile da tenerlo nella libreria standard.
Fred Foo

13
@ JBernardo, quindi significa che ogni utilizzo di fold in Haskell e Scheme è ugualmente negativo? È solo uno stile di programmazione diverso, ignorarlo e mettere le dita nelle orecchie e dire che non è chiaro non lo rende così. Come la maggior parte delle cose che hanno uno stile diverso, ci vuole pratica per abituarsi . L'idea è di mettere le cose in categorie generali in modo che sia più facile ragionare sui programmi. "Oh, voglio farlo, hmm, sembra una piega" (o una mappa, o una spiegazione, o una spiegazione e poi una piega su quella)
Wes

3
Lambda in Python non può contenere più di un'espressione. Non puoi renderlo complesso anche se ci provi. Quindi i Pythonisti a cui non piacciono probabilmente non sono abituati e quindi non amano lo stile di programmazione funzionale.
golem

16

Haskell

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

Pitone

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

Ovviamente, questo è un esempio banale per illustrare un punto. In Python lo faresti sum([1,2,3,4,5])e anche i puristi Haskell lo preferirebbero generalmente sum [1,2,3,4,5].

Per scenari non banali in cui non esiste un'ovvia funzione di convenienza, l'approccio idiomatico pitonico consiste nello scrivere esplicitamente il ciclo for e utilizzare l'assegnazione di variabili mutabili invece di utilizzare reduceo a fold.

Questo non è affatto lo stile funzionale, ma questo è il modo "pitonico". Python non è progettato per i puristi funzionali. Guarda come Python favorisce le eccezioni per il controllo del flusso per vedere quanto è idiomatico Python non funzionale.


12
le pieghe sono utili a più che funzionali "puristi". Sono astrazioni generiche. I problemi ricorsivi sono pervasivi nell'informatica. Le pieghe offrono un modo per rimuovere il boilerplate e un modo per rendere sicure le soluzioni ricorsive nelle lingue che non supportano nativamente la ricorsione. Quindi una cosa molto pratica. I pregiudizi di GvR in questo settore sono sfortunati.
itsbruce

12

In Python 3, reduceè stato rimosso: Note di rilascio . Tuttavia puoi usare il modulo functools

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

D'altra parte, la documentazione esprime la preferenza verso for-loop invece di reduce, quindi:

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

8
reducenon è stato rimosso dalla libreria standard di Python 3. reducespostato nel functoolsmodulo come mostrato.
terra

@clay, ho appena preso la frase dalle note di rilascio di Guido, ma potresti avere ragione :)
Kyr

6

A partire Python 3.8, e con l'introduzione di espressioni di assegnazione (PEP 572) ( :=operatore), che danno la possibilità di nominare il risultato di un'espressione, possiamo usare una comprensione di lista per replicare ciò che altri linguaggi chiamano operazioni fold / foldleft / reduce:

Dato un elenco, una funzione riducente e un accumulatore:

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

possiamo piegare itemscon fper ottenere il risultato accumulation:

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

o in un formato condensato:

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

Nota che questa è in realtà anche un'operazione "scanleft" poiché il risultato della comprensione della lista rappresenta lo stato dell'accumulazione ad ogni passaggio:

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

5

Puoi anche reinventare la ruota:

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)

Scambia gli argomenti in fgiro nel tuo caso ricorsivo.
KayEss

7
Poiché Python non ha la ricorsione della coda, ciò si interromperà su elenchi più lunghi ed è uno spreco. Inoltre, questa non è veramente la funzione "fold", ma semplicemente una piega a sinistra, cioè foldl, cioè esattamente ciò che reducegià offre (nota che la firma della funzione di reduce è reduce(function, sequence[, initial]) -> value- essa, inoltre, include la funzionalità di dare un valore iniziale per il accumulatore).
cemper93

5

Non risponde veramente alla domanda, ma una riga per foldl e 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
Penso che questo sia un modo migliore per scrivere il vostro foldr: reduce(lambda y, x: x**y, reversed(a)). Ora ha un utilizzo più naturale, funziona con gli iteratori e consuma meno memoria.
Mateen Ulhaq

2

La risposta effettiva a questo problema (ridurre) è: usa semplicemente un ciclo!

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

Questo sarà più veloce di una riduzione e cose come PyPy possono ottimizzare i loop in questo modo.

A proposito, il caso della somma dovrebbe essere risolto con la sumfunzione


5
Questo non sarebbe considerato pitonico per un esempio come questo.
jamylak

7
I cicli Python sono notoriamente lenti. Usare (o abusare) reduceè un modo comune per ottimizzare un programma Python.
Fred Foo

2
@larsmans Per favore, non dire che reduce è più veloce di un semplice ciclo ... Avrà sempre un overhead di chiamata di funzione per ogni iterazione. Inoltre, ancora una volta, Pypy può ottimizzare i loop alla velocità C
JBernardo

1
@ JBernardo: sì, è quello che sto affermando. Ho appena profilato la mia versione di productcontro una nel tuo stile, ed è più veloce (marginalmente, però).
Fred Foo

1
@JBernardo Assumendo una funzione incorporata (come operator.add) come argomento per ridurre: quella chiamata extra è una chiamata C (che è molto più economica di una chiamata Python), e salva l'invio e l'interpretazione di un paio di istruzioni bytecode, che possono facilmente causare dozzine di chiamate di funzione.

1

Credo che alcuni degli intervistati a questa domanda abbiano perso la più ampia implicazione della foldfunzione come strumento astratto. Sì, sumpuò fare la stessa cosa per un elenco di numeri interi, ma questo è un caso banale. foldè più generico. È utile quando si dispone di una sequenza di strutture di dati di forma variabile e si desidera esprimere in modo pulito un'aggregazione. Quindi, invece di dover costruire un forciclo con una variabile aggregata e ricalcolarlo manualmente ogni volta, una foldfunzione (o la versione Python, che reducesembra corrispondere) consente al programmatore di esprimere l'intento dell'aggregazione in modo molto più chiaro semplicemente fornendo due cose:

  • Un valore iniziale o "seme" predefinito per l'aggregazione.
  • Una funzione che prende il valore corrente dell'aggregazione (a partire dal "seme") e l'elemento successivo nell'elenco e restituisce il valore di aggregazione successivo.

Ciao rq_! Penso che la tua risposta sarebbe migliorata e aggiungerebbe molto se fornissi un esempio non banale di foldciò che è difficile da fare in modo pulito in Python, e poi " fold" quello in Python :-)
Scott Skiles

0

Potrei essere abbastanza in ritardo per la festa, ma possiamo creare personalizzato foldrutilizzando semplici lambda calcolo e funzione curry. Ecco la mia implementazione di foldr in 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))

Anche se l'implementazione è ricorsiva (potrebbe essere lenta), stamperà i valori 15e 120rispettivamente

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.