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?

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


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

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

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.


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.


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


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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.