❄️ Wstęp - funkcje
Wstęp
Funkcje pozwalają na organizowanie i strukturyzowanie kodu w logiczne bloki, które można wielokrotnie wywoływać. Dzięki funkcjom możemy uprościć programy, zmniejszyć ilość powtarzającego się kodu, a także sprawić, że nasze rozwiązania staną się bardziej modularne i łatwiejsze do utrzymania.
Zalety używania funkcji:
-
Modularność - dzielisz duży problem na mniejsze części, które są łatwiejsze do zarządzania.
-
Ponowne wykorzystanie - funkcję można wywoływać wielokrotnie w różnych miejscach programu.
-
Łatwiejsze utrzymanie - zmiana logiki w jednym miejscu (w funkcji) automatycznie wprowadza zmiany w całym programie.
-
Czytelność - funkcje pomagają tworzyć bardziej zrozumiały i uporządkowany kod.
import random
def generuj_losowa(seed=None):
# Ustawienie ziarna (seed) generatora liczb losowych
if seed is not None:
random.seed(seed)
# Generowanie losowej liczby z zakresu od 0 do 100
return random.randint(0, 100)
liczba = generuj_losowa(seed=42)
print(f"Wygenerowana liczba: {liczba}")
Trzy kluczowe elementy każdej funkcji:
-
Słowo kluczowe służace definiowaniu funkcji -
def. -
Argumenty: definiowanie i podawane wewnątrz
(). -
Zwracane wartości - słowo kluczowe
return.
Docstringi
Docstring to ciąg znaków umieszczony bezpośrednio po nagłówku funkcji (lub klasy czy modułu) służący jako jej dokumentacja. Python udostępnia go przez atrybut __doc__ i wyświetla w help().
def oblicz_bmi(masa: float, wzrost: float) -> float:
"""Oblicza wskaźnik BMI na podstawie masy i wzrostu.
Args:
masa: masa ciała w kilogramach.
wzrost: wzrost w metrach.
Returns:
Wartość BMI zaokrąglona do 2 miejsc po przecinku.
Raises:
ValueError: jeśli wzrost jest równy zero.
"""
if wzrost == 0:
raise ValueError("Wzrost nie może być zerem.")
return round(masa / wzrost ** 2, 2)
print(oblicz_bmi.__doc__)
help(oblicz_bmi)
Konwencja
Najczęściej stosowane style docstringów to Google style (jak powyżej) i NumPy style. Ważne, żeby w ramach jednego projektu trzymać się jednego stylu.
Docstring ≠ komentarz
Komentarz (#) jest pomijany przez interpreter i nie jest dostępny programistycznie. Docstring jest pełnoprawnym obiektem tekstowym przypisanym do funkcji - można go odczytać w runtime.
📝 Zadania
Stwórz plik python1course/zaj02/funkcje.py i wykonaj w nim poniższe zadania.
-
Napisz funkcję
zmien_wartosc(arg), która przyjmuje jeden argument i próbuje zmodyfikować ten argument w różny sposób w zależności od tego, czy jest on niemutowalny (w tym przypadku integerem) czy mutowalny (w tym przypadku listą).-
Jeśli jest listą, wykonaj
arg[0] = 'kalafior'. -
Jeśli jest integerem, wykonaj
arg = 65482652.
Wypisz przykłady dla obu przypadków, wypisz wartości przed i po wykonaniu funkcji. Jak się zachowują te obiekty?
Tip
Warto skorzystać z funkcji
isinstance(). -
Teoria: mutowalne i niemutowalne obiekty w funkcjach
Kiedy zmienne są przekazywane do funkcji jako argumenty, Python nie tworzy ich kopii, lecz przekazuje referencję do oryginalnego obiektu. W związku z tym sposób, w jaki te obiekty zachowują się wewnątrz funkcji, zależy od ich typu - mutowalne lub niemutowalne.
Obiekty mutowalne (np. listy, słowniki):
Zmienne tego typu mogą być modyfikowane w miejscu. Jeśli zostaną przekazane jako argumenty do funkcji i ich zawartość zostanie zmieniona, zmiana ta wpłynie na oryginalny obiekt, który istnieje poza funkcją.
Obiekty niemutowalne (np. liczby, napisy, krotki):
Zmienne niemutowalne nie mogą być modyfikowane w miejscu. Każda próba modyfikacji powoduje utworzenie nowego obiektu. Z tego powodu, zmiany wprowadzone wewnątrz funkcji nie wpływają na oryginalny obiekt poza funkcją.
Dopasowywanie argumentów
Funkcje mogą przyjmować argumenty na różne sposoby, co umożliwia elastyczne przekazywanie danych. Kluczowe elementy to: argumenty pozycyjne, argumenty nazwane, wartości domyślne, oraz specjalne operatory *args i **kwargs, które pozwalają na przekazywanie zmiennej liczby argumentów.
# dodanie wartości domyślnych
def dodaj(a = 0, b = 0):
return a + b
# argumenty pozycyjne przekazywane są w podanej kolejności
print(dodaj(3, 5))
# argumenty nazwane można mieszać w dowolnej kolejności
print(dodaj(b=5, a=3))
*args - zmienna liczba argumentów pozycyjnych
Wszystkie dodatkowe argumenty są zbierane w krotkę, dzięki czemu możemy obsłużyć więcej argumentów, niż zdefiniowano w sygnaturze funkcji.
def suma(*liczby):
return sum(liczby)
print(suma(1, 2, 3))
print(suma(10, 20))
**kwargs - zmienna liczba argumentów nazwanych
Argumenty są zbierane w słownik, co umożliwia przekazanie większej liczby argumentów nazwanych, niż przewidziano w sygnaturze funkcji.
def przedstaw_sie(**dane):
for klucz, wartosc in dane.items():
print(f"{klucz}: {wartosc}")
przedstaw_sie(imie="Jan", wiek=30, miasto="Kraków")
Pułapka mutowalnych wartości domyślnych
Wartości domyślne argumentów są ewaluowane raz w momencie definiowania funkcji, nie przy każdym wywołaniu. Użycie mutowalnego obiektu (np. listy) jako wartości domyślnej prowadzi do nieoczekiwanych zachowań:
def dodaj_element(element, lista=[]): # ŹLE
lista.append(element)
return lista
print(dodaj_element(1)) # [1]
print(dodaj_element(2)) # [1, 2] - lista przetrwała między wywołaniami!
print(dodaj_element(3)) # [1, 2, 3]
Poprawne rozwiązanie - użyj None jako wartości domyślnej i twórz nową listę wewnątrz funkcji:
def dodaj_element(element, lista=None): # DOBRZE
if lista is None:
lista = []
lista.append(element)
return lista
print(dodaj_element(1)) # [1]
print(dodaj_element(2)) # [2] - za każdym razem nowa lista
Mieszane użycie argumentów nie zawsze jest możliwe
Ważne jest, aby przestrzegać kolejności: najpierw argumenty pozycyjne, potem domyślne, następnie *args, a na końcu **kwargs.
def funkcja_mieszana(a, b=10, *args, **kwargs):
print(f"a: {a}, b: {b}")
print(f"Argumenty dodatkowe (args): {args}")
print(f"Argumenty nazwane (kwargs): {kwargs}")
funkcja_mieszana(1, 2, 3, 4, imie="Ania", wiek=25)
Oraz kilka niepoprawnych wywołań:
funkcja_mieszana()
funkcja_mieszana(1, 2, 3, 4, 5, a=6)
funkcja_mieszana(1, 2, 3, imie="Jan")
funkcja_mieszana(a=1, 20)
Sama definicja również może być niepoprawna:
def funkcja_mieszana(a=10, b):
print(f"a: {a}, b: {b}")
📝 Zadania
Kontynuuj pracę w pliku python1course/zaj02/funkcje.py.
-
Napisz funkcję
zamowienie_produktu, która przyjmuje jeden obowiązkowy argument pozycyjnynazwa_produktui dwa obowiązkowe argumenty nazwane:cenaiilosc. Funkcja powinna zwracać text podsumowujący zamówienie, zawierające nazwę produktu, łączną cenę (cena * ilość) oraz ilość zamówionego produktu.-
Stwórz pustą listę, do której wstawisz wartości zwracane przez funkcję dla 3 różnych produktów.
-
Przeiteruj po wypełnionej liście, wyświetl teksty.
-
Zmodyfikuj funkcję tak, żeby oprócz tekstu podsumowującego zwracała także wartość zamówienia.
-
Na koniec wyświetl sumaryczną wartość zamówień (sumę z każdego zamówionego produktu).
-
Dodaj wartość domyślną dla argumentu
iloscrówną 1.
Ważna informacja
Wykorzystaj poniższy początek definicji i go nie modyfikuj. Wymusi to podawanie argumentów po gwiazdce jedynie w formie nazwanej.
def zamowienie_produktu(nazwa_produktu, *, cena, ilosc): -
-
Napisz funkcję
oblicz_srednia_ocen, która przyjmuje nieograniczoną liczbę ocen (użyj*args) i zwraca ich średnią. Dodatkowo funkcja powinna przyjmować opcjonalny argument nazwanywagi(słownik), który mapuje oceny na ich wagi. Jeśli wagi są podane, funkcja powinna obliczyć średnią ważoną.Przykład wywołania:
# Średnia zwykła print(oblicz_srednia_ocen(4, 5, 3, 5)) # 4.25 # Średnia ważona print(oblicz_srednia_ocen(4, 5, 3, 5, wagi={4: 2, 5: 3, 3: 1})) -
Stwórz funkcję
polynomial_calculator, która implementuje kalkulator wielomianów. Funkcja powinna przyjmować:x- wartość dla której obliczamy wielomian,*args- współczynniki wielomianu (od najwyższej potęgi),**kwargs- opcjonalne parametry:precyzja(domyślnie 2),dziedzina(wspomagający dict z informacją o dziedzinie),
Funkcja oblicza wartość wielomianu dla podanego
xi zwraca wynik zaokrąglony do podanej precyzji.Przykład: wielomian 2x³ + 3x² + x + 1 dla x=2
result = polynomial_calculator(2, 2, 3, 1, 1) # Obliczy: 2*(2³) + 3*(2²) + 1*(2) + 1 = 16 + 12 + 2 + 1 = 21Dodatkowo, jeśli podano
dziedzinaw kwargs, funkcja powinna sprawdzić czyxnie wykracza poza dziedzinę i jeśli tak, wypisać odpowiednie ostrzeżenie.Wskazówka
Wielomian n-tego stopnia ma n+1 współczynników. Współczynniki w
*argssą podawane od najwyższej potęgi, np. dla ax² + bx + c przekazujemy (a, b, c).
Zwracanie wielu wartości
Python pozwala zwracać wiele wartości z funkcji - w rzeczywistości jest to zwracanie jednej krotki, którą można od razu rozpakować.
def podziel_z_reszta(a, b):
iloraz = a // b
reszta = a % b
return iloraz, reszta # Python pakuje to w krotkę (iloraz, reszta)
wynik = podziel_z_reszta(17, 5)
print(wynik) # (3, 2)
print(type(wynik)) # <class 'tuple'>
# Bezpośrednie rozpakowywanie
iloraz, reszta = podziel_z_reszta(17, 5)
print(f"17 / 5 = {iloraz} reszty {reszta}")
Gdy nie potrzebujemy wszystkich wartości, używamy _ do pominięcia wybranych:
iloraz, _ = podziel_z_reszta(17, 5) # interesuje nas tylko iloraz
Funkcje - praktyczne porady
- Funkcje powinny być niezależne od otoczenia - argumenty jako input, return jako output.
- Unikamy zmiennych globalnych.
- Nie modyfikujemy argumentów mutowalnych.
- Funkcja ma być mała i mieć jeden cel.
- Nie zmieniamy wartości zmiennych z innych modułów.
Atrybuty
Każdy obiekt w Pythonie może mieć swoje atrybuty. Służą one przechowywaniu dodatkowych informacji na ich temat lub umożliwiają dostęp do ich stanów wewnętrznych.
Do atrybutów odwołujemy się za pomocą notacji kropkowej, np. obiekt.atrybut. Funkcje, tak samo jak inne obiekty, mogą mieć swoje atrybuty.
def sample_function():
return "Hello, world!"
sample_function.description = "To jest przykładowa funkcja." # Dodanie niestandardowego atrybutu
print(sample_function.description)
Adnotacje
Jest to sposób na dodawanie informacji o typach danych używanych w kodzie. Choć Python jest językiem dynamicznie typowanym i nie wymaga jawnego określania typów, adnotacje dają programiście możliwość wskazania, jakie typy danych powinny być używane, co poprawia czytelność i ułatwia pracę w zespołach.
Idea adnotacji
Adnotacje stanowią coś w rodzaju "podpowiedzi" dla innych programistów oraz narzędzi analizujących kod (np. linterów, IDE), które mogą je wykorzystać do ułatwienia debugowania, uzupełniania kodu, czy znajdowania potencjalnych błędów.
W poniższym przykładzie argument name ma być typu str, tak samo zwracana przez funkcję wartość.
def greet(name: str) -> str:
return f"Hello, {name}!"
Zmienne także mogą posiadać swoje adnotacje.
age: int = 25
name: str = "Alice"
Zaawansowane typy importuje się z modułu typing.
from typing import List, Optional, Dict
def get_user_info(user_id: int) -> Optional[Dict[str, str]]:
if user_id == 1:
return {"name": "Hubert", "email": "hubert@example.com"}
return None # Funkcja może zwrócić słownik lub None