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:
#!/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:
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:
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 wypadkustr
(czyli konwersji na napis), do każdego elementu sekwencji, w tym wypadku zbiorutrafione
.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
:
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()
.
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
:
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łutime
!),"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
:
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:
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:
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: