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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import random


def ustawienia():
    """Funkcja pobiera ilość losowanych liczb, maksymalną losowaną wartość
    oraz ilość prób. Pozwala określić stopień trudności gry."""
    while True:
        try:
            ile = int(input("Podaj ilość typowanych liczb: "))
            maks = int(input("Podaj maksymalną losowaną liczbę: "))
            if ile > maks:
                print("Błędne dane!")
                continue
            ilelos = int(input("Ile losowań: "))
            return (ile, maks, ilelos)
        except ValueError:
            print("Błędne dane!")
            continue


def losujliczby(ile, maks):
    """Funkcja losuje ile unikalnych liczb całkowitych od 1 do maks"""
    liczby = []
    i = 0
    while i < ile:
        liczba = random.randint(1, maks)
        if liczby.count(liczba) == 0:
            liczby.append(liczba)
            i = i + 1
    return liczby


def pobierztypy(ile, maks):
    """Funkcja pobiera od użytkownika jego typy wylosowanych liczb"""
    print("Wytypuj %s z %s liczb: " % (ile, maks))
    typy = set()
    i = 0
    while i < ile:
        try:
            typ = int(input("Podaj liczbę %s: " % (i + 1)))
        except ValueError:
            print("Błędne dane!")
            continue

        if 0 < typ <= maks and typ not in typy:
            typy.add(typ)
            i = i + 1
    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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from totomodul import ustawienia, losujliczby, pobierztypy


def main(args):
    # ustawienia gry
    ileliczb, maksliczba, ilerazy = ustawienia()

    # losujemy liczby
    liczby = losujliczby(ileliczb, maksliczba)

    # pobieramy typy użytkownika i sprawdzamy, ile liczb trafił
    for i in range(ilerazy):
        typy = pobierztypy(ileliczb, maksliczba)
        trafione = set(liczby) & typy
        if trafione:
            print("\nIlość trafień: %s" % len(trafione))
            print("Trafione liczby: %s" % trafione)
        else:
            print("Brak trafień. Spróbuj jeszcze raz!")

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

    print("Wylosowane liczby:", liczby)
    return 0


if __name__ == '__main__':
    import sys
    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
9
    # ustawienia gry
    nick, ileliczb, maksliczba, ilerazy = ustawienia()

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

Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import random
import os


def ustawienia():
    """Funkcja pobiera nick użytkownika, ilość losowanych liczb, maksymalną
    losowaną wartość oraz ilość typowań. Ustawienia zapisuje."""

    nick = input("Podaj nick: ")
    nazwapliku = nick + ".ini"
    gracz = czytaj_ust(nazwapliku)
    odp = None
    if gracz:
        print("Twoje ustawienia:\nLiczb: %s\nZ Maks: %s\nLosowań: %s" %
              (gracz[1], gracz[2], gracz[3]))
        odp = input("Zmieniasz (t/n)? ")

    if not gracz or odp.lower() == "t":
        while True:
            try:
                ile = int(input("Podaj ilość typowanych liczb: "))
                maks = int(input("Podaj maksymalną losowaną liczbę: "))
                if ile > maks:
                    print("Błędne dane!")
                    continue
                ilelos = int(input("Ile losowań: "))
                break
            except ValueError:
                print("Błędne dane!")
                continue
        gracz = [nick, str(ile), str(maks), str(ilelos)]
        zapisz_ust(nazwapliku, gracz)

    return gracz[0:1] + [int(x) for x in gracz[1:4]]


def czytaj_ust(nazwapliku):
    if os.path.isfile(nazwapliku):
        plik = open(nazwapliku, "r")
        linia = plik.readline()
        plik.close()
        if linia:
            return linia.split(";")
    return False


def zapisz_ust(nazwapliku, gracz):
    plik = open(nazwapliku, "w")
    plik.write(";".join(gracz))
    plik.close()


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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from totomodul import ustawienia, losujliczby, pobierztypy, wyniki
from totomodul import czytaj_json, zapisz_json
import time


def main(args):
    # ustawienia gry
    nick, ileliczb, maksliczba, ilerazy = ustawienia()

    # losujemy liczby
    liczby = losujliczby(ileliczb, maksliczba)

    # pobieramy typy użytkownika i sprawdzamy, ile liczb trafił
    for i in range(ilerazy):
        typy = pobierztypy(ileliczb, maksliczba)
        iletraf = wyniki(set(liczby), typy)

    nazwapliku = nick + ".json"  # nazwa pliku z historią losowań
    losowania = czytaj_json(nazwapliku)

    losowania.append({
        "czas": time.time(),
        "dane": (ileliczb, maksliczba),
        "wylosowane": liczby,
        "ile": iletraf
    })

    zapisz_json(nazwapliku, losowania)

    print("\nLosowania:", liczby)
    return 0


if __name__ == '__main__':
    import sys
    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
102
103
104
105
106
107
108
109
110
111
112
113
114
def czytaj_json(nazwapliku):
    """Funkcja odczytuje dane w formacie json z pliku"""
    dane = []
    if os.path.isfile(nazwapliku):
        with open(nazwapliku, "r") as plik:
            dane = json.load(plik)
    return dane


def zapisz_json(nazwapliku, dane):
    """Funkcja zapisuje dane w formacie json do pliku"""
    with open(nazwapliku, "w") as plik:
        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:2022-05-22 o 19:52 w Sphinx 1.5.3
Autorzy:Patrz plik “Autorzy”