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.