Reaktionszeittest misst etwas, aber keine Reaktionszeiten

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Hallo

Bei meinem aktuellen Übungsprojekt geht es um die Reaktion des Benutzers. Aus einer Liste wird jede Sekunde zufällig eine Farbe für den Bildschirmhintergrund ausgewählt und geändert. Sobald der Bildschirmhintergrund rot wird, soll die Startzeit gespeichert und die Reaktionszeit bis der Benutzer die Leertaste drückt in einer Liste gespeichert werden.

Mehrere Sachen funktionieren leider nicht:
  • In die Liste wird immer etwas geschrieben, egal ob Tastendruck oder nicht.
  • Der in der Liste geschriebene Wert ist viel zu niedrig für eine Reaktionszeit.
Der fehlerhafte Code sieht so aus:

Code: Alles auswählen

import tkinter as tk
from random import choice
import time


switch_time = 1000
reaction_times = []


class Application:

    def __init__(self, app_win):
        self.app_win = app_win
        self.width = app_win.winfo_screenwidth()
        self.height = app_win.winfo_screenheight()
        self.colors = ["green", "red", "blue", "yellow", "black"]
        self.current_color = "red"
        self.color = ""
        self.canvas = tk.Canvas(app_win, width=self.width, height=self.height,
                                highlightthickness=0)
        self.canvas.pack()
        self.change_bg_color(self.current_color)


    def change_bg_color(self, current_color):
        global reaction_times
        self.color = choice(self.colors)

        while self.color == current_color:
            self.color = choice(self.colors)

        if self.color == "red":
            self.start_time = time.monotonic()
            print("Startzeit:", self.start_time)

            if self.key_pressed() == True:
                print("Rot und Taste gedrückt bei Stoppzeit:", time.monotonic())
                reaction_time = time.monotonic() - self.start_time
                reaction_times.append(reaction_time)
                print(reaction_times)


        self.canvas.config(bg = self.color)
        self.app_win.after(switch_time, self.change_bg_color, self.color)


    def key_pressed(self):
        return True


def main():
    app_win = tk.Tk()
    app_win.attributes("-fullscreen", True)
    app_win.bind("<Escape>", lambda e: app_win.quit())
    app = Application(app_win)
    app_win.bind("<space>", lambda e: app.key_pressed())
    app_win.mainloop()


if __name__ == '__main__':
    main()
Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Da `key_pressed()` immer `True` liefert wird ganz offensichtlich die Laufzeit der drei Zeilen Code zwischen dem ermitteln der Startzeit und dem ermitteln von `reaction_time` gemessen.

Ein Druck auf die Leertaste bewirkt letztlich gar nichts, denn da wird ja nur ``return True`` ausgeführt, wobei dieses `True` an den Aufrufer, also die GUI-Hauptschleife zurückgegeben wird, die das ignoriert, weil sie keine Rückgabewerte von so einer Ereignisbehandlung erwartet.

In dieser Behandlung der Leertaste müsste die Prüfung erfolgen ob die Farbe gerade rot ist und dann die Ermittlung der Zeit. Die Startzeit würde ich einfach bei jedem Farbwechsel setzen.

Ich vermute ich habe schon mal was zu ``global`` gesagt‽ Falls nicht: Vergiss das es ``global`` gibt! Insbesondere wenn Du eh schon objektorientiert programmierst gibt es dafür so gar keine Ausrede.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Atalanttore: Dir fehlt noch das Verständnis, wie Ereignisgesteuerte Programmierung funktioniert. Die Ereignisse sind bei Dir der Timer und der Tastendruck. Die Ereignisbehandlung ist dann Farbe wechseln, bzw. Zeitdauer speichern. Du machst beim einen Ereignis beides und beim anderen nichts.

Außer Konstanten und Definitionen sollte nichts auf oberster Ebene stehen, Konstanten schreibt man komplett groß: SWITCH_TIME. reaction_times sollte ein Attribut einer Applicationinstanz sein.

self.current_color sollte eine lokale Variable sein; was ist der semantische Unterschied zu self.color?
self.start_time wird erst in change_bg_color eingeführt, sollte es aber schon in __init__.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Danke für die Erklärungen. Damit macht der Reaktionszeittest jetzt was er soll und ich habe die Verarbeitung der Tastendrücke sogar noch ausgebaut.

Was haltet ihr von dem Code?

Code: Alles auswählen

import tkinter as tk
from random import choice
import time
from statistics import mean


SWITCH_TIME = 1000
DESIGNATED_COLOR = "red"



class Application:

    def __init__(self, app_win):
        self.app_win = app_win
        self.width = app_win.winfo_screenwidth()
        self.height = app_win.winfo_screenheight()
        self.colors = ["green", "red", "blue", "yellow", "black"]
        self.current_color = DESIGNATED_COLOR
        self.color = ""
        self.reaction_times = []
        self.false_positives = 0
        self.color_missed = 0
        self.key_too_often_pressed = 0
        self.key_pressed_in_time = True
        self.color_locked = False
        self.canvas = tk.Canvas(app_win, width=self.width, height=self.height,
                                highlightthickness=0)
        self.canvas.pack()
        self.change_bg_color(self.current_color)


    def change_bg_color(self, current_color, color_locked = False):
        self.color_locked = color_locked
        self.color = choice(self.colors)

        # Farbauswahl nochmal ausführen, wenn zufällig
        # ausgewählte Farbe gleich letzter Farbe
        while self.color == current_color:
            self.color = choice(self.colors)

        self.canvas.config(bg=self.color)
        self.start_time = time.monotonic() # Ereignis Timer
        self.app_win.after(SWITCH_TIME, self.check_for_key_press)


    def check_for_key_press(self):
        if self.color_locked == False and self.color == DESIGNATED_COLOR:
            self.color_missed += 1
        self.change_bg_color(self.color)


    def key_pressed(self):
        if self.color == DESIGNATED_COLOR and self.color_locked == True:
            self.key_too_often_pressed += 1
        elif self.color == DESIGNATED_COLOR:
            reaction_time = time.monotonic() - self.start_time
            self.reaction_times.append(reaction_time)
            self.color_locked = True
        elif self.color != DESIGNATED_COLOR:
            self.false_positives += 1


    def end_app(self):
        if len(self.reaction_times) >= 1:
            # mean() auf Liste ohne Einträge führt zu einem Fehler
            print("Gemittelte Reaktionszeit:", mean(self.reaction_times))
        print("Reaktionszeit pro richtiger Farbe:", self.reaction_times)
        print("Bei richtiger Farbe zu oft gedrückt:", self.key_too_often_pressed)
        print("Bei richtiger Farbe nicht gedrückt:", self.color_missed)
        print("Bei falscher Farbe gedrückt:", self.false_positives)
        self.app_win.quit()


def main():
    app_win = tk.Tk()
    app_win.attributes("-fullscreen", True)
    app_win.bind("<Escape>", lambda e: app.end_app())
    app = Application(app_win)
    app_win.bind("<space>", lambda e: app.key_pressed()) # Ereignis Tastendruck
    app_win.mainloop()


if __name__ == '__main__':
    main()
Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich denke es gibt zu viele Attribute. `width` und `height` beispielsweise. `key_pressed_in_time` wird nie benutzt. `current_color` wird in der `__init__()` gesetzt und in der gleichen Methode wird einmal lesend darauf zugegriffen und sonst nirgends‽

`change_bg_color()` bekommt unnötigerweise `current_color` übergeben, wo die Method doch ganz einfach `self.color` selbst an einen lokalen Namen binden könnte.

Das optionale `color_locked`-Argument wird nie bei einem Aufruf verwendet.

Die Zufallsfarbe würde ich ohne `choice()` an zwei Stellen aufrufen zu müssen ermitteln. In einer ”Endlosschleife” einfach so oft `choice()` aufrufen bis man eine neue, andere Farbe gefunden hat, und die Schleife dann mit ``break`` verlassen.

Vergleiche mit literalen `True`- und `False`-Werten ergeben nur wieder einen Wahrheitswert. Den hatte man ja schon. Also kann man den auch gleich direkt verwenden, oder mit ``not`` negieren wenn man auf das Gegenteil testen möchte.

Die Tests in `key_pressed()` sind IMHO ungünstig aufgeteilt. Statt drei Zweige auf oberster Ebene zu haben, würde ich das wohl eine Ebene tiefer verschachteln.

``if len(self.reaction_times) >= 1:`` würde man kürzer als ``if self.reaction_times:`` schreiben.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Vielen Dank für die Analyse. Sehr hilfreich zum Lernen.
__blackjack__ hat geschrieben: Samstag 14. Juli 2018, 19:56 `change_bg_color()` bekommt unnötigerweise `current_color` übergeben, wo die Method doch ganz einfach `self.color` selbst an einen lokalen Namen binden könnte.
Ich habe vor einiger Zeit gelesen/gehört, dass eine Methode alle nötigen Werte als Argumente übergeben bekommen soll und nicht auf Variablen außerhalb der Methode zugreifen sollte. Bei meinen bisherigen Übungsprojekten habe ich versucht mich einigermaßen daran zu halten.

__blackjack__ hat geschrieben: Samstag 14. Juli 2018, 19:56 ``if len(self.reaction_times) >= 1:`` würde man kürzer als ``if self.reaction_times:`` schreiben.
Diese Schreibweise kannte ich bisher noch nicht.


Der Code sieht nach den Optimierungen jetzt so aus:

Code: Alles auswählen

import tkinter as tk
from random import choice
import time
from statistics import mean


SWITCH_TIME = 1000
DESIGNATED_COLOR = "red"



class Application:

    def __init__(self, app_win):
        self.app_win = app_win
        self.colors = ["green", "red", "blue", "yellow", "black"]
        self.color = None
        self.reaction_times = []
        self.false_positives = 0
        self.color_missed = 0
        self.key_too_often_pressed = 0
        self.color_locked = False
        self.canvas = tk.Canvas(app_win, width=self.app_win.winfo_screenwidth(), height=self.app_win.winfo_screenheight(),
                                highlightthickness=0)
        self.canvas.pack()
        self.change_bg_color(DESIGNATED_COLOR)


    def change_bg_color(self, color_locked = False):
        self.color_locked = color_locked
        current_color = self.color

        while True:
            if self.color == current_color:
                self.color = choice(self.colors)
            else:
                break

        self.canvas.config(bg=self.color)
        self.start_time = time.monotonic() # Ereignis Timer
        self.app_win.after(SWITCH_TIME, self.check_for_key_press)


    def check_for_key_press(self):
        if not self.color_locked and self.color == DESIGNATED_COLOR:
            self.color_missed += 1
        self.change_bg_color()


    def key_pressed(self):
        if self.color == DESIGNATED_COLOR:
            if self.color_locked:
                self.key_too_often_pressed += 1
            else:
                reaction_time = time.monotonic() - self.start_time
                self.reaction_times.append(reaction_time)
                self.color_locked = True
        else:
            self.false_positives += 1


    def end_app(self):
        if self.reaction_times:
            # mean() auf Liste ohne Einträge führt zu einem Fehler
            print("Gemittelte Reaktionszeit:", mean(self.reaction_times))
        print("Reaktionszeit pro richtiger Farbe:", self.reaction_times)
        print("Bei richtiger Farbe zu oft gedrückt:", self.key_too_often_pressed)
        print("Bei richtiger Farbe nicht gedrückt:", self.color_missed)
        print("Bei falscher Farbe gedrückt:", self.false_positives)
        self.app_win.quit()


def main():
    app_win = tk.Tk()
    app_win.attributes("-fullscreen", True)
    app_win.bind("<Escape>", lambda e: app.end_app())
    app = Application(app_win)
    app_win.bind("<space>", lambda e: app.key_pressed()) # Ereignis Tastendruck
    app_win.mainloop()


if __name__ == '__main__':
    main()
Gruß
Atalanttore
Antworten