1.3. Extra Lotek

Kod Toto Lotka wypracowany w dwóch poprzednich częściach wprowadził podstawy programowania w Pythonie: podstawowe typy danych (napisy, liczby, listy, zbiory), instrukcje sterujące (warunkową i pętlę) oraz operacje wejścia-wyjścia w konsoli. Uzyskany skrypt wygląda następująco:

Kod nr
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import random

try:
    ileliczb = int(input("Podaj ilość typowanych liczb: "))
    maksliczba = int(input("Podaj maksymalną losowaną liczbę: "))
    if ileliczb > maksliczba:
        print("Błędne dane!")
        exit()
except ValueError:
    print("Błędne dane!")
    exit()

liczby = []
i = 0
while i < ileliczb:
    liczba = random.randint(1, maksliczba)
    if liczby.count(liczba) == 0:
        liczby.append(liczba)
        i = i + 1

for i in range(3):
    print("Wytypuj %s z %s liczb: " % (ileliczb, maksliczba))
    typy = set()
    i = 0
    while i < ileliczb:
        try:
            typ = int(input("Podaj liczbę %s: " % (i + 1)))
        except ValueError:
            print("Błędne dane!")
            continue

        if 0 < typ <= maksliczba and typ not in typy:
            typy.add(typ)
            i = i + 1

    trafione = set(liczby) & typy
    if trafione:
        print("\nIlość trafień: %s" % len(trafione))
        print("Trafione liczby: ", trafione)
    else:
        print("Brak trafień. Spróbuj jeszcze raz!")

    print("\n" + "x" * 40 + "\n")  # wydrukuj 40 znaków x

print("Wylosowane liczby:", liczby)

1.3.1. Funkcje i moduły

Tam, gdzie w programie występują powtarzające się operacje lub zestaw poleceń realizujący wyodrębnione zadanie, wskazane jest używanie funkcji. Są to nazwane bloki kodu, które można grupować w ramach modułów (zob. funkcja, moduł). Funkcje zawarte w modułach można importować do różnych programów. Do tej pory korzystaliśmy np. z funkcji randit() zawartej w module random.

Wyodrębnienie funkcji ułatwia sprawdzanie i poprawianie kodu, ponieważ wymusza podział programu na logicznie uporządkowane kroki. Jeżeli program korzysta z niewielu funkcji, można umieszczać je na początku pliku programu głównego.

Tworzymy więc nowy plik totomodul.py i umieszczamy w nim następujący kod:

Kod nr
 1#!/usr/bin/env python3
 2# -*- coding: utf-8 -*-
 3
 4import random
 5
 6
 7def ustawienia():
 8    """Funkcja pobiera ilość losowanych liczb, maksymalną losowaną wartość
 9    oraz ilość prób. Pozwala określić stopień trudności gry."""
10    while True:
11        try:
12            ile = int(input("Podaj ilość typowanych liczb: "))
13            maks = int(input("Podaj maksymalną losowaną liczbę: "))
14            if ile > maks:
15                print("Błędne dane!")
16                continue
17            ilelos = int(input("Ile losowań: "))
18            return (ile, maks, ilelos)
19        except ValueError:
20            print("Błędne dane!")
21            continue
22
23
24def losujliczby(ile, maks):
25    """Funkcja losuje ile unikalnych liczb całkowitych od 1 do maks"""
26    liczby = []
27    i = 0
28    while i < ile:
29        liczba = random.randint(1, maks)
30        if liczby.count(liczba) == 0:
31            liczby.append(liczba)
32            i = i + 1
33    return liczby
34
35
36def pobierztypy(ile, maks):
37    """Funkcja pobiera od użytkownika jego typy wylosowanych liczb"""
38    print("Wytypuj %s z %s liczb: " % (ile, maks))
39    typy = set()
40    i = 0
41    while i < ile:
42        try:
43            typ = int(input("Podaj liczbę %s: " % (i + 1)))
44        except ValueError:
45            print("Błędne dane!")
46            continue
47
48        if 0 < typ <= maks and typ not in typy:
49            typy.add(typ)
50            i = i + 1
51    return typy

Funkcja w Pythonie składa się ze słowa kluczowego def, nazwy, obowiązkowych nawiasów okrągłych i opcjonalnych parametrów. Na końcu umieszczamy dwukropek. Funkcje zazwyczaj zwracają jakieś dane za pomocą instrukcji return.

Zmienne lokalne w funkcjach są niezależne od zmiennych w programie głównym, ponieważ definiowane są w różnych zasięgach, a więc w różnych przestrzeniach nazw. Możliwe jest modyfikowanie zmiennych globalnych dostępnych w całym programie, o ile wskażemy je w funkcji instrukcją typu: global nazwa_zmiennej.

Program główny po zmianach przedstawia się następująco:

Kod nr
 1#!/usr/bin/env python3
 2# -*- coding: utf-8 -*-
 3
 4from totomodul import ustawienia, losujliczby, pobierztypy
 5
 6
 7def main(args):
 8    # ustawienia gry
 9    ileliczb, maksliczba, ilerazy = ustawienia()
10
11    # losujemy liczby
12    liczby = losujliczby(ileliczb, maksliczba)
13
14    # pobieramy typy użytkownika i sprawdzamy, ile liczb trafił
15    for i in range(ilerazy):
16        typy = pobierztypy(ileliczb, maksliczba)
17        trafione = set(liczby) & typy
18        if trafione:
19            print("\nIlość trafień: %s" % len(trafione))
20            print("Trafione liczby: %s" % trafione)
21        else:
22            print("Brak trafień. Spróbuj jeszcze raz!")
23
24        print("\n" + "x" * 40 + "\n")  # wydrukuj 40 znaków x
25
26    print("Wylosowane liczby:", liczby)
27    return 0
28
29
30if __name__ == '__main__':
31    import sys
32    sys.exit(main(sys.argv))

Na początku z modułu totomodul, którego nazwa jest taka sama jak nazwa pliku, importujemy potrzebne funkcje. Następnie w funkcji głównej main() wywołujemy je podając nazwę i ewentualne argumenty. Zwracane przez nie wartości zostają przypisane podanym zmiennym.

Warto zauważyć, że funkcja może zwracać więcej niż jedną wartość naraz, np. w postaci tupli return (ile, maks, ilelos). Tupla to rodzaj listy, w której nie możemy zmieniać wartości (zob. tupla). Jest często stosowana do przechowywania i przekazywania danych, których nie należy modyfikować.

Wiele wartości zwracanych w tupli można jednocześnie przypisać kilku zmiennym dzięki operacji tzw. rozpakowania tupli: ileliczb, maksliczba, ilerazy = ustawienia(). Należy jednak pamiętać, aby ilość zmiennych z lewej strony wyrażenia odpowiadała ilości elementów w tupli.

Konstrukcja while True oznacza nieskończoną pętlę. Stosujemy ją w funkcji ustawienia(), aby wymusić na użytkowniku podanie poprawnych danych.

Cały program zawarty został w funkcji głównej main(). O tym, czy zostanie ona wykonana decyduje warunek if __name__ == '__main__':, który będzie prawdziwy, kiedy nasz skrypt zostanie uruchomiony jako główny. Wtedy nazwa specjalna __name__ ustawiana jest na __main__. Jeżeli korzystamy ze skryptu jako modułu, importując go, __main__ ustawiane jest na nazwę pliku, dzięki czemu kod się nie wykonuje.

Informacja

Komentarze: w rozbudowanych programach dobrą praktyką ułatwiającą późniejsze przeglądanie i poprawianie kodu jest opatrywanie jego fragmentów komentarzami. Zazwyczaj umieszczamy je po znaku #. Z kolei funkcje opatruje się krótkim opisem działania i/lub wymaganych argumentów, ograniczanym potrójnymi cudzysłowami. Notacja """...""" lub '''...''' pozwala zamieszczać teksty wielowierszowe.

1.3.1.1. Ćwiczenie

  • Przenieś kod powtarzany w pętli for (linie 17-24) do funkcji zapisanej w module programu.Wywołanie funkcji: iletraf = wyniki(set(liczby), typy) umieść w linii 17 programu głównego. Wykorzystaj szkielet funkcji:

def wyniki(liczby, typy):
    """Funkcja porównuje wylosowane i wytypowane liczby,
    zwraca ilość trafień"""
    ...

    return len(trafione)
  • Popraw wyświetlanie listy trafionych liczb. W funkcji wyniki() przed instrukcją print("Trafione liczby: %s" % trafione) wstaw: trafione = ", ".join(map(str, trafione)).

    Funkcja map() (zob. mapowanie funkcji) pozwala na zastosowanie jakiejś innej funkcji, w tym wypadku str (czyli konwersji na napis), do każdego elementu sekwencji, w tym wypadku zbioru trafione.

    Metoda napisów join() pozwala połączyć elementy listy (muszą być typu string) podanymi znakami, np. przecinkami (", ").

1.3.2. Zapis/odczyt plików

Uruchamiając wielokrotnie program, musimy podawać wiele danych, aby zadziałał. Dodamy więc możliwość zapamiętywania ustawień i ich zmiany. Dane zapisywać będziemy w zwykłym pliku tekstowym. W pliku toto2.py dodajemy tylko jedną zmienną nick:

Kod nr
8    # ustawienia gry
9    nick, ileliczb, maksliczba, ilerazy = ustawienia()

W pliku totomodul.py zmieniamy funkcję ustawienia() oraz dodajemy dwie nowe: czytaj_ust() i zapisz_ust().

Kod nr
 1#! /usr/bin/env python
 2# -*- coding: utf-8 -*-
 3
 4import random
 5import os
 6
 7
 8def ustawienia():
 9    """Funkcja pobiera nick użytkownika, ilość losowanych liczb, maksymalną
10    losowaną wartość oraz ilość typowań. Ustawienia zapisuje."""
11
12    nick = input("Podaj nick: ")
13    nazwapliku = nick + ".ini"
14    gracz = czytaj_ust(nazwapliku)
15    odp = None
16    if gracz:
17        print("Twoje ustawienia:\nLiczb: %s\nZ Maks: %s\nLosowań: %s" %
18              (gracz[1], gracz[2], gracz[3]))
19        odp = input("Zmieniasz (t/n)? ")
20
21    if not gracz or odp.lower() == "t":
22        while True:
23            try:
24                ile = int(input("Podaj ilość typowanych liczb: "))
25                maks = int(input("Podaj maksymalną losowaną liczbę: "))
26                if ile > maks:
27                    print("Błędne dane!")
28                    continue
29                ilelos = int(input("Ile losowań: "))
30                break
31            except ValueError:
32                print("Błędne dane!")
33                continue
34        gracz = [nick, str(ile), str(maks), str(ilelos)]
35        zapisz_ust(nazwapliku, gracz)
36
37    return gracz[0:1] + [int(x) for x in gracz[1:4]]
38
39
40def czytaj_ust(nazwapliku):
41    if os.path.isfile(nazwapliku):
42        plik = open(nazwapliku, "r")
43        linia = plik.readline()
44        plik.close()
45        if linia:
46            return linia.split(";")
47    return False
48
49
50def zapisz_ust(nazwapliku, gracz):
51    plik = open(nazwapliku, "w")
52    plik.write(";".join(gracz))
53    plik.close()
54
55

Operacje na plikach:

  • plik = open(nazwapliku, tryb) – otwarcie pliku w trybie "w" (zapis), „r” (odczyt) lub „a” (dopisywanie);

  • plik.readline() – odczytanie pojedynczej linii z pliku;

  • plik.write(napis) – zapisanie podanego napisu do pliku;

  • plik.close() – zamknięcie pliku.

Operacje na tekstach:

  • operator +: konkatenacja, czyli łączenie tekstów,

  • linia.split(";") – rozbijanie tekstu wg podanego znaku na elementy listy,

  • ";".join(gracz) – wspomniane już złączanie elementów listy za pomocą podanego znaku,

  • odp.lower() – zmiana wszystkich znaków na małe litery,

  • str(arg) – przekształcanie podanego argumentu na typ tekstowy.

W funkcji ustawienia() pobieramy nick użytkownika i tworzymy nazwę pliku z ustawieniami, następnie próbujemy je odczytać wywołując funkcję czytaj_ust(). Funkcja ta sprawdza, czy podany plik istnieje na dysku i otwiera go do odczytu. Plik powinien zawierać 1 linię, która przechowuje ustawienia w formacie: nick;ile_liczb;maks_liczba;ile_prób. Po jej odczytaniu i rozbiciu na elementy (linia.split(";")) zwracamy ją jako listę gracz.

Jeżeli uda się odczytać zapisane ustawienia, pytamy użytkownika, czy chce je zmienić. Jeżeli brak ustawień lub użytkownik chce je zmienić, pobieramy informacje, tworzymy z nich listę i przekazujemy do zapisania: zapisz_ust(nazwapliku, gracz).

Ponieważ w programie głównym oczekujemy, że funkcja ustawienia() zwróci dane typu napis, liczba, liczba, liczba – używamy konstrukcji: return gracz[0:1] + [int(x) for x in gracz[1:4]].

Na początku za pomocą notacji wycinkowej (ang. slice, notacja wycinkowa) tworzymy 1-elementową listę zawierającą nick użytkownika (gracz[0:1]). Pozostałe elementy z listy gracz (gracz[1:4]) umieszczamy w wyrażeniu listowym (wyrażenie listowe). Przy użyciu pętli przekształca ono każdy element na liczbę całkowitą i umieszcza w nowej liście.

Na końcu operator + ponownie tworzy nową listę, która zawiera wartości oczekiwanych typów.

1.3.2.1. Ćwiczenie

Przećwicz w konsoli notację wycinkową, wyrażenia listowe i łączenie list:

~$ 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

1.3.3. Słowniki

Skoro umiemy już zapamiętywać wstępne ustawienia programu, możemy również zapamiętywać losowania użytkownika, tworząc rejestr do celów informacyjnych i/lub statystycznych. Zadanie wymaga po pierwsze zdefiniowania jakieś struktury, w której będziemy przechowywali dane, po drugie zapisu danych albo w plikach, albo w bazie danych.

Na początku dopiszemy kod w programie głównym toto2.py:

Kod nr
 1#! /usr/bin/env python3
 2# -*- coding: utf-8 -*-
 3
 4from totomodul import ustawienia, losujliczby, pobierztypy, wyniki
 5from totomodul import czytaj_json, zapisz_json
 6import time
 7
 8
 9def main(args):
10    # ustawienia gry
11    nick, ileliczb, maksliczba, ilerazy = ustawienia()
12
13    # losujemy liczby
14    liczby = losujliczby(ileliczb, maksliczba)
15
16    # pobieramy typy użytkownika i sprawdzamy, ile liczb trafił
17    for i in range(ilerazy):
18        typy = pobierztypy(ileliczb, maksliczba)
19        iletraf = wyniki(set(liczby), typy)
20
21    nazwapliku = nick + ".json"  # nazwa pliku z historią losowań
22    losowania = czytaj_json(nazwapliku)
23
24    losowania.append({
25        "czas": time.time(),
26        "dane": (ileliczb, maksliczba),
27        "wylosowane": liczby,
28        "ile": iletraf
29    })
30
31    zapisz_json(nazwapliku, losowania)
32
33    print("\nLosowania:", liczby)
34    return 0
35
36
37if __name__ == '__main__':
38    import sys
39    sys.exit(main(sys.argv))

Dane graczy zapisywać będziemy w plikach nazwanych nickiem użytkownika z rozszerzeniem „.json”: nazwapliku = nick + ".json". Informacje o grach umieścimy w liście losowania, którą na początku zainicjujemy danymi o grach zapisanymi wcześniej: losowania = czytaj(nazwapliku).

Każda gra w liście losowania to słownik. Struktura ta pozwala przechowywać dane w parach „klucz: wartość”, przy czym indeksami mogą być napisy:

  • "czas" – będzie indeksem daty gry (potrzebny import modułu time!),

  • "dane" – będzie wskazywał tuplę z ustawieniami,

  • "wylosowane" – listę wylosowanych liczb,

  • "ile" – ilość trafień.

Na koniec dane ostatniej gry dopiszemy do listy (losowania.append()), a całą listę zapiszemy do pliku: zapisz(nazwapliku, losowania).

Teraz zobaczmy, jak wyglądają funkcje czytaj_json() i zapisz_json() w module totomodul.py:

Kod nr
102def czytaj_json(nazwapliku):
103    """Funkcja odczytuje dane w formacie json z pliku"""
104    dane = []
105    if os.path.isfile(nazwapliku):
106        with open(nazwapliku, "r") as plik:
107            dane = json.load(plik)
108    return dane
109
110
111def zapisz_json(nazwapliku, dane):
112    """Funkcja zapisuje dane w formacie json do pliku"""
113    with open(nazwapliku, "w") as plik:
114        json.dump(dane, plik)

Kiedy czytamy i zapisujemy dane, 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. serializacja). Często stosowany jest prosty format tekstowy JSON.

W funkcji czytaj() zawartość podanego pliki dekodujemy do listy: dane = json.load(plik). Funkcja zapisz() oprócz nazwy pliku wymaga listy danych. Po otwarciu pliku w trybie zapisu "w", co powoduje wyczyszczenie jego zawartości, dane są serializowane i zapisywane formacie JSON: json.dump(dane, plik).

Dobrą praktyką jest zwalnianie uchwytu do otwartego pliku i przydzielonych mu zasobów poprzez jego zamknięcie: plik.close(). Tak robiliśmy w funkcjach czytających i zapisujących ustawienia. Teraz jednak pliki otworzyliśmy przy użyciu konstrukcji typu with open(nazwapliku, "r") as plik:, która zadba o ich właściwe zamknięcie.

Przetestuj, przynajmniej kilkukrotnie, działanie programu.

1.3.3.1. Ćwiczenie

Załóżmy, że jednak chcielibyśmy zapisywać historię losowań w pliku tekstowym, którego poszczególne linie zawierałyby dane jednego losowania, np.: wylosowane:[4, 5, 7];dane:(3, 10);ile:0;czas:1434482711.67

Funkcja zapisująca dane mogłaby wyglądać np. tak:

Kod nr
def zapisz_str(nazwapliku, dane):
    """Funkcja zapisuje dane w formacie txt do pliku"""
    with open(nazwapliku, "w") as plik:
        for slownik in dane:
            linia = [k + ":" + str(w) for k, w in slownik.iteritems()]
            linia = ";".join(linia)
            # plik.write(linia+"\n") – zamiast tak, można:
            print >>plik, linia

Napisz funkcję czytaj_str() odczytującą tak zapisane dane. Funkcja powinna zwrócić listę słowników.

1.3.4. Materiały

Źródła:


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:

2025-04-12 o 10:21 w Sphinx 7.3.7

Autorzy:

Patrz plik „Autorzy”