Wie macht functools teilweise das, was es tut?

180

Ich kann mir nicht vorstellen, wie das Teil in Funktionswerkzeugen funktioniert. Ich habe den folgenden Code von hier :

>>> sum = lambda x, y : x + y
>>> sum(1, 2)
3
>>> incr = lambda y : sum(1, y)
>>> incr(2)
3
>>> def sum2(x, y):
    return x + y

>>> incr2 = functools.partial(sum2, 1)
>>> incr2(4)
5

Jetzt in der Reihe

incr = lambda y : sum(1, y)

Ich verstehe, dass jedes Argument, an das ich incres weitergebe, weitergegeben wird y, lambdawelches zurückkehren wird, sum(1, y)dh 1 + y.

Ich verstehe das. Aber ich habe das nicht verstanden incr2(4).

Wie 4wird das wie xin Teilfunktion übergeben? Für mich 4sollte das ersetzen sum2. Wie ist die Beziehung zwischen xund 4?

user1865341
quelle

Antworten:

218

Tut ungefähr partialso etwas (abgesehen von der Unterstützung von Keyword-Argumenten usw.):

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*args)

    return wrapper

Wenn partial(sum2, 4)Sie also aufrufen , erstellen Sie eine neue Funktion (genauer gesagt eine aufrufbare), die sich wie verhält sum2, aber ein Positionsargument weniger hat. Dieses fehlende Argument wird immer durch ersetzt 4, so dasspartial(sum2, 4)(2) == sum2(4, 2)

Es gibt verschiedene Fälle, warum es benötigt wird. Angenommen, Sie müssen eine Funktion an einer Stelle übergeben, an der zwei Argumente erwartet werden:

class EventNotifier(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        ''' callback should accept two positional arguments, event and params '''
        self._listeners.append(callback)
        # ...

    def notify(self, event, *params):
        for f in self._listeners:
            f(event, params)

Eine Funktion, die Sie bereits haben, benötigt jedoch Zugriff auf ein drittes contextObjekt, um ihre Aufgabe zu erfüllen :

def log_event(context, event, params):
    context.log_event("Something happened %s, %s", event, params)

Es gibt also mehrere Lösungen:

Ein benutzerdefiniertes Objekt:

class Listener(object):
   def __init__(self, context):
       self._context = context

   def __call__(self, event, params):
       self._context.log_event("Something happened %s, %s", event, params)


 notifier.add_listener(Listener(context))

Lambda:

log_listener = lambda event, params: log_event(context, event, params)
notifier.add_listener(log_listener)

Mit Teiltönen:

context = get_context()  # whatever
notifier.add_listener(partial(log_event, context))

Von diesen dreien partialist es das kürzeste und das schnellste. (Für einen komplexeren Fall möchten Sie möglicherweise ein benutzerdefiniertes Objekt.)

sei ehrlich
quelle
1
extra_args
Woher
2
extra_argsist etwas , das in dem Beispiel durch die partiellen Anrufer, geleitet , die mit p = partial(func, 1); f(2, 3, 4)er ist (2, 3, 4).
Bereal
1
aber warum wir das tun würden, jeder spezielle Anwendungsfall, in dem etwas nur teilweise erledigt werden muss und nicht mit anderen Dingen erledigt werden kann
user1865341
@ user1865341 Ich habe der Antwort ein Beispiel hinzugefügt.
Bereal
In Ihrem Beispiel, wie ist die Beziehung zwischen callbackundmy_callback
user1865341
92

Partials sind unglaublich nützlich.

Zum Beispiel in einer 'Pipeline'-Folge von Funktionsaufrufen (in der der von einer Funktion zurückgegebene Wert das an die nächste übergebene Argument ist).

Manchmal erfordert eine Funktion in einer solchen Pipeline ein einzelnes Argument , aber die unmittelbar vorgelagerte Funktion gibt zwei Werte zurück .

In diesem Szenario functools.partialkönnen Sie diese Funktionspipeline möglicherweise intakt halten.

Hier ist ein spezielles, isoliertes Beispiel: Angenommen, Sie möchten einige Daten nach der Entfernung jedes Datenpunkts von einem Ziel sortieren:

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

Um diese Daten nach Entfernung vom Ziel zu sortieren, möchten Sie natürlich Folgendes tun:

data.sort(key=euclid_dist)

aber du nicht - die Art der Methode Schlüsselparameter nur Funktionen übernehmen , die einen nehmen einzelnes Argument.

Schreiben Sie also euclid_distals Funktion neu, indem Sie einen einzelnen Parameter verwenden:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist akzeptiert jetzt ein einziges Argument,

>>> p_euclid_dist((3, 3))
  1.4142135623730951

Jetzt können Sie Ihre Daten sortieren, indem Sie die Teilfunktion für das Schlüsselargument der Sortiermethode übergeben:

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

Zum Beispiel ändert sich eines der Argumente der Funktion in einer äußeren Schleife, wird jedoch während der Iteration in der inneren Schleife festgelegt. Bei Verwendung eines Teils müssen Sie den zusätzlichen Parameter während der Iteration der inneren Schleife nicht übergeben, da die modifizierte (Teil-) Funktion dies nicht erfordert.

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

Erstellen Sie eine Teilfunktion (mit dem Schlüsselwort arg)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

Sie können auch eine Teilfunktion mit einem Positionsargument erstellen

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

Dies wird jedoch ausgelöst (z. B. Erstellen eines Teils mit einem Schlüsselwortargument und anschließendes Aufrufen mit Positionsargumenten).

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

Ein weiterer Anwendungsfall: Schreiben von verteiltem Code mithilfe der Python- multiprocessingBibliothek. Ein Pool von Prozessen wird mit der Pool-Methode erstellt:

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Pool hat eine Map-Methode, benötigt aber nur eine einzige iterable. Wenn Sie also eine Funktion mit einer längeren Parameterliste übergeben müssen, definieren Sie die Funktion als Teil neu, um alle bis auf eine zu reparieren:

>>> ppool.map(pfnx, [4, 6, 7, 8])
Doug
quelle
1
Gibt es
3
@ user1865341 fügte meiner Antwort zwei beispielhafte Anwendungsfälle hinzu
Doug
IMHO, dies ist eine bessere Antwort, da es nicht verwandte Konzepte wie Objekte und Klassen fernhält und sich auf Funktionen konzentriert, worum es hier geht.
Akhan
35

Kurze Antwort: partialGibt den Parametern einer Funktion Standardwerte, die sonst keine Standardwerte hätten.

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10
Alex Fortin
quelle
5
Dies ist zur Hälfte wahr, weil wir Standardwerte überschreiben können, wir können sogar Überschreibungsparameter durch nachfolgende überschreiben partialund so weiter
Azat Ibrakov
33

Partials können verwendet werden, um neue abgeleitete Funktionen zu erstellen, denen einige Eingabeparameter vorab zugewiesen wurden

Informationen zur Verwendung von Partials in der Praxis finden Sie in diesem wirklich guten Blog-Beitrag:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Eine einfache , aber ordentlich Anfänger Beispiel aus dem Blog, Abdeckungen , wie man verwenden könnte , partialauf re.searchCode besser lesbar zu machen. re.searchDie Signatur der Methode lautet:

search(pattern, string, flags=0) 

Durch Anwenden können partialwir mehrere Versionen des regulären Ausdrucks erstellen search, um unseren Anforderungen zu entsprechen, zum Beispiel:

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

Nun is_spaced_apartund is_grouped_togethersind zwei neue Funktionen abgeleitet, von re.searchdenen das patternArgument angewendet wurde (da dies patterndas erste Argument in der re.searchSignatur der Methode ist).

Die Signatur dieser beiden neuen Funktionen (aufrufbar) lautet:

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

So können Sie dann diese Teilfunktionen für einen Text verwenden:

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

Sie können auf den obigen Link verweisen , um ein tieferes Verständnis des Themas zu erhalten, da es dieses spezielle Beispiel und vieles mehr abdeckt.

sisanared
quelle
1
Ist das nicht gleichbedeutend mit is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Wenn ja, gibt es eine Garantie dafür, dass das partialIdiom den regulären Ausdruck für eine schnellere Wiederverwendung kompiliert?
Aristide
10

Meiner Meinung nach ist es eine Möglichkeit, Currying in Python zu implementieren .

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __name__ == "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

Das Ergebnis ist 3 und 4.

Hanzhou Tang
quelle
1

Erwähnenswert ist auch, dass, wenn eine Teilfunktion eine andere Funktion übergeben hat, bei der einige Parameter "hart codiert" werden sollen, dies der Parameter ganz rechts sein sollte

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

aber wenn wir das gleiche tun, aber stattdessen einen Parameter ändern

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

es wird der Fehler "TypeError: func () hat mehrere Werte für das Argument 'a'" ausgelöst.

MSK
quelle
Huh? Sie machen den Parameter ganz links wie prt=partial(func, 7)
folgt
0

Diese Antwort ist eher ein Beispielcode. Alle obigen Antworten geben gute Erklärungen, warum man teilweise verwenden sollte. Ich werde meine Beobachtungen und Anwendungsfälle über teilweise geben.

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

Die Ausgabe des obigen Codes sollte sein:

a:1,b:2,c:3
6

Beachten Sie, dass im obigen Beispiel eine neue aufrufbare Datei zurückgegeben wurde, die den Parameter (c) als Argument verwendet. Beachten Sie, dass dies auch das letzte Argument für die Funktion ist.

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

Die Ausgabe des obigen Codes ist auch:

a:1,b:2,c:3
6

Beachten Sie, dass * zum Entpacken der Nicht-Schlüsselwort-Argumente verwendet wurde und der zurückgegebene Aufruf in Bezug auf das Argument, das er verwenden kann, dasselbe wie oben ist.

Eine weitere Beobachtung ist: Das folgende Beispiel zeigt, dass ein Teil einen aufrufbaren Wert zurückgibt, der den nicht deklarierten Parameter (a) als Argument verwendet.

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

Die Ausgabe des obigen Codes sollte sein:

a:20,b:10,c:2,d:3,e:4
39

Ähnlich,

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

Der obige Code wird gedruckt

a:20,b:10,c:2,d:3,e:4
39

Ich musste es verwenden, als ich die Pool.map_asyncMethode aus dem multiprocessingModul verwendete. Sie können nur ein Argument an die Worker-Funktion übergeben, sodass ich partialmeine Worker-Funktion wie eine aufrufbare Funktion mit nur einem Eingabeargument aussehen lassen musste. In Wirklichkeit hatte meine Worker-Funktion jedoch mehrere Eingabeargumente.

Ruthvik Vaila
quelle