article banner (priority)

Funkcje w Pythonie

Wydzielanie części kodu

W programowaniu wielokrotnie wykonuje się te same lub podobne operacje. Naturalnym jest, że zamiast pisać je po wielokroć, wolelibyśmy napisać powtarzalny kod tylko raz, a potem go wywoływać, kiedy tylko chcemy. Właśnie tutaj z pomocą przychodzą funkcje.

Żeby je lepiej zrozumieć, wyobraź sobie Jacka, który bardzo lubi hamburgery. Stanowią jego posiłek jako: śniadanie, obiad i kolację. Aby zjeść takiego hamburgera, wymagane jest jednak kilka kroków przygotowań: podsmażenie mięsa, podsmażenie bułki, dodanie pomidora i wreszcie sama konsumpcja. Jego dzień można opisać następująco:

print("Wstań z łóżka")
print("Umyj zęby")
print("Podsmaż mięso")
print("Podsmaż bułkę")
print("Dodaj pomidora")
print("Zjedz hamburgera")
print("Pracuj")
print("Podsmaż mięso")
print("Podsmaż bułkę")
print("Dodaj pomidora")
print("Zjedz hamburgera")
print("Pracuj")
print("Podsmaż mięso")
print("Podsmaż bułkę")
print("Dodaj pomidora")
print("Zjedz hamburgera")
print("Graj w gry")
print("Idź spać")

Powtarzalny proces przygotowywania i konsumpcji hamburgera nie tylko jest uciążliwy, ale także zaciemnia obraz tego, jak naprawdę wygląda dzień Jacka. Możemy poprawić ten kod poprzez wyciągnięcie funkcji make_and_eat_hamburger. Jej ciało będzie zawierało poszczególne kroki związane z przygotowaniem i konsumpcją hamburgera, dzięki czemu będziemy mogli je uruchomić poprzez wywołanie tej funkcji.

# Definicja funkcji
def make_and_eat_hamburger():
    # Ciało funkcji
    print("Podsmaż mięso")
    print("Podsmaż bułkę")
    print("Dodaj pomidora")
    print("Zjedz hamburgera")


print("Wstań z łóżka")
print("Umyj zęby")
make_and_eat_hamburger()  # Użycie funkcji
print("Pracuj")
make_and_eat_hamburger()  # Użycie funkcji
print("Pracuj")
make_and_eat_hamburger()  # Użycie funkcji
print("Graj w gry")
print("Idź spać")

Standardowa funkcja zaczyna się od słówka def1, potem następuje jej nazwa, nawiasy zwykłe, w których mogą znajdować się definicje parametrów (o nich pomówimy później), i wreszcie dwukropek i od następnej linii ciało funkcji. W ciele funkcji możemy użyć wszystkiego, czego nauczyliśmy się do tej pory — zmienne, pętle czy nawet same funkcje. Instrukcje w ciele funkcji muszą być poprzedzone odpowiednim wcięciem, gdyż ono właśnie określa, co jest częścią ciała, a co już nie.

Aby wywołać funkcję, używamy jej nazwy oraz nawiasów okrągłych. Gdy wywołujemy funkcję, kolejno wołane są instrukcje jej ciała. Wywołanie funkcji jest instrukcją, podobnie jak przypisanie wartości do zmiennej, albo wywołanie funkcji print. Zresztą print również jest funkcją i takie print("a") jest wywołaniem funkcji.

def print_a():
    print("a")


def print_a_and_b():
    print_a()
    print("b")


print_a()  # a
print_a_and_b()
# a
# b
print_a()  # a

Użycie funkcji do ukrycia wielu operacji pod pojedynczą nazwą jest bardzo ważne. Pomyśl tylko o dowolnym działaniu, na przykład wyjściu do sklepu. Co robisz? Wkładasz buty, ubierasz się w kurtkę, wychodzisz z domu... ale każda z tych operacji to składowa wielu mniejszych. Czym jest na przykład zakładanie butów? Nałożenie na nogę, wiązanie sznurówek... Czym jest to wiązanie sznurówek? Jest to skoordynowana sekwencja ruchów wielu mięśni. Myślimy o abstrakcyjnych działaniach, pod którymi stoi wiele mniejszych działań. W programowaniu to funkcje służą do ukrywania sekwencji działań za pojedynczą nazwą.

# ...

def wear_shoes():
    put_on_shoe()
    tie_shoelace()
    # ...

def go_to_store():
    wear_shoes()
    put_on_jacket()
    leave_house()
    # ...

Funkcja może reprezentować nawet bardzo złożone czynności, a sama być prosta, bo używa tylko nieco mniej złożonych elementów. Tak właśnie skonstruowane są programy.

Jak działają funkcje?

Przeanalizujmy krok po kroku, jak działają funkcje. Program wykonuje kolejne instrukcje linijka po linijce. Gdy dochodzi do definicji funkcji, zapamiętuje ją, ale nie wywołuje jej ciała. Ciało wywołujemy wtedy, gdy funkcja jest używana. Tak więc przy wywołaniu funkcji przeskakujemy na sam początek jej ciała i od tego miejsca idziemy dalej. Gdy dojdziemy do końca ciała (albo słówka return, o którym powiemy niedługo), wracamy do miejsca, gdzie funkcja była wywołana i stamtąd kontynuujemy. Tak w skrócie działają funkcje. Poniżej znajduje się prezentacja przykładowych funkcji wraz z kolejnymi numerami reprezentującymi kolejność ich wypisywania.

def first_function():
    print("3")  # 3
    print("4")  # 4


def second_function():
    print("6")  # 6
    print("7")  # 7


print("1")  # 1
print("2")  # 2
first_function()
print("5")  # 5
second_function()
print("8")  # 8

# Wypisze się kolejno 1 2 3 4 5 6 7 8

Warto spędzić chwilę i poćwiczyć wyobrażanie sobie jak nasz program idzie po kolejnych linijkach, jak przeskakuje do funkcji, a potem jak przeskakuje z powrotem do miejsca wywołania.

Nazywanie funkcji

W języku Python nazwy funkcji powinny być zgodne z konwencją snake_case, czyli wszystkie słowa z małych liter, oddzielone znakiem podkreślenia _. Ta konwencja określona jest przez twórców tego języka i większość programistów się jej trzyma. Nie jest nam ona obca, bo używamy jej również przy nazywaniu zmiennych.

Nazwa zmiennych określa obiekt, na który ta zmienna wskazuje. Jest najczęściej rzeczownikiem (name, user, result). Nazwa funkcji określa, co się powinno wydarzyć, gdy tę funkcję wywołamy. Jest więc najczęściej czasownikiem (cheer, go_to_store, print).

Parametry i argumenty funkcji

Wywoływane przez nas do tej pory funkcje zawsze działały tak samo. Przydatność takich funkcji jest jednak ograniczona. Znacznie częściej chcemy, by funkcja wykonała pewne operacje, ale dla specyficznych dla nas wartości. Pomyśl o funkcji print. Gdybyśmy nie mogli przekazać do niej wartości do wypisania, byłaby mało przydatna. Aby nasze funkcje mogły przyjmować wartości, wystarczy zdefiniować zmienne wewnątrz nawiasów w definicji funkcji (po prostu określamy ich nazwy). Takie zmienne nazywane są parametrami. Definiują one to, co powinno być przekazane do konkretnej funkcji. Z drugiej strony, przy wywołaniu funkcji powinniśmy podać wartości, które będą przekazane do tych parametrów. Takie wartości nazywane są argumentami. W poniższym przykładzie who_to_cheer to parametr funkcji, a "Czytelniku", "Wszystkim" oraz 42 to wartości używane jako argumenty.

def cheer(who_to_cheer):
    print(f"Siema {who_to_cheer}!")


cheer("Czytelniku")  # Siema Czytelniku!
cheer("Wszystkim")  # Siema Wszystkim!
cheer(42)  # Siema 42!

Funkcja może mieć więcej parametrów, a wywołanie składać się z większej liczby argumentów — w takim przypadku oddzielamy je przecinkami.

Argumentów (wartości w nawiasie przy wywołaniu) powinno być dokładnie tyle, ile zdefiniowaliśmy parametrów (zmiennych w nawiasie w definicji). Jeśli się pomylimy, to wystąpi wyjątek, który przerwie działanie naszego programu. Na szczęście wyjątek ten poinformuje nas, gdzie popełniliśmy błąd i łatwo będziemy mogli go naprawić.

def b(x):
    print(x)


b()
#  Traceback (most recent call last):
#    File "<input>", line 1, in <module>
#  TypeError: b() takes exactly 1 argument (0 given)
print("To się nie wypisze")

Ćwiczenie: Funkcje

Napisz funkcję, która...

  • wypisuje sumę dwóch liczb przekazywanych jako argumenty,
  • wypisuje kolejne liczby od a do b, gdzie ab to parametry funkcji (zakładamy, że wartość a < b),
  • wypisuje określoną liczbę gwiazdek w jednej linii (najłatwiej to będzie zrobić poprzez utworzenie zmiennej z pustym stringiem, a następnie w pętli dodawanie do tej zmiennej kolejne gwiazdki, a na koniec wypisanie tej zmiennej),
  • wypisuje kwadrat z gwiazdek o określonym rozmiarze (pamiętaj, że możesz użyć funkcji print_stars z poprzedniego podpunktu),
  • wypisuje trójkąt równoramienny z gwiazdek o określonym rozmiarze.

Przykłady użycia poniżej.

print_sum(1, 2)  # 3
print_sum(3, 4)  # 7
print_sum(20, 10)  # 30

print_numbers(2, 4)
# 2
# 3
# 4

print_stars(3)  # ***
print_stars(5)  # *****
print_stars(8)  # ********

print_square(2)
# **
# **

print_square(3)
# ***
# ***
# ***

print_triangle(3)
# *
# **
# ***

print_triangle(4)
# *
# **
# ***
# ****

Odpowiedzi na końcu książki.

Wynik funkcji

Funkcje poznaliśmy już na lekcjach matematyki. Tam były one definiowane, aby obliczyć wynik dla wartości wejściowych. W szkole poznaliśmy funkcje podnoszenia do kwadratu, wartości bezwzględnej czy silni. One wszystkie zwracały wartości, utworzone poprzez przekształcenie argumentów. Aby w programowaniu funkcje mogły zwracać wartość, używamy słówka return, a za nim wartości, jaką wywołanie funkcji ma zwrócić.

def return_number():
    return 42


result = return_number()
print(result)  # 42
def return_num(num):
    return num


res = return_num(10)
print(res)  # 10
def double(num):
    return num * 2


print(double(13))  # 26

Zdefiniujmy wspomniane wcześniej funkcje, omawiane na lekcjach matematyki. Zaczynając do obliczenia kwadratu liczby (ang. square).

def square(x):
    return x * x


print(square(2))  # 4
print(square(4))  # 16
print(square(10))  # 100

W przypadku wartości bezwzględnej (ang. absolute value) nie jest już tak łatwo. Wartość bezwzględna "pozbywa" się znaku z liczby. Na przykład liczbę 10 zostawia więc bez zmian, ale -5 zamienia na 5. Zgodnie z definicją, ta funkcja powinna zwracać:

  • wartość wejściową, gdy jest ona większa lub równa zero,
  • przeciwność wartości wejściowej, gdy jest ona mniejsza od zera.

Aby to zrobić, możemy wykorzystać instrukcję if z blokiem else.

def absolute(x):
    if x >= 0:
        return x
    else:
        return -x


print(absolute(0))  # 0
print(absolute(2))  # 2
print(absolute(-2))  # 2
print(absolute(10))  # 10
print(absolute(-10))  # 10

return natychmiast kończy wywołanie funkcji i zwraca wynik. Dlatego formalnie blok else nie jest potrzebny, bo jeśli pierwszy warunek używa return, to nic po tej komendzie już się w tej funkcji nie wykona. W związku z tym, działanie powyższych funkcji będzie analogiczne do poniższych.

def square(x):
    return x * x
    print("To się nigdy nie wypisze")


def absolute(x):
    if x >= 0:
        return x
    return -x

Wreszcie silnia (ang. factorial). Silnia z n jest to iloczyn liczb od 1 do n, czyli to, co powstaje w wyniku ich pomnożenia. Dla przykładu silnia z 5 jest równa 1 * 2 * 3 * 4 * 5, a więc 120. Dla liczb mniejszych od 1 zakłada się, że ich silnia jest równa 1. Możemy ją zaimplementować przy użyciu pętli for. Aby przeiterować po liczbach od 1 do n, z n włącznie, użyjemy range(1, num + 1). Zdefiniujemy zmienną res, która przechowywać będzie nasz wynik i w każdym kroku pętli pomnożymy ten wynik przez kolejną liczbę.

def factorial(num):
    result = 1
    for i in range(1, num + 1):
        result *= i
    return result

Jest to poprawne rozwiązanie, ale nie jedyne. Chciałbym pokazać zupełnie inny tok rozumowania, który doprowadzi nas do rozwiązania zwanego rekurencyjnym. Popatrzmy na to, jak obliczana jest silnia dla kolejnych liczb (w matematyce silnię zapisuje się poprzez ! po liczbie):

  • 1! = 1.
  • 2! = 1 * 2.
  • 3! = 1 * 2 * 3.
  • 4! = 1 * 2 * 3 * 4.
  • 5! = 1 * 2 * 3 * 4 * 5.
  • ...

Zauważ, że dla kolejnych liczb do naszego równania dochodzi po prostu kolejne mnożenie. Tak więc:

  • 2! = 1! * 2.
  • 3! = 2! * 3.
  • 4! = 3! * 4.
  • 5! = 4! * 5.
  • ...

A więc aby obliczyć silnię z 5, wystarczy policzyć silnię z 4 i wynik pomnożyć przez 5. Tak samo dla wszystkich innych liczb większych od 1: Aby obliczyć silnię z n, wystarczy obliczyć silnię z n - 1 i pomnożyć ją przez n. Wyjątkiem są liczby mniejsze od 1, dla których powinniśmy zawsze zwracać wartość 1. Ten algorytm można przedstawić następująco:

def factorial(num):
    if num <= 1:
        return 1

    return num * factorial(num - 1)


print(factorial(0))  # 1
print(factorial(1))  # 1
print(factorial(2))  # 2
print(factorial(3))  # 6
print(factorial(4))  # 24
print(factorial(5))  # 120

Aby zrozumieć jak ta funkcja zadziała, prześledźmy jak obliczana jest wartość factorial(3). Ponieważ 3 jest większe od 1, możemy podstawić 3 * factorial(2). Analogicznie, ponieważ 2 jest większe od 1, możemy podstawić 3 * 2 * factorial(1). Ponieważ 1 jest równe 1, to factorial(1) zwraca 1, a więc factorial(3) zwraca 3 * 2 * 1, czyli 6.

Technika polegająca na wołaniu funkcji w niej samej zwana jest rekurencją. Jest ona bardzo istotna w programowaniu. Gdy używamy rekurencji, zawsze warto się zastanowić, czy funkcja nie będzie wołała samą siebie w nieskończoność. W tym przypadku jesteśmy bezpieczni, bo za każdym razem wartość num jest coraz mniejsza, a gdy dojdzie do 1 to rekurencja się kończy.

Ćwiczenie: Funkcje zwracające wartości

Napisz funkcję, która...

  • zamienia liczbę dni na liczbę milisekund (jedna sekunda to 1000 milisekund),
  • oblicza wielkość pola powierzchni trójkąta prostokątnego, na podstawie długości jego prostopadłych boków (wzór to a * b / 2),
  • zwraca największą z trzech wartości,
  • oblicza sumę liczb w zakresie podanym jako argument (bez ostatniej, jak w funkcji range).

Przykłady użycia poniżej.

print(days_to_millis(1))  # 86400000
print(days_to_millis(3))  # 259200000

print(triangle_area(1, 1))  # 0.5
print(triangle_area(10, 20))  # 100

print(biggest(2, 3, 1))  # 3
print(biggest(2, 3, 5))  # 5
print(biggest(3, 3, 1))  # 3

print(sum_range(1, 4))  # 1 + 2 + 3, więc 6
print(sum_range(2, 5))  # 2 + 3 + 4, więc 9
print(sum_range(10, 12))  # 10 + 11, więc 21

Odpowiedzi na końcu książki.

Domyślna wartość zwracana z funkcji

Jeśli funkcja nie zwraca żadnej wartości przy użyciu return, to domyślnie zwraca ona wartość None.

def fun():
    print("Just chillin'")


ret = fun() # Just chillin'
print(ret) # None

Domyślne oraz nazwane argumenty

Wspaniałą funkcjonalnością języka Python jest możliwość określania domyślnych wartości dla parametrów. Dla przykładu wyobraź sobie, że piszesz funkcję, która ma uruchomić stronę internetową w przeglądarce Chrome. Stronę można jednak otworzyć w trybie zwykłym albo incognito. Chcemy, aby nie było konieczne ustawianie trybu przy każdym wywołaniu tej funkcji. Jeśli użytkownik go nie określi, to zostanie użyty tryb zwykły, zawsze możliwe jest także użycie trybu incognito. W takim właśnie przypadku przydaje się parametr z domyślną wartością argumentu.

Wartość domyślną parametru (czyli zmiennej w definicji funkcji) określamy po znaku równości. Jeśli parametr ma taką wartość, to możemy wywołać tę funkcję bez argumentu na tej pozycji. W takim przypadku zostanie użyta wartość domyślna.

def open_website(url, incognito=False):
    # Domyślną wartością incognito jest False
    if incognito:
        print(f"Opening {url} in incognito")
    else:
        print(f"Opening {url}")


open_website("a.com")  # Opening a.com
open_website("b.com", True)  
# Opening b.com in incognito

Wczuj się jednak przez chwilę w rolę osoby, która czyta ten kod i widzi open_website("b.com", True). Co oznacza to True? Nie jest to jasne. W takim przypadku przydatna okazuje się kolejna funkcjonalność: nazywanie argumentów. Używamy jej poprzez dodanie przed argumentem (czyli w wywołaniu funkcji) nazwy parametru i znaku równości.

open_website(url="c.com")  # Opening c.com
open_website("b.com", incognito=True)  
# Opening b.com in incognito
open_website(url="b.com", incognito=True)  
# Opening b.com in incognito

Gdy argumenty są nazwane, ich kolejność nie ma znaczenia.

open_website(incognito=True, url="b.com")  
# Opening b.com in incognito

Funkcjonalność ta jest bardzo przydatna w wielu praktycznych zastosowaniach. Często funkcje mają wiele parametrów z domyślnymi wartościami, a przy użyciu określa się bardzo niewiele z nich.

1:

"def" może być skrótem od "define", czyli "zdefiniuj", albo "definition", czyli "definicja".

2:

Na początku i na końcu tej nazwy są po dwa znaki podkreślenia. Jeśli zaś chodzi a atrybuty, to pomówimy o nich w następnym rozdziale.