1.4. 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 python
# -*- coding: utf-8 -*-

import random

try:
    ileliczb = int(raw_input("Podaj ilość typowanych liczb: "))
    maksliczba = int(raw_input("Podaj maksymalną losowaną liczbę: "))
    if ileliczb > maksliczba:
        print "Błędne dane!"
        exit()
except:
    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(raw_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.4.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 python
# -*- 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(raw_input("Podaj ilość typowanych liczb: "))
            maks = int(raw_input("Podaj maksymalną losowaną liczbę: "))
            if ile > maks:
                print "Błędne dane!"
                continue
            ilelos = int(raw_input("Ile losowań: "))
            return (ile, maks, ilelos)
        except:
            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(raw_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. Funkcje zazwyczaj zwracają jakieś dane za pomocą instrukcji return.

Warto zauważyć, że można 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 stałych danych.

Nazwy zmiennych lokalnych w funkcjach są niezależne od nazw 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 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 python
# -*- 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.

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.

Funkcja główna main() zostaje wywołana, o ile warunek if __name__ == '__main__': jest prawdziwy. Jest on prawdziwy wtedy, 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.

Note

W rozbudowanych programach dobrą praktyką ułatwiającą późniejsze przeglądanie i poprawianie kodu jest opatrywanie jego fragmentów komentarzami. Można je umieszczać 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.4.1.1. Ćwiczenie

  • Przenieś kod powtarzany w pętli for (linie 17-24) do funkcji zapisanej w module programu i nazwanej np. wyniki(). Zdefiniuj listę argumentów, zadbaj, aby funkcja zwracała ilość trafionych liczb. Wywołanie funkcji: iletraf = wyniki(set(liczby), typy) umieść w linii 17.

  • Przy okazji popraw wyświetlanie listy trafionych liczb. Przed instrukcją print "Trafione liczby: %s" % trafione wstaw linię: 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.4.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 = raw_input("Podaj nick: ")
    nazwapliku = nick + ".ini"
    gracz = czytaj_ust(nazwapliku)
    odp = None
    if gracz:
        print "Twoje ustawienia:"
        print "Liczb:", gracz[1]
        print "Z Maks:", gracz[2]
        print "Losowań:", gracz[3]
        odp = raw_input("Zmieniasz (t/n)? ")

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

    return gracz[0:1] + map(int, gracz[1:4])


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


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

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 = open(nazwapliku, "r"). Plik powinien zawierać 1 linię, która przechowuje ustawienia w formacie: nick;ile_liczb;maks_liczba;ile_prób. Po jej odczytaniu za pomocą metody .readline() i rozbiciu na elementy zwracamy ją jako listę gracz.

Jeżeli uda się odczytać zapisane ustawienia, drukujemy je, a następnie pytamy, czy użytkownik chce je zmienić. Jeżeli nie znaleźliśmy zapisanych ustawień lub użytkownik nacisnął klawisz “t” lub “T”, wykonujemy poprzedni kod. Na koniec zmiennej gracz przypisujemy listę ustawień przekazaną do zapisu funkcji zapisz_ust(). Funkcja ta zapisuje dane złączone za pomocą średnika w jedną linię do pliku: plik.write(";".join(gracz)).

W powyższym kodzie widać, jakie operacje można wykonywać na tekstach, tj.:

  • operator +: łą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.

Zwróćmy uwagę na konstrukcję return gracz[0:1] + map(int, gracz[1:4]), której używamy, aby zwrócić odczytane/zapisane ustawienia do programu głównego. Dane w pliku przechowujemy, a także pobieramy od użytkownika jako znaki. Natomiast program główny oczekuje 4 wartości typu: znak, liczba, liczba, liczba. Stosujemy więc notację wycinkową (ang. slice), aby wydobyć nick użytkownika: gracz[0:1]. Pierwsza wartość mówi od którego elementu, a druga do którego elementu wycinamy wartości z listy (przećwicz w konsoli Pythona!). Wspominana już funkcja map() pozwala zastosować do pozostałych 3 elementów (gracz[1:4]) funkcję int(), która zamienia je w wartości liczbowe.

1.4.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 python
# -*- 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"
    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
103
104
105
106
107
108
109
110
111
112
113
114
115
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.4.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.4.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:2017-09-08 o 19:38 w Sphinx 1.4.5
Autorzy:Patrz plik “Autorzy”