Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: Python: Ajanoton aikarajan ylitys + tulostus

mapa16 [22.04.2019 11:25:08]

#

Kyseinen koodin pätkä on isommasta kokonaisuudesta, joten osa kommenteista voi olla epäjohdonmukaisia.

Ongelma1:
Kyseisen ohjelma mittaa kahden nappulanpainalluksen välistä aikaa.
Miten painallusten välisen ajan saa tallennettua ja tulostettua ?

Ongelma2:
Mikäli aika ylittää esimerkiksi 15 sekuntia, tulisi nappuloiden väri muuttua punaiseksi.
Millä tavoin homma kannattaa hoitaa?

import tkinter as Tkinter




counter = 0
running = False


def counter_label(label):
    def count():
        if running:
            global counter

            # To manage the intial delay.
            if counter == -1:
                display = "Starting..."
            else:
                display = str(counter)

            label['text'] = display  # Or label.config(text=display)

            # label.after(arg1, arg2) delays by
            # first argument given in milliseconds
            # and then calls the function given as second argument.
            # Generally like here we need to call the
            # function in which it is present repeatedly.
            # Delays by 1000ms=1 seconds and call count again.
            label.after(1000, count)
            counter += 1


    count()


# Kellotusohjelman käynnistysfunktio ()

def Start(label):
    global running
    running = True
    counter_label(label)
    start['state'] = 'disabled'
    stop['state'] = 'normal'
    reset['state'] = 'normal'
    global counter
    counter = -1

# Resetoidaan stoppi nappia painettaessa

    if running == False:
        reset['state'] = 'disabled'
        start['state'] = 'normal'
        label['text'] = 'KELLO'



    else:
        label['text'] = 'KELLO'


# Kellotusohjelman pysäytysfunktio ()

def Stop():
    global running
    start['state'] = 'normal'
    stop['state'] = 'disabled'
    reset['state'] = 'normal'
    running = False
root = Tkinter.Tk()
root.title("Ajanotto")

def Reset(label):
    global counter
    counter = -1

    # Jos resettiä painetaan pysäytysfunktioiden jälkeen, palaa alkutilaan

    if running == False:
        reset['state'] = 'disabled'
        start['state'] = 'normal'
        label['text'] = 'KELLO'

    # Jos resettiä painetaan ajastimen ollessa päällä, aloittaa laskurin alusta

    else:
        label['text'] = 'Starting...'

# Muokataan ikkuna ja nappulat oikeanlaisiksi

root.minsize(width=500, height=350)
label = Tkinter.Label(root, text="TIME", fg="black", font="Verdana 30 bold")
label.pack()
start = Tkinter.Button(root,font="Verdana 20 bold",bg="grey",fg="white", text='ALKU',
                       width=15, command=lambda: Start(label))
stop = Tkinter.Button(root,font="Verdana 20 bold",bg="green",fg="white", text='LOPPU',
                      width=15, state='disabled', command=Stop)
reset = Tkinter.Button(root,font="Verdana 20 bold", text='Reset',
                       width=15, state='disabled', command=lambda: Reset(label))

start.pack()
stop.pack()
reset.pack()
root.mainloop()

The Alchemist [22.04.2019 14:27:08]

#

Älä käytä globaaleja muuttujia ja funktioiden argumentteja sekaisin, saat aikaan vain bugista roskaa. Jos kaikki muut muuttujat ovat globaaleja, niin ei ole mitään järkeä yhtäkkiä heittää tuota labelia argumenttina funktille; teet siitäkin vain globaalin. Parempi olisi tietysti opetella kirjoittamaan koodia, joka ei nojaa globaaleihin ollenkaan.

from timeit import default_timer as timer

class Watcher:
    def __init__(self, label):
        self.__display = label

    @property
    def tstart(self):
        return self.__started

    @property
    def running(self):
        return self.__started is not None

    def start(self):
        self.__started = timer()
        self.update()

    def stop(self):
        self.__started = None

    def reset(self):
        if self.running:
            self.__started = timer()
        else:
            self.__display['text'] = 'STOPPED'

    def update(self):
        if self.running:
            self.__display['text'] = f'{self.tstart} seconds since started'
            self.__display.after(1000, self.update)

# watcher = Watcher(label)
# start = Button(command=watcher.start)
# stop = Button(command=watcher.stop)
# reset = Button(command=watcher.reset)

En testannut, koska minulla ei ole Tkinteriä, mutta olettaisin tuon olevan aika virheetön. Nyt voit toteuttaa nappien värittämisen esimerkiksi antamalla ne uusina argumentteina Watcher-luokan konstruktorille tai kirjoittaa sitä varten oman luokkansa tai...

Tkinteriä tuntematta vaan tuntuu siltä, että parasta olisi, että teet käyttöliittymästä luokan ja konstruktorissa luot jäsenmuuttujiin eri gui-komponentit. Sitten voi tämän luokan sisällä kätevästi tehdä niillä mitä haluat, eikä sinun tarvitse arpoa millään globaaleilla. (Eli et luo niitä luokan ulkopuolella ja anna niitä konstruktorille argumentteina, vaan luot ne konstruktorissa itsessään.)

Tämä minun esimerkkini on laadittu siltä pohjalta, että Watcher-luokka tuottaa yhden yksinkertaisen toiminnon, joka voidaan ottaa käyttöön yksinkertaisesti luomalla Watcher-instanssi ja antamalla sille Label-olio, johon se kirjoittaa arvojaan. Joskus ihan kätevää tehdä näinkin, niin pääluokkakin pysyy yksinkertaisena.

Muoxxxu:
Eräs toteutustapa olisi myös tehdä aikaa laskevasta luokasta Labelista periytyvä widgetti ja nappien tilaa seuraava olio rakentaa Watcherin pohjalta... Tkinterissä lienee jokin event-systeemi, jolla voit lähettää viestin luokasta toiseen, että nyt laskuri on käynnistynyt ja muut sitä kuuntelevat luokat voivat myös alkaa pollata laskuria. (Tai voit suoraan lähettää viestin, että laskurin uusi arvo on X ja viestiä kuuntelevat luokat voivat suoraan reagoida siihen.)

Tässä esimerkissäni ButtonWatcher pollaa koko ajan ja se on huono tapa tehdä asiat.

from timeit import default_timer as timer

class Stopwatch(Tkinter.Label):
    @property
    def tstart(self):
        return self.__started

    @property
    def running(self):
        return self.__started is not None

    def start(self):
        self.__started = timer()
        self.update()

    def stop(self):
        self.__started = None

    def reset(self):
        if self.running:
            self.__started = timer()
        else:
            self['text'] = 'STOPPED'

    def update(self):
        if self.running:
            self['text'] = f'{self.tstart} seconds since started'
            self.after(1000, self.update)

class ButtonStateWatcher:
    def __init__(self, button, stopwatch):
        self.__button = button
        self.__clock = stopwatch
        self.update()

    def update(self):
        if self.__clock.running:
            delta = timer() - self.__clock.tstart

            if delta > 15000:
                # Aseta napin väri, en tiedä kuinka se oikeasti tehdään...
                self.__button['color'] = 'red'
            else:
                # Palauta väri, koska aika on jossain välissä nollattu
                self.__button['color'] = 'normal'

        self.__button.after(300, self.update)


# label = StopWatch()
# start = Button(command=watcher.start)
# stop = Button(command=watcher.stop)
# reset = Button(command=watcher.reset)
# start_state = ButtonStateWatcher(start, label)
# stop_watch = ButtonStateWatcher(stop, label)

Metabolix [23.04.2019 21:04:12]

#

mapa16 kirjoitti:

def count():
    # ...
    label.after(1000, count)
    counter += 1

Kannattaa muistaa, että odottaminen ei ole tarkka tapa mitata aikaa, vaan silloin laskuri jätättää: kun suoritetaan jokin koodi ja sitten odotetaan 1 sekunti ennen seuraavaa ajokertaa, tulee ylimääräistä viivettä siitä, että koodi ajetaan, ja siitä, että käyttöjärjestelmä yleensä odottaa vähintään yhden sekunnin mutta mittaustarkkuudesta ja koneen kuormituksesta riippuen ehkä enemmänkin.

Annetut korjausehdotukset ovat sikäli parempia, että niissä ajan mittaus perustuu kelloon. Laskuria varten pitäisi toki laskea nykyisen ajan ja aloituksen ero.

Koodissa on vielä se heikkous, että laskurin näyttämistä ei ole synkronoitu laskurin arvon vaihtumisen kanssa. Voi siis olla, että näyttäminen jätättää hitaasti, vaikka kello käy oikein, ja lopulta ruudulla hypätäänkin sitten kaksi sekuntia kerralla. Koodiin voisi siis lisätä vielä ominaisuuden, että after-funktion aikamääräksi annettaisiin se, mitä seuraavasta täydestä sekunnista puuttuu.

Tässä on tekstipohjainen esimerkki ajanotosta ja seuraavan sekunnin odotuksesta. Täytteenä on turhaa listan generointia, jotta näkyy, miten kutsujen välissä kulunut aika ei suinkaan ole nolla. Koodi on helppo soveltaa myös Tkinterille.

import time

alku = time.perf_counter()

def päivitys():
	kulunut = time.perf_counter() - alku
	kulunut_s = int(kulunut)
	print(f"Kulunut: {kulunut_s} sekuntia (tarkemmin {kulunut})")
	seuraava_ms = 1000 - int(1000 * (kulunut % 1))
	odota(seuraava_ms)

def odota(ms):
	print(f"Odotetaan {ms} millisekuntia.")
	time.sleep(ms / 1000)
	print("Odotettu!")

# Demonstraatio ajankäytöstä ja odotuksesta:
[i for i in range(1, 1000000)]
päivitys()

[i for i in range(1, 10000000)]
päivitys()

[i for i in range(1, 30000000)]
päivitys()

The Alchemist [27.04.2019 08:47:47]

#

Metabolix kirjoitti:

Kannattaa muistaa, että odottaminen ei ole tarkka tapa mitata aikaa, vaan silloin laskuri jätättää: kun suoritetaan jokin koodi ja sitten odotetaan 1 sekunti ennen seuraavaa ajokertaa, tulee ylimääräistä viivettä siitä, että koodi ajetaan, ja siitä, että käyttöjärjestelmä yleensä odottaa vähintään yhden sekunnin mutta mittaustarkkuudesta ja koneen kuormituksesta riippuen ehkä enemmänkin.

No ihan mutulla veikkaisin, ettei tässä tapauksessa yli sekunnin viivettä tule. Sitä reunatapausta ei tarvitse huomioida, että kone olisi niin täydellisen jumissa, että vaivoin hiiren osoitin liikkuu ruudulla.

Metabolix kirjoitti:

Tässä on tekstipohjainen esimerkki ajanotosta ja seuraavan sekunnin odotuksesta. Täytteenä on turhaa listan generointia, jotta näkyy, miten kutsujen välissä kulunut aika ei suinkaan ole nolla. Koodi on helppo soveltaa myös Tkinterille.

Pythonista ja Tkinteristä en tiedä vieläkään, mutta joidenkin toisten freimisten kanssa sleep-kutsu aiheuttaisi käyttöliittymän jäätymisen. Tästä syystä tulee käyttää niitä eri säikeessä pyöriviä timereita, jotka taas voivat olla millisekuntitasolla epäluotettavia.

Metabolix [27.04.2019 19:25:33]

#

The Alchemist kirjoitti:

No ihan mutulla veikkaisin, ettei tässä tapauksessa yli sekunnin viivettä tule.

Kannattaa sitten ottaa selvää eikä fiilistellä.

MSDN:ssä kerrotaan Windowsin osalta, että ”the resolution of the system timer – – is typically in the range of 10 milliseconds to 16 milliseconds”. Tämä on myös Windowsissa tyypillisesti prosessin nukkumisen tarkkuus. Asian voi vahvistaa esimerkiksi seuraavalla C++-koodilla:

#include <chrono>
#include <thread>
#include <iostream>

int main() {
	using namespace std::chrono_literals;
	for (int i = 0; i < 8; ++i) {
		auto alku = std::chrono::high_resolution_clock::now();
		std::this_thread::sleep_for(1s);
		// tai:
		// std::this_thread::sleep_for(1ms);
		// std::this_thread::sleep_for(999ms);
		auto kesto = std::chrono::high_resolution_clock::now() - alku;
		auto ns = std::chrono::nanoseconds(kesto).count();
		auto virhe = ns - 1000000000;
		std::cout
			<< ns << " ns "
			<< "= 1 ja 1 / " << (1000000000 / virhe) << " sekuntia. "
			<< "Virhe " << (86400 * virhe / 1000000000) << " s/vrk.\n";
	}
}

Koodin nopeus osuu omalla koneellani hauskasti juuri sille rajalle, että välillä nukkuminen ylittyy tuon 15–16 ms ja välillä taas ylittyy vain 0,01–0,04 ms. Jos silmukan sisältö vaihtelisi vähän enemmän, voisi tulla virheitä myös tältä väliltä. Linuxissa (nykyisillä asetuksilla) tulee vakaasti 0,1–0,2 millisekunnin ylitys.

Jos virhe on vaikka keskimäärin 8 ms sekunnissa, tästä kertyy yli 11 minuuttia vuorokaudessa. Eli virheen havaitsemiseksi ei todellakaan tarvitse olla ”niin täydellisen jumissa, että vaivoin hiiren osoitin liikkuu ruudulla”.

The Alchemist kirjoitti:

Pythonista ja Tkinteristä en tiedä vieläkään, mutta joidenkin toisten freimisten kanssa sleep-kutsu aiheuttaisi käyttöliittymän jäätymisen.

Juttu ei ollut tässä sleepin käyttö vaan kuluneen ajan huomiointi. Ongelma on aivan sama sleepillä ja Tkinterin ajastimilla, koska molempien tarkkuutta rajoittaa käyttöjärjestelmä. Kummassakaan tapauksessa tarkkaan ajastukseen optimaalinen parametri ei ole tasan yksi sekunti, vaan kannattaa laskea, minkä verran nukuttavaa aikaa on oikeasti jäljellä.

Kun asia näköjään on ammattilaisellekin vaikea hahmottaa, tein tästä Tkinter-esimerkin ja myös JavaScript-version, jonka voi tallentaa HTML-sivuna ja testata selaimessa. Ohjelmointityylistä sinänsä en kannusta ottamaan mallia, vaan esimerkin painopiste on tässä ajastustekniikassa.

Python, Tkinter:

import time
import tkinter

class Silmukka:
	def __init__(self, label, väärin = False):
		self.label = label
		self.väärin = väärin
		if väärin:
			self.kuvaus = "Väärin, nukutaan 1 s + hilut"
		else:
			self.kuvaus = "Oikein, pidetään 1 s/kierros"

	def start(self):
		self.kierros = 0
		self.callback()

	def callback(self):
		nyt = time.perf_counter()
		if self.kierros == 0:
			self.alku = nyt
			self.edellinen = nyt
			self.nukkumaanmeno = nyt
		kulunut = nyt - self.edellinen
		nukuttu = nyt - self.nukkumaanmeno
		summa = nyt - self.alku
		self.label['text'] = f"{self.kuvaus}:\n"\
			f"kierros = {self.kierros} = {summa:.5f} s\n"\
			f"kului {kulunut:.5f} s, josta nukuttiin {nukuttu:.5f} s\n"
		self.kierros += 1
		self.edellinen = nyt
		self.nukkumaanmeno = time.perf_counter()
		if self.väärin:
			self.label.after(1000, lambda: self.callback())
		else:
			seuraava_ms = 1000 - int(1000 * (summa % 1))
			self.label.after(seuraava_ms, lambda: self.callback())

def aloita():
	Silmukka(laskuri1).start()
	Silmukka(laskuri2, True).start()

root = tkinter.Tk()
root.minsize(width = 500, height = 350)
root.title("Ajanotto")
laskuri1 = tkinter.Label(root)
laskuri1.pack()
laskuri2 = tkinter.Label(root)
laskuri2.pack()
nappi = tkinter.Button(root, text = 'Aloita', command = aloita)
nappi.pack()
root.mainloop()

HTML, JavaScript:

<!DOCTYPE html>
<meta charset="UTF-8">
<title>Laskuri</title>
<script>
function time() {
	return new Date().getTime() / 1000
}

function silmukka(label, väärin) {
	var kuvaus = "Oikein, pidetään 1 s/kierros"
	if (väärin) {
		kuvaus = "Väärin, nukutaan 1 s + hilut"
	}
	var kierros, alku, edellinen, nukkumaanmeno
	function callback() {
		var nyt = time()
		if (kierros == 0) {
			alku = nyt
			edellinen = nyt
			nukkumaanmeno = nyt
		}
		var kulunut = nyt - edellinen
		var nukuttu = nyt - nukkumaanmeno
		var summa = nyt - alku
		label.textContent =
			kuvaus + ":\n" +
			"kierros = " + kierros + " = " + summa.toFixed(3) + " s\n" +
			"kului " + kulunut.toFixed(3) + " s, " +
			"josta nukuttiin " + nukuttu.toFixed(3) + " s\n"
		kierros += 1
		edellinen = nyt
		nukkumaanmeno = time()
		if (väärin) {
			setTimeout(callback, 1000)
		} else {
			var seuraava_ms = 1000 - Math.floor(1000 * (summa % 1))
			setTimeout(callback, seuraava_ms)
		}
	}
	kierros = 0
	callback()
}

window.onload = function() {
	silmukka(document.getElementById("laskuri1"))
	silmukka(document.getElementById("laskuri2"), true)
}
</script>
<pre id="laskuri1"></pre>
<pre id="laskuri2"></pre>

Tosin JavaScriptissa vielä oman mausteensa tuo se, että kun tabin jättää taustalle, ajastus saattaa käydä vielä harvemmaksi, jolloin myös tämä korjattu versio saattaa suorittaa kierroksen harvemmin kuin kerran sekunnissa.

The Alchemist [29.04.2019 09:20:30]

#

Metabolix kirjoitti:

Jos virhe on vaikka keskimäärin 8 ms sekunnissa, tästä kertyy yli 11 minuuttia vuorokaudessa. Eli virheen havaitsemiseksi ei todellakaan tarvitse olla ”niin täydellisen jumissa, että vaivoin hiiren osoitin liikkuu ruudulla”.

Miten tämä nyt liittyy siihen, että ruudulla olevaa sekuntilaskuria päivitetään "noin sekunnin välein"? Jos tekisin paskaa koodia, niin laskurissani varmaan olisi "sekunnit += 1" jokaisella kutsukerralla, mutta silloin vika ei ole epätarkassa ajastimessa vaan täysin luokattomassa koodarissa.

Ajastimen epätarkkuushan ei johda siihen, että yhtäkkiä sekunnin välein kutsuttavaksi tarkoitettu ajastin alkaisin toimia vain 10 minuutin välein, vaan että "kutsujen määrä * ajastusväli" voi antaa eri tuloksen kuin sen paperilla pitäisi. (Millä ei koodissa pitäisi olla mitään väliä.)

Metabolix [29.04.2019 17:00:54]

#

The Alchemist kirjoitti:

Miten tämä nyt liittyy siihen, että ruudulla olevaa sekuntilaskuria päivitetään "noin sekunnin välein"? Jos tekisin paskaa koodia, niin laskurissani varmaan olisi "sekunnit += 1" jokaisella kutsukerralla, mutta silloin vika ei ole epätarkassa ajastimessa vaan täysin luokattomassa koodarissa.

Olet sinänsä oikeassa, että esittämäni laskelma virheestä ei liity suoraan tuohon sinun tapaasi. Kerroinhan tämän jo ensimmäisessä viestissäni: sinun koodissasi ”näyttäminen jätättää hitaasti, vaikka kello käy oikein, ja lopulta ruudulla hypätäänkin sitten kaksi sekuntia kerralla”, kun esimerkiksi 100,994 sekunnista noustaan 102,002 sekuntiin.

Laskelma virheestä liittyy kysyjän alkuperäiseen koodiin, jossa on juurikin tämä ratkaisu sekunnit += 1. Viittasin koodiin ensimmäisessä viestissäni, ja oletin, että jatkaisit keskustelua loogisesti samassa kontekstissa.

Mikä tavoite on se, että ruudulla olevaa sekuntilaskuria päivitetään ”noin sekunnin välein”? Jos koodi korjataan, kai sen voi saman tien korjata kunnolla niin, että laskuri päivitetään keskimäärin sekunnin välein eli mahdollisimman pian sekunnin vaihtumisen jälkeen.

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta