Funkcje - elementy zaawansowane#

Funkcje jako obiekty#

Funkcje są w Pythonie “first-class objects”. Oznacza to, że funkcje:

  • można przekazywać jako argument do innych funkcji, np. do funkcji print,

  • mogą być rezultatem działania innej funkcji, np. dekoratory,

  • mogą być dynamicznie tworzone, np. funkcje zagnieżdżone.

def foo():
    print("foo is happening")

foo()
foo is happening
type(foo)
function
bar = foo

id(bar) == id(foo)
True
print(foo)
<function foo at 0x7f48f81f4af0>
print(foo)
<function foo at 0x7f48f81f4af0>

Atrybuty funkcji#

W szczególności, funkcje są instancjami typu function i mają atrybuty:

foo.__class__
function
foo.__name__
'foo'
bar.__name__
'foo'

Klasy jako funkcje#

class CallableClass:
    def __init__(self):
        self._counter = 0
    
    def __call__(self):
        self._counter += 1
        print("You have called me {0} times".format(self._counter))
callable_object = CallableClass()
callable_object()
You have called me 1 times
callable_object()
You have called me 2 times

Preferowane jest tworzenie i używanie zwykłych funkcji. Później zawsze istnieje możliwość przekształcenia takiej funkcji w instancję klasy z metodą __call__.

Wywoływanie funkcji#

callable jest wbudowaną funkcją - nie trzeba jej importować, jest zawsze dostępna. Pozwala sprawdzić, czy dany obiekt da się wywołać (czy jest funkcją lub klasą lub instancją klasy z metodą __call__):

callable(foo)
True
callable(callable_object)
True
x = 2
callable(x)
False

W Pythonie 3.0 i 3.1 usunięto funkcję callable, jednak w Pythonie 3.2+ stała się ponownie wbudowaną funkcją. Jeżeli Twój kod musi działać także pod Pythonem 3.0, 3.1, wówczas należy użyć jednego z dwóch poniższych idiomów:

hasattr(foo, '__call__')
True
import collections.abc

isinstance(foo, collections.abc.Callable)
True

Funkcje zagnieżdżone#

Funkcje można zagnieżdżać, to znaczy zdefiniować jedną funkcję (wewnętrzną) w ciele drugiej funkcji (zewnętrznej).

Ponieważ funkcje są obiektami (first-class citizen), funkcja wewnętrzna może zostać zwrócona przez funkcję zewnętrzną.

Jest to szczególnie użyteczne przy tworzeniu dekoratorów.

Warto zauważyć, że w poniższym przykładzie funkcja add jest tworzona dynamicznie przy każdym wywołaniu funkcji bind_add.

Oznacza to, że przy każdym wywołaniu bind_add zwracana jest inna funkcja, mającą własną tożsamość (identity), co można sprawdzić przy pomocy wbudowanej funkcji id.

def bind_add(a):
    def add(b):
        return a + b
    return add
add_1 = bind_add(1)
type(add_1)
id(add_1)
139951377176688
add_1(5)
6
add_42 = bind_add(42)
id(add_42)
139951377176976
add_42(58)
100

Funkcje wyższego rzędu#

Funkcja wyższego rzędu wymaga podania jako argumentu innej funkcji lub zwraca funkcję jako rezultat. Przykładową funkcją wyższego rzędu w Pythonie jest funkcja sorted. Opcjonalny argument key umożliwia przekazanie funkcji, która będzie wywołana dla każdego sortowanego elementu

gadgets = ['mp3', 'smartwatch', 'ipod', 'pendrive', 'ipad']
sorted(gadgets, key=len)
['mp3', 'ipod', 'ipad', 'pendrive', 'smartwatch']

Często jako argumenty funkcji wyższego rzędu przekazywane są wyrażenia lambda.

lst_numbers = [(0, "zero"), (1, "one"), (2, "two"), (3, "three"), (4, "four"), (5, "five")]
sorted(lst_numbers, key=lambda item : item[1])
[(5, 'five'), (4, 'four'), (1, 'one'), (3, 'three'), (2, 'two'), (0, 'zero')]

Wyrażenia lambda#

Wyrażenia lambda pozwalają na zwięzłe stworzenie funkcji bez nazwy, tzw. funkcji anonimowej.

add = lambda a, b: a + b

add(2, 3)
5
type(add)
function
callable(add)
True
add.__name__
'<lambda>'

W ciele funkcji nie można umieścić instrukcji, a jedynie pojedyncze wyrażenie (np. a + b), które jest rezultatem takiej funkcji.

Dlatego wyrażenia lambda najczęściej wykorzystuje się razem z funkcjami wyższego rzędu filter i map.

Każdy element z jakiejś kolekcji może zostać przekształcony za pomocą funkcji (map) albo przefiltrowany przy pomocy predykatu (filter).

numbers = [1, -3, 4, -5, 0, 8, 42, 665, 54, -65]
positive_numbers = filter(lambda n: n > 0, numbers)
list(positive_numbers)
[1, 4, 8, 42, 665, 54]
squares = map(lambda n: n * n, numbers)
list(squares)
[1, 9, 16, 25, 0, 64, 1764, 442225, 2916, 4225]

Lepiej jest jednak zastąpić filter i map wyrażeniami listowymi lub generatorowymi, ponieważ wywoływanie funkcji w Pythonie jest związane z dużym narzutem czasowym. Użycie wyrażeń listowych lub generatorowych nie powoduje wielokrotnego wywoływania funkcji.

[x for x in numbers if x > 0]
[1, 4, 8, 42, 665, 54]
[x * x for x in numbers]
[1, 9, 16, 25, 0, 64, 1764, 442225, 2916, 4225]

Zmienne lokalne, nielokalne i globalne#

Python korzysta z przestrzeni nazw (namespace), aby śledzić zmienne. Są to słowniki, których kluczami są nazwy zmiennych, a wartościami wartości tych zmiennych. W środku funkcji mamy dostępu do wielu przestrzeni nazw.

Najważniejszą z nich jest lokalna przestrzeń nazw, która zawiera argumenty funkcji i lokalnie zdefiniowane zmienne. Zmienne z tej przestrzeni nie są widoczne na zewnątrz funkcji.

Globalna przestrzeń nazw zawiera wszystkie zmienne zdefiniowane w module. Są to wszystkie zmienne, które nie są “wcięte”. Funkcje i klasy to także obiekty, więc w tej przestrzeni nazw znajdują się również one.

W przypadku funkcji zagnieżdżonych, w środku wewnętrznej funkcji możemy mieć do czynienia z przestrzenią nazw zewnętrznej funkcji. Nie jest to ani globalna, ani lokalna przestrzeń.

Gdy odwołujemy się do zmiennej, Python musi zdecydować, z której przestrzeni ma skorzystać. Jeżeli próbujemy odczytać wartość zmiennej, wówczas wykorzystywana jest najbliższa przestrzeń, w której dana zmienna jest zadeklarowana. Najbliższą jest lokalna przestrzeń nazw, potem nielokalne i na końcu globalna.

Jeżeli przypisujemy coś do zmiennej, to Python zakłada, że chcemy ją stworzyć w przestrzeni lokalnej, chyba że użyjemy słów kluczowych nonlocal lub global.

global_var = 2
var = 4

def outer():
    nonlocal_var = 3
    
    def inner():
        global global_var
        nonlocal nonlocal_var
        global_var = -2  # modyfikujemy zmienną globalną
        var = -4  # tworzymy zmienną lokalną niezależną od zmiennej globalnej var = 3
        nonlocal_var = -3  # modyfikujemy zmienną nielokalną
        print("inner", global_var, nonlocal_var, var)
    
    inner()
    
    print("outer", global_var, nonlocal_var, var)
outer()

print("global", global_var, var)
inner -2 -3 -4
outer -2 -3 4
global -2 4

Warto zauważyć, że decyzja o tym, która przestrzeń nazw zostanie wykorzystana, jest podejmowana już w czasie kompilacji funkcji:

x = 2

def foo():
    print(x)
    x = 3

foo()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[33], line 7
      4     print(x)
      5     x = 3
----> 7 foo()

Cell In[33], line 4, in foo()
      3 def foo():
----> 4     print(x)
      5     x = 3

UnboundLocalError: local variable 'x' referenced before assignment

W powyższym przykładzie Python założył, że x jest zmienną lokalną, ponieważ w środku funkcji znajduje się przypisanie do tej zmiennej. print(x) odwołuje się dalej do zmiennej lokalnej, a nie globalnej.

Parametry kontra argumenty#

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

x = 3
y = 2

add(x, y)
5

a i b są parametrami funkcji, natomiast x i y argumentami.

Parametry funkcji#

Wprowadzenie#

W Pythonie rozróżniamy cztery różne typy parametrów:

  • normalne (normal parameters) mają nazwę i pozycję

  • nazwane (keyword parameters) mają nazwę i domyślną wartość

  • zmienne (variable parameters) poprzedzone gwiazdką *, mają pozycję

  • zmienne nazwane (variable keyword parameters) poprzedzone **, mają nazwę

Parametry normalne i nazwane#

def generate_signature(person, year=2000, place="Paris"):
    print(person, year, place)
    
generate_signature("Ola", 1995, "Wrocław")
Ola 1995 Wrocław
generate_signature("Ala")
Ala 2000 Paris
generate_signature("Olek", place="New York")
Olek 2000 New York
generate_signature("Alek", year=2010)
Alek 2010 Paris

Parametry zmienne (*args)#

Operator * służy do tworzenia funkcji akceptujących dowolną liczbę argumentów:

def my_sum(*numbers):
    total = 0
    for number in numbers:
        total += number
    return total

my_sum(1, 2, 3, 4)
10

Parametry zmienne nazwane (**kwargs)#

Operator ** służy do tworzenia funkcji akceptujących dowolną liczbę argumentów nazwanych:

def dict_without_Nones(**kwargs):
    result = {}
    for k, v in kwargs.items():
        if v is not None:
            result[k] = v
    return result

dict_without_Nones(a="1999", b="2000", c=None)
{'a': '1999', 'b': '2000'}

Parametry zmienne razem (*args, **kwargs)#

Funkcja może przyjmować jednocześnie parametry zmienne i zmienne nazwane:

def multi(first, *args, **kwargs):
    print(first)
    print(args)
    print(kwargs)

multi(1, 2, 3, 4, 5, ala="1999", ola="2000")
1
(2, 3, 4, 5)
{'ala': '1999', 'ola': '2000'}

Pułapki domyślnego atrybutu#

Domyślna wartość dla argumentów nazwanych jest wyliczana w momencie deklarowania funkcji. Wartość ta nie jest ponownie wyliczana przy wywoływaniu funkcji. Zachowanie to nie jest intuicyjne:

def its_a_trap(item, seq=[]):
    seq.append(item)
    print(seq)
its_a_trap(1)
[1]
its_a_trap(2)
[1, 2]

Dlatego jako wartości domyślnych należy używać tylko niemodyfikowalnych obiektów, takich jak prymitywne wartości (0, True, None, 'string' itp.).

Jeżeli wartość domyślna musi koniecznie być modyfikowalnym obiektem (np. listą), wówczas należy domyślnie użyć None i w środku funkcji przypisać pożądaną wartość:

def now_its_fine(item, seq=None):
    if seq is None:
        seq = []
    seq.append(item)
    print(seq)
now_its_fine(1)
[1]
now_its_fine(2)
[2]

Adnotacje funkcji#

W Pythonie 3 wprowadzono składnię pozwalającą na powiązanie argumentów funkcji i metod oraz zwracaną wartość z dowolnym obiektem. W szczególności, dla każdego:

def clip(text:str, max_len:'int > 0'=80) -> str:
    return text[:max_len]

Adnotacje funkcji (function annotations) są nietypową funkcjonalnością, ponieważ nie określono, do czego konkretnie takie adnotacje mogą zostać użyte.

Adnotacje są dostępne jako specjalny atrybut:

clip.__annotations__
{'text': str, 'max_len': 'int > 0', 'return': str}

Przykładowe zastosowanie to dodanie informacji o typach (statyczne typowanie).

Dzięki temu narzędzia takie jak mypy mogą zanalizować kod, sprawdzić zgodność typów i w ten sposób wykryć ewentualne błędy jeszcze przed uruchomieniem kodu.

Atrybuty funkcji#

W Pythonie wszystko jest obiektem, także funkcje.

Funkcje posiadają specjalne atrybuty ułatwiające ich introspekcję:

def foo(arg, kwarg=42, *, kwarg2=43):
    '''docstring'''
    return arg + kwarg + kwarg2
foo.__name__
'foo'
foo.__doc__
'docstring'
foo.__defaults__
(42,)
foo.__kwdefaults__
{'kwarg2': 43}