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 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:
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 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 9 | # ustawienia gry
nick, ileliczb, maksliczba, ilerazy = ustawienia()
|
W pliku totomodul.py
zmieniamy funkcję ustawienia()
oraz dodajemy
dwie nowe: czytaj_ust()
i zapisz_ust()
.
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
:
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ł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
:
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:
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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |