Create your own
Lesson illustration

Funkcje z parametrami i wartościami zwracanymi

Cześć! W poprzedniej lekcji poznaliśmy listy, słowniki, zbiory i krotki — struktury, w których będziesz przechowywać dane testowe, odpowiedzi API i oczekiwane rezultaty. Teraz zrobimy kolejny ważny krok: zamiast powielać operacje na tych danych w wielu miejscach, zamkniemy je w funkcjach.

Funkcja to nazwany fragment kodu, który można uruchamiać wielokrotnie, przekazując mu różne dane. W automatyzacji testów funkcje są podstawą utrzymywalności: mogą np. budować dane użytkownika, sprawdzać fragment odpowiedzi API lub przygotowywać powtarzalny stan testowy.

Po tej lekcji będziesz umieć definiować funkcje, przekazywać do nich argumenty pozycyjne, nazwane i domyślne oraz zwracać wyniki za pomocą return.


Funkcja: definicja nie jest jeszcze wykonaniem

Najprostsza funkcja w Pythonie ma:

  1. słowo kluczowe def,
  2. nazwę,
  3. nawiasy z parametrami,
  4. dwukropek,
  5. wcięte ciało.
def show_test_started():
    print("Test rozpoczęty")

Ta instrukcja definiuje funkcję, ale jeszcze jej nie uruchamia. Aby kod w środku został wykonany, trzeba funkcję wywołać:

show_test_started()

Wynik:

Test rozpoczęty
Schemat pokazuje elementy definicji funkcji: słowo `def`, nazwę, argumenty w nawiasach oraz wcięte ciało funkcji.

W Pythonie wcięcie jest częścią składni. Wszystko, co należy do funkcji, musi być przesunięte o jednakowy poziom, najczęściej cztery spacje:

def show_test_started():
    print("Test rozpoczęty")
    print("Przygotowuję dane")

Natomiast ten zapis jest błędny, bo ciało funkcji nie ma wcięcia:

# Błędnie
def show_test_started():
print("Test rozpoczęty")

Nazwy funkcji pisz zwykle małymi literami, używając podkreśleń między wyrazami — jest to konwencja snake_case.

def create_test_user():
    print("Tworzę użytkownika testowego")

Dobra nazwa opisuje czynność: create_test_user, build_payload, check_status_code. W testach szczególnie warto unikać nazw ogólnych, takich jak process_data() czy do_test().

Obejrzyj początkową część materiału, aby zobaczyć definicję i wywołanie funkcji w działającym programie.

9. Kurs Python 3 - funkcje

Materiał „9. Kurs Python 3 – funkcje” z kanału Kanał o Wszystkim pokazuje podstawową składnię funkcji i sposób przekazywania danych do jej wnętrza.

W części definicja funkcji zwróć uwagę na rolę def, nawiasów, dwukropka i wcięcia oraz na to, dlaczego definicja nie uruchamia kodu. Następnie obejrzyj parametry funkcji, obserwując, jak wartości podane przy wywołaniu stają się dostępne wewnątrz ciała funkcji.


Parametry i argumenty: dwa podobne, lecz różne pojęcia

Rozróżnienie jest proste, ale bardzo przydatne podczas czytania błędów i dokumentacji:

  • parametr to nazwa w definicji funkcji;
  • argument to konkretna wartość przekazana przy jej wywołaniu.
def greet_user(name):
    print(f"Cześć, {name}!")

W tym przykładzie name jest parametrem. W wywołaniu:

greet_user("Ala")

napis "Ala" jest argumentem.

Przykład rozróżnia parametry `how` i `who` zapisane w definicji funkcji od argumentów „Cześć” i „czytelniku” podanych podczas wywołania.

Parametr działa wewnątrz funkcji jak lokalna zmienna:

def show_status(status_code):
    print(f"Otrzymany kod: {status_code}")

show_status(201)
show_status(404)

Ta sama funkcja wykonuje ten sam rodzaj pracy, ale na różnych danych. To właśnie daje jej wartość: nie kopiujesz logiki dla każdego przypadku.

W automatyzacji testów może to wyglądać tak:

def show_response_summary(status_code, endpoint):
    print(f"Endpoint: {endpoint}")
    print(f"Status: {status_code}")

show_response_summary(201, "/users")
show_response_summary(404, "/users/999")

Argumenty pozycyjne: kolejność ma znaczenie

Gdy przekazujesz argumenty bez nazw, Python przypisuje je według kolejności parametrów w definicji. Są to argumenty pozycyjne.

def build_user(email, role):
    return {
        "email": email,
        "role": role,
    }

user = build_user("ania@example.test", "viewer")

Python przypisuje tu wartości następująco:

email = "ania@example.test"
role = "viewer"

Jeżeli odwrócisz argumenty, funkcja nadal formalnie się wykona, ale dane będą semantycznie błędne:

# Kod zadziała, ale wartości trafią do niewłaściwych pól.
user = build_user("viewer", "ania@example.test")

To jeden z powodów, dla których warto dobierać kolejność parametrów świadomie. Najpierw zwykle umieszcza się dane obowiązkowe i najważniejsze.

Brak wymaganego argumentu powoduje TypeError:

# build_user("ania@example.test")
# TypeError: build_user() missing 1 required positional argument: 'role'

Nie jest to błąd testowanej aplikacji. To błąd w sposobie wywołania własnej funkcji: jej definicja wymagała dwóch danych, a przekazano jedną.

Dla funkcji o jednym lub dwóch bardzo oczywistych argumentach zapis pozycyjny jest zwięzły i czytelny:

assert_response_status(201, 201)

Przy większej liczbie argumentów lub wartościach tego samego typu łatwo jednak pomylić kolejność. Wtedy użyj argumentów nazwanych.


Argumenty nazwane: kod, który sam wyjaśnia dane

Argument nazwany podajesz w formie nazwa_parametru=wartość.

def build_user(email, role, active):
    return {
        "email": email,
        "role": role,
        "active": active,
    }

user = build_user(
    email="ania@example.test",
    role="viewer",
    active=True,
)

Ten zapis jest nieco dłuższy, ale wyraźnie pokazuje znaczenie każdej wartości. Możesz też zmienić kolejność argumentów nazwanych:

user = build_user(
    active=True,
    email="ania@example.test",
    role="viewer",
)

Rezultat będzie taki sam, ponieważ Python dopasowuje wartości po nazwach, a nie po miejscach.

Argumenty nazwane są szczególnie przydatne w kodzie testowym, gdy funkcja przyjmuje kilka parametrów albo kiedy wartości są trudne do odróżnienia na pierwszy rzut oka:

def build_request_options(timeout_seconds, follow_redirects, verify_ssl):
    return {
        "timeout_seconds": timeout_seconds,
        "follow_redirects": follow_redirects,
        "verify_ssl": verify_ssl,
    }

options = build_request_options(
    timeout_seconds=10,
    follow_redirects=False,
    verify_ssl=True,
)

Porównaj to z wersją pozycyjną:

options = build_request_options(10, False, True)

Druga wersja jest poprawna, ale po kilku dniach trudno pamiętać, co dokładnie oznaczają False i True.

Łączenie argumentów pozycyjnych i nazwanych

Python pozwala użyć obu stylów w jednym wywołaniu:

def create_account(email, role, active):
    return {
        "email": email,
        "role": role,
        "active": active,
    }

account = create_account(
    "ania@example.test",
    role="viewer",
    active=True,
)

Obowiązuje jednak ważna reguła:

Argumenty pozycyjne muszą wystąpić przed nazwanymi.

# Błędnie:
# create_account(email="ania@example.test", "viewer", True)

Po rozpoczęciu przekazywania argumentów po nazwie podawaj już kolejne również po nazwie. Dzięki temu wywołanie pozostaje jednoznaczne.


Parametry domyślne: rozsądny standard, możliwość zmiany

Nie każdy parametr musi być wymagany. Parametr domyślny ma wartość zapisaną już w definicji funkcji. Gdy wywołanie nie poda argumentu, Python użyje tej wartości.

def build_user(email, role="viewer"):
    return {
        "email": email,
        "role": role,
    }

Teraz możesz wywołać funkcję na dwa sposoby:

basic_user = build_user("ania@example.test")

admin_user = build_user(
    "piotr@example.test",
    role="admin",
)

W pierwszym przypadku role przyjmuje domyślnie "viewer". W drugim wartość domyślna jest zastąpiona przez "admin".

To dobrze pasuje do automatyzacji. Jeżeli większość danych testowych opisuje aktywnego użytkownika, możesz zapisać tę regułę w funkcji:

def build_user(email, role="viewer", active=True):
    return {
        "email": email,
        "role": role,
        "active": active,
    }

regular_user = build_user("ania@example.test")

blocked_admin = build_user(
    "admin@example.test",
    role="admin",
    active=False,
)

Parametry domyślne powinny znajdować się po parametrach wymaganych:

# Poprawnie
def build_user(email, role="viewer"):
    return {"email": email, "role": role}
# Błędnie — Python nie wie, jak interpretować wywołanie z jednym argumentem.
# def build_user(role="viewer", email):
#     return {"email": email, "role": role}

Gdyby taki zapis był dozwolony, build_user("ania@example.test") nie mówiłoby jasno, czy przekazana wartość ma trafić do role, czy do email.

Poniższy fragment materiału utrwala parametry domyślne oraz przejście do zwracania wartości.

9. Kurs Python 3 - funkcje

Kontynuuj materiał „9. Kurs Python 3 – funkcje”, aby zobaczyć parametry opcjonalne i praktyczną różnicę między print() a return.

Obejrzyj wartości domyślne, zwłaszcza zasadę, że parametry z domyślną wartością stoją na końcu definicji. Następnie przejdź do return w praktyce; skup się na tym, jak wynik funkcji można przypisać do zmiennej i dlaczego instrukcje po return nie zostaną wykonane.


print() a return: pokazanie wyniku to nie to samo co jego zwrócenie

To jedno z najważniejszych rozróżnień w Pythonie.

Funkcja z print() wyświetla informację człowiekowi:

def print_total(items_count):
    print(f"Liczba elementów: {items_count}")

result = print_total(3)

print(result)

Wynik będzie następujący:

Liczba elementów: 3
None

Dlaczego result zawiera None? Ponieważ funkcja niczego nie zwróciła. print() jedynie wyświetlił tekst w konsoli.

Jeśli rezultat ma zostać wykorzystany w dalszym kodzie — zapisany, porównany asercją, przekazany do innej funkcji — użyj return:

def calculate_total(price, quantity):
    return price * quantity

total = calculate_total(19.99, 3)

print(total)
# 59.97

Teraz total zawiera liczbę, z którą można dalej pracować:

assert calculate_total(19.99, 3) == 59.97

W testach z reguły potrzebujesz właśnie wartości zwracanych, a nie funkcji, które tylko coś wypisują. Asercja porównuje dane, nie komunikaty w terminalu.

return kończy wykonywanie funkcji

Po wykonaniu return funkcja natychmiast kończy działanie:

def get_access_label(is_active):
    if is_active:
        return "access granted"

    return "access denied"

Dla True zostanie zwrócone "access granted" i druga instrukcja return nie zostanie już osiągnięta. Dla False warunek zostanie pominięty, a funkcja zwróci "access denied".

Kod zapisany po bezwarunkowym return jest nieosiągalny:

def get_status():
    return "ok"
    print("Ten tekst nigdy się nie wyświetli")

W prostych funkcjach return zwykle znajduje się na końcu. W funkcjach z warunkami może wystąpić w kilku gałęziach, ale każda możliwa ścieżka powinna mieć jasny rezultat.


Budujemy użyteczną funkcję dla danych testowych

Połączmy elementy poznane dzisiaj oraz struktury danych z poprzedniej lekcji. Chcemy tworzyć dane użytkownika do żądania API.

def build_user_payload(email, role="viewer", active=True):
    return {
        "email": email,
        "role": role,
        "active": active,
    }

Definicja mówi, że:

  • email jest wymagany;
  • role jest opcjonalny i domyślnie ma wartość "viewer";
  • active jest opcjonalny i domyślnie ma wartość True;
  • funkcja zwraca słownik, który później będzie można przekazać do żądania HTTP.

Możemy stworzyć kilka wariantów danych bez kopiowania całego słownika:

new_viewer = build_user_payload("ania@example.test")

new_admin = build_user_payload(
    email="piotr@example.test",
    role="admin",
)

inactive_user = build_user_payload(
    email="ola@example.test",
    active=False,
)

Po wywołaniu new_admin będzie mieć wartość:

{
    "email": "piotr@example.test",
    "role": "admin",
    "active": True,
}

Najważniejsze jest to, że funkcja nie wysyła jeszcze żadnego żądania API. Jej pojedyncza odpowiedzialność to przygotowanie danych. W przyszłych modułach wykorzystasz takie funkcje jako małe, przewidywalne elementy kodu testowego.

Możesz od razu sprawdzać jej wynik zwykłą asercją:

payload = build_user_payload(
    email="tester@example.test",
    role="editor",
)

assert payload["email"] == "tester@example.test"
assert payload["role"] == "editor"
assert payload["active"] is True

To dobry wzorzec myślenia: funkcja przyjmuje dane wejściowe, zwraca konkretny wynik, a test porównuje wynik z oczekiwaniem.


Typowe błędy i jak je rozpoznać

SytuacjaPrzyczynaPoprawa
TypeError: missing ... required positional argumentBrakuje wymaganego argumentu.Podaj wartość albo zdefiniuj uzasadnioną wartość domyślną.
Wynik funkcji to NoneFunkcja używa print(), ale nie ma return.Zwróć potrzebną wartość przez return.
SyntaxError: non-default argument follows default argumentParametr wymagany został zapisany po domyślnym.Najpierw parametry wymagane, potem domyślne.
Dane trafiają do niewłaściwych pólPomyłka w kolejności argumentów pozycyjnych.Użyj argumentów nazwanych.
Kod funkcji wykonuje się od razu po defTo błędne założenie — definicja tylko tworzy funkcję.Wywołaj ją przez nazwa_funkcji(...).
IndentationErrorCiało funkcji ma nieprawidłowe wcięcie.Wcięcie całego ciała ustaw konsekwentnie na cztery spacje.

W trakcie nauki warto często uruchamiać małe fragmenty kodu w swoim projekcie z .venv. Błędy Pythona bywają bardzo konkretne: przeczytaj komunikat od końca i sprawdź nazwę funkcji oraz liczbę podanych argumentów.


Zasady, które warto stosować od teraz

Dobra funkcja pomocnicza w testach zwykle:

  • ma nazwę opisującą działanie;
  • wykonuje jedną, spójną rzecz;
  • przyjmuje tylko dane, których rzeczywiście potrzebuje;
  • używa wartości domyślnych wyłącznie dla sensownych standardów;
  • zwraca dane, jeśli dalszy kod ma je sprawdzić lub przetworzyć;
  • nie polega na ukrytej kolejności wielu podobnych argumentów — w takim przypadku korzysta z argumentów nazwanych.

Krótka ściąga składni:

def function_name(required_value, optional_value="default"):
    result = required_value + optional_value
    return result


value = function_name("A", optional_value="B")

W definicji required_value i optional_value są parametrami. W wywołaniu "A" oraz "B" są argumentami. Argument "A" przekazano pozycyjnie, a "B" nazwano wyraźnie przez optional_value=.


Najważniejsze wnioski:

  • Funkcję definiujesz przez def, nazwę, nawiasy, dwukropek i wcięte ciało.
  • Parametry są zapisane w definicji, a argumenty przekazujesz przy wywołaniu.
  • Argumenty pozycyjne zależą od kolejności; argumenty nazwane są dopasowywane po nazwie i zwiększają czytelność.
  • Parametry domyślne czynią argument opcjonalnym, ale muszą występować po wymaganych parametrach.
  • print() pokazuje wartość, natomiast return oddaje ją kodowi wywołującemu.
  • Instrukcja return kończy wykonywanie funkcji.
  • Funkcje zwracające dane są szczególnie użyteczne w automatyzacji, ponieważ ich wyniki można bezpośrednio sprawdzać asercjami.

W następnej lekcji dodasz do funkcji adnotacje typów. Dzięki nim podpis funkcji będzie informował nie tylko o nazwach parametrów, lecz także o oczekiwanych typach danych i typie zwracanego wyniku.

Can't find a good explanation? Sign up and we'll make it for you

Sign up