Zadanie - kwestionariusz

Zadanie - kwestionariusz
Photo by Nguyen Dang Hoang Nhu / Unsplash

Jednym z naszych pierwszych zadań było stworzenie kwestionariusza, ale możemy nazwać to też mini quizem.

Należało napisać program, który zbiera nasze odpowiedzi i zlicza punkty. Dla chętnych była też możliwość rozbudowania programu np. o możliwość dodania swoich pytań co też zrobiłam 😊 Wykorzystałam kawałek kodu który napisaliśmy na zajęciach (chodzi o utworzony słownik i dwa pierwsze pytania). Resztę starałam się pisać samodzielnie z pomocą dokumentacji Pythona.

Tak oto prezentuje się mój kod (u mnie działa! 😁):

# zadanie z kwestionariuszem i pytaniami
import random

print ("Witaj w mini quizie.\nDobra odpowiedź to 1 punkt, zła odejmuje 0,5 punktu.\nPowodzenia!")
punkty = 0

# gotowe pytania

kwestionariusz = {
    "Jak węże 'czują' zapachy?" : [
        ("a. Nie czują", -0.5),
        ("b. Nosem", -0.5),
        ("c. Językiem", 1)
      ],
    "Jaki wąż jest najdłuższy?" : [
        ("a. Pyton siatkowy", 1),
        ("b. Kobra", -0.5),
        ("c. Pyton królewski", -0.5)
      ],
    "Ile liter 'a' jest w tym pytaniu?" : [
        ("a. 1", -0.5),
        ("b. 2", 1),
        ("c. 3", -0.5)
    ],
    "Jakiego koloru jest niebo?" : [
        ("a. Zielone", -0.5),
        ("b. Różowe", -0.5),
        ("c. Niebieskie", 1)
    ],
    "Ile jest 2+3*2?" : [
        ("a. 8", 1),
        ("b. 6", -0.5),
        ("c. 10", -0.5)
    ]
}

# dodawanie pytania

dodaj = input("Zanim zaczniemy. Czy chcesz dodać swoje pytanie? [t/n]: ")

if dodaj == "t":
    nowe_pyt = input("Wpisz pytanie: ")
    # odp1 = input("Odpowiedź a: ")
    # pkt1 = float(input("Punkty: "))
    # odp2 = input("Odpowiedź b: ")
    # pkt2 = float(input("Punkty: "))
    # odp3 = input("Odpowiedź c: ")
    # pkt3 = float(input("Punkty: "))

    # kwestionariusz[nowe_pyt] = [
    #     (f"a. {odp1}", pkt1),
    #     (f"b. {odp2}", pkt2),
    #     (f"c. {odp3}", pkt3)
    # ]
    odpowiedzi = []

    for litera in ["a", "b", "c"]:
        tekst = input(f"Odpowiedź {litera}: ")
        punkty = float(input("Punkty: "))
        odpowiedzi.append((f"{litera}. {tekst}", punkty))
    
    kwestionariusz[nowe_pyt] = odpowiedzi
    print("Dodano nowe pytanie.")

else:
    print("OK. W takim razie nie dodajemy nowego pytania.")

# mieszanie pytan

lista_pytan = list(kwestionariusz.items())
random.shuffle(lista_pytan)

for pytanie, odp in lista_pytan:
# for pytanie in kwestionariusz:
    print(pytanie)
    # odp = kwestionariusz[pytanie]
    # for i in range(len(odp)):
    #     print(odp[i][0])
    for odpowiedz in odp:
        print(odpowiedz[0])

# liczenie punktow

    wybor = input("Wybierz poprawną odpowiedź (a/b/c): ").lower()
    mapa_odp = {"a": 0, "b": 1, "c": 2}

    if wybor in mapa_odp:
        indeks = mapa_odp[wybor]
        punkty += odp[indeks][1]
    # if wybor == "a":
    #     punkty += odp[0][1]
    # elif wybor == "b":
    #     punkty += odp[1][1]
    # elif wybor == "c":
    #     punkty += odp[2][1]
    else:
        print("Nie ma takiej odpowiedzi!")
        punkty -= 100

print(f"Twój wynik to: {punkty} punktów.")

Teraz może pokrótce wytłumaczę co tu się właściwie wydarzyło.

Początek programu

Program rozpoczyna się od import random. Import to instrukcja, która pozwala nam załadować (importować) zewnętrzne moduły (biblioteki) czyli dodatkowe funkcje i narzędzia. W tym przypadku wykorzystanym modułem jest moduł random, który zawiera definicje funkcji związane z losowością (np. shuffle() ) co pomaga nam w mieszaniu kolejności wyświetlanych obiektów. Moduły możemy stworzyć sami lub możemy skorzystać z tych już istniejących, które napisane zostały przez innych programistów (czasem są one już wbudowane, czasem musimy je sami zainstalować). Dzięki modułom możemy zaoszczędzić czas i miejsce, ponieważ nie musimy pisać wszystkiego od zera.

W dalszej części program drukuje powitanie oraz tworzy zmienną punkty z przypisaną wartością 0. Będzie to potrzebne później przy zliczeniu punktów za udzielone odpowiedzi.

Kwestionariusz

Dalej mamy kwestionariusz. W pewnym sensie główna część programu, można nazwać to mini quizem.

kwestionariusz = {
          .
          .
          .
    "Ile jest 2+3*2?" : [
        ("a. 8", 1),
        ("b. 6", -0.5),
        ("c. 10", -0.5)
    ]
}

Tutaj wycięłam kawałek kodu aby lepiej wytłumaczyć o co chodzi.

Został stworzony słownik o nazwie kwestionariusz. Słownik jest jedną z podstawowych struktur danych w Pythonie. W słowniku przechowujemy dane w postaci klucz : wartość. Tutaj kluczem jest nasze pytanie "Ile jest 2+3*2?" natomiast wartością jest lista trzech krotek, które zawierają możliwe odpowiedzi oraz punktację. Spójrzmy na pierwszą z nich: ("a. 8", 1). Oznacza ona, że ta odpowiedź daje nam 1 punk (czyli jest poprawna 😉). Każda krotka oznaczona jest nawiasami (). Należy pamiętać, że krotkiniemutowalne - oznacza to tyle, że nie można zmieniać ich wartości po utworzeniu. Dostęp do danych w krotce uzyskujemy przez rozpakowywanie lub indeksowanie (co zostanie wytłumaczone później).

Dodawanie pytań

Następnie dopisałam możliwość dodawania pytań przez użytkownika. Zrobiłam to na dwa sposoby. W obu wykorzystałam funkcję input() oraz instrukcję if. Dałam użytkownikowi możliwość decyzji czy chce ("t") czy nie chce ("n") dodać pytania. Jego odpowiedź zostanie zapisana w zmiennej dodaj. Spójrzmy na kod pierwszego sposobu:

if dodaj == "t":
    nowe_pyt = input("Wpisz pytanie: ")
    odp1 = input("Odpowiedź a: ")
    pkt1 = float(input("Punkty: "))
    odp2 = input("Odpowiedź b: ")
    pkt2 = float(input("Punkty: "))
    odp3 = input("Odpowiedź c: ")
    pkt3 = float(input("Punkty: "))

    kwestionariusz[nowe_pyt] = [
        (f"a. {odp1}", pkt1),
        (f"b. {odp2}", pkt2),
        (f"c. {odp3}", pkt3)
    ]

    print("Dodano nowe pytanie.")

else:
    print("OK. W takim razie nie dodajemy nowego pytania.")

Zacznijmy od sytuacji w której użytkownik nie chce dodać pytania, czyli wpisze "n" lub jakąkolwiek inną literę/słowo/zdanie/cyfrę. W takiej sytuacji program przechodzi do bloku else i wyświetli odpowiednią informację, pomijając dodawanie pytania. Jeżeli jednak użytkownik wpisze "t" program wykona kod w bloku if.

Najpierw użytkownik zostanie poproszony o wpisanie treści pytania (zmienna nowe_pyt), następnie będzie musiał wpisać trzy odpowiedzi (kolejno zmienne odp1, odp2, odp3) oraz punktów za każdą z nich (zmienne pkt1, pkt2, pkt3). Wartości punktowe zostają konwertowane na typ float, ponieważ mogą być ułamkami (należy pamiętać, że wszystko co wpisane przez użytkownika, jest traktowane jako tekst, więc jeżeli zależy nam aby liczby nie były tekstem musimy dokonać konwersji na int lub float lub inne klasy). Następnie nowy element (czyli nasze pytanie z odpowiedziami i przypisanymi punktami) zostaje dodany jako nowy element do słownika kwestionariusz. Treść pytania staje się kluczem a odpowiedzi i punktacja stają się wartością czyli listą trzech krotek. Zastosowałam tutaj f-string (f"a. {odp1}, pkt1), do automatycznego oznaczenia odpowiedzi (a., b., c.,) przed treścią wpisaną przez użytkownika - dzięki temu nie musi tego robić samodzielnie. Na końcu instrukcji drukowane jest zdanie "Dodano nowe pytanie." aby potwierdzić użytkownikowi, że wszystko wykonało się w sposób prawidłowy i pytanie zostało dodane do naszej "bazy pytań".

Drugi sposób jest trochę bardziej zwięzły i przejrzysty. Pozwala na szybsze dostosowanie kodu w przypadku np. potrzeby dodania większej liczby odpowiedzi albo innych ewentualnych zmian. Spójrzmy:

if dodaj == "t":
    nowe_pyt = input("Wpisz pytanie: ")
    odpowiedzi = []

    for litera in ["a", "b", "c"]:
        tekst = input(f"Odpowiedź {litera}: ")
        punkty = float(input("Punkty: "))
        odpowiedzi.append((f"{litera}. {tekst}", punkty))
    
    kwestionariusz[nowe_pyt] = odpowiedzi
    print("Dodano nowe pytanie.")

Początek jest taki sam jak w poprzednim przykładzie - poprzez input prosimy użytkownika o dodanie pytania. Dalej tworzymy pustą listę odpowiedzi = [] do której będziemy dodawać krotki. Tworzymy je w pętli for (jest zagnieżdżona, zwróćmy uwagę na wcięcia), która automatycznie przechodzi przez kolejne kroki które nazwałam "a", "b" i "c". Dzięki temu nie muszę powtarzać kodu jak w przypadku przykładu wyżej. W każdej iteracji prosimy użytkownika o treść odpowiedzi oraz punktację, którą od razu konwertujemy na typ float, dopuszczając wartości ułamkowe. Każda odpowiedź z punktacją trafia do listy dzięki odpowiedzi.append(...), która dodaje te elementy na końcu istniejącej już listy. Gdy pętla się zakończy (w tym wypadku po 3 iteracjach) przechodzimy do kwestionariusz[nowe_pyt] = odpowiedzi. W ten sposób cała lista odpowiedzi zostaje przypisana jako wartość w słowniku kwestionariusz, a kluczem jest nowo dodane pytanie. Na koniec informujemy użytkownika, że pytanie zostało dodane. Gotowe 😀

Zmienianie kolejności wyświetlanych pytań

Dalej pojawia nam się kod który pozwala na pomieszanie pytań. Aby pytania w quizie nie pojawiały się zawsze w tej samej kolejności (czyli w takiej jakiej zostały utworzone) zastosowałam funkcję shuffle() z modułu random, który został zaimportowany na początku programu. Pojawia się tutaj jednak jeden problem - funkcja ta działa jedynie na listach. Nie jest to jednak coś czego nie da się obejść. Przyjrzyjmy się temu (i zignorujmy to co napisane jest z # w kodzie głównym, to tylko komentarz i pokazuje, że próbowałam też czegoś innego 😅):

lista_pytan = list(kwestionariusz.items())
random.shuffle(lista_pytan)

for pytanie, odp in lista_pytan:
    print(pytanie)
    for odpowiedz in odp:
        print(odpowiedz[0])

Utworzyłam zmienną lista_pytan do której przypisałam list(kwestionariusz.items()). W ten sposób przekształciłam słownik kwestionariusz w listę. Samo kwestionariusz.items() zwraca pary (w tym przypadku pytanie i odpowiedzi) ze słownika a funkcja list(...) zmienia to na listę krotek. Kolejna instrukcja miesza pytania. Samo pomieszanie pytań nie sprawia jeszcze, że pytania będą automatycznie wyświetlone - musimy to zrobić sami w kodzie. By program po uruchomieniu wydrukował pytania i poprosił użytkownika o odpowiedź zastosowałam pętlę for.

Pierwsza pętla for pytanie, odp in lista_pytan oznacza, że iteracja pobiera tekst pytania oraz listę trzech odpowiedzi razem z punktami. Druga pętla for odpowiedz in odp przechodzi przez każdą z tych krotek i drukuje jedynie pytania bez punktacji. Dlaczego? Ponieważ odwołujemy się do odpowiedz[0], czyli do części tekstowej. Każda krotka zawiera dwie informacje: treść odpowiedzi oraz przypisaną punktacje i każda ma swój "adres", czyli indeksy gdzie 0 jest dla tekstu a 1 dla liczby punktów. Gdybyśmy chcieli wypisać tylko punkty wpisalibyśmy odpowiedz[1].

Warto zwrócić uwagę, że druga pętla jest wpisana w pierwszą pętlę. Nazywamy to zagnieżdżeniem pętli. Dzięki temu każde pytanie wyświetli swoje odpowiedzi. W innym wypadku program wypisałby wyłącznie pytania i odpowiedzi jedynie do pytania które wyświetliłoby się jako ostatnie.

Zliczanie punktów

Teraz przejdziemy do zliczania punktów. Zliczanie również jest zagnieżdżone w pętli! Można to zrobić na dwa sposoby. Zanim jednak przejdziemy do liczenia spójrzmy na ten kawałek kodu:

    wybor = input("Wybierz poprawną odpowiedź (a/b/c): ").lower()

Utworzona zostaje zmienna wybor do której przypisana zostaje odpowiedź użytkownika. Zaznaczone zostało, żeby użytkownik wpisał jedynie litery a, b lub c, bo tylko te będą interpretowane przez program. W razie czego zastosowałam .lower() ponieważ zdarza się, że użytkownik ma włączony caps lock. Chciałabym uniknąć sytuacji gdzie np. litera "A" nie zostaje rozpoznana przez program i tym samym nie zaliczy poprawnej odpowiedzi. Musimy pamiętać, że Python jest case-sensitive, rozróżnia on zatem wielkie i małe litery, i może je traktować jako inne instrukcje (zarówno podczas pisania kodu jak i jego egzekucji).

Następnie przechodzimy do zliczania punktów. Pierwsza wersja jest bardziej "klasyczna". Spójrzmy na kod:

    if wybor == "a":
        punkty += odp[0][1]
    elif wybor == "b":
        punkty += odp[1][1]
    elif wybor == "c":
        punkty += odp[2][1]
    else:
        print("Nie ma takiej odpowiedzi!")
        punkty -= 100

Ta wersja zliczania punktów działa poprawnie, ale wymaga od programisty pisania wielu niemal identycznych linijek powtarzającego się kodu. To zwiększa ryzyko pomyłek, utrudnia wprowadzanie zmian i wydłuża kod, co zmniejsza jego czytelność i utrudnia utrzymanie programu, szczególnie przy większej liczbie opcji lub bardziej skomplikowanych projektach. W tym przypadku mam tylko 3 odpowiedzi więc nie stanowi to dużego problemu, ale gdybym chciała dodać więcej odpowiedzi (np. d, e, ...) musiałabym ręcznie dopisywać kolejne bloki elif co jest mało efektywne. Ale przejdźmy do kodu. Co się tutaj dzieje? Program "sprawdza" po kolei czy odpowiedź wpisana przez użytkownika spełnia warunek czyli czy wybor odpowiada jednej z liter a, b, c. Załóżmy, że użytkownik udzielił odpowiedzi b, program sprawdzi wtedy pierwsze if, ale warunek nie został spełniony więc je pomija. Program przechodzi do drugiego warunku elif, który jest już prawdziwy więc przechodzi do zapisanej instrukcji. Przypisanie punktów poprzez instrukcje punkty += odp[1][1] oznacza, że program pobiera drugą (indeksowanie rozpoczyna się od zera) krotkę z listy odpowiedzi i z tej krotki bierze drugi element czyli liczbę punktów. Właśnie ta liczba punktów zostaje dodana do zmiennej punkty.

Druga wersja, preferowana przeze mnie to:

    mapa_odp = {"a": 0, "b": 1, "c": 2}
    if wybor in mapa_odp:
        indeks = mapa_odp[wybor]
        punkty += odp[indeks][1]
    else:
        print("Nie ma takiej odpowiedzi!")
        punkty -= 100

W tej wersji tworzę słownik mapa_odp, który przypisuje literkom odpowiadające im indeksy w liście odpowiedzi. W ten sposób możliwe jest dynamiczne pobieranie indeksów odpowiedzi bez powtarzania kodu i nie ma potrzeby pisać osobnych instrukcji if dla każdej opcji. Zatem, jeżeli użytkownik wpisze "b", to mapa_odp["b"] zwróci 1, czyli indeks drugiej odpowiedzi. Następnie dodajemy punkty przypisane do tej odpowiedzi, czyli program sam dopisuje indeks, więc przy odpowiedzi b będzie to punkty += odp[1][1]. Tak więc wszystko działa dalej tak samo jak w kodzie wyżej.

Zaletą tego rozwiązania jest elastyczność i zwięzłość. W przypadku gdyby odpowiedzi było więcej wystarczy jedynie rozbudować słownik bez konieczności dopisywania linijek kodu.

W obu wersjach instrukcja else odejmuje 100 punktów - użytkownicy lubią testować cierpliwość programistów więc uważam, że to odpowiednia kara za nieposłuszeństwo 😊

Koniec programu

Po przejściu przez wszystkie pytania program drukuje komunikat z sumą otrzymanych punktów która zapisana jest w zmiennej punkty.

Jest to prosty program, który w przyszłości można rozbudować poprzez np. możliwość zapisania dodanych pytań przez użytkowników do pliku z którego byłyby one sczytywane. Jest to jednak wiedza której jeszcze nie posiadłam, więc zatrzymam się na tym.

Tak że... to by było na tyle.

Dziękuję za dotarcie do końca mojego tłumaczenia. Mam nadzieję, że było ono jasne i zrozumiałe, i pozwoliło na utrwalenie wiedzy z zajęć 😄