Zrozumieć od zera program budzet.py

Utworzono: 17 września 2026 Zaktualizowano: 17 września 2026

Gdyby w metodzie suma przesunąć return razem o cztery spacje w prawo, raport pokazałby „Razem: 6.5 zł”, a Python nie zgłosiłby żadnego błędu. To jeden z drobiazgów, na które trafiłem, rozbierając od zera krótki program budzet.py, czyli domowy budżet z chlebem, biletem miesięcznym i serem.

Więcej

Artykuł idzie linijka po linijce: wcięcia, nazwy, argumenty pozycyjne i nazwane, skąd się bierze self i dlaczego __init__ musi się tak nazywać, słownik i .items(), a na końcu None z funkcji bez return. Trochę jak z border 0 na ZX Spectrum, każde polecenie robi jedną konkretną rzecz i da się to prześledzić. Dla początkujących to wejście w Pythona na jednym przykładzie. Dla tych, co piszą od lat, raczej sprawdzian, czy umieją to komuś wyjaśnić (ja przy okazji znalazłem podwójne liczenie sumy i zostawiłem, jak jest).

Czym jest program

Program to spisana lista poleceń dla komputera. Przypomina przepis kulinarny, w którym kroki idą jeden po drugim, a ktoś je po kolei wykonuje, i tym kimś jest tu komputer. Przepis musi być zapisany według ścisłych reguł, bo komputer niczego się nie domyśla. Taki zestaw reguł to język programowania, tu Python. Plik z programem nazywa się budzet.py, a końcówka .py mówi, że to Python. Komputer czyta go i wykonuje krok po kroku, zasadniczo od góry do dołu. Zasadniczo, bo część kodu to przepisy przygotowane zawczasu. Same z siebie nic nie robią i ruszają dopiero wtedy, gdy ktoś niżej z nich skorzysta.

Ten konkretny program prowadzi domowy budżet, czyli zapisuje wydatki i liczy, ile na co poszło. Mówi o tym już pierwsza linijka, ta w potrójnym cudzysłowie. To opis dla człowieka, komputer go pomija. Poniżej jest całość i przy pierwszym czytaniu nie trzeba rozumieć każdego znaku. Po drodze trafią się rzeczy, które na razie nic nie mówią. self znaczy mniej więcej „ten konkretny”, np. ten jeden wydatek. __init__ to część, która wypełnia świeżo zrobiony wydatek albo budżet danymi. for powtarza coś dla każdego elementu po kolei, append dopisuje na koniec listy, a puste {} to pusty słownik, czyli miejsce na pary w rodzaju „jedzenie: tyle złotych”. Litera f przed cudzysłowem pozwala wstawić do napisu wartości z programu. Na razie wystarczy te rzeczy przeskoczyć wzrokiem i złapać ogólny kształt:

python
"""Domowy budżet: zapisuje wydatki i liczy, ile poszło na każdą kategorię."""


class Wydatek:
    def __init__(self, nazwa, kwota, kategoria):
        self.nazwa = nazwa
        self.kwota = kwota
        self.kategoria = kategoria

    def opis(self):
        return f"{self.nazwa}: {self.kwota:.2f} zł ({self.kategoria})"


class Budzet:
    def __init__(self, limit):
        self.limit = limit
        self.wydatki = []

    def dodaj(self, wydatek):
        self.wydatki.append(wydatek)

    def suma(self):
        razem = 0
        for wydatek in self.wydatki:
            razem = razem + wydatek.kwota
        return razem

    def wedlug_kategorii(self):
        kategorie = {}
        for wydatek in self.wydatki:
            if wydatek.kategoria not in kategorie:
                kategorie[wydatek.kategoria] = 0
            kategorie[wydatek.kategoria] += wydatek.kwota
        return kategorie

    def czy_przekroczony(self):
        return self.suma() > self.limit


def pokaz_raport(budzet):
    print("Wydatki:")
    for wydatek in budzet.wydatki:
        print(" -", wydatek.opis())
    print("Razem:", budzet.suma(), "zł")
    for kategoria, kwota in budzet.wedlug_kategorii().items():
        print(f"{kategoria}: {kwota} zł")
    if budzet.czy_przekroczony():
        print("Uwaga: przekroczono limit!")
    else:
        print("Mieścisz się w limicie.")


budzet = Budzet(limit=500)
budzet.dodaj(Wydatek("chleb", 6.5, "jedzenie"))
budzet.dodaj(Wydatek("bilet miesięczny", 110, "transport"))
budzet.dodaj(Wydatek("ser", 18.99, "jedzenie"))
pokaz_raport(budzet)

Najłatwiej czytać go od dołu. Ostatnie pięć linijek to właściwa akcja: ustalamy limit 500 zł, dopisujemy chleb, bilet miesięczny i ser, a na koniec prosimy o raport. Wszystko powyżej to przygotowanie, czyli opis narzędzi, zanim zacznie się gotować. Najpierw jest wyjaśnione, czym jest pojedynczy wydatek (linijki pod class Wydatek), potem czym jest cały budżet (class Budzet), a na końcu, jak wydrukować raport (def pokaz_raport). Słowa class i def oznaczają właśnie te przepisy przygotowane zawczasu. Komputer, idąc od góry, tylko je sobie zapamiętuje, a naprawdę coś zaczyna się dziać dopiero od linijki budzet = Budzet(limit=500).

Zacznijmy od spacji na początku linijek, czyli wcięć. W Pythonie one mają znaczenie, bo to po nich komputer poznaje, co do czego należy:

python
class Wydatek:
    def __init__(self, nazwa, kwota, kategoria):
        self.nazwa = nazwa

Linijka def __init__ jest przesunięta o cztery spacje, więc należy do class Wydatek. self.nazwa = nazwa jest przesunięte o kolejne cztery, więc należy do __init__. Wracając do przepisu, to jak przepis na ciasto, w którym osobno są kroki na spód, a osobno na krem. Gdzie wcięcie wraca do lewej krawędzi, tam zaczyna się coś nowego. W programie dzieje się tak przy class Budzet, przy def pokaz_raport i przy ostatnich pięciu linijkach, które nie należą już do niczego. Ta zasada działa w całym pliku, od klas po pojedyncze linijki w środku metod.

Drugi element to nazwy, czyli etykiety wymyślone przez autora programu. budzet i wydatek to zmienne, czyli pudełka, w których coś trzymamy. dodaj, suma czy pokaz_raport to nazwy poleceń, które program ma do dyspozycji. Wydatek i Budzet to nazwy klas, zwyczajowo pisane wielką literą. Dla komputera same słowa nic nie znaczą. Zmienna budzet mogłaby się nazywać x i program działałby tak samo, tyle że człowiek miałby trudniej. Za to wielkość liter jest ważna, budzet i Budzet to dla Pythona dwie zupełnie różne nazwy. Nazwy nie mogą mieć spacji, stąd podkreślenie w pokaz_raport czy wedlug_kategorii.

Tekst w cudzysłowie, np. "chleb" albo "jedzenie", to napis. Komputer go przechowuje i w odpowiednim momencie wypisuje, w treść nie wnika. Liczby piszemy bez cudzysłowu, a część dziesiętną oddziela kropka, więc 6.5 to sześć i pół złotego, a 18.99 to cena sera. Różnicę najlepiej sprawdzić na ekranie, a do tego służy print. To polecenie wypisuje na ekran to, co dostanie w nawiasie, i program używa go w pokaz_raport do drukowania raportu. Tu dostaje do wypisania wynik dodawania:

python
print(6.5 + 110)
print("6.5" + "110")

Pierwsza linijka wypisze 116.5, bo to dwie liczby. Druga wypisze 6.5110, bo napisy Python dokleja jeden do drugiego, znak po znaku.

Zostały nawiasy okrągłe i kropka. Nawias zaraz po nazwie oznacza „zrób to”, a w środku jest to, na czym ma to być zrobione, rozdzielone przecinkami. W Wydatek("chleb", 6.5, "jedzenie") do środka idą trzy rzeczy, nazwa, kwota i kategoria. Nawias bywa też pusty, jak w budzet.suma(), i wtedy dalej oznacza „zrób to”, tylko nic dodatkowego nie trzeba podawać. Kropka między dwiema nazwami łączy coś z jego częścią. wydatek.kwota to kwota tego konkretnego wydatku, a budzet.dodaj(...) to polecenie dodaj, które należy do budżetu. Kropka wewnątrz liczby, jak w 6.5, to co innego, zwykły przecinek dziesiętny zapisany po angielsku.

Wywołanie programu: gdzie dzieje się akcja

Wróćmy na dół pliku, do pięciu linijek, od których komputer naprawdę zaczyna robotę:

python
budzet = Budzet(limit=500)
budzet.dodaj(Wydatek("chleb", 6.5, "jedzenie"))
budzet.dodaj(Wydatek("bilet miesięczny", 110, "transport"))
budzet.dodaj(Wydatek("ser", 18.99, "jedzenie"))
pokaz_raport(budzet)

Pierwsza linijka tworzy budżet z limitem 500 zł i wkłada go do zmiennej budzet. Od tej chwili pod tą nazwą siedzi jeden konkretny budżet, na razie bez żadnych wydatków. Trzy kolejne linijki dopisują do niego po jednym wydatku, każdy z nazwą, kwotą i kategorią, czyli chleb za 6,50 zł, bilet miesięczny za 110 zł i ser za 18,99 zł. Ostatnia prosi o raport. Cały kod wyżej pracuje na rzecz tych pięciu linijek. Gdyby je skasować, program by się uruchomił, zapamiętał swoje przepisy i skończył, nie wypisując ani słowa.

budzet.dodaj(...) czytam jako „budżecie, dodaj do siebie ten wydatek”. Najpierw pada, do kogo mówimy, a dopiero po kropce, co ma zrobić. Polecenie dodaj jest zapisane wewnątrz class Budzet (widać to po wcięciu), więc wykonać je umie tylko budżet. Samo dodaj(...) skończyłoby się błędem, że Python nie zna takiej nazwy, bo poza budżetem ona po prostu nie istnieje. Poza tym w programie mogłyby być dwa budżety, np. domowy i wakacyjny, i dopiero nazwa przed kropką mówi, na którą listę trafia chleb. Inaczej jest z pokaz_raport(budzet) w ostatniej linijce. Ta funkcja stoi w pliku samodzielnie, przy lewej krawędzi, więc woła się ją bez kropki, a budżet, o którym ma coś powiedzieć, podaje się jej w nawiasie.

W pierwszej linijce w nawiasie stoi limit=500, ze znakiem równości. Python przyjąłby też samo Budzet(500) i efekt byłby identyczny, budżet z limitem 500 zł. Różnica jest dla człowieka, który to czyta. limit=500 to liczba podpisana, od razu widać, że te 500 to limit. Przy gołym 500 trzeba by przewinąć plik w górę do class Budzet i sprawdzić, co ta liczba znaczy, bo równie dobrze mogłaby to być kwota na start albo liczba dni. Tam, w linijce def __init__(self, limit):, jest właśnie słowo limit. To rubryka czekająca na wartość i to jej nazwę dopisujemy przed znakiem równości.

W wydatkach podpisów nie ma. Wydatek("chleb", 6.5, "jedzenie") to trzy gołe wartości, więc Python rozkłada je według kolejności, w jakiej rubryki stoją w def __init__(self, nazwa, kwota, kategoria):. Pierwsza idzie do nazwa, druga do kwota, trzecia do kategoria. Gdyby ktoś przez pomyłkę napisał Wydatek("jedzenie", 6.5, "chleb"), program przyjmie to bez słowa, a w raporcie pojawi się wydatek o nazwie jedzenie w kategorii chleb. Przy podpisanych wartościach kolejność przestaje mieć znaczenie i te dwa zapisy robią dokładnie to samo:

python
Wydatek(nazwa="chleb", kwota=6.5, kategoria="jedzenie")
Wydatek(kategoria="jedzenie", kwota=6.5, nazwa="chleb")

Obie formy mają swoje nazwy. Wartość podana razem z nazwą rubryki, jak limit=500, to argument nazwany, po angielsku keyword argument. Wartość bez podpisu, którą Python rozpoznaje po miejscu w kolejności, to argument pozycyjny. Przy okazji jeszcze jedno rozróżnienie, które w tekstach o Pythonie ciągle się przewija, a łatwo je pomylić. Parametr to rubryka we wzorze, czyli limit w def __init__(self, limit):, a argument to konkretna wartość wpisana w tę rubrykę przy wywołaniu, czyli 500 w Budzet(limit=500).

Klasa Wydatek: przepis na pojedynczy wydatek

W poprzednich akapitach kilka razy padło słowo „rubryka” i to dobre wejście do tego, czym jest class. Po polsku to klasa, a w praktyce wzór formularza, czyli opis, jakie rubryki ma mieć każda rzecz danego rodzaju. Sam wzór nie zawiera jeszcze żadnych danych, mówi tylko, jak wypełniony formularz ma wyglądać. W programie są dwa takie wzory, bo program ma do czynienia z dwoma rodzajami rzeczy. Jeden opisuje pojedynczy wydatek, drugi cały budżet, stąd class Wydatek i class Budzet. Pierwszy jest krótszy, więc od niego zaczniemy, a zajmuje osiem linijek:

python
class Wydatek:
    def __init__(self, nazwa, kwota, kategoria):
        self.nazwa = nazwa
        self.kwota = kwota
        self.kategoria = kategoria

    def opis(self):
        return f"{self.nazwa}: {self.kwota:.2f} zł ({self.kategoria})"

Po wcięciach widać, że w środku są dwie części, każda zaczyna się od def. Pierwsza, __init__, ustala rubryki. Każdy wydatek ma mieć nazwę, kwotę i kategorię, i to są właśnie te trzy linijki z self.nazwa, self.kwota i self.kategoria. Druga, opis, to polecenie, które umie wykonać każdy wydatek, czyli złożyć o sobie jedną linijkę tekstu do raportu. Idąc od góry, komputer ten wzór tylko zapamiętuje i nie powstaje przy tym ani jeden wydatek. Wypełnione formularze pojawiają się dopiero na dole pliku. Wydatek("chleb", 6.5, "jedzenie") to jeden, a że takie linijki są tam trzy, to według jednego wzoru Wydatek powstają trzy osobne wydatki, każdy z własną nazwą, kwotą i kategorią.

self to po angielsku „sam” albo „siebie”, a w programie oznacza ten jeden konkretny wydatek, który akurat powstaje. Wzór Wydatek jest wspólny, ale chleb i ser to dwa osobne wypełnione formularze i każdy ma mieć w rubryce z nazwą co innego. Stąd przedrostek self. przy każdej rubryce w __init__. Najprościej czytać go jako „mój”, czyli należący do tego wydatku, którego to dotyczy: moja nazwa, moja kwota, moja kategoria. Na dole pliku __init__ wykonuje się trzy razy, raz dla chleba, raz dla biletu i raz dla sera, i za każdym razem self to inny wydatek. Dlatego chleb ma w rubryce kwota 6.5, a ser 18.99, choć obie liczby wpisała ta sama linijka self.kwota = kwota.

W linijce self.nazwa = nazwa słowo „nazwa” pada dwa razy i wygląda to trochę jak przepisywanie czegoś do samego siebie. Dla Pythona to jednak dwie różne nazwy, bo self.nazwa z kropką i nazwa bez kropki to osobne etykiety. Kropka wskazuje rubrykę w środku, więc self.nazwa to miejsce na nazwę w tym konkretnym wydatku. Samo nazwa po prawej to wartość, która przyszła z zewnątrz, stąd jest wymieniona w nawiasie przy __init__. Przy tworzeniu chleba jest w niej napis "chleb". Znak = w programie znaczy „włóż do” i czyta się go od prawej. Python najpierw bierze to, co stoi po prawej stronie, a potem wkłada to w miejsce wskazane po lewej. Wartość z nawiasu znika, gdy __init__ skończy pracę, a zostaje tylko to, co zdążyło trafić do rubryki z self..

Skąd się bierze samo self? Nigdzie go nie podajemy, Python dokłada go sam. Widać to po liczeniu. W nawiasie przy __init__ są cztery miejsca, pierwsze to self, a za nim trzy na dane, tymczasem w Wydatek("chleb", 6.5, "jedzenie") stoją tylko trzy wartości. Kiedy Python trafia na taką linijkę, najpierw bierze nowy, pusty formularz według wzoru Wydatek i wstawia go w miejsce self. Nasze trzy wartości wypełniają potem pozostałe miejsca w tej samej kolejności, w jakiej je podaliśmy, tak jak przy argumentach pozycyjnych. Na koniec gotowy wydatek wraca tam, skąd go zawołano, czyli do nawiasu w budzet.dodaj(...).

Nazwa __init__ jest za to sztywna, z dwoma podkreśleniami przed i dwoma po. Python sam szuka tego słowa. Przy każdym Wydatek(...) zagląda do wzoru, znajduje polecenie o dokładnie tej nazwie i je uruchamia, zanim oddaje gotowy wydatek. Gdyby ktoś zmienił __init__ na, powiedzmy, przygotuj, Python by go przy tworzeniu nie znalazł i już linijka z chlebem skończyłaby się błędem, bo wzór bez __init__ nie przyjmuje żadnych wartości. Z pozostałymi poleceniami jest inaczej. opis, dodaj czy suma wymyślił autor programu i można je nazwać po swojemu, trzeba tylko poprawić wszystkie miejsca, w których są używane. Przy opis byłaby to linijka print(" -", wydatek.opis()) w pokaz_raport.

Druga część wzoru Wydatek ma tylko jedną linijkę treści:

python
def opis(self):
    return f"{self.nazwa}: {self.kwota:.2f} zł ({self.kategoria})"

Polecenie opis składa z rubryk jednego wydatku linijkę tekstu, która nadaje się do pokazania. Dla chleba jest to chleb: 6.50 zł (jedzenie). W nawiasie przy def stoi samo self, bo z zewnątrz nie trzeba nic podawać. Nazwę, kwotę i kategorię wydatek ma już w swoich rubrykach, więc opis tylko z nich czyta. Słowo return na początku znaczy „oddaj”. Gotowy tekst wraca do miejsca, w którym ktoś napisał wydatek.opis(). Sam opis niczego na ekranie nie wypisuje. Robi to dopiero print w pokaz_raport, któremu ten tekst zostaje przekazany.

Najdziwniej wygląda to, co stoi po return, czyli litera f tuż przed cudzysłowem i nawiasy klamrowe w środku napisu. Taki napis nazywa się f-string. Wszystko poza klamrami trafia do wyniku dokładnie tak, jak jest napisane: dwukropek ze spacją, „zł” i nawiasy okrągłe wokół kategorii. W klamrach stoją nazwy rubryk, a Python w ich miejsce wkleja to, co w nich akurat jest. Dla sera w pierwsze klamry wejdzie ser, a w ostatnie jedzenie. To f z przodu jest potrzebne. Bez niego klamry byłyby zwykłymi znakami i raport pokazałby dosłownie {self.nazwa}: {self.kwota:.2f} zł ({self.kategoria}), ten sam napis przy każdym wydatku.

W środkowych klamrach po self.kwota jest jeszcze dopisek :.2f. To instrukcja, jak liczbę pokazać: zawsze z dwoma miejscami po kropce. Wracając do formularza, to dwie kratki na grosze, które trzeba wypełnić zawsze, a jak groszy nie ma, wpisuje się zera. Różnicę widać od razu:

python
kwota = 6.5
print(f"{kwota} zł")
print(f"{kwota:.2f} zł")

Pierwsza linijka wypisze 6.5 zł, druga 6.50 zł. Bilet miesięczny, wpisany na dole jako 110, w raporcie pojawi się jako 110.00 zł. Ser za 18.99 zostanie bez zmian, bo dwa miejsca już ma. Gdyby któraś kwota miała więcej cyfr po kropce, np. 18.999, Python zaokrągli ją do groszy i pokaże 19.00. Kropka zamiast polskiego przecinka zostaje, bo :.2f pilnuje tylko liczby miejsc, a nie zwyczajów zapisu.

Klasa Budzet: przepis na cały budżet

Pod wydatkiem, znowu od lewej krawędzi, zaczyna się drugi wzór. Jego pierwsze linijki to ten sam rodzaj przygotowania, jaki widzieliśmy przy wydatku:

python
class Budzet:
    def __init__(self, limit):
        self.limit = limit
        self.wydatki = []

Tym razem w nawiasie przy __init__ poza self jest tylko jedno miejsce, limit. Do niego trafia 500 z linijki Budzet(limit=500) na dole pliku, a self.limit = limit robi z nim to samo, co self.nazwa = nazwa w wydatku. Druga rubryka, self.wydatki, nie ma w nawiasie swojego odpowiednika, więc nikt jej z zewnątrz nie podaje. Budżet zakłada ją sobie sam, w chwili powstania, i od razu wpisuje do niej [].

Nawiasy kwadratowe, między którymi nic nie stoi, to nowa, pusta lista. Lista to jedna rzecz, w której mieści się wiele innych, ułożonych po kolei. Gdyby coś miało w niej być od początku, wpisałoby się to między nawiasy, oddzielone przecinkami, np. [6.5, 110, 18.99]. Tu na starcie nie ma nic, bo budżet dopiero powstał i nic jeszcze nie zostało kupione. Chleb, bilet i ser trafią do tej listy później, każdy przez osobne budzet.dodaj(...). Obie rubryki mają przed sobą self., tak jak nazwa czy kwota w wydatku, więc należą do tego jednego budżetu. Gdyby w programie były dwa budżety, domowy i wakacyjny, każdy miałby swoją pustą listę na start i swój limit, a chleb dopisany do domowego nie pojawiłby się w wakacyjnym.

Pod __init__ budżetu stoi pierwsze polecenie, które budżet umie wykonać. Ma tylko dwie linijki:

python
def dodaj(self, wydatek):
    self.wydatki.append(wydatek)

append to po angielsku „dołącz”, a przy liście znaczy „dopisz na koniec”. self.wydatki to lista założona chwilę wcześniej w __init__, więc całą linijkę czyta się tak: do mojej listy wydatków dopisz na koniec ten wydatek. Nowe rzeczy zawsze lądują za ostatnią, więc lista zachowuje kolejność. Chleb jest w niej pierwszy, bilet drugi, a ser trzeci, bo w takiej kolejności stoją linijki budzet.dodaj(...) na dole pliku. W tej samej kolejności wydatki pokażą się potem w raporcie. dodaj nie ma return, bo nie musi niczego oddawać. Jego cała robota to dopisanie wydatku do listy, która siedzi w budżecie.

Przy def są dwa miejsca, self i wydatek, a w budzet.dodaj(Wydatek("chleb", 6.5, "jedzenie")) w nawiasie stoi jedna rzecz. Te dwa miejsca wypełniają się z dwóch stron. self Python podstawia sam, biorąc to, co stoi przed kropką, czyli budżet z limitem 500. wydatek wypełniamy my, tym, co podaliśmy w nawiasie, czyli świeżo zrobionym chlebem. Przy jednym wywołaniu trafiają więc do środka dwie różne rzeczy, jedna od Pythona, druga od nas. W linijce z append spotykają się obie, self mówi, do czyjej listy dopisać, a wydatek mówi, co dopisać. Stąd bierze się to, że chleb trafia na listę akurat tego budżetu, którego nazwa stoi przed kropką, a nie jakiegokolwiek.

Wydatek("chleb", 6.5, "jedzenie") rządzi się tą samą zasadą, self dokłada Python, a resztę podajemy my. Tyle że kropki tu nie ma, więc self nie może się wziąć sprzed niej. Tak jak przy __init__, Python sam szykuje świeży wydatek, jeszcze bez żadnych danych, i to on ląduje w self, a trzy wartości z nawiasu rozchodzą się po pozostałych miejscach, po kolei. Różni się więc tylko to, skąd przychodzi self. Przy poleceniu takim jak dodaj jest to coś, co już istnieje i stoi przed kropką, a przy tworzeniu coś, co Python zrobił ułamek chwili wcześniej. W linijce z chlebem dzieje się to dwa razy, jedno po drugim. Najpierw w nawiasie powstaje chleb, a w jego __init__ jako self występuje on sam, dopiero co zrobiony i jeszcze pusty. Potem gotowy chleb idzie do dodaj i tam self to już budżet, ten sprzed kropki.

Pod dodaj stoi kolejne polecenie budżetu, które odpowiada na pytanie, ile w sumie poszło pieniędzy:

python
def suma(self):
    razem = 0
    for wydatek in self.wydatki:
        razem = razem + wydatek.kwota
    return razem

Linijkę z for czytam jako „dla każdego wydatku z mojej listy zrób to, co jest wcięte niżej”. Wcięta jest tylko jedna linijka, więc przy trzech wydatkach wykona się trzy razy. Przy każdym obrocie Python wkłada do wydatek kolejną rzecz z listy. Najpierw jest to chleb, potem bilet, na końcu ser, w tej kolejności, w jakiej dopisywał je append. Zmienna razem rośnie przy tym krok po kroku. Na starcie jest w niej 0, po chlebie 6,5, po bilecie 116,5, a po serze 135,49. Tak wygląda pętla, czyli fragment kodu powtarzany dla każdej rzeczy z listy. Gdy lista się kończy, Python schodzi do pierwszej linijki pod pętlą, która nie jest już wcięta w jej środek.

Zero na początku jest potrzebne, bo linijka w pętli najpierw czyta razem, a dopiero potem wpisuje tam nową wartość (prawa strona = idzie pierwsza, jak przy self.nazwa = nazwa). Przy chlebie Python ma więc policzyć „to, co jest w razem, plus 6,5”. Bez razem = 0 w pierwszym obrocie nie byłoby czego dodawać i program zatrzymałby się z błędem, że razem nie ma jeszcze żadnej wartości. Wartości startowej nie potrzebuje za to wydatek, choć też stoi w pętli. Tę nazwę wypełnia sama pętla, zanim cokolwiek z niej przeczytamy. Zasada jest więc taka, że nazwa musi coś zawierać w chwili, gdy ktoś chce z niej czytać, a przy dodawaniu po kawałku tym czymś jest zero.

return pojawił się już przy opis, gdzie oddawał gotowy tekst, i tu znaczy to samo, tylko oddawana jest liczba. Sumę program liczy w dwóch miejscach, w pokaz_raport jako budzet.suma() i w czy_przekroczony jako self.suma(). Za każdym razem Python wchodzi do suma i przechodzi pętlę od nowa. Przy return razem zabiera wynik ze sobą, więc w miejscu wywołania staje gotowe 135,49. Gdyby return usunąć, pętla dalej by liczyła, tylko wynik zostałby w środku i przepadł razem z razem, gdy metoda skończy pracę (tak jak znika wartość z nawiasu przy __init__). Python oddałby wtedy None, swoje specjalne „nic”. return od razu też kończy metodę. Linijki pod nim nie wykonałyby się już wcale, choć w suma akurat żadnych nie ma, bo to ostatnia linijka.

Najwięcej robi tu wcięcie. return razem stoi na tej samej wysokości co for, więc do pętli nie należy i wykonuje się raz, po wszystkich obrotach. Do pętli należy tylko to, co jest wcięte głębiej niż for, czyli tu jedna linijka z dodawaniem. Gdyby return razem dostał cztery spacje więcej i stanął równo z razem = razem + wydatek.kwota, znalazłby się w środku pętli i wykonał już przy pierwszym obrocie, zaraz po doliczeniu chleba. Metoda oddałaby 6.5, a do biletu i sera pętla w ogóle by nie doszła. Python nie zgłosiłby przy tym żadnego błędu, bo z jego punktu widzenia to poprawny program, a w raporcie stałoby „Razem: 6.5 zł”.

Suma mówi, że poszło 135,49 zł, ale nie mówi, ile z tego zjedliśmy, a ile przejechaliśmy. Tym zajmuje się trzecie polecenie budżetu, najdłuższe z nich:

python
def wedlug_kategorii(self):
    kategorie = {}
    for wydatek in self.wydatki:
        if wydatek.kategoria not in kategorie:
            kategorie[wydatek.kategoria] = 0
        kategorie[wydatek.kategoria] += wydatek.kwota
    return kategorie

Na koniec oddaje coś takiego: {"jedzenie": 25.49, "transport": 110}. To słownik, czyli zbiór par. Po lewej od dwukropka stoi nazwa, a po prawej przypisana do niej wartość. Nazwę nazywa się kluczem. Trochę przypomina to formularz z wcześniejszych akapitów, ale rubryki nie są ustalone z góry we wzorze. Dopisują się w trakcie, po jednej na każdą kategorię, jaka się trafi. Same klamry {}, bez zawartości, oznaczają pusty słownik, podobnie jak [] oznacza pustą listę. Pętla przechodzi przez listę wydatków w tej samej kolejności co w suma.

W środku pętli są trzy nowe rzeczy naraz, więc po kolei. Pierwsza to nawiasy kwadratowe po nazwie kategorie, które pojawiają się w dwóch linijkach. Znaczą „sięgnij do środka, w to miejsce”. Ten sam znak działa i przy liście, i przy słowniku, różni się tylko to, co się wpisuje w nawias. Lista trzyma rzeczy po kolei, więc sięga się do niej po numerze miejsca, przy czym Python liczy od zera, self.wydatki[0] to chleb, a self.wydatki[2] ser. Do słownika sięga się po kluczu, więc kategorie["jedzenie"] to kwota zapisana przy jedzeniu. W programie w nawiasie stoi wydatek.kategoria, czyli kategoria akurat obsługiwanego wydatku, przy chlebie "jedzenie", przy bilecie "transport".

Przy zapisie brak klucza nie przeszkadza, Python go po prostu zakłada. Przy odczycie przeszkadza i program zatrzymuje się z błędem, bo nie ma czego przeczytać.

Druga nowość to if, czyli „jeśli”. Linijka wcięta pod nim wykona się tylko wtedy, gdy warunek jest spełniony, w przeciwnym razie Python ją przeskakuje. W warunku stoi słowo in, które w tej metodzie pada dwa razy, za każdym razem w innym znaczeniu. W for wydatek in self.wydatki znaczy „bierz po kolei z listy” i przy każdym obrocie wkłada do wydatek następną rzecz. W if wydatek.kategoria not in kategorie jest pytaniem, na które odpowiedź brzmi tak albo nie: czy ta nazwa już jest w słowniku (sprawdzane są same klucze, wartości nie). not odwraca to pytanie, więc cała linijka pyta, czy tej kategorii jeszcze nie ma.

Jeśli jej nie ma, kategorie[wydatek.kategoria] = 0 zakłada ją z zerem. To jest zapis, więc brak klucza nie przeszkadza. Zero jest potrzebne z tego samego powodu co razem = 0 w suma, bo następna linijka zaczyna od odczytu.

Trzecia nowość to +=. To skrót, a += b znaczy dokładnie to samo co a = a + b. Python bierze to, co już jest, dodaje nowe i wkłada wynik z powrotem w to samo miejsce. W suma było to zapisane w pełnej wersji, razem = razem + wydatek.kwota. Tu też można by napisać kategorie[wydatek.kategoria] = kategorie[wydatek.kategoria] + wydatek.kwota, a wtedy długi klucz trzeba by powtórzyć dwa razy w jednej linijce.

Dla naszych trzech wydatków przebieg jest taki. Przy chlebie „jedzenia” jeszcze nie ma, więc powstaje z zerem i od razu dostaje 6,5. Przy bilecie tak samo powstaje „transport” i dostaje 110. Przy serze „jedzenie” już jest, więc Python przeskakuje linijkę z zerem i do 6,5 dolicza 18,99, co daje 25,49. Po trzecim obrocie return oddaje cały słownik z dwoma kluczami.

Ostatnie polecenie budżetu jest najkrótsze. Cała jego treść mieści się w jednej linijce po return:

python
def czy_przekroczony(self):
    return self.suma() > self.limit

Metoda odpowiada na jedno pytanie: czy wydaliśmy więcej, niż pozwala limit. Znak > znaczy „większe niż”, tak jak w matematyce, i Python porównuje lewą stronę z prawą. Wynikiem porównania jest jedna z dwóch specjalnych wartości, True (tak) albo False (nie). Pisze się je bez cudzysłowu i wielką literą, bo to osobny rodzaj wartości, obok liczb i napisów. Przy naszych zakupach po lewej jest 135,49, po prawej 500, więc czy_przekroczony odda False. Gdyby dopisać jeszcze czynsz za 400 zł, suma doszłaby do 535,49 i odpowiedź zmieniłaby się na True. Przy sumie równej dokładnie 500 wyszłoby False, bo 500 nie jest większe od 500.

Z prawej strony znaku > jest self.limit, rubryka, do której na starcie trafiło 500. Z lewej jest self.suma(), czyli jedno polecenie budżetu zawołane ze środka drugiego. Zasada jest ta sama co przy budzet.dodaj(...), nazwa z lewej strony kropki idzie do self. Tu tą nazwą jest samo self, więc do suma trafia ten sam budżet, który właśnie pyta o limit, i pętla liczy jego listę. Pusty nawias niczego w tym nie zmienia. W pokaz_raport jest budzet.czy_przekroczony() i Python też sam wstawia budzet jako self. Przy def czy_przekroczony(self): innych miejsc nie ma, więc nic więcej nie trzeba podawać. Tak samo jest z suma i wedlug_kategorii. Listę wydatków i limit budżet nosi w sobie od chwili, gdy __init__ je założył.

Funkcja pokaz_raport: wszystko razem

Po czy_przekroczony kończy się klasa Budzet, a zaraz pod nią leży ostatni przepis przygotowany zawczasu. Z całego budżetu robi raport na ekranie:

python
def pokaz_raport(budzet):
    print("Wydatki:")
    for wydatek in budzet.wydatki:
        print(" -", wydatek.opis())
    print("Razem:", budzet.suma(), "zł")
    for kategoria, kwota in budzet.wedlug_kategorii().items():
        print(f"{kategoria}: {kwota} zł")
    if budzet.czy_przekroczony():
        print("Uwaga: przekroczono limit!")
    else:
        print("Mieścisz się w limicie.")

Sama niczego nie liczy. Po kolei korzysta z tego, co już umieją wydatek i budżet. Najpierw dla każdego wydatku woła opis(), potem suma(), potem wedlug_kategorii(), a na końcu czy_przekroczony(). To są wszystkie cztery polecenia opisane wyżej, a raport pokaże ich wyniki właśnie w takim porządku. Rolą pokaz_raport jest poukładać te wyniki jeden pod drugim i każdy wypisać przez print.

def pokaz_raport zaczyna się od pierwszej kolumny, bez żadnego wcięcia, i do class Budzet już nie należy. Polecenie zapisane wewnątrz klasy nazywa się metodą, tak jak dodaj, suma czy opis. Polecenie, które stoi samodzielnie, poza jakąkolwiek klasą, nazywa się funkcją. pokaz_raport jest więc funkcją, a w ostatniej linijce programu jej wywołanie wygląda tak: pokaz_raport(budzet), z samą nazwą na początku. Przed nazwą nic nie stoi, więc Python nie ma skąd wziąć self i nie wstawia go. Stąd w def pokaz_raport(budzet): nie ma self. Jest za to zwykłe miejsce na budżet, który trzeba podać samemu, w nawiasie.

Można by pomyśleć, że metoda ma dane budżetu pod ręką, a funkcja musi o nie prosić. W programie widać jednak, że metoda też nie ma do nich żadnego skrótu. Pętla w suma chodzi po self.wydatki, a w czy_przekroczony jest self.limit, zawsze z self. z przodu. Bez tego przedrostka Python zgłosiłby, że nie zna nazwy wydatki. W pokaz_raport pętla chodzi po budzet.wydatki, czyli po tej samej liście, a zmieniła się tylko nazwa przed kropką. Metoda dostaje budżet do self od Pythona, który bierze go sprzed kropki. Funkcja dostaje go do budzet, bo sami wpisaliśmy go w nawias.

Zbieżność nazw jest tu przypadkowa. budzet w def pokaz_raport(budzet): to rubryka tej funkcji, a zmienna budzet na dole pliku to osobna nazwa, która akurat brzmi tak samo. Gdyby rubrykę nazwać b i w środku pisać b.wydatki i b.suma(), raport wyszedłby identyczny. Przy wywołaniu i tak trafia do niej ten budżet z limitem 500, który stoi w nawiasie w ostatniej linijce programu.

Raport wypisuje się linijka za linijką, od góry, tak jak stoją w kodzie. Pierwsza, print("Wydatki:"), drukuje nagłówek. W nawiasie jest napis w cudzysłowie, więc na ekran trafia dokładnie Wydatki:, razem z dwukropkiem. Dalej jest pętla, taka sama jak w suma, i też bierze chleb, potem bilet, potem ser. Przy każdym obrocie wykonuje się wcięty pod nią print, który dostaje dwie rzeczy oddzielone przecinkiem. Pierwsza to krótki napis z myślnikiem, druga to tekst, który oddał opis(). Między nie print sam wstawia spację (przy przecinku zawsze tak robi). Pod nagłówkiem pojawiają się więc trzy linijki: - chleb: 6.50 zł (jedzenie), - bilet miesięczny: 110.00 zł (transport) i - ser: 18.99 zł (jedzenie).

Następna linijka, print("Razem:", budzet.suma(), "zł"), ma w nawiasie trzy rzeczy. Środkowa to wywołanie metody, więc zanim cokolwiek pojawi się na ekranie, Python liczy sumę od nowa, pętlą po wydatkach, i w to miejsce podstawia wynik, 135.49. Dopiero wtedy print wypisuje wszystkie trzy części, rozdzielone spacjami: Razem: 135.49 zł. Liczba nie musi tu być w cudzysłowie ani w f-stringu, print przyjmuje obok siebie napisy i liczby i każdą rzecz sam zamienia na tekst.

Potem jest najbardziej zawiła linijka w całym raporcie:

python
for kategoria, kwota in budzet.wedlug_kategorii().items():
    print(f"{kategoria}: {kwota} zł")

Czyta się ją od lewej do prawej. budzet.wedlug_kategorii() przechodzi po wydatkach i przez return oddaje słownik {"jedzenie": 25.49, "transport": 110}. Zaraz za nawiasem stoi kropka, więc następne polecenie dostaje ten gotowy słownik. items() to polecenie, które ma każdy słownik, a oddaje jego zawartość po kolei, parami: klucz z przypisaną do niego wartością. Tu są dwie pary, "jedzenie" z 25.49 i "transport" ze 110. Bez .items() pętla dostawałaby same nazwy kategorii, bez kwot. Z jedną nazwą przed in, np. for kategoria in budzet.wedlug_kategorii():, wypisałaby tylko jedzenie i transport. W tej linijce przed in stoją jednak dwie nazwy, więc program zatrzymałby się z błędem ValueError, bo sama nazwa kategorii nie daje się rozłożyć na dwie części.

Te dwie nazwy biorą się z tego, że za każdym obrotem przychodzi para, a Python rozkłada ją od razu na dwie części, pierwsza trafia do kategoria, druga do kwota. Widać to, jak się da pętli tylko jedną nazwę:

python
for para in budzet.wedlug_kategorii().items():
    print(para)

Wypisze się ('jedzenie', 25.49), a potem ('transport', 110), czyli obie części naraz, zamknięte w okrągłym nawiasie. Gdy nazwy są dwie, każda część ma swoje miejsce i f-string może wstawić je osobno w różne miejsca napisu. Za pierwszym obrotem wychodzi jedzenie: 25.49 zł, za drugim transport: 110 zł.

Przy bilecie widać, że ta sama kwota wyszła w raporcie dwa razy i za każdym razem inaczej. Na liście wydatków jest 110.00 zł, bo opis() ma w klamrach :.2f, a to, jak w formularzu, każe zawsze wypełnić miejsce na grosze, choćby zerami. W podsumowaniu kategorii jest transport: 110 zł. W {kwota} tego dopisku nie ma, więc Python pokazuje liczbę tak, jak ją ma, a na dole pliku bilet wpisano jako 110, bez kropki, czyli jako liczbę całkowitą. Zer nikt tu nie dopisuje. To samo dotyczy linijki z „Razem”. Przy 135.49 i przy 25.49 różnicy akurat nie widać, bo obie kwoty i tak mają dwie cyfry po kropce. Gdyby w budżecie był tylko chleb, raport pokazałby Razem: 6.5 zł, a nad nim - chleb: 6.50 zł (jedzenie).

Ostatni fragment to if z nowym słowem else, czyli „w przeciwnym razie”. budzet.czy_przekroczony() oddaje True albo False. Przy True wykonuje się linijka wcięta pod if i na ekranie pojawia się ostrzeżenie o przekroczonym limicie. Przy False Python ją przeskakuje i wykonuje linijkę wciętą pod else. Zawsze wykona się dokładnie jedna z tych dwóch. U nas suma to 135.49, limit 500, więc wychodzi False i raport kończy się napisem Mieścisz się w limicie.

W całej funkcji pokaz_raport nie ma ani jednego return i Python nie ma nic przeciwko. Metody opis, suma, wedlug_kategorii i czy_przekroczony go mają, bo ich robota polega na tym, żeby coś policzyć albo złożyć i oddać temu, kto je zawołał. Tutaj robota wygląda inaczej. Każdy print w środku od razu wypisuje swoją linijkę na ekran, więc gdy funkcja dochodzi do końca, raport już tam stoi i nie ma czego nikomu przekazywać. Funkcja może w ogóle nie mieć return (metoda dodaj w budżecie też go nie miała). Wtedy po ostatniej wciętej linijce, u nas po print spod if albo spod else, po prostu się kończy, a Python po cichu oddaje to specjalne „nic”, czyli None. To samo None wyszłoby z suma, gdyby wykreślić z niej return.

Widać to po ostatniej linijce programu. pokaz_raport(budzet) stoi tam sama, bez = z lewej i bez print dookoła, bo nie ma czego odebrać. Gdyby ktoś jednak próbował wynik złapać:

python
wynik = pokaz_raport(budzet)
print(wynik)

najpierw pojawi się cały raport, od Wydatki: do Mieścisz się w limicie., bo te linijki funkcja wypisuje w trakcie swojej pracy. Pod nimi wyjdzie jeszcze jedna, None, i tyle właśnie trafiło do zmiennej wynik. Dla porównania print(budzet.suma()) wypisze 135.49, bo suma swój wynik oddaje przez return.

Cały program od początku do końca

Wszystkie przepisy są już opisane, więc można prześledzić, co się dzieje po uruchomieniu pliku, linijka po linijce. Python idzie od góry. Przy class Wydatek zapamiętuje wzór wydatku, przy class Budzet wzór budżetu, przy def pokaz_raport funkcję do raportu, i tyle. Po tych trzech krokach nie istnieje jeszcze żaden budżet ani wydatek, a na ekranie nic się nie pojawiło. Pierwsza linijka, która naprawdę coś tworzy, to ta:

python
budzet = Budzet(limit=500)

Python bierze świeży, pusty budżet według wzoru, wstawia go jako self i uruchamia __init__. Do limit trafia 500, stamtąd do rubryki self.limit, a self.wydatki dostaje pustą listę []. Gotowy budżet wraca z nawiasu i = przypina mu etykietę budzet. W pamięci siedzi teraz jeden budżet, a w nim dwie rubryki, limit równy 500 i lista, na której jeszcze nic nie ma.

Kolejne trzy linijki wyglądają prawie tak samo:

python
budzet.dodaj(Wydatek("chleb", 6.5, "jedzenie"))
budzet.dodaj(Wydatek("bilet miesięczny", 110, "transport"))
budzet.dodaj(Wydatek("ser", 18.99, "jedzenie"))

W pierwszej z nich Python zaczyna od środka. Zanim zawoła dodaj, musi mieć gotowe to, co ma mu podać, a w nawiasie stoi dopiero przepis na wydatek. Najpierw więc robi chleb. Powstaje nowy wydatek, w jego __init__ self.nazwa dostaje "chleb", self.kwota dostaje 6.5, a self.kategoria dostaje "jedzenie". Gotowy chleb staje w miejscu Wydatek(...) i dopiero wtedy rusza dodaj, z budżetem w self i chlebem w wydatek. append dopisuje go na koniec listy, która do tej pory była pusta, więc teraz jest na niej jeden wydatek.

Druga i trzecia linijka robią to samo, tylko z innymi wartościami. Najpierw powstaje bilet i ląduje za chlebem, potem powstaje ser i ląduje za biletem. Chleb i ser mają tę samą kategorię, ale to dwa osobne wydatki, każdy z własną nazwą i kwotą. Żaden z nich nie dostał też własnej etykiety, tak jak dostał ją budżet. Powstały w nawiasie, poszły prosto do dodaj i jedyne miejsce, gdzie są, to lista w budżecie. Dostać się do nich da się tylko przez nią, po numerze miejsca:

python
print(budzet.wydatki[0].nazwa)
print(budzet.wydatki[2].kwota)

Wypisze się chleb, a potem 18.99. Po tych trzech linijkach lista budzet.wydatki ma trzy elementy, w kolejności chleb, bilet miesięczny, ser, a limit dalej wynosi 500, bo dodaj go nie dotyka.

Została ostatnia linijka, pokaz_raport(budzet). Przed nazwą nie ma kropki, więc Python szuka jej wśród przepisów zapamiętanych na samym początku, znajduje def pokaz_raport i przeskakuje w górę pliku. Budżet z nawiasu, ten z limitem 500 i trzema wydatkami na liście, trafia do rubryki budzet, a potem po kolei wykonują się wcięte linijki funkcji. Każdą z nich rozebraliśmy już wyżej, razem z liczbami, więc tu ciekawsze jest to, ile razy Python wychodzi z funkcji do którejś metody i z niej wraca. W pętli z opis() robi to trzy razy, raz na każdy wydatek. Przy „Razem” zagląda raz do suma i przynosi 135.49, a zaraz potem raz do wedlug_kategorii i przynosi słownik z 25.49 przy jedzeniu i 110 przy transporcie, który .items() rozkłada na dwie pary.

Przy if Python wychodzi jeszcze raz, do czy_przekroczony, a ona ze swojego środka woła suma. W tym raporcie to drugie przejście sumy przez całą listę, od zera, bo wynik z linijki „Razem” poszedł na ekran i nigdzie nie został zapamiętany. (Swoją drogą, trochę mnie to kłuje w oczy, można by sumę policzyć raz, włożyć do zmiennej i użyć w obu miejscach. Przy trzech wydatkach nie ma to jednak żadnego znaczenia, więc zostawiam, jak jest.) Znowu wychodzi 135.49, porównanie z 500 daje False, więc ostrzeżenie zostaje przeskoczone, a wykonuje się linijka spod else. Cały ekran po uruchomieniu programu wygląda tak:

Wydatki:
 - chleb: 6.50 zł (jedzenie)
 - bilet miesięczny: 110.00 zł (transport)
 - ser: 18.99 zł (jedzenie)
Razem: 135.49 zł
jedzenie: 25.49 zł
transport: 110 zł
Mieścisz się w limicie.

Po ostatnim print funkcja się kończy i oddaje None, którego nikt nie odbiera, bo przed pokaz_raport(budzet) nie stoi żadne =. Pod tą linijką w pliku nie ma już nic, więc Python kończy też cały program, a budżet razem z chlebem, biletem i serem znika z pamięci. Przy następnym uruchomieniu wszystko powstaje od nowa, z tych samych pięciu linijek na dole pliku.