W jaki sposób functools częściowe robi to, co robi?

181

Nie jestem w stanie pojąć, jak działa podrzędność w functools. Mam stąd następujący kod :

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

Teraz w kolejce

incr = lambda y : sum(1, y)

I niezależnie od tego dostać argumentu mijam do incrniej zostanie przekazany jako ydo lambdaktórej powróci sum(1, y)IE 1 + y.

Rozumiem, że. Ale ja tego nie rozumiałem incr2(4).

W jaki sposób 4przekazywana jest xfunkcja częściowa? Dla mnie 4powinien zastąpić sum2. Jaka jest zależność między xi 4?

user1865341
źródło

Odpowiedzi:

218

Z grubsza partialrobi coś takiego (oprócz obsługi argumentów słów kluczowych itp.):

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

    return wrapper

Tak więc, wywołując partial(sum2, 4), tworzysz nową funkcję (wywoływalną, aby być precyzyjnym), która zachowuje się podobnie sum2, ale ma o jeden argument pozycyjny mniej. Ten brakujący argument jest zawsze zastępowany przez 4, więcpartial(sum2, 4)(2) == sum2(4, 2)

Jeśli chodzi o powody, dla których jest to potrzebne, istnieje wiele przypadków. Na przykład załóżmy, że musisz przekazać funkcję gdzieś, gdzie oczekuje się, że będzie miała 2 argumenty:

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)

Ale funkcja, którą już masz, potrzebuje dostępu do trzeciego contextobiektu, aby wykonać swoją pracę:

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

Tak więc istnieje kilka rozwiązań:

Obiekt niestandardowy:

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)

Z częściami:

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

Z tych trzech partialjest najkrótszy i najszybszy. (Jednak w przypadku bardziej złożonego przypadku możesz potrzebować obiektu niestandardowego).

być prawdziwym
źródło
1
skąd masz extra_argszmienną
user1865341
2
extra_argsjest czymś, co zostało przekazane przez częściowego wywołującego, w przykładzie z p = partial(func, 1); f(2, 3, 4)nim jest (2, 3, 4).
bereal
1
ale dlaczego mielibyśmy to robić, każdy specjalny przypadek użycia, w którym coś musi być zrobione tylko częściowo i nie można tego zrobić z inną rzeczą
user1865341
@ user1865341 Dodałem przykład do odpowiedzi.
bereal
na twoim przykładzie, jaka jest relacja między callbackamy_callback
user1865341
92

części niepełne są niezwykle przydatne.

Na przykład w sekwencji wywołań funkcji wyłożonych pionową kreską (w której wartość zwracana z jednej funkcji jest argumentem przekazywanym do następnej).

Czasami funkcja w takim potoku wymaga pojedynczego argumentu , ale funkcja bezpośrednio przed nią zwraca dwie wartości .

W tym scenariuszu functools.partialmoże pozwolić na zachowanie tego potoku funkcji w stanie nienaruszonym.

Oto konkretny, odizolowany przykład: przypuśćmy, że chcesz posortować dane według odległości każdego punktu danych od jakiegoś celu:

# 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)

Aby posortować te dane według odległości od celu, chciałbyś oczywiście zrobić to:

data.sort(key=euclid_dist)

ale nie możesz - parametr key metody sort akceptuje tylko funkcje, które przyjmują jeden argument.

więc przepisz ponownie euclid_distjako funkcję pobierającą pojedynczy parametr:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist teraz przyjmuje pojedynczy argument,

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

więc teraz możesz sortować swoje dane, przekazując funkcję częściową dla argumentu klucza metody sort:

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

Lub na przykład jeden z argumentów funkcji zmienia się w pętli zewnętrznej, ale jest ustalany podczas iteracji w pętli wewnętrznej. Używając częściowej, nie musisz przekazywać dodatkowego parametru podczas iteracji wewnętrznej pętli, ponieważ zmodyfikowana (częściowa) funkcja tego nie wymaga.

>>> from functools import partial

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

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

utwórz funkcję częściową (używając słowa kluczowego arg)

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

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

możesz również utworzyć funkcję częściową z argumentem pozycyjnym

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

ale to zwróci (np. utworzenie częściowego argumentu słowa kluczowego, a następnie wywołanie za pomocą argumentów pozycyjnych)

>>> 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'

inny przypadek użycia: pisanie rozproszonego kodu przy użyciu multiprocessingbiblioteki Pythona . Pula procesów jest tworzona metodą Pool:

>>> import multiprocessing as MP

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

Pool ma metodę mapowania, ale wymaga tylko jednej iteracji, więc jeśli chcesz przekazać funkcję z dłuższą listą parametrów, ponownie zdefiniuj funkcję jako częściową, aby naprawić wszystkie oprócz jednej:

>>> ppool.map(pfnx, [4, 6, 7, 8])
Doug
źródło
1
czy jest jakieś praktyczne zastosowanie tej funkcji
user1865341
3
@ user1865341 dodał dwa przykładowe przypadki użycia do mojej odpowiedzi
Doug
IMHO, to jest lepsza odpowiedź, ponieważ zapobiega niepowiązanym pojęciom, takim jak obiekty i klasy, i koncentruje się na funkcjach, o co w tym wszystkim chodzi.
akhan
35

krótka odpowiedź, partialpodaje domyślne wartości parametrów funkcji, które w innym przypadku nie miałyby wartości domyślnych.

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
źródło
5
jest to w połowie prawdziwe, ponieważ możemy przesłonić wartości domyślne, możemy nawet nadpisać parametry przez kolejne partiali tak dalej
Azat Ibrakov
33

Częściowe mogą być używane do tworzenia nowych funkcji pochodnych, które mają wstępnie przypisane pewne parametry wejściowe

Aby zobaczyć rzeczywiste wykorzystanie części składowych, zapoznaj się z tym naprawdę dobrym wpisem na blogu:
http://chriskiehl.com/article/Cleaner-coding-through-partually-applied-functions/

Prosty, ale zgrabny przykład dla początkujących z bloga, pokazuje, jak można użyć partialon, re.searchaby uczynić kod bardziej czytelnym. re.searchpodpis metody to:

search(pattern, string, flags=0) 

Stosując partial, możemy stworzyć wiele wersji wyrażenia regularnego, searchktóre będą odpowiadały naszym wymaganiom, na przykład:

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

Teraz is_spaced_aparti is_grouped_togethersą dwiema nowymi funkcjami pochodzącymi z re.search, do których patternzastosowano argument (ponieważ patternjest to pierwszy argument wre.search sygnaturze metody).

Sygnatura tych dwóch nowych funkcji (wywoływalnych) to:

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

Oto jak możesz następnie użyć tych częściowych funkcji na jakimś tekście:

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()

Możesz odnieść się do powyższego linku, aby uzyskać bardziej szczegółowe zrozumienie tematu, ponieważ obejmuje on ten konkretny przykład i wiele więcej.

sisanared
źródło
1
Czy to nie jest równoważne is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Jeśli tak, czy istnieje gwarancja, że partialidiom skompiluje wyrażenie regularne w celu szybszego ponownego użycia?
Aristide
10

Moim zdaniem to sposób na zaimplementowanie curry w Pythonie.

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))

Wynik to 3 i 4.

Hanzhou Tang
źródło
1

Warto również wspomnieć, że gdy funkcja częściowa przekazuje inną funkcję, w której chcemy „zakodować” niektóre parametry, powinien to być parametr znajdujący się najbardziej po prawej stronie

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

ale jeśli zrobimy to samo, ale zamiast tego zmieniamy parametr

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

zwróci błąd, "TypeError: func () ma wiele wartości dla argumentu 'a'"

MSK
źródło
Co? Wykonujesz skrajny lewy parametr w ten sposób:prt=partial(func, 7)
DylanYoung
0

Ta odpowiedź jest bardziej przykładowym kodem. Wszystkie powyższe odpowiedzi dobrze wyjaśniają, dlaczego należy używać częściowych. Podam moje obserwacje i wykorzystam przypadki o częściowej.

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

Wynik powyższego kodu powinien wyglądać następująco:

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

Zauważ, że w powyższym przykładzie zostało zwrócone nowe wywołanie, które przyjmie parametr (c) jako argument. Zauważ, że jest to również ostatni argument funkcji.

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

Wynik powyższego kodu to również:

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

Zauważ, że * został użyty do rozpakowania argumentów niebędących słowami kluczowymi, a zwracana wartość wywoływana w zakresie tego, który argument może przyjąć, jest taka sama jak powyżej.

Inna obserwacja: Poniższy przykład pokazuje, że częściowe zwraca wywoływalny parametr, który przyjmie niezadeklarowany parametr (a) jako argument.

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)

Wynik powyższego kodu powinien wyglądać następująco:

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

Podobnie,

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

Nadrukowany kod

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

Musiałem go użyć, gdy korzystałem Pool.map_asyncz metody z multiprocessingmodułu. Możesz przekazać tylko jeden argument do funkcji roboczej, więc musiałem użyć, partialaby moja funkcja robocza wyglądała jak wywoływalna z tylko jednym argumentem wejściowym, ale w rzeczywistości moja funkcja robocza miała wiele argumentów wejściowych.

Ruthvik Vaila
źródło