Partial w Pythonie – zastosowanie, przykłady

Cześć, po małym skoku w bok – wracamy do programowania.

Poopowiadam dziś trochę o ciekawej funkcji, którą możemy zaleźć w bibliotece standardowej Python’a, a mianowicie o funkcji partial.

Zwraca ona obiekt partial który zachowuje się jak funkcja przekazana w pierwszym argumencie omawianej metody, ale z już wypełnionymi argumentami . Brzmi nudno? Poczekajcie dalej ;)

Jak działa funkcja partial „od kuchni” ?

Tak mniej więcej mogłaby wyglądać implementacja funkcji partial

1
2
3
4
5
6
7
8
9
def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

Jak widać wyżej, działanie funkcji partial opiera się o stworzenie nowej funkcji która będzie miała zapamiętane te argumenty (pozycyjne i jak nazwane) które były podane jako kolejne, po funkcji która ma być dekorowana. Innymi słowy – powyższy kod pokazuje sposób na zapamiętanie pewnej porcji argumentów funkcji, tak by zredukować ich ilość przy jej wywołaniu, nadpisując tą „pewną porcję argumentów”, argumentami podanymi przy tworzeniu udekorowanej funkcji.

……

Ale że cooo?!

Zobaczmy pierwszy, prosty przykład

Załóżmy że mamy funkcję:

1
2
def power(x: int, y: int):
    return x**y

Prosty kawałek kodu który podnosi liczbę x do potęgi y. Nie potrzeba ogromnej wiedzy matematycznej by zauważyć że takie wywołania:

1
2
3
print(power(2, 2))
print(power(2, 3))
print(power(2, 4))

wyprodukują nam liczby:

4
8
16

szok i niedowierzanie, prawda?:)

Teraz chcemy stworzyć funkcję która będzie miała jeden argument który zawsze będzie podnosić do kwadratu, możemy oczywiście zrobić to tak:

1
2
3
4
5
def power(x: int, y: int):
    return x**y

def square(x: int):
    return power(x, 2)

i wtedy nasza funkcja square będzie robić dokładnie to co napisałem, a wiec wywołania:

1
2
3
print(square(2))
print(square(3))
print(square(4))

wyprodukują:

4
9
16

Ale, ale, ale… Znając funkcję partial możemy zrobić to ładniej, np tak:

1
2
3
4
5
6
7
8
9
10
11
12
from functools import partial


def power(x: int, y: int):
    return x**y


square = partial(power, y=2)

print(square(2))
print(square(3))
print(square(4))

Funkcja partial w tym przypadku weźmie funkcję power oraz zapisze argument nazwany „y” oraz zwróci funkcję która automatycznie przy wywołaniu przekaże zapisany argument jako argument dekorowanej funkcji. I ten kod również wyprodukuje ten sam wynik:

4
9
16

Drugi prosty, ale już sprytny przykład

Czy wiesz że Python’owa funkcja int konwertująca argument na typ int posiada drugi argument który mówi jej jak ma traktować swój pierwszy argument? Dokładnie mówi o tym jaka jest podstawa pierwszego argumentu, domyślna wartość to oczywiście 10.

Mając tę wiedzę plus wiedzę o funkcji partial możemy bardzo łatwo napisać funkcję która konwertuje zapis dwójkowy na zapis dziesiętny:

1
2
bin_to_dec = partial(int, base=2)
print(bin_to_dec('111'))

Tutaj nadpisujemy drugi argument funkcji int wartością dwa i w sumie tyle! Rezultat tego kawałka kodu to oczywiście… 7!

Bardziej życiowy przykład

Wyobraźmy sobie że mamy system który reaguje na jakieś zdarzenie, trochę na zasadzie komunikacji pomiędzy komponentami, coś jak wzorzec obserwatora – jeden obiekt powiadamia drugi o czymś tam.

I teraz załóżmy że mamy taki kawałek kodu:

1
2
3
4
5
6
7
8
9
10
11
12
from datetime import datetime
class Event(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        print('Listener added')
        self._listeners.append(callback)

    def fire(self):
        for f in self._listeners:
            f(self, datetime.now())

Czyli klasa która jest Zdarzeniem i pozwala zasubskrybować się na moment wywołania tego zdarzenia. Kiedy ten moment nadejdzie, odpalana jest funkcje fire i zasubskrybowani obserwatorzy są o tym powiadamiani.

Czyli – łatwe, proste i przyjemne!

Zobaczmy przykład użycia teraz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from datetime import datetime
class Event(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        print('Listener added')
        self._listeners.append(callback)

    def fire(self):
        for f in self._listeners:
            f(self, datetime.now())


def callback(log, event, datetime):
    log('Event ' + str(event) + ' raised at ' + str(datetime))


e = Event()
e.add_listener(callback)

e.fire()

Którego próba wykonania zakończy się błędem:

    f(self, datetime.now()) 
TypeError: callback() missing 1 required positional argument: 'datetime'

Powód jest jasny – jako callback podaliśmy funkcję przyjmującą 3 argumenty a Event obsługuje callbacki mające 2 argumenty. No i Python nas informuje o brakującym argumencie pozycyjnym.

Zakładając że klasa Event jest klasą pochodzącą z bibliotek zależnych naszego projektu i nie możemy jej zmienić to zaczyna się robić trochę słabo.

Na szczęście dzięki funkcji partial to możemy rozwiązać problem całkiem prosto, a dokładnie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from functools import partial
from datetime import datetime


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

    def add_listener(self, callback):
        print('Listener added')
        self._listeners.append(callback)

    def fire(self):
        for f in self._listeners:
            f(self, datetime.now())


def callback(log, event, datetime):
    log('Event ' + str(event) + ' raised at ' + str(datetime))


log = print

callback = partial(callback, log)

e = Event()
e.add_listener(callback)

e.fire()

I taki kawałek kodu już się uruchomi poprawnie:

Listener added
Event <main.Event object at 0x7fc8a8bb3ba8> raised at 2019-02-08 17:53:36.533804

Zmierzając do brzegu

Widać że funkcja partial może szybko i łatwo sprawić że nasze życie będzie prostsze – warto pamiętać o niej – czasem w elegancki sposób rozwiązuje pewne problemy.

Dzięki za wizytę,
Mateusz Mazurek
Podziel się na:
    Facebook email PDF Wykop Twitter

Dodaj komentarz

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Powiadom o