Viikko 6
Tämän viikon tehtävät
Allaolevien tehtävien deadline on tiistai 8.12. klo 23:59
Apua tehtävien tekoon kurssin Discord-kanavalla sekä kampuksella pajassa BK107:
- ma 16-19
- ti 13-16
- to 16-19
- pe 13-16
Liittyminen kurssin Discord-kanavalle tapahtuu komennolla /join course TKT20006 - Ohjelmistotuotanto - ohtu
Muista myös tämän viikon monivalintatehtävät, joiden deadline on ei tehtäviä .
Tehtävät liittyvät materiaalin ohjelmistosuunnittelua käsittelevän osan 4 niihin lukuihin, joihin on merkitty [viikko 6]. Tehtävässä 7 jatketaan kurssin [tekoäly]-tehtävien sarjaa.
Typoja tai epäselvyyksiä tehtävissä?
Tee korjausehdotus editoimalla tätä tiedostoa GitHubissa.
Kurssipalaute
Kurssilla on käytössä normaalin lopussa kerättävän palautteen lisäksi ns. jatkuva palaute: voit antaa milloin vain kurssihenkilökunnalle anonyymiä palautetta osoitteessa https://norppa.helsinki.fi/targets/112400522/feedback
Ongelmia uv:n kanssa?
Muutamia ohjeita täällä
Tehtävien palauttaminen
Tehtävät palautetaan GitHubiin, sekä merkitsemällä tehdyt tehtävät palautussovellukseen https://study.cs.helsinki.fi/stats/courses/ohtu-2026 välilehdelle “my submission”.
Tämän viikon tehtävät palautetaan jo edellisillä viikoilla käyttämääsi palautusrepositorioon, hakemiston viikko6 sisälle.
Katso tarkempi ohje palautusrepositorioita koskien täältä.
Tehtävien checkboxien rastit tallentuvat selaimesi paikalliseen muistiin.
1. Laskin ja komento-oliot
HUOM jos olet käyttänyt kontainerisoitua Poetry-ympäristöä, tämä tehtävä tulee tuottamaan haasteta, sillä sovelluksella on graafinen käyttöliittymä. Googlaa esim. hakusanoilla linux docker gui apps jos haluat saada tehtävän tehtyä kontainerissa.
HUOM2 tässä tehtävässä saattaa riittää että koneellasi on Python. uv:tä ei välttämättä tarvita ollenkaan. Katso hieman alempaa Korjaustapa 1
Kurssirepositorion hakemistossa viikko6/laskin löytyy yksinkertaisen laskimen toteutus. Laskimelle on toteutettu graafinen käyttöliittymä Tkinter-kirjaston avulla.
Kopioi projekti palautusrepositorioosi, hakemiston viikko6 sisälle.
Jos tarvetta, lue ensin kurssin Ohjelmistotekniikka materiaalissa oleva Tkinter-tutoriaali. Toinen vaihtoehto on koodin tutkiminen tekoälyn avustuksella, ks. alla oleva vihje.
Vihje: tekoälyn hyödyntäminen tehtävässä
Tekoäly on oivallinen apuväline tutustuttaessa itselle etukäteen tuntemattomaan koodiin (ks. tekoälymateriaali). Erityisen hyödyllinen se on, jos koodissa käytetään itselle ennalta tuntemattomia kirjastoja.
Koodista voi kysyä suoraan VS Codessa Copilotin Ask-tilassa (jos tilaa ei löydy valikosta, ks. tämä). Kysymyksessä voi viitata tiedostoon, esim. selitä miten #file:kayttoliittyma.py toimii, tai editorissa voi maalata osan koodista, jolloin valittu koodi liitetään kysymykseen automaattisesti. Omassa kokeilussani maalasin osan sovelluksen käyttöliittymän koodista (tiedoston kayttoliittyma.py rivit 12–28) ja pyysin Copilotia selittämään sen:

Selitys jatkuu melko hyvänä, suomen kieli ei kyllä kaikin osin ole ihan optimaalista:

Koodissa on luokka StringVar, jonka toiminta on hieman hämärän peitossa. Kysytään tarkennusta:

Koodissa esiintyvä lambda on hämmentävä. Onko se ylipäätään tarpeen? Kysytään täsmennystä sen suhteen:

Koodi tuntuu nyt selkeämmältä, ja opimme taas uutta tekoälyn ansiosta!
Asenna projektin riippuvuudet komennolla uv sync ja käynnistä laskin virtuaaliympäristössä komennolla python src/index.py.
Komennon suorittamisen tulisi avata ikkuna, jossa on laskimen käyttöliittymä.
Jos törmäät virheilmoitukseen
ModuleNotFoundError: No module named 'tkinter', syynä saattaa olla se, että koneessasi ei ole Pythonin mukana tavallisesti asentuvaa Tkinteriä.Korjaustapa 1:
- Kokeile toimisiko sovellus koneesi Pythonilla “suoraan”, eli ilman virtuaaliympäristöä, eli komennolla
python src/index.pytaipython3 src/index.pyilman, että olet virtuaaliympäristössä. Jos toimii, niin se riittää.Korjaustapa 2:
Jos käytät Ubuntu/Linuxia, ongelma saattaa ratketa suorittamalla asennus komennolla
sudo apt-get install python3-tkMac-käyttäjillä eräs tapa ratkaista ongelma on komento
brew install python-tk@3.13, olettaen brew on käytössä. Asennuksessa kannattaa käyttää samaa versionumeroa kuin mikä on käytössäsi olevalla Pythonilla.Paketin
python-tkasennuksen jälkeen kannattaa poistaa virtuaaliympäristö, eli tiedosto .venv, ja suorittaauv syncuudelleen.
Sovelluksen avulla pystyy tällä hetkellä tekemään yhteen- ja vähennyslaskuja, sekä nollaamaan laskimen arvon. Laskutoimituksen kumoamista varten on lisätty jo painike “Kumoa”, joka ei vielä toistaiseksi tee mitään. Sovelluksen varsinainen toimintalogiikka on luokassa Kayttoliittyma. Koodissa on tällä hetkellä hieman ikävä if-hässäkkä:
def _suorita_komento(self, komento):
arvo = 0
try:
arvo = int(self._syote_kentta.get())
except Exception:
pass
if komento == Komento.SUMMA:
self._sovelluslogiikka.plus(arvo)
elif komento == Komento.EROTUS:
self._sovelluslogiikka.miinus(arvo)
elif komento == Komento.NOLLAUS:
self._sovelluslogiikka.nollaa()
elif komento == Komento.KUMOA:
pass
self._kumoa_painike["state"] = constants.NORMAL
if self._sovelluslogiikka.arvo() == 0:
self._nollaus_painike["state"] = constants.DISABLED
else:
self._nollaus_painike["state"] = constants.NORMAL
self._syote_kentta.delete(0, constants.END)
self._arvo_var.set(self._sovelluslogiikka.arvo())
Refaktoroi koodi niin, ettei _suorita_komento-metodi sisällä pitkää if-hässäkkää. Hyödynnä kurssimateriaalin osassa 4 esiteltyä suunnittelumallia command.
Tässä tehtävässä ei tarvitse vielä toteuttaa kumoa-komennon toiminnallisuutta!
Luokka Kayttoliittyma voi näyttää refaktoroituna esimerkiksi seuraavalta:
class Komento(Enum):
SUMMA = 1
EROTUS = 2
NOLLAUS = 3
KUMOA = 4
class Kayttoliittyma:
def __init__(self, sovelluslogiikka, root):
self._sovelluslogiikka = sovelluslogiikka
self._root = root
# talletetaan komennot sanakirjaan eli dict-rakenteeseen
self._komennot = {
Komento.SUMMA: Summa(sovelluslogiikka, self._lue_syote),
Komento.EROTUS: Erotus(sovelluslogiikka, self._lue_syote),
Komento.NOLLAUS: Nollaus(sovelluslogiikka, self._lue_syote),
Komento.KUMOA: Kumoa(sovelluslogiikka, self._lue_syote) # ei ehkä tarvita täällä...
}
# ...
def _lue_syote(self):
return self._syote_kentta.get()
def _suorita_komento(self, komento):
# haetaan dict:istä oikea komento
komento_olio = self._komennot[komento]
komento_olio.suorita()
self._kumoa_painike["state"] = constants.NORMAL
if self._sovelluslogiikka.arvo() == 0:
self._nollaus_painike["state"] = constants.DISABLED
else:
self._nollaus_painike["state"] = constants.NORMAL
self._syote_kentta.delete(0, constants.END)
self._arvo_var.set(self._sovelluslogiikka.arvo())
Komennoilla on nyt siis metodi suorita ja ne saavat konstruktorin kautta Sovelluslogiikka-olion ja funktion, jota kutsumalla syötteen voi lukea.
2. Komentojen kumoaminen
Toteuta laskimeen myös kumoa-toiminto.
Periaatteena on siis toteuttaa jokaiseen komento-olioon metodi kumoa.
Komento-olion tulee siis muistaa, mikä oli tuloksen arvo ennen komennon suoritusta, jotta se osaa palauttaa laskimen suoritusta edeltävään tilaan.
Jos kumoa-nappia painetaan, suoritetaan sitten edelliseksi suoritetun komento-olion metodi kumoa.
Riittää, että ohjelma muistaa edellisen tuloksen, eli kumoa-toimintoa ei tarvitse osata suorittaa kahta tai useampaa kertaa peräkkäin. Tosin tämänkään toiminnallisuuden toteutus ei olisi kovin hankalaa, jos edelliset tulokset tallennettaisiin esimerkiksi listaan.
Vapaaehtoinen bonus-tehtävä
Laajenna ohjelmaasi siten, että se mahdollistaa mielivaltaisen määrän peräkkäisiä kumoamisia.
Eli jos olet esim. laskenut summan 1 + 2 + 3 + 4 + 5 (jonka tulos 15), napin kumoa peräkkäinen painelu vie laskimen tilaan missä tulos on ensin 10 sitten 6, 3, 2, 1 ja lopulta 0.
Myös esim. seuraavanlaisen monimutkaisemman operaatiosarjan pitää toimia oikein: Summa 10, Erotus 6, Erotus 2, Kumoa (kumoaa komennon Erotus 2), Summa 4, Kumoa (Kumoaa komennon Summa 4), Kumoa (kumoaa komennon Erotus 6), Kumoa (kumoaa komennon Summa 10)
3. Kyselykieli NHL-tilastoihin, osa 1
Kurssirepositorion hakemistosta viikko6/query-language löytyy jälleen yksi versio tutusta NHL-tilastojen tarkasteluun tarkoitetusta ohjelmasta.
Kopioi projekti palautusrepositorioosi, hakemiston viikko6 sisälle.
Tätä tehtävää tehdessä luentomateriaalin Gutenberg-lukija voi olla eräs inspiraation lähde. Esimerkkiin kannattaa tutustua joka tapauksessa, jos strategy-suunnittelumalli ei ole tuttu.
Tällä kertaa olemme kiinnostuneita tekemään hieman monimutkaisempia “kyselyjä” pelaajatietoihin, esim. listaa kaikki joukkueen PHI pelaajat, joilla on vähintään 5 maalia ja vähintään 20 syöttöä.
Koodiin onkin luotu hieman valmista kalustoa, josta pääset liikkeelle. Yllä olevan kyselyn voi suorittaa seuraavasti:
def main():
url = "https://studies.cs.helsinki.fi/nhlstats/2024-25/players.txt"
reader = PlayerReader(url)
stats = Statistics(reader)
matcher = And(
HasAtLeast(5, "goals"),
HasAtLeast(20, "assists"),
PlaysIn("PHI")
)
for player in stats.matches(matcher):
print(player)
Luokalle Statistics on tehty metodi matches, joka palauttaa listan niistä pelaajista, joille parametrina annettun olion metodi test palauttaa True.
Tutustu ohjelman rakenteeseen.
- Huomioi miten
HasAtLeast-luokan metoditestkäyttää funktiota getattr saadakseen parametrina annetun attribuutin arvon - Toinen huomioinarvoinen piirre on
And-luokan konstruktorissa käytetty vaihtuvamittainen parametrilista, jonka tunnistaa*-etuliitteestä. Syntaksin avulla*matcherssisältää listan konstruktorille annetuista argumenteista
Vihje: tekoälyn hyödyntäminen tehtävässä
Jos getattr tai jokin muu koodin kohta on epäselvä, maalaa se editorissa ja pyydä tekoälyä selittämään, miten koodi toimii (ks. tekoälymateriaali). Omassa kokeilussani maalasin tiedostosta matchers.py luokan HasAtLeast ja kysyin, miten se toimii:

Selitys on selkeä, ja mukana on konkreettinen esimerkki: HasAtLeast(10, "goals").test(player) tarkoittaa samaa kuin player.goals >= 10. Kannattaa kysyä myös jatkokysymyksiä, esim. miksi getattr-funktiota käytetään sen sijaan, että jokaiselle attribuutille tehtäisiin oma luokkansa.
Toteuta test-metodin toteuttavat luokat, joiden avulla voit tehdä seuraavat operaatiot:
All (tosi kaikille pelaajille)
Not (parametrina olevan ehdon negaatio)
HasFewerThan (HasAtLeast-komennon negaatio eli esim. on vähemmän kuin 10 maalia)
Kaikille pelaajille tosi ehto All ei ole vielä tämän tehtävän kannalta kovin mielenkiintoinen, mutta sitä hyödynnetään neljännessä tehtävässä.
Voit tarkistaa toteutuksesi toimivuuden tekemällä kyselyn:
matcher = And(
Not(HasAtLeast(2, "goals")),
PlaysIn("NYR")
)
Varmista, että vastauksena on joukkueen NYR pelaajista ne, joilla ei ole vähintään kahta maalia, eli 0 tai 1 maalia tehneet:
Gabe Perreault NYR 0 + 0 = 0 Zac Jones NYR 1 + 10 = 11 Connor Mackey NYR 0 + 0 = 0 Brennan Othmann NYR 0 + 2 = 2 Chad Ruhwedel NYR 0 + 1 = 1 Matthew Robertson NYR 0 + 0 = 0
Kaikissa esimerkitulostuksissa on käytetty vuoden 2024-25 tilastoja. Tilastoissa käytettävän vuoden voi valita tilastojen URL:sta, joka on koodipohjassa https://studies.cs.helsinki.fi/nhlstats/2024-25/players.txt
Varmista, että kyselyn
matcher = And(
HasFewerThan(2, "goals"),
PlaysIn("NYR")
)
palauttaa täsmälleen saman listan.
Ehdon All pitäisi palauttaa kaikki pelaajat.
Varmista, että koodi
filtered_with_all = stats.matches(All())
print(len(filtered_with_all))
tulostaa 899
4. Kyselykieli NHL-tilastoihin, osa 2
Toteuta test-metodin toteuttava luokka Or, joka on tosi silloin jos ainakin yksi sen parametrina saamista ehdoista on tosi.
Varmista, että kysely
matcher = Or(
HasAtLeast(45, "goals"),
HasAtLeast(70, "assists")
)
palauttaa ne, joilla on vähintään 45 maalia tai 70 syöttöä, eli seuraavan listan
Mitch Marner TOR 27 + 75 = 102
Connor McDavid EDM 26 + 74 = 100
William Nylander TOR 45 + 39 = 84
Nathan MacKinnon COL 32 + 84 = 116
Leon Draisaitl EDM 52 + 54 = 106
Nikita Kucherov TBL 37 + 84 = 121
Varmista, että kysely
matcher = And(
HasAtLeast(70, "points"),
Or(
PlaysIn("COL"),
PlaysIn("FLA"),
PlaysIn("BOS")
)
)
palauttaa kaikki vähintään 70 pistettä tehneet jotka pelaavat jossain seuraavista joukkueista COL, FLA tai BOS, eli listan:
David Pastrnak BOS 43 + 63 = 106
Sam Reinhart FLA 39 + 42 = 81
Nathan MacKinnon COL 32 + 84 = 116
Cale Makar COL 30 + 62 = 92
Aleksander Barkov FLA 20 + 51 = 71
Kyselyt perustuvat rakenteeltaan decorator-suunnittelumalliin, vastaavasti kuten materiaalin osan 4 esimerkissä dekoroitu pino. And- ja Or-muotoiset kyselyt on muodostettu myös erään suunnittelumallin, compositen hengessä, ne ovat Matcher-rajapinnan toteuttavia olioita, jotka sisältävät itse monta Matcher-olioa. Niiden käyttäjä ei kuitenkaan tiedä sisäisestä rakenteesta mitään.
5. Parannettu kyselykieli, osa 1
Matcher-olioiden avulla tehtyä kyselykieltä vaivaa se, että kyselyjen rakentaminen on ikävää, sillä jokaista kyselyn osaa kohti on luotava uusi olio. Toinen ikävä puoli on se, että kyselyjä käyttävällä koodilla on suora riippuvuus sen käyttämiin Matcher-olioihin.
Tee materiaalin osassa 4 esitellyn pinorakentajan hengessä kyselyrakentaja, jonka avulla voit luoda Matcher-olioita.
Rakentaja voi toimia esim. seuraavaan tapaan.
Ensin kysely, joka palauttaa jokaisen pelaajan:
def main():
url = "https://studies.cs.helsinki.fi//nhlstats/2024-25/players.txt"
reader = PlayerReader(url)
stats = Statistics(reader)
query = QueryBuilder()
matcher = query.build()
for player in stats.matches(matcher):
print(player)
Tässä kyselyssä voi ja kannattaa hyödyntää edellisen tehtävän All-matcheria.
Seuraavaksi kysely, missä tulostetaan pelaajat, joiden joukkue on NYR:
def main():
url = "https://studies.cs.helsinki.fi//nhlstats/2024-25/players.txt"
reader = PlayerReader(url)
stats = Statistics(reader)
query = QueryBuilder()
matcher = query.plays_in("NYR").build()
for player in stats.matches(matcher):
print(player)
Seuraavaksi kysely, missä tulostetaan pelaajat joiden joukkue on NYR, joilla on vähintään 10 mutta kuitenkin vähemmän kuin 20 maalia:
def main():
url = "https://studies.cs.helsinki.fi//nhlstats/2024-25/players.txt"
reader = PlayerReader(url)
stats = Statistics(reader)
query = QueryBuilder()
matcher = query.plays_in("NYR").has_at_least(10, "goals").has_fewer_than(20, "goals").build()
for player in stats.matches(matcher):
print(player)
Pelaajien lista on seuraava:
Alexis Lafrenière NYR 17 + 28 = 45
Jonny Brodzinski NYR 12 + 7 = 19
Adam Fox NYR 10 + 51 = 61
Peräkkäin ketjutetut ehdot siis toimivat “and”-periaatteella.
Varmista, että ratkaisusi toimii edellisen esimerkin kyselyllä.
Tässä tehtävässä riittää, että kyselyrakentaja osaa muodostaa and-periaatteella yhdistettyjä ehtoja.
Pitkät metodikutsuketjut, esim.
matcher = query.plays_in("NYR").has_at_least(10, "goals").has_fewer_than(20, "goals").build()
ovat luettavuudeltaan hieman ikäviä, jos ne kirjoitetaan monelle riville. Usein ne onkin tapana jakaa “kutsu per rivi”-periaatteella:
matcher = (
query
.plays_in("NYR")
.has_at_least(10, "goals")
.has_fewer_than(20, "goals")
.build()
)
Python edellyttää ikävä kyllä tässä tapauksessa “ylimääräisten” sulkujen käyttöä.
6. Parannettu kyselykieli, osa 2
Laajennetaan kyselyrakentajaa siten, että sen avulla voi muodostaa myös or-ehdolla muodostettuja kyselyjä.
Or-ehdon sisältävä kysely voi olla muodostettu esim. seuraavasti:
matcher = (
query
.one_of(
query.plays_in("PHI")
.has_at_least(10, "assists")
.has_fewer_than(10, "goals"),
query.plays_in("EDM")
.has_at_least(50, "points")
)
.build()
)
Pelaajalistan tulisi olla:
Egor Zamula PHI 3 + 12 = 15
Connor McDavid EDM 26 + 74 = 100
Evan Bouchard EDM 14 + 53 = 67
Rasmus Ristolainen PHI 4 + 15 = 19
Nick Seeler PHI 3 + 17 = 20
Leon Draisaitl EDM 52 + 54 = 106
Cam York PHI 4 + 13 = 17
Jamie Drysdale PHI 7 + 13 = 20
Travis Sanheim PHI 8 + 22 = 30
Varmista, että ratkaisusi toimii edellisen esimerkin kyselyllä.
7. Suunnittele ensin, toteuta sitten [tekoäly]
Lue ennen tehtävän tekemistä materiaalin Tekoäly ohjelmistotuotannossa viikon 6 osuus Suunnittele ensin.
Kyselyrakentajan ansiosta kyselyjen muodostaminen on nyt aiempaa mukavampaa. Kyselyt ovat kuitenkin edelleen Python-koodia. Laajennetaan ohjelmaa siten, että kyselyn voi antaa myös merkkijonona:
matcher = parse("team = NYR and goals >= 10 and goals < 20")
for player in stats.matches(matcher):
print(player)
Tuloksen tulee olla sama kuin tehtävän 5 esimerkissä:
Alexis Lafrenière NYR 17 + 28 = 45
Jonny Brodzinski NYR 12 + 7 = 19
Adam Fox NYR 10 + 51 = 61
Kyselykielessä on seuraavat ehdot:
team = XXXeli pelaaja pelaa joukkueessa XXX<attribuutti> >= <luku>eli pelaajalla on vähintään annettu määrä esim. maaleja<attribuutti> < <luku>eli pelaajalla on vähemmän kuin annettu määrä
Ehtoja voi yhdistää sanoilla and ja or, ja and sitoo vahvemmin kuin or. Eli tehtävän 6 esimerkkikysely olisi muotoa:
matcher = parse("team = PHI and assists >= 10 and goals < 10 or team = EDM and points >= 50")
ja sen tulos on sama kuin tehtävän 6 esimerkissä.
Tehdään toteutus agentin avulla, mutta tällä kertaa suunnitellaan ensin.
Varmista, että tehtävien 1-6 koodi on commitoitu
Avaa hakemisto viikko6/query-language VS Codessa omana workspacenaan ja luo projektille tarvittaessa ohjetiedosto
Valitse Copilotin Chat-näkymässä tilaksi Plan (jos Plan puuttuu valikosta, ks. tämä), ja anna sille tehtävänanto. Kerro pyynnössä ainakin seuraavat reunaehdot:
- toteutuksen tulee hyödyntää olemassa olevaa
QueryBuilder-luokkaa, eikä matcher-luokkien logiikkaa saa toteuttaa uudelleen - toteutukselle tulee tehdä yksikkötestit
- toteutus tulee jakaa pieniin askeliin, joiden jokaisen toimivuus voidaan varmistaa erikseen
Lue agentin laatima suunnitelma huolellisesti. Jos et ymmärrä jotain suunnitelmasta, pyydä agentilta konkreettista esimerkkiä.
Pyydä agenttia muuttamaan suunnitelmaa ainakin kerran. Jos suunnitelma on mielestäsi valmiiksi hyvä, voit esim. pyytää agenttia lisäämään kyselykieleen not-operaation tai perustelemaan jonkin suunnitelmansa ratkaisun
Kun olet tyytyväinen suunnitelmaan, käynnistä toteutus
Kun agentti on valmiina varmista, että molemmat yllä olevat esimerkkikyselyt tuottavat oikean tuloksen
Käy lopuksi läpi valmis koodi ja agentin kirjoittamat testit. Pyydä agenttia selittämään koodin toimintaa, jotta saat siitä vähintään kohtuullisen hyvän käsityksen.
Kirjoita raportti kokemuksistasi hakemistoon viikko6 talletettavaan tiedostoon plan.md
Kerro raportissa
- Miltä agentin ensimmäinen suunnitelma näytti, ja mitä siihen muutettiin?
- Noudattiko agentti suunnitelmaa toteutusvaiheessa?
- Oliko agentin tuottama koodi ymmärrettävää ja rakenteeltaan selkeää?
- Opitko tehtävässä jotain uutta?
Tehtävien palautus
Pushaa kaikki tekemäsi tehtävät (paitsi ne, joissa mainitaan, että tehtävää ei palauteta mihinkään) GitHubiin palautusrepositorioosi ja merkkaa tekemäsi tehtävät palautussovellukseen https://study.cs.helsinki.fi/stats/courses/ohtu-2026