Was ist die 'pythonische' Entsprechung zur 'Fold'-Funktion aus der funktionalen Programmierung?

115

Was ist der idiomatischste Weg, um in Haskell so etwas wie das Folgende zu erreichen:

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

Oder das Äquivalent in Ruby:

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

Offensichtlich bietet Python die reduceFunktion, die eine Implementierung von Fold ist, genau wie oben, jedoch wurde mir gesagt, dass die "pythonische" Art der Programmierung darin bestand, lambdaBegriffe und Funktionen höherer Ordnung zu vermeiden und Listenverständnisse nach Möglichkeit zu bevorzugen. Gibt es daher eine bevorzugte Methode zum Falten einer Liste oder einer listenähnlichen Struktur in Python, die nicht die reduceFunktion ist, oder reducedie idiomatische Methode, um dies zu erreichen?

Mistertim
quelle
2
sumist nicht gut genug
JBernardo
3
Ich bin mir nicht sicher, ob dies ein gutes Beispiel für Ihre Frage ist. Dies kann leicht erreicht werden sum, wenn Sie verschiedene Arten von Beispielen angeben möchten.
Jamylak
14
Hey JBernardo - Das Summieren über eine Liste von Zahlen war als ziemlich entartetes Beispiel gedacht. Ich bin mehr an der allgemeinen Idee interessiert, die Elemente einer Liste mit einer binären Operation und einem Startwert zu akkumulieren, ohne Ganzzahlen spezifisch zu summieren.
Mistertim
1
@mistertim: sum()bietet damit tatsächlich eingeschränkte Funktionalität. sum([[a], [b, c, d], [e, f]], [])gibt [a, b, c, d, e, f]zum Beispiel zurück.
Joel Cornett
Obwohl der Fall, dass es mit Listen gemacht wird, eine gute Demonstration der Dinge ist, auf die mit dieser Technik geachtet werden muss - +auf Listen ist eine lineare Zeitoperation sowohl in der Zeit als auch im Speicher, wodurch der gesamte Aufruf quadratisch wird. Die Verwendung list(itertools.chain.from_iterable([a], [b,c,d],[e,f],[]])ist insgesamt linear - und wenn Sie sie nur einmal durchlaufen müssen, können Sie den Anruf abbrechen list, um ihn in Bezug auf den Speicher konstant zu halten.
lvc

Antworten:

114

Die pythonische Methode zum Summieren eines Arrays wird verwendet sum. Für andere Zwecke können Sie manchmal eine Kombination aus reduce(aus dem functoolsModul) und dem operatorModul verwenden, z.

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

Seien Sie sich bewusst, dass dies reducetatsächlich ein foldlHaskell-Begriff ist. Es gibt keine spezielle Syntax zum Ausführen von Faltungen, es gibt keine integrierte Syntax foldr, und die Verwendung reducemit nicht assoziativen Operatoren wird als schlechter Stil angesehen.

Die Verwendung von Funktionen höherer Ordnung ist ziemlich pythonisch. Es nutzt das Python-Prinzip, dass alles ein Objekt ist, einschließlich Funktionen und Klassen. Sie haben Recht, dass Lambdas von einigen Pythonisten missbilligt werden, aber hauptsächlich, weil sie nicht sehr gut lesbar sind, wenn sie komplex werden.

Fred Foo
quelle
4
@JBernardo: Sie sagen, dass alles, was nicht im eingebauten Modul enthalten ist, nicht pythonisch ist?
Fred Foo
4
Nein, das wäre dumm zu sagen. Aber geben Sie mir einen einzigen Grund, warum GvR Ihrer Meinung nach die Reduktionsfunktion so sehr hassen würde , wenn sie aus den integrierten Funktionen entfernt wird?
JBernardo
6
@JBernardo: weil die Leute versuchen, damit zu kluge Streiche zu spielen. Um aus diesem Blog-Beitrag zu zitieren: "Die Anwendbarkeit von reduce()ist weitgehend auf assoziative Operatoren beschränkt, und in allen anderen Fällen ist es besser, die Akkumulationsschleife explizit aufzuschreiben." Daher ist seine Verwendung begrenzt, aber selbst GvR musste offenbar zugeben, dass es nützlich genug ist, um es in der Standardbibliothek zu behalten.
Fred Foo
13
@JBernardo, bedeutet das, dass jede Verwendung von Fold in Haskell und Scheme gleich schlecht ist? Es ist nur eine andere Art des Programmierens, es zu ignorieren und die Finger in die Ohren zu stecken und zu sagen, dass es unklar ist, macht es nicht so. Wie die meisten Dinge, die einen anderen Stil haben, braucht es Übung, um sich daran zu gewöhnen . Die Idee ist, Dinge in allgemeine Kategorien einzuteilen, damit es einfacher ist, über Programme nachzudenken. "Oh, ich möchte das tun, hmm, sieht aus wie eine Falte" (oder eine Karte oder eine Entfaltung oder eine Entfaltung, dann eine Faltung darüber)
Wes
3
Lambda in Python kann nicht mehr als einen Ausdruck enthalten. Sie können es nicht komplex machen, selbst wenn Sie sich anstrengen. Pythonisten, die sie nicht mögen, sind wahrscheinlich nicht daran gewöhnt und mögen daher keinen funktionalen Programmierstil.
Golem
16

Haskell

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

Python

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

Offensichtlich ist dies ein triviales Beispiel, um einen Punkt zu veranschaulichen. In Python würden Sie es einfach tun sum([1,2,3,4,5])und sogar Haskell-Puristen würden es im Allgemeinen vorziehen sum [1,2,3,4,5].

Für nicht triviale Szenarien, in denen es keine offensichtliche Komfortfunktion gibt, besteht der idiomatische pythonische Ansatz darin, die for-Schleife explizit auszuschreiben und die Zuweisung veränderlicher Variablen anstelle von reduceoder a zu verwenden fold.

Das ist überhaupt nicht der funktionale Stil, aber das ist der "pythonische" Weg. Python ist nicht für funktionale Puristen konzipiert. Sehen Sie, wie Python Ausnahmen für die Flusskontrolle bevorzugt, um zu sehen, wie nicht funktionsfähig idiomatisches Python ist.

Lehm
quelle
11
Falten sind mehr als nur für funktionale "Puristen" nützlich. Sie sind Allzweckabstraktionen. Rekursive Probleme sind beim Rechnen weit verbreitet. Falten bieten eine Möglichkeit, die Boilerplate zu entfernen und rekursive Lösungen in Sprachen sicher zu machen, die die Rekursion nicht nativ unterstützen. Also eine sehr praktische Sache. Die Vorurteile von GvR in diesem Bereich sind unglücklich.
Itsbruce
12

In Python 3 wurde das reduceentfernt: Versionshinweise . Trotzdem können Sie das functools-Modul verwenden

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

Andererseits drückt die Dokumentation die Präferenz für for-loop aus, anstatt reduce:

def product(xs):
    result = 1
    for i in xs:
        result *= i
    return result
Kyr
quelle
7
reducewurde nicht aus der Python 3-Standardbibliothek entfernt. reducewie gezeigt in das functoolsModul verschoben .
Ton
@clay, ich habe gerade den Satz aus Guidos Versionshinweisen genommen, aber Sie haben vielleicht Recht :)
Kyr
5

Sie können das Rad auch neu erfinden:

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)
Frédéric
quelle
Sie tauschen die Argumente fin Ihrem rekursiven Fall um.
KayEss
7
Da es in Python an einer Schwanzrekursion mangelt, wird dies bei längeren Listen unterbrochen und ist verschwenderisch. Darüber hinaus ist dies nicht wirklich die "Fold" -Funktion, sondern lediglich eine Linksfalte, dh Foldl, dh genau das , was reducebereits angeboten wird (beachten Sie, dass die Funktionssignatur von Reduce ist reduce(function, sequence[, initial]) -> value- es beinhaltet auch die Funktionalität, einen Anfangswert für die zu geben Akkumulator).
Cemper93
5

Keine wirkliche Antwort auf die Frage, aber Einzeiler für Foldl und Foldr:

a = [8,3,4]

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

## Foldr
reduce(lambda x,y: y**x, a[::-1])
#14134776518227074636666380005943348126619871175004951664972849610340958208L
Mehdi Nellen
quelle
2
Ich denke, dies ist ein besserer Weg, um Ihre Falte zu schreiben : reduce(lambda y, x: x**y, reversed(a)). Es wird jetzt natürlicher verwendet, arbeitet mit Iteratoren und verbraucht weniger Speicher.
Mateen Ulhaq
5

Ab Python 3.8dem Start und der Einführung von Zuweisungsausdrücken (PEP 572) ( :=Operator), die die Möglichkeit bieten, das Ergebnis eines Ausdrucks zu benennen, können wir ein Listenverständnis verwenden, um das zu replizieren, was andere Sprachen als Fold / Foldleft / Reduce-Operationen bezeichnen:

Gegeben eine Liste, eine Reduktionsfunktion und ein Akkumulator:

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

wir können itemsmit ffolden, um das Ergebnis zu erhalten accumulation:

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

oder in einer kondensierten Form:

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

Beachten Sie, dass dies tatsächlich auch eine "Scanleft" -Operation ist, da das Ergebnis des Listenverständnisses den Zustand der Akkumulation bei jedem Schritt darstellt:

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

Die eigentliche Antwort auf dieses (Reduktions-) Problem lautet: Verwenden Sie einfach eine Schleife!

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

Dies ist schneller als eine Reduzierung und Dinge wie PyPy können solche Schleifen optimieren.

Übrigens sollte der Summenfall mit der sumFunktion gelöst werden

JBernardo
quelle
4
Dies würde für ein Beispiel wie dieses nicht als pythonisch angesehen.
Jamylak
7
Python-Schleifen sind notorisch langsam. Die Verwendung (oder der Missbrauch) reduceist eine gängige Methode zur Optimierung eines Python-Programms.
Fred Foo
1
@larsmans Bitte sagen Sie nicht, dass Reduzieren schneller ist als eine einfache Schleife ... Es wird immer einen Funktionsaufruf-Overhead für jede Iteration geben. Auch hier kann Pypy Schleifen auf C-Geschwindigkeit optimieren
JBernardo
1
@JBernardo: Ja, das behaupte ich. Ich habe gerade meine Version von productgegen eine in Ihrem Stil profiliert und sie ist schneller (allerdings geringfügig).
Fred Foo
1
@JBernardo Angenommen, eine integrierte Funktion (wie operator.add) als Argument zum Reduzieren: Dieser zusätzliche Aufruf ist ein C-Aufruf (der viel billiger als ein Python-Aufruf ist) und erspart das Versenden und Interpretieren einiger Bytecode-Anweisungen, die leicht Dutzende von Anweisungen verursachen können Funktionsaufrufe.
1

Ich glaube, einige der Befragten dieser Frage haben die breitere Implikation der foldFunktion als abstraktes Werkzeug übersehen . Ja, sumkann dasselbe für eine Liste von ganzen Zahlen tun, aber dies ist ein trivialer Fall. foldist allgemeiner. Dies ist nützlich, wenn Sie eine Folge von Datenstrukturen unterschiedlicher Form haben und eine Aggregation sauber ausdrücken möchten. Also anstatt ein aufbauen zu müssenfor Schleife mit einer Aggregatvariablen aufzubauen und jedes Mal manuell neu zu berechnen, ermöglicht eine foldFunktion (oder die Python-Version, die reducezu entsprechen scheint) dem Programmierer, die Absicht der Aggregation durch einfaches Bereitstellen viel klarer auszudrücken Zwei Dinge:

  • Ein Standardstart- oder "Startwert" für die Aggregation.
  • Eine Funktion, die den aktuellen Wert der Aggregation (beginnend mit dem "Startwert") und das nächste Element in der Liste verwendet und den nächsten Aggregationswert zurückgibt.
rq_
quelle
Hallo rq_! Ich denke, Ihre Antwort würde verbessert und viel hinzugefügt, wenn Sie ein nicht triviales Beispiel folddafür geben würden, das in Python nur schwer sauber zu machen ist, und dann " fold" das in Python :-)
Scott Skiles
0

Ich mag ziemlich spät zur Party kommen, aber wir können benutzerdefinierte foldrmit einfachen Lambda-Kalkül und Curry-Funktion erstellen . Hier ist meine Implementierung von 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))

Auch wenn die Umsetzung rekursiv ist (möglicherweise langsam), werden die Werte gedruckt 15und 120jeweils

Keuchen
quelle