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 0x7f8773995af0>
print(foo)
<function foo at 0x7f8773995af0>
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)
140219736742208
add_1(5)
6
add_42 = bind_add(42)
id(add_42)
140219736742928
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)
<ipython-input-33-2c6cdb366438> in <module>
5 x = 3
6
----> 7 foo()
<ipython-input-33-2c6cdb366438> in foo()
2
3 def foo():
----> 4 print(x)
5 x = 3
6
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}