.. _extra-lotek: Extra Lotek ########### Program "Duży Lotek" omówiony w poprzednim przykładzie można podzielić na części kodu realizujące pojedyncze zadania, np. pobranie danych wejściowych, wylosowanie liczb, pobranie typów użytkownika, sprawdzenie i wypisanie danych wyjściowych. Realizację takich zadań możemy umieścić w funkcjach, których będzie można używać w innych programach. .. note:: Przykład "Extra Lotek" pokazuje, jak używać funkcji i modułów oraz jak zapisywać i odczytywać dane z plików tekstowych w różnych formatach. Zadanie ******* Napisz program :file:`extra_lotek.py`, który losuje ``n`` liczb naturalnych z podanego zakresu ``maks``, a następnie pobiera z klawiatury ``n`` typów, sprawdza i wypisuje, ile z nich zostało trafionych. Pobieranie typów, sprawdzanie i wypisywanie wyników powtarza się tyle razy, ile poda użytkownik. Dodatkowo program powinien: * sprawdzać poprawność podawanych danych wejściowych, * zapisywać w pliku i odczytywać dane wejściowe: nick użytkownika, liczbę losowanych liczb, wartość maksymalną oraz liczbę typowań, * pozwalać na zmianę danych wejściowych. Realizację kolejnych zadań zakoduj w funkcjach umieszczonych w module. **Dane**: * ``nick`` – nick użytkownika, ciąg znaków pobierany z klawiatury, * ``n`` – liczba naturalna pobierana z klawiatury większa od ``n``, * ``maks`` – liczba całkowita pobierana z klawiatury, * ``ile_typowan`` – liczba całkowita pobierana z klawiatury, * ``typ`` – liczba całkowita pobierana z klawiatury z zakresu ``<0; maks>``. **Wynik** Komunikaty podczas pierwszego uruchomienia: .. code:: Podaj nick: adam Podaj liczbę losowanych liczb: 3 Podaj maksymalną losowaną liczbę: 10 Podaj liczbę typowań: 1 Wytypuj 3 z 10 liczb: Podaj typ: 1 Podaj typ: 3 Podaj typ: 5 Wylosowane liczby: [10, 3, 4] Trafione liczby: {3} Liczba trafień 1 Początkowe komunikaty po kolejnym uruchomieniu przez tego samego użytkownika: .. code:: Podaj nick: adam Ustawienia: Liczb: 3 Maks: 10 Typowań: 1 Zmieniasz (t/n)? n Wytypuj 3 z 10 liczb: ... Funkcja główna *************** Tam, gdzie w programie występują powtarzające się operacje lub zestaw poleceń realizujący wyodrębnione zadanie, wskazane jest używanie funkcji. Wyodrębnienie funkcji poprawia czytelność działania programu, ułatwia sprawdzanie i poprawianie kodu. Jeżeli zadania realizowane przez program umieszczamy w funkcjach, to jedną z nich trzeba wywołać na początku, aby program zaczął działać. Taka funkcja zwyczajowo nazywana jest ``main()`` i zazwyczaj zawiera logikę działania programu, tj. wywołuje inne funkcje wykonujące osobne zadania. W nowym pliku :file:`extra_lotek.py` umieszczamy początkowy kod: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. code:: def main(args): nick = input('Podaj nick: ') pass return 0 if __name__ == '__main__': import sys sys.exit(main(sys.argv)) Funkcja w Pythonie to wyodrębniony i nazwany blok kodu. Definicja funkcji ``def main(args):`` zawiera słowo kluczowe ``def``, nazwę funkcji z okrągłymi nawiasami, np. ``main()``, opcjonalne parametry, np. ``args`` oraz dwukropek. Instrukcja warunkowa ``if __name__ == '__main__':`` sprawdza, czy nasz plik został uruchomiony bezpośrednio przez interpreter, który zmienną specjalną ``__name__`` ustawia wtedy na ``__main__``. Jeżeli plik zostałby zaimportowany, zmienna ``__name__`` ustawiona zostałaby na nazwę pliku. Jeżeli plik wykonywany jest bezpośrednio, uruchamiana jest funkcja główna ``main(args)`` jako argument metody ``sys.exit()``, która kończy wykonywanie programu. Wartość zwracana przez funkcję główną informuje o tym, jak zakończył się program, np. zero (``0``) oznacza, że program zakończył się poprawnie. .. note:: W prostych programach pisanych na własny użytek można zrezygnować z bloku ``if __name__ == '__main__':``, a także usunąć argument ``args`` z definicji funkcji głównej. Trzeba jednak pamiętać o wywołaniu funkcji głównej, tj. umieszczeniu instrukcji ``main()`` bez wcięcia po definicji funkcji głównej. Instrukcja ``pass`` nie jest wykonywana, to informacja, że w danym miejscu zostanie wstawiony jakiś kod. Pozwala również uniknąć błędu w miejscach, w których brak kodu jest niedozwolony, np. po instrukcji warunkowej lub pętli. Moduł ****** Często używane funkcje umieszczamy w osobnych modułach (zob. :term:`moduł`), z których importujemy je do różnych programów za pomocą instrukcji ``import`` lub ``from ... import``. .. note:: Jeżeli program korzysta z niewielu i/lub unikalnych funkcji, możemy umieścić je na początku programu w jednym pliku. Wczytywanie ustawień -------------------- W katalogu ze skryptem naszego programu tworzymy nowy plik ``modul_lotek.py``, dodajemy importy modułów wbudowanych oraz funkcji wczytującej zapisanie ustawienia: .. raw:: html
Plik modul_lotek.pyKod nr
.. highlight:: python .. literalinclude:: modul_lotek.py :linenos: :lineno-start: 1 :lines: 1-14 Funkcja ``wczytaj_ustawienia()`` otrzymuje parametr ``nick``, który wykorzystujemy do zdefiniowania nazwy pliku z ustawieniami użytkownika. Za pomocą metody ``isfile()`` sprawdzamy, czy plik znajduje się w ścieżce przeszukiwania, czyli w praktyce w katalogu naszego programu głównego. Jeżeli tak, otwieramy go w domyślnym trybie "do odczytu" – ``with open(nazwa_pliku) as plik:`` – i udostępniamy w zmiennej ``plik``. Plik zawiera jeden wiersz w formacie ``nick;liczba losowanych liczb;maksymalna liczba;liczba typowań``. Do jego odczytania używamy metody ``readline()``. Z otrzymanego ciągu znaków usuwamy znaki końca wiersza przy użyciu metody ``strip()``. Następnie dzielimy tekst za pomocą metody ``split()`` i znaku średnika na części, które zwracane są w postaci listy ``dane``. Pobieranie ustawień -------------------- Do naszego modułu dodajemy kolejną funkcję o nazwie ``pobierz_ustawienia()``, której zadaniem będzie pobieranie danych wejściowych z klawiatury: .. raw:: html
Plik modul_lotek.pyKod nr
.. highlight:: python .. literalinclude:: modul_lotek.py :linenos: :lineno-start: 16 :lines: 16-36 Funkcja ``pobierz_ustawienia()`` wykorzystuje nieskończoną pętlę warunkową ``while True``, w której pobierane są kolejne dane wejściowe. W przypadku podania błędnych danych pętla zaczyna działanie od początku dzięki instrukcji ``continue``, jeżeli dane są poprawne przerywamy działanie pętli instrukcją ``break`` i zwracamy je w postaci listy ``dane``. .. note:: Wyjaśnienie instrukcji ``try ... except`` znajdziesz w materiale :ref:`Duży lotek ` w sekcji "Poprawność danych". Zapisywanie ustawień -------------------- Dodajemy do modułu funkcję ``zapisz_ustawienia()``, która pobierać będzie dwa parametry zawierające nick użytkownika i dane do zapisania. .. raw:: html
Plik modul_lotek.pyKod nr
.. highlight:: python .. literalinclude:: modul_lotek.py :linenos: :lineno-start: 38 :lines: 38-42 Początek funkcji jest prawie identyczny jak w funkcji ``wczytaj_ustawienia()`` z tą tylko różnicą, że plik otwierany jest w trybie do zapisu, tj, podajemy drugi argument w funkcji ``open()`` – znak ``w``. W efekcie, jeżeli pliku nie ma na dysku – zostanie zapisany, jeżeli jest – zostanie nadpisany. Parametr ``dane`` jest 4-elementową listą zawierająca pobrane od użytkownika lub odczytane z dysku dane wejściowe. Dane te łączymy za pomocą metody ``join()`` i znaku średnika. Otrzymany ciąg znaków uzupełniamy znakiem końca wiersza ``\n`` i zapisujemy w pliku za pomocą metody ``.write()``. Dane wejściowe ************** Wykorzystajmy przygotowane funkcje w programie głównym. Na początku pliku :file:`extra_lotek.py` importujemy dodane funkcje: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. literalinclude:: extra_lotek.py :linenos: :lineno-start: 1 :lines: 1 Następnie instrukcję ``pass`` zastępujemy poniższym kodem: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. literalinclude:: extra_lotek.py :linenos: :lineno-start: 7 :lines: 9-20 Po pobraniu nicka z klawiatury wywołujemy funkcję ``wczytaj_ustawienia()``, która zwróci listę z odczytanymi danymi lub wartość ``False``. W instrukcji warunkowej, jeżeli lista nie jest pusta (``if dane:``), wypisujemy dane oprócz nicka. Jeżeli nie udało się odczytać danych (``if not dane``) lub jeżeli użytkownik chce zmienić odczytane dane (``or input('Zmieniasz (t/n)? ').lower() == 't'``), wywołujemy funkcję ``pobierz_dane()``, a następnie funkcję ``zapisz_ustawienia()``, która zapisuje listę ``dane`` zawierającą nick i pozostałe informacje. W kolejnej instrukcji przypisujemy elementy listy ``dane`` do osobnych zmiennych. Do wskazania poszczególnych elementów listy używamy notacji wycinkowej. Instrukcja ``[int(x) for x in dane[1:4]]`` to :term:`wyrażenie listowe`. Można je rozumieć jako skrócony zapis pętli ``for``, która odczytuje trzy ostatnie elementy listy ``dane[1:4]``, zamienia je na liczby całkowite ``int(x)`` i tworzy nową listę. Jej elementy to liczby przypisywane do zmiennych ``m, maks`` i ``ile_typowan``. Losowanie i sprawdzanie *********************** Funkcje, które losują liczby do odgadnięcia, pobierają typy użytkownika, sprawdzają i wypisują wyniki umieszczamy w module :file:`modul_lotek.py`: .. raw:: html
Plik modul_lotek.pyKod nr
.. highlight:: python .. literalinclude:: modul_lotek.py :linenos: :lineno-start: 44 :lines: 44-84 Dodane instrukcje to kod omówiony w scenariuszu :ref:`Duży lotek ` przekształcony na funkcje. Warto zwrócić uwagę, że zwracają one wartości, tj. listę wylosowanych liczb ``liczby``, zbiór typów użytkownika ``typy`` oraz liczbę trafień ``len(trafione)``. Następnie w programie głównym uzupełniamy importy na początku pliku: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. literalinclude:: extra_lotek.py :linenos: :lineno-start: 1 :lines: 1-2 :emphasize-lines: 2 Następnie dopisujemy kod w funkcji głównej przed instrukcją ``return 0``: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. literalinclude:: extra_lotek.py :linenos: :lineno-start: 21 :lines: 21-30 Po wylosowaniu liczb wykorzystujemy pętlę ``for i in range(ile_typowan)``, aby użytkownik mógł kilka razy typować liczby. Wewnątrz pętli pobieramy typy, sprawdzamy i wypisujemy wyniki. Jeżeli liczba odgadniętych liczb, czyli wartość zwrócona przez funkcję ``wypisz_wyniki()`` równa jest liczbie losowanych liczb, kończymy pętlę instrukcją ``break``. Historia typowań ******************* Skoro umiemy już zapamiętywać wstępne ustawienia programu, możemy również zapamiętywać losowania i typowania użytkownika, tworząc rejestr do celów informacyjnych i/lub statystycznych. Przyjmijmy, że będziemy zapisywali dane ostatniego typowania i będą one zawierały: * ``czas`` – data typowania, * ``n`` – liczba losowanych liczb, * ``maks`` – maksymalna losowana liczba, * ``ile_typowan`` – liczba typowań, * ``ustawienia`` – krotka z liczbą losowanych liczb, liczbą maksymalną i liczbą typowań, * ``wylosowane`` – krotka z wylosowanymi liczbami, * ``ile_trafionych`` – liczba trafionych liczb. Do przechowywania tych informacji wykorzystamy słownik, w którym kluczami będą nazwy danych. Zaczniemy od dodania dwóch funkcji w pliku modułu: .. raw:: html
Plik modul_lotek.pyKod nr
.. highlight:: python .. literalinclude:: modul_lotek.py :linenos: :lineno-start: 86 :lines: 86-100 Kiedy czytamy i zapisujemy dane w plikach, ważną sprawą staje się ich format. Najprościej zapisywać dane jako znaki, tak jak zrobiliśmy to z ustawieniami, jednak często programy użytkowe potrzebują zapisywać złożone struktury danych, np. listy, zbiory czy słowniki. Znakowy zapis wymagałby wtedy wielu dodatkowych manipulacji, aby możliwe było poprawne odtworzenie informacji. Prościej jest skorzystać z *serializacji*, czyli zapisu danych obiektowych (zob. :term:`serializacja`). Często stosowany jest wtedy prosty format tekstowy `JSON `_. W funkcji ``wczytaj_dane()`` zawartość pliku o podanej nazwie dekoduje do listy: ``dane = json.load(plik)``, która jest zwracana. Funkcja ``zapisz_dane()`` otwiera plik o podanej nazwie w trybie do zapisu i zapisuje podaną listę danych w formacie JSON: ``json.dump(dane, plik)``. Ewentualne dane znajdujące się w pliku są nadpisywane. Pozostaje nam wykorzystać przygotowane funkcje w programie głównym. Na początku dodajemy importy: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. literalinclude:: extra_lotek.py :linenos: :lineno-start: 1 :lines: 1-3 :emphasize-lines: 3 Dalej na końcu funkcji ``main()``, ale przed instrukcją ``return 0`` dopisujemy: .. raw:: html
Plik extra_lotek.pyKod nr
.. highlight:: python .. literalinclude:: extra_lotek.py :linenos: :lineno-start: 31 :lines: 31-42 Do zmiennej ``losowania`` przypisujemy listę zwróconą przez funkcję ``wczytaj_dane()``. Następnie tworzymy słownik ``losowanie``, zawierający wszystkie omówione wyżej dane aktualnej "gry". Na uwagę zasługuje obsługa daty i czasu przy użyciu metod z podmodułu ``datetime``: * ``now()`` – metoda zwraca obiekt reprezentujący aktualną datę i czas, * ``strftime()`` – metoda pozwala przekształcić obiekt w ciąg znaków według ciągu formatującego ``%d.%m.%Y %H:%M:%S``, który oznacza zapis w postaci ``dzień.miesiąc.rok godzina:minuta:sekunda``, np.: ``21.10.2025 16:07:23``. .. note:: Serializacja JSON nie obsługuje obiektów typu ``datetime``, dlatego trzeba je zamieniać na ciąg znaków. Alternatywnie do pokazanego wyżej sposobu można użyć metody zwracającej datę w formacie ISO 8601, np. ``datetime.now().isoformat()``. Przygotowany słownik dodajemy do listy słowników: ``losowania.append(losowanie)``, którą wypisujemy w celach poglądowych ;-) i zapisujemy za pomocą funkcji ``zapisz_dane()``. Ćwiczenia ********* 1) Przećwicz w trybie interaktywnym notację wycinkową, wyrażenia listowe i łączenie list: .. code-block:: bash ~$ python3 >>> dane = ['a', 'b', 'c', '1', '2', '3'] >>> dane[0:3] >>> dane[3:6] >>> duze = [x.upper() for x in dane[0:3]] >>> kwadraty = [int(x)**2 for x in dane[3:6]] >>> duze + kwadraty 2) Sprawdź w trybie interaktywnym, jak wygląda znakowa reprezentacja aktualnej daty i czasu w formacie ISO 8601. 3) Załóżmy, że jednak chcielibyśmy zapisywać historię losowań w pliku tekstowym, którego poszczególne linie zawierałyby dane jednego losowania, np.: ``czas:2025-10-28T15:31:27.047438;n:2;maks:10;ile_typowan:2;wylosowane:(5, 7);trafionych:2`` Funkcja zapisująca dane mogłaby wyglądać np. tak: .. raw:: html
Kod nr
.. code:: python def zapisz_txt(nick, dane): """Funkcja zapisuje dane w formacie txt do pliku""" nazwa_pliku = nick + "_losowania.txt" with open(nazwapliku, "w") as plik: for slownik in dane: wiersz = [k + ":" + str(w) for k, w in slownik.iteritems()] wiersz = ";".join(wiersz) print >>plik, wiersz + '\n' Napisz funkcję ``czytaj_txt()`` odczytującą tak zapisane dane, która zwraca listę słowników. Użyj obydwu funkcji w programie ``extra_lotek.py`` tak, aby wypisywane były dane zwracane przez funkcję ``zapisz_txt()``. .. tip:: Nie musisz zastępować funkcji zapisujących dane w formacie JSON. .. admonition:: Pojęcia :term:`funkcja`, :term:`moduł`, :term:`notacja wycinkowa`, :term:`serializacja`