Pytanie

Co to jest callable object i czym on się różni od funkcji ?

Doprecyzujmy pytanie

Nie chodzi tu o różnice składniowe. Raczej interesuje nas rodzaj problemów jakie można przez te dwa podejścia rozwiązać.

Szybkie przypomnienie

Zwykła funkcja może wyglądać na przykład tak:

def nic_nie_rob():
    print('serio nic nie robie')

nic_nie_rob()

Output:

serio nic nie robie

Callable object (obiekt funkcyjny)

Jest to obiekt, do którego możesz odwoływać się jak do zwyczajnej funkcji. Oto jak wyglądałaby klasa, którą moglibyśmy zastąpić naszą funkcję:

class NicNieRob:
    def __call__(self):
        print('serio nic nie robie')

nic_nie_rob = NicNieRob()
nic_nie_rob()

Output:

serio nic nie robie

O co tu chodzi ?

Ok jeśli jednym i drugim sposobem osiągnęliśmy ten sam efekt to po co to wszytko. Odpowiedzi jest wiele. Przede wszystkim funkcjebezstanowe. Zatem nie możemy zapisywać w nich informacji. Kiedy funkcja uruchamia się otrzymuje parametryzwraca wartość. Każde kolejne uruchomienie funkcji robi dokładnie to samo. Funkcja nie pamięta nic ze swoich wcześniejszych uruchomień. Można powiedzieć, że funkcje mają bardzo krótką pamięć :). Inaczej wygląda sprawa z obiektami. Obiekty mogą mieć swój zapisany stan. Zatem ich pamięć może sięgać dalej niż od momentu wywołania do zwrócenia wartości.

Wykorzystajmy tą wiedzę

Jak już mówiliśmy callable object można zawołać jak funkcję. Potrafią one także zapamiętać swój stan. Napiszmy zatem funkcję, która będzie zliczać ilość swoich wywołań:

class NicNieRob:
    def __init__(self):
        self.licznik_wywolan = 0

    def __call__(self):
        self.licznik_wywolan += 1
        print('serio nic nie robie')

nic_nie_rob = NicNieRob()
nic_nie_rob()
nic_nie_rob()
nic_nie_rob()
print(nic_nie_rob.licznik_wywolan)

Output:

serio nic nie robie
serio nic nie robie
serio nic nie robie
3

Udało się. Mamy funkcję (a dokładniej obiekt funkcyjny) robiącą dokładnie to samo co wcześniej czyli nic :). Dodatkowo każde wywołanie powoduje zwiększenie wskaźnika o jeden co w umożliwia zliczanie wywołań.

Czy to jedyna opcja ?

Czy to znaczy, że tylko przy użyciu callable object można osiągnąć taki efekt. Oczywiście, że nie.

Mamy jeszcze do dyspozycji funkcje wewnętrzne. Oto jak osiągnąć tą samą funkcjonalność bez użycia callable object:

def nic_nie_rob():
    licznik_wywolan = 0
    def funkcja_wewnetrzna():
        nonlocal licznik_wywolan
        licznik_wywolan += 1
        print('serio nic nie robie')
        funkcja_wewnetrzna.licznik_wywolan = licznik_wywolan
    return funkcja_wewnetrzna

nic_nie_rob = nic_nie_rob()
nic_nie_rob()
nic_nie_rob()
nic_nie_rob()
print(nic_nie_rob.licznik_wywolan)

Output:

serio nic nie robie
serio nic nie robie
serio nic nie robie
3

W zasadzie to jesteśmy już o krok od dekoratorów, ale o tym już w kolejnym zadaniu :).