Python-kielen perusteita
Tämä materiaali käy läpi ne Pythonin piirteet, joita kurssin viikkotehtävissä tarvitaan.
Luentotallenne pohjautuu vielä materiaalin vanhempaan versioon.
1. Asennus ja versiot
Python on korkean tason ohjelmointikieli, jonka suunnittelussa on panostettu erityisesti luettavuuteen. Monesta muusta kielestä poiketen Python merkitsee koodilohkot sisennyksellä eikä aaltosulkeilla.
Asenna koneellesi jokin tuetuista Python 3 -versioista osoitteesta python.org/downloads. Kurssilla riittää mikä tahansa versio, joka on vielä tuen piirissä; esimerkit eivät käytä aivan uusimpien versioiden erikoisuuksia.
Tarkista versio ennen kuin aloitat. Python 2:n tuki
päättyi vuonna 2020 eikä sitä käytetä tällä kurssilla lainkaan. Jos
python käynnistää koneellasi Python 2:n, käytä komentoa
python3.
$ python3 --version
Python 3.12.3
Virtuaaliympäristö ja pip
Älä asenna kurssin kirjastoja järjestelmän Pythoniin, vaan tee jokaista
projektia varten oma virtuaaliympäristö.
Näin eri projektien riippuvuudet eivät sotke toisiaan ja
requirements.txt kertoo täsmälleen, mitä projekti
tarvitsee.
$ python3 -m venv .venv # luo ympäristön hakemistoon .venv
$ source .venv/bin/activate # Linux ja macOS
$ .venv\Scripts\activate # Windows
(.venv) $ pip install flask
(.venv) $ pip freeze > requirements.txt
(.venv) $ deactivate
Toisessa koneessa ympäristö rakennetaan uudelleen komennolla
pip install -r requirements.txt. Älä lisää
.venv-hakemistoa versionhallintaan.
2. Tulkin käyttäminen ja ohjelman suorittaminen
Pythonia voi kokeilla interaktiivisesti käynnistämällä tulkin komentoriviltä. Interaktiivinen tulkki on paras tapa kokeilla, mitä jokin funktio oikeasti tekee.
$ python3
Python 3.12.3 (main, Jan 17 2026, 10:05:12) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 / 2
0.5
>>> help(str.split)
>>> dir("") # listaa merkkijonon metodit
>>> exit()
Ohjelmatiedosto suoritetaan antamalla se tulkille parametrina:
$ python3 malli.py
Hello world!
print-funktion parametrit
print ottaa useita argumentteja ja kolme hyödyllistä
nimettyä parametria: sep (erotin argumenttien välissä,
oletuksena välilyönti), end (rivin loppu, oletuksena
rivinvaihto) ja file (mihin virtaan tulostetaan).
import sys
print("a", "b", "c") # a b c
print("a", "b", sep="\t") # a<sarkain>b
print("ei rivinvaihtoa", end="")
print("virheilmoitus", file=sys.stderr) # menee STDERR-virtaan
file=sys.stderr on tärkeä palvelinympäristöissä:
tavallinen tuloste ja virhetuloste päätyvät eri lokitiedostoihin.
Suoraan suoritettava ohjelma
Linuxissa ja macOS:ssä ohjelman voi tehdä suoraan suoritettavaksi lisäämällä tiedoston ensimmäiseksi riviksi shebang-rivin ja antamalla tiedostolle suoritusoikeudet.
#!/usr/bin/env python3
print("Hello world!")
$ chmod u+x malli.py
$ ./malli.py
Hello world!
Käytä muotoa #!/usr/bin/env python3 äläkä kovakoodaa
tulkin polkua. Kovakoodattu polku toimii vain yhdessä koneessa ja
hajoaa heti, kun ohjelma siirretään toiseen ympäristöön tai
virtuaaliympäristöön.
Python 3 olettaa lähdekoodin olevan UTF-8-koodattua (PEP 3120), joten erillistä
# -*- coding: utf-8 -*- -riviä ei
tarvita. Se on Python 2:n jäänne. Varmista sen sijaan, että
editorisi tallentaa tiedostot UTF-8-muodossa ilman
BOM-merkintää (UTF-8, ei UTF-8 with BOM).
Tiedoston alussa oleva BOM estää
shebang-rivin toiminnan ja aiheuttaa palvelimella virheen
Exec format error.
3. Koodityyli
Python-koodin tyyliohje on PEP 8. Sen keskeisimmät kohdat:
- Sisennä neljä välilyöntiä kerrallaan. Älä käytä tabulaattoria — tarkista editorisi asetuksista, että tabulaattori korvautuu neljällä välilyönnillä.
- Sisennys on Pythonissa merkitsevä: samaan lohkoon kuuluvat rivit sisennetään samalle tasolle.
- Pidä rivit korkeintaan 79 merkin mittaisina. Osa projekteista käyttää nykyisin väljempää rajaa (esimerkiksi Blackin oletus on 88); noudata aina projektin omaa sopimusta.
- Jätä operaattorien ympärille välilyönnit:
if a > 0:, eiif a>0:. - Kommentti alkaa
#-merkillä. - Isot ja pienet kirjaimet ovat merkitseviä:
lukujaLukuovat eri muuttujat. - Useamman lauseen voi erottaa samalle riville puolipisteellä, mutta älä tee niin. Kirjoita yksi lause riville, jolloin erotinta ei tarvita.
- Käytä UNIX-tyyppisiä rivinvaihtoja
(
\n) eikä DOS-tyyppisiä.
Tyylin tarkastaminen kannattaa automatisoida: pip install
ruff ja ruff check . löytää valtaosan ongelmista
ennen kuin ne päätyvät palautukseen.
4. Muuttujat ja tyypit
- Muuttujia ei esitellä erikseen eikä niille määritellä tyyppiä; nimi syntyy ensimmäisessä sijoituksessa.
- Tyyppi määräytyy arvon mukaan, ja sama nimi voi myöhemmin viitata toisen tyyppiseen arvoon.
- Tyyppiä voi muuntaa muunnosfunktioilla:
int(x)taiint(x, base)muuntaa kokonaisluvuksifloat(x)muuntaa liukuluvuksistr(x)muuntaa merkkijonoksibool(x)muuntaa totuusarvoksi
- Tyypin voi testata
isinstance(x, tyyppi)-funktiolla.foo = "100" isinstance(foo, str) # tosi isinstance(foo, int) # epätosi isinstance(int(foo), int) # tosi
Muunnos voi epäonnistua, jolloin syntyy poikkeus. Tätä ei pidä jättää käsittelemättä, kun arvo tulee käyttäjältä tai verkosta:
>>> int("42")
42
>>> int("kissa")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'kissa'
5. Merkkijonot
Python 3:ssa on kahdenlaisia jonoja: merkkijonot (str,
aina Unicodea) ja tavujonot (bytes).
>>> foo = "testi"
>>> bar = b"testi"
>>> type(foo)
<class 'str'>
>>> type(bar)
<class 'bytes'>
Merkkijonon ja tavujonon välillä liikutaan aina eksplisiittisellä merkistömuunnoksella. Tätä tarvitset heti, kun luet tai kirjoitat verkkoyhteyttä tai binääritiedostoa:
>>> "kissä".encode("utf-8")
b'kiss\xc3\xa4'
>>> b'kiss\xc3\xa4'.decode("utf-8")
'kissä'
Merkkijonojen kirjoittaminen
- Heittomerkein:
'teksti' - Lainausmerkein:
"teksti" - Kolminkertaisin merkein (
'''tai""") kirjoitetaan usean rivin mittaisia merkkijonoja. - Erikoismerkit merkitään kenoviivalla:
\',\",\\,\n,\t. - Raakamerkkijonossa
r"\d+"kenoviiva ei ole erikoismerkki. Käytä sitä säännöllisissä lausekkeissa ja Windows-poluissa.
Yhdistäminen ja muotoilu
Vierekkäiset merkkijonovakiot yhdistyvät automaattisesti, ja
+-operaattori yhdistää muuttujienkin sisällöt:
print("kukku" "luuru") # kukkuluuru
print("kukku" + "luuru") # kukkuluuru
Arvojen upottamiseen käytetään f-merkkijonoja.
Ne ovat nykyisin oletustapa; vanhempia %-muotoiluja ja
str.format-kutsuja näkee lähinnä vanhassa koodissa.
nimi = "Tommi"
saldo = 1234.5678
print(f"Hei {nimi}, saldo on {saldo:.2f} euroa.")
# Hei Tommi, saldo on 1234.57 euroa.
# kaksoispisteen jälkeen annetaan muotoilu, huutomerkin jälkeen muunnos
print(f"{nimi!r} on {len(nimi)} merkkiä pitkä")
# 'Tommi' on 5 merkkiä pitkä
# = näyttää sekä lausekkeen että arvon (kätevä debuggauksessa)
print(f"{saldo = }")
# saldo = 1234.5678
Huomaa ero pyöristämisen ja muotoilun välillä: round(x, 2)
pyöristää itse luvun, kun taas f"{x:.2f}" muotoilee vain
tulosteen ja säilyttää aina kaksi desimaalia.
>>> round(3.14159, 2)
3.14
>>> round(2.5) # pyöristää parilliseen, ei ylöspäin
2
>>> f"{3.1:.2f}"
'3.10'
Vanhemmissa esimerkeissä näkee kahta muuta muotoilutapaa:
"%s: %d" % (nimi, ika) ja "{}: {}".format(nimi,
ika). Molemmat toimivat yhä, ja niitä esiintyy esimerkiksi
WSGI-esimerkeissä, mutta uutta koodia ei kannata kirjoittaa niillä.
Älä koskaan rakenna SQL-kyselyä tai HTML-sivua f-merkkijonolla käyttäjän syötteestä. Käytä tietokannan parametrisoituja kyselyitä ja sabluunakielen automaattista escapetusta.
Palasteleminen
sana = "abcdefghijklmnopqrstuvwxyz"
print(sana[4]) # e
print(sana[0:2]) # ab
print(sana[3:5]) # de
print(sana[:5]) # abcde
print(sana[3:]) # defghijklmnopqrstuvwxyz
print(sana[-3:]) # xyz (negatiivinen indeksi lasketaan lopusta)
print(sana[::2]) # acegikmoqsuwy (joka toinen merkki)
Merkkijonoa ei voi muuttaa — se on immutable. Muokkaus tehdään luomalla uusi merkkijono.
sana[4] = "q" # TypeError
sana = sana[:4] + "q" + sana[5:] # näin uusi arvo syntyy
Yleisiä merkkijonometodeja
Metodit palauttavat aina uuden merkkijonon eivätkä muuta alkuperäistä.
| Metodi | Tekee |
|---|---|
str.strip([chars]) | poistaa tyhjät merkit alusta ja lopusta (myös lstrip, rstrip) |
str.lower() / str.upper() | muuttaa kirjainkokoa |
str.split([sep[, maxsplit]]) | pilkkoo listaksi |
str.splitlines([keepends]) | pilkkoo riveiksi |
str.join(iterable) | liittää jonon alkiot yhteen erottimella |
str.replace(old, new[, count]) | korvaa osamerkkijonon |
str.find(sub) / str.rfind(sub) | etsii indeksin, palauttaa -1 jos ei löydy |
str.startswith(x) / str.endswith(x) | testaa alun tai lopun |
str.isalnum(), str.isalpha(), str.isdigit() | testaa merkkien laadun |
6. Listat
Lista vastaa esimerkiksi JavaScriptin Array-tyyppiä.
Toisin kuin merkkijonoja, listoja voi muuttaa.
Palasteleminen toimii samoin kuin merkkijonoilla.
a = [1, 2, 3, "foo", "bar"]
a[0] = 3 # [3, 2, 3, 'foo', 'bar']
a[1] = a[2] * 3 # [3, 9, 3, 'foo', 'bar']
Viipaleeseen sijoittaminen korvaa viipaleen sisällön. Tyhjä viipale
a[2:2] ei korvaa mitään, joten sijoitus lisää alkiot
ennen indeksiä 2:
a[2:2] = ["kukku", "luuru"]
# [3, 9, 'kukku', 'luuru', 3, 'foo', 'bar']
Käytännössä lisäämiseen käytetään selkeämpiä metodeja:
a.append("uusi") # lisää loppuun
a.insert(2, "kukku") # lisää indeksiin 2
a.extend([1, 2]) # lisää toisen listan alkiot
del a[0] # poistaa indeksin perusteella
a.remove("foo") # poistaa arvon perusteella (ensimmäisen osuman)
arvo = a.pop() # poistaa ja palauttaa viimeisen
Listakoosteet
Uuden listan rakentaminen toisesta onnistuu tyypillisesti yhdellä rivillä listakoosteella (list comprehension). Tämä on Pythonissa tavallisin tapa suodattaa ja muuntaa aineistoa.
luvut = [1, 2, 3, 4, 5]
neliot = [x * x for x in luvut] # [1, 4, 9, 16, 25]
parittomat = [x for x in luvut if x % 2] # [1, 3, 5]
Jonoille yhteisiä operaatioita
Seuraavat toimivat sekä merkkijonoille, listoille että monikoille:
x in s— tosi, josssisältää alkionxx not in s— edellisen käänteinens + t— yhdistää jonot peräkkäins[i]— alkio indeksistäi, numerointi alkaa nollastas[i:j]— viipale indeksistäiindeksiinj(loppu ei kuulu mukaan)s[i:j:k]— viipale askeleellaklen(s)— pituusmin(s)/max(s)— pienin ja suurin alkiosorted(s)— palauttaa järjestetyn listan muuttamatta alkuperäistä (list.sort()puolestaan järjestää listan paikallaan)
Listojen metodit dokumentaatiossa
7. Operaattorit
| Ryhmä | Operaattorit |
|---|---|
| Sijoitus | =, +=, -=,
*=, /=, //=,
%= |
| Aritmetiikka | +, -, *, /,
//, %, ** |
| Vertailu | <, >, <=,
>=, ==, != |
| Totuusarvot | not, and, or |
| Sisältyminen ja identiteetti | in, not in, is,
is not |
| Bittioperaatiot | &, |, ^,
~, <<, >> |
Kaksi kohtaa kannattaa huomata erikseen:
/tuottaa aina liukuluvun, myös kokonaisluvuille:1 / 2on0.5.//on kokonaislukujako, joka pyöristää alaspäin eikä kohti nollaa:7 // 2on3, mutta-7 // 2on-4. Vastaava koskee jakojäännöstä:-7 % 2on1.
== vertaa arvoja, is vertaa identiteettiä
eli sitä, onko kyseessä sama olio. Käytä is-operaattoria
käytännössä vain vertailuun None-arvoon:
if tulos is None:.
8. Ehtolauseet
a = 3
if a > 0:
print("a on suurempi kuin 0")
elif a < 0:
print("a on pienempi kuin 0")
else:
print("a on 0")
Vertailut voi ketjuttaa luonnollisesti, ja ehtolauseke toimii myös sijoituksessa:
if 0 < a < 10:
print("a on välillä 1–9")
merkki = "positiivinen" if a > 0 else "ei-positiivinen"
Tyhjä merkkijono, tyhjä lista, tyhjä sanakirja, 0 ja
None ovat epätosia, joten if lista: riittää
testaamaan, onko listassa alkioita.
9. Toistorakenteet
Pythonissa on while-
ja for-rakenteet.
do..while-rakennetta ei ole. for käy jonon
alkiot järjestyksessä läpi, kuten muiden kielten
foreach.
laskuri = 12
while laskuri > 0:
laskuri = laskuri - 1
a = [1, 2, 3, "foo", "bar"]
for x in a:
print(x)
for i in range(1, 5): # 1, 2, 3, 4 — loppuarvo ei kuulu mukaan
print(i)
range ei luo listaa, vaan laiskan
range-olion, joka tuottaa arvot vasta läpikäynnin aikana.
Siksi range(10_000_000) ei vie muistia. Listan saa
tarvittaessa kutsulla list(range(5)).
Indeksit ja rinnakkaiset jonot
Indeksiä tarvitaan harvoin. Kun sitä tarvitaan, käytä
enumerate-funktiota; kahden jonon rinnakkaiseen läpikäyntiin
sopii zip.
for i, luku in enumerate(luvut):
print(i, luku)
for nimi, ika in zip(nimet, iat):
print(nimi, ika)
Rakenteen muuttaminen läpikäynnin aikana
Läpikäytävää listaa ei saa muuttaa läpikäynnin aikana: alkioiden indeksit siirtyvät ja osa alkioista jää käymättä läpi. Vaihtoehtoja on kolme, ja niistä ensimmäinen on selvästi paras.
luvut = [1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 10, 10]
# Seuraavat ovat vaihtoehtoisia tapoja, eivät peräkkäin suoritettavia rivejä.
# 1. Suositeltavin: rakenna uusi lista ja jätä alkuperäinen rauhaan
parittomat = [x for x in luvut if x % 2]
# 2. Viipalepoisto, kun poistettavat ovat säännöllisin välein
del luvut[::2] # poistaa joka toisen alkion (indeksit 0, 2, 4, ...)
# 3. Jos on pakko poistaa paikallaan indeksin perusteella,
# käy lista läpi lopusta alkuun
for i in reversed(range(len(luvut))):
if luvut[i] % 2 == 0:
del luvut[i]
Vertailun vuoksi seuraava toimii väärin, koska lista lyhenee kesken läpikäynnin:
# älä tee näin
for i in range(len(luvut)):
if luvut[i] % 2 == 0:
del luvut[i] # IndexError tai ohitettuja alkioita
Jos alkuperäinen lista pitää käydä läpi ja muuttaa samalla, iteroi
kopiota: for x in luvut[:]:.
10. Funktiot ja parametrien välitys
def summa(a, b):
"""Palauttaa lukujen a ja b summan."""
return a + b
def tervehdi(nimi, tervehdys="Hei"): # oletusarvo
return f"{tervehdys} {nimi}!"
print(summa(2, 3))
print(tervehdi("Tommi"))
print(tervehdi("Tommi", tervehdys="Moi")) # nimetty argumentti
Älä käytä muuttuvaa oletusarvoa kuten
def lisaa(alkio, lista=[]):. Oletusarvo luodaan vain
kerran funktiota määriteltäessä, joten sama lista säilyy kutsujen
välillä. Kirjoita def lisaa(alkio, lista=None): ja luo
lista funktion sisällä, jos lista is None.
Miten parametrit välittyvät
Pythonissa parametrina välitetään viittaus olioon, mutta parametrin nimi on funktion oma paikallinen nimi. Tätä kutsutaan nimillä call by object reference tai call by assignment. Python ei ole call-by-reference-kieli.
Käytännön sääntö on yksinkertainen:
- Uusi sijoitus parametrin nimeen ei koskaan näy kutsujalle. Se vain sitoo funktion paikallisen nimen toiseen olioon.
- Olion sisällön muuttaminen näkyy kutsujalle, jos olio on muutettava (lista, sanakirja, joukko, oma olio).
- Muuttumattomia (immutable) tyyppejä — kokonaisluku, liukuluku, merkkijono, monikko — ei voi muuttaa lainkaan, joten niiden kohdalla vain ensimmäinen sääntö on merkityksellinen.
#!/usr/bin/env python3
def sijoita(lista):
lista = ["uusi"] # sitoo vain paikallisen nimen — ei näy kutsujalle
return lista
def muuta(lista):
lista[0] = "uusi" # muuttaa itse oliota — näkyy kutsujalle
alkuperainen = ["kissa", "kissa"]
sijoita(alkuperainen)
print(alkuperainen) # ['kissa', 'kissa'] — ei muuttunut
muuta(alkuperainen)
print(alkuperainen) # ['uusi', 'kissa'] — muuttui
teksti = "kissa"
def muuta_teksti(s):
s = "kissakoira" # merkkijono on immutable; sijoitus ei näy
return s
print(muuta_teksti(teksti)) # kissakoira
print(teksti) # kissa
Huomaa, että sijoita ei jätä jälkeä vaikka parametri on
lista. Ero ei siis ole muuttuvien ja muuttumattomien tyyppien välillä
vaan sijoituksen ja muuttamisen välillä. Lisälukemista: Pass by Reference
in Python.
Lambda-lausekkeet ja järjestäminen
Yhden lausekkeen mittaisen funktion voi kirjoittaa nimettömänä lambda-lausekkeena.
Tavallisin käyttökohde on kertoa sort- tai
sorted-kutsulle, minkä perusteella alkioita verrataan.
henkilot = [
{"nimi": "Ville", "palkka": 1000},
{"nimi": "Kalle", "palkka": 2000},
{"nimi": "Maija", "palkka": 3000},
]
# lambda ottaa alkion ja palauttaa vertailtavan arvon
henkilot.sort(key=lambda h: h["nimi"]) # nimen mukaan
uusimmat = sorted(henkilot, key=lambda h: h["palkka"], reverse=True)
# täsmälleen sama erillisenä funktiona kirjoitettuna
def palkka(henkilo):
return henkilo["palkka"]
henkilot.sort(key=palkka)
Ero metodien välillä: list.sort() järjestää listan
paikallaan ja palauttaa None, kun taas sorted()
palauttaa uuden listan ja jättää alkuperäisen ennalleen. Kirjoitusvirhe
lista = lista.sort() hävittää siis koko listan.
Jos avain puuttuu joltakin alkiolta tai sen arvo on
None, järjestäminen kaatuu virheeseen, joka alkaa
TypeError: '<' not supported between instances of ja
nimeää vertaillut tyypit. Anna silloin puuttuvalle arvolle korvike:
henkilot.sort(key=lambda h: h.get("palkka") or 0)
Dekoraattorit
Funktion määrittelyn edellä oleva @-alkuinen rivi on dekoraattori.
Se antaa juuri määritellyn funktion toiselle funktiolle, joka voi
rekisteröidä sen tai kääriä sen uuteen käytökseen.
@app.route("/laskuri")
def laskuri():
return "1"
# tarkoittaa käytännössä samaa kuin:
def laskuri():
return "1"
laskuri = app.route("/laskuri")(laskuri)
Omia dekoraattoreita ei tarvitse osata kirjoittaa, mutta merkinnän tunnistaminen kannattaa: Flaskissa reitit, virhekäsittelijät ja dataclassit määritellään juuri näin.
11. Näkyvyysalueet ja global
Funktion sisällä syntynyt nimi on funktion oma paikallinen muuttuja, joka häviää funktion päättyessä. Funktion ulkopuolella määriteltyä nimeä voi lukea funktion sisällä ilman erityistoimia:
vakio = 10
def lue():
print(vakio) # 10 — lukeminen onnistuu
Sijoitus on eri asia. Heti kun funktiossa sijoitetaan nimeen, siitä tulee paikallinen koko funktion ajaksi — myös ennen sijoitusriviä:
laskuri = 0
def kasvata():
laskuri = laskuri + 1 # UnboundLocalError
def kasvata_oikein():
global laskuri # nyt sijoitus kohdistuu moduulitason nimeen
laskuri = laskuri + 1
Muuttuvan olion sisältöä voi muuttaa ilman
global-määrettä, koska silloin ei sijoiteta nimeen vaan
muutetaan oliota:
tapahtumat = []
def lisaa(x):
tapahtumat.append(x) # toimii ilman globalia
Älä käytä globaaleja muuttujia tilan säilyttämiseen web-sovelluksessa. Globaali muuttuja on aina prosessikohtainen: CGI-ohjelmassa tulkki käynnistyy uudelleen jokaisella pyynnöllä ja arvo nollautuu, WSGI-sovelluksessa arvo säilyy vain siihen asti kunnes prosessi käynnistetään uudelleen, ja kuormantasatussa ympäristössä jokaisella instanssilla on oma kopionsa. Sovelluksen tila kuuluu istuntoon, tietokantaan tai tiedostoon.
Funktion sisäkkäisessä funktiossa vastaava avainsana ulomman funktion
muuttujalle on nonlocal.
12. Moduulit
Ohjelman voi jakaa moduuleihin.
Oma moduuli syntyy kirjoittamalla funktiot .py-päätteiseen
tiedostoon, ja sitä käytetään import-lauseella.
import json # koko moduuli
import urllib.parse as parse # aliaksella
from pathlib import Path # yksittäinen nimi
data = json.dumps({"a": 1})
polku = Path("tiedosto.txt")
Vältä muotoa from moduuli import *: se tuo näkyviin
tuntemattoman määrän nimiä ja voi ylikirjoittaa omia muuttujiasi.
Suoritus vs. import
Moduulin koodi suoritetaan, kun se importataan. Siksi ohjelman
käynnistyskoodi kuuluu if __name__ == "__main__": -lohkon
sisään — muuten se suoritetaan myös silloin, kun joku importtaa
moduulin.
#!/usr/bin/env python3
def paaohjelma():
print("Hello world!")
if __name__ == "__main__":
paaohjelma()
13. Poikkeukset
Pythonissa lähes kaikki virheet aiheuttavat poikkeuksen.
Poikkeus napataan try–except-rakenteella.
try:
luku = int(syote)
except ValueError:
print("Syöte ei ollut kokonaisluku")
Älä kirjoita paljasta except:-lauseketta.
Se nappaa myös kirjoitsvirheet muuttujien ja funktioiden nimissä sekä
Ctrl+C-keskeytyksen, ja piilottaa tulkin oman
virheilmoituksen. Nappaa aina se poikkeustyyppi, jonka osaat käsitellä.
Jos joudut nappaamaan laajasti, käytä except Exception as
virhe: ja kirjaa virhe lokiin.
Ero näkyy heti käytännössä. Ensimmäisessä esimerkissä
Int on kirjoitusvirhe (oikea nimi on int), ja
paljas except peittää sen täysin:
# huono: virheilmoitus katoaa, koodi näyttää "toimivan"
try:
luku = Int("a")
except:
print("Syöte ei ollut kokonaisluku")
# hyvä: NameError ei ole ValueError, joten kirjoitusvirhe tulee näkyviin
try:
luku = Int("a")
except ValueError:
print("Syöte ei ollut kokonaisluku")
Koko rakenne
try:
f = open("tiedosto.txt", encoding="utf-8")
except FileNotFoundError:
print("Tiedostoa ei ole")
except PermissionError as virhe:
print(f"Ei lukuoikeutta: {virhe}")
else:
# suoritetaan, jos poikkeusta ei tullut
print(f.read())
f.close()
finally:
# suoritetaan aina, myös poikkeuksen sattuessa
print("Valmis")
Poikkeuksen voi myös nostaa itse, ja omat poikkeusluokat periytetään
luokasta Exception:
class VirheellinenSaldo(Exception):
"""Nostetaan, kun tilin saldo ei riitä."""
def nosta(saldo, summa):
if summa > saldo:
raise VirheellinenSaldo(f"Saldo {saldo} ei riitä nostoon {summa}")
return saldo - summa
Palvelinohjelmassa poikkeus kannattaa kirjata lokiin täydellä pinolistauksella eikä tulostaa käyttäjälle:
import logging
try:
kasittele_pyynto()
except Exception:
logging.exception("Pyynnön käsittely epäonnistui")
return "Palvelinvirhe", 500
14. Sanakirja (dict)
Sanakirja on kokoelma, jossa arvoihin viitataan uniikeilla avaimilla. Muissa kielissä vastaavaa kutsutaan assosiatiiviseksi taulukoksi; JavaScriptissa lähin vastine on Map.
Python 3.7:stä lähtien sanakirja säilyttää alkioiden lisäysjärjestyksen, ja tämä on osa kielimäärittelyä. Vanhoissa materiaaleissa toistuva väite "dict on järjestämätön" ei siis enää pidä paikkaansa.
>>> henkilo = {"nimi": "Tommi", "ika": 30, "kaupunki": "Jyväskylä"}
>>> henkilo["nimi"]
'Tommi'
>>> henkilo["sposti"] = "tommi@example.org" # lisäys
>>> "ika" in henkilo # avaimen olemassaolo
True
>>> henkilo.get("puhelin") # None, ei poikkeusta
>>> henkilo.get("puhelin", "ei tiedossa")
'ei tiedossa'
>>> del henkilo["sposti"]
Läpikäynti tapahtuu yleensä items()-metodilla, jolloin
saa avaimen ja arvon kerralla. Pelkkä for avain in
sanakirja: käy avaimet läpi — erillistä
.keys()-kutsua ei tarvita.
for avain, arvo in henkilo.items():
print(f"{avain}: {arvo}")
for avain in henkilo: # sama kuin henkilo.keys()
print(avain)
for arvo in henkilo.values():
print(arvo)
Puuttuvaan avaimeen viittaaminen nostaa poikkeuksen, joten käytä
get-metodia aina, kun avaimen olemassaolo ei ole varmaa
— esimerkiksi lomakedataa käsitellessä:
>>> henkilo["puhelin"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'puhelin'
Myös sanakirjan voi rakentaa koosteella:
pituudet = {sana: len(sana) for sana in ["kissa", "koira", "hevonen"]}
# {'kissa': 5, 'koira': 5, 'hevonen': 7}
15. Monikko (tuple)
Monikko on listaa vastaava rakenne, jonka sisältöä ei voi muuttaa.
>>> a = (1, 2, 3, "testi")
>>> a[0]
1
>>> a[0] = 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
Huomaa ero: a[0] = 0 yrittää muuttaa monikkoa ja
epäonnistuu, mutta a = [0] onnistuu aina — se ei muuta
monikkoa, vaan sitoo nimen a kokonaan uuteen listaan.
Monikkoa käytetään esimerkiksi funktion palauttaessa useita arvoja, jolloin ne voi purkaa suoraan muuttujiin:
def jaa(a, b):
return a // b, a % b # palauttaa monikon
osamaara, jaannos = jaa(17, 5) # 3, 2
Yhden alkion monikko kirjoitetaan pilkun kanssa:
(1,). Pelkkä (1) on luku yksi sulkeissa.
16. Joukko (set)
Joukko ei voi sisältää duplikaatteja, eikä sen alkioilla ole järjestystä. Joukkoon voi soveltaa joukko-opin operaatioita.
>>> lista = ["tommi", "antti", "ville", "tommi"]
>>> joukko = set(lista)
>>> joukko
{'antti', 'ville', 'tommi'}
>>> "tommi" in joukko
True
>>> joukko2 = {"joonas", "antti", "kalle"}
>>> joukko - joukko2 # erotus: joukon jäsenet, jotka eivät ole joukossa 2
{'ville', 'tommi'}
>>> joukko & joukko2 # leikkaus: molemmissa olevat
{'antti'}
>>> joukko ^ joukko2 # symmetrinen erotus: vain toisessa olevat
{'joonas', 'kalle', 'ville', 'tommi'}
>>> joukko | joukko2 # yhdiste: kummassa tahansa olevat
{'joonas', 'kalle', 'antti', 'ville', 'tommi'}
Joukon tulostusjärjestys on määrittelemätön eikä sitä pidä
käyttää mihinkään. Yllä olevat tulosteet voivat siis näyttää sinun
koneellasi eri järjestyksessä. Kun järjestystä tarvitaan, kirjoita
sorted(joukko).
Tyhjä joukko luodaan kutsulla set(). Merkintä
{} tarkoittaa tyhjää sanakirjaa. Joukon alkioiden on oltava
muuttumattomia, joten listaa ei voi laittaa joukkoon — monikon voi.
Duplikaattien poisto listasta on joukon tavallisin käyttötapaus:
uniikit = list(set(lista)) # järjestys katoaa
uniikit = sorted(set(lista)) # aakkosjärjestyksessä
17. Sisäkkäiset tietorakenteet
Käytännön data on lähes aina listojen ja sanakirjojen yhdistelmä: lista sanakirjoja, sanakirja jonka arvoina on listoja, tai näiden sekoitus. Juuri tällaista rakennetta selain lähettää ja JSON kuljettaa, joten sen läpikäynti on kurssin keskeisin taito.
data = [
{
"nimi": "Kalle",
"ammatti": "Yliopistonopettaja",
"syntymävuosi": 1980,
"palkka": 2000,
},
{
"nimi": "Ville",
"ammatti": "Opiskelija",
"syntymävuosi": 1995,
"kotipaikka": "Jyväskylä",
"palkka": None,
},
]
Rakenne käydään läpi sisäkkäisillä silmukoilla. Kirjoita läpikäynti niin, ettei se tunne avainten nimiä etukäteen — silloin se toimii myös, kun rakenteeseen lisätään kenttiä:
for henkilo in data:
for avain, arvo in henkilo.items():
print(avain, arvo)
print()
Jos avaimet halutaan aakkosjärjestyksessä, järjestä ne läpikäynnissä. Sanakirja säilyttää lisäysjärjestyksen, joka harvoin on sama kuin haluttu esitysjärjestys:
for avain in sorted(henkilo):
print(avain, henkilo[avain])
Kaikilla alkioilla ei välttämättä ole samoja avaimia. Puuttuva avain
käsitellään joko get-metodilla tai
try–except KeyError -rakenteella;
ensimmäinen on lyhyempi, kun oletusarvo riittää:
# oletusarvo puuttuvalle avaimelle
for henkilo in data:
print(henkilo.get("kotipaikka", ""))
# arvo voi myös olla None, jolloin get palauttaa None eikä oletusarvoa
palkat = [h["palkka"] for h in data if h.get("palkka") is not None]
keskiarvo = sum(palkat) / len(palkat) if palkat else 0
Syvemmällä rakenteessa listan sisällä voi olla lisää sanakirjoja. Silloin silmukoita on yksinkertaisesti yksi taso enemmän:
tuotteet = [
{
"nimi": "Munkki",
"taytteet": [{"nimi": "sokeri"}, {"nimi": "kaneli"}],
},
]
for tuote in tuotteet:
print(tuote["nimi"])
for tayte in tuote["taytteet"]:
print(" -", tayte["nimi"])
Kun rakenne on syvä, kannattaa tulostaa se ensin
json.dumps(data, indent=2, ensure_ascii=False) -kutsulla ja
katsoa, millainen se todella on, ennen kuin kirjoittaa läpikäynnin.
18. Luokat
Luokat toimivat pitkälti kuten muissakin kielissä, mutta muutama erikoisuus on syytä huomioida.
#!/usr/bin/env python3
class Tili:
# Suoraan luokan alla esitellyt muuttujat ovat LUOKKAKOHTAISIA
# eli yhteisiä kaikille instansseille.
korko = 0.02
tapahtumat = [] # ANSA: tämä lista on yhteinen kaikille tileille!
def __init__(self, omistaja, saldo=0.0):
# Konstruktorissa self:iin sijoitetut ovat INSTANSSIKOHTAISIA
self.omistaja = omistaja
self.saldo = saldo
self.omat_tapahtumat = []
def talleta(self, summa):
# metodissa on aina käytettävä self-etuliitettä
self.saldo += summa
self.tapahtumat.append(summa)
self.omat_tapahtumat.append(summa)
def __str__(self):
# määrittää, miltä olio näyttää tulostettaessa
return f"{self.omistaja}: {self.saldo:.2f} €"
tili_a = Tili("Tommi", 100.0)
tili_b = Tili("Hellu")
tili_a.talleta(50.0)
print(tili_a) # Tommi: 150.00 €
print(tili_b) # Hellu: 0.00 €
# Instanssikohtainen lista on kummallakin omansa
print(tili_a.omat_tapahtumat) # [50.0]
print(tili_b.omat_tapahtumat) # []
# Luokkakohtainen lista on YHTEINEN — tästä ansasta on syytä olla tietoinen
print(tili_a.tapahtumat) # [50.0]
print(tili_b.tapahtumat) # [50.0]
# Metodia kutsuttaessa self-parametria ei anneta; Python välittää sen itse.
# Olioon voi myös lisätä uusia ominaisuuksia lennosta (vrt. JavaScript).
tili_a.pankki = "Esimerkkipankki"
Muuttuva luokkamuuttuja (yllä tapahtumat) on yleinen
virhelähde: kaikki oliot jakavat saman listan. Kun jokainen olio
tarvitsee omansa, luo se konstruktorissa.
Perintä
class Sailytystili(Tili):
korko = 0.035
def __init__(self, omistaja, saldo=0.0, nostoraja=1000.0):
super().__init__(omistaja, saldo)
self.nostoraja = nostoraja
def __str__(self):
return f"{super().__str__()} (säästötili)"
Pelkkää tietoa säilövään luokkaan kannattaa käyttää dataclassia,
joka kirjoittaa konstruktorin, vertailun ja
__repr__-metodin puolestasi:
from dataclasses import dataclass
@dataclass
class Piste:
x: float
y: float = 0.0
p = Piste(1.0, 2.0)
print(p) # Piste(x=1.0, y=2.0)
19. Tyyppivihjeet
Tyyppivihjeet
eivät vaikuta ohjelman suoritukseen, mutta ne dokumentoivat funktion
käyttötarkoituksen ja antavat editorille sekä tarkistustyökaluille
(esimerkiksi mypy) mahdollisuuden löytää virheitä ennen
ajoa.
def summa(a: int, b: int) -> int:
return a + b
def etsi(nimet: list[str], haku: str) -> str | None:
for nimi in nimet:
if haku in nimi:
return nimi
return None
20. Tiedostojen käsitteleminen
Tiedostot avataan open-funktiolla.
Käytä aina with-lohkoa, jolloin tiedosto sulkeutuu
automaattisesti myös poikkeuksen sattuessa, ja anna aina
encoding-parametri — muuten merkistö riippuu käyttöjärjestelmän
asetuksista ja ohjelma toimii eri tavalla eri koneissa.
#!/usr/bin/env python3
# koko tiedosto kerralla
with open("test.txt", encoding="utf-8") as f:
sisalto = f.read()
print(sisalto)
# rivi kerrallaan (ei lue koko tiedostoa muistiin)
with open("test.txt", encoding="utf-8") as f:
for rivi in f:
print(rivi.rstrip("\n"))
# kirjoittaminen: "w" korvaa tiedoston, "a" lisää loppuun
with open("tiedosto.txt", "w", encoding="utf-8") as f:
f.write("Kirjoitetaan tiedostoon\n")
Vanhoissa materiaaleissa neuvotaan käyttämään
io.open-funktiota open-funktion sijaan. Neuvo
on Python 2:n ajalta: Python 3:ssa open
on io.open, eli täsmälleen sama funktio.
Käytä lyhyempää muotoa.
Binääritiedostoa luettaessa käytetään tilaa "rb", jolloin
encoding-parametria ei anneta ja tulos on
bytes. Polkujen käsittelyyn kannattaa käyttää pathlib-moduulia,
joka toimii samoin sekä Linuxissa että Windowsissa:
from pathlib import Path
polku = Path("data") / "test.txt"
if polku.exists():
sisalto = polku.read_text(encoding="utf-8")
21. Serialisointi
Kun muuttujien tai tietorakenteiden sisältö halutaan tallentaa tiedostoon tai siirtää verkon yli, rakenne on serialisoitava siirrettävään muotoon.
Useimmiten oikea valinta on JSON: se on tekstimuotoista, luettavaa ja kaikkien kielten ymmärtämää. Linkkiin tai lomakkeelle sijoitettaessa JSON pitää vielä url-koodata tai muuntaa base64-muotoon. Pythonin oma binääriformaatti on pickle.
Älä koskaan lue picklellä dataa, jota et ole itse
tuottanut. pickle.loads voi suorittaa mielivaltaista
koodia, joten selaimelta, lomakkeelta tai linkistä palaava pickle-data
on suora tie palvelimen valtaamiseen. Web-sovelluksessa käytä JSONia.
JSON merkkijonona
json-moduulissa on neljä funktiota, jotka on helppo sekoittaa keskenään. Loppu-s tarkoittaa merkkijonoa (string), ilman s:ää käsitellään tiedosto-oliota.
| Funktio | Suunta | Kohde |
|---|---|---|
json.dumps(obj) | Python → JSON | merkkijono |
json.loads(s) | JSON → Python | merkkijono |
json.dump(obj, f) | Python → JSON | tiedosto-olio |
json.load(f) | JSON → Python | tiedosto-olio |
import json
teksti = json.dumps(data) # rakenne merkkijonoksi
rakenne = json.loads(teksti) # merkkijono takaisin rakenteeksi
Oletuksena json.dumps pakenee ääkköset muotoon
\u00e4. Se on kelvollista JSONia, mutta lukukelvotonta
ihmiselle. Anna ensure_ascii=False, niin merkit säilyvät
sellaisinaan — tiedostoon kirjoitettaessa muista silloin myös
encoding="utf-8".
>>> json.dumps({"kaupunki": "Jyväskylä"})
'{"kaupunki": "Jyv\\u00e4skyl\\u00e4"}'
>>> json.dumps({"kaupunki": "Jyväskylä"}, ensure_ascii=False)
'{"kaupunki": "Jyväskylä"}'
JSON tiedostoon ja tiedostosta
import json
# luku
with open("tietorakenne.json", encoding="utf-8") as f:
data = json.load(f)
# kirjoitus: indent tekee tuloksesta luettavaa, ensure_ascii säilyttää ääkköset
with open("tietorakenne.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
Huomaa, että "w"-tila tyhjentää tiedoston heti
avattaessa. Jos ohjelma kaatuu kirjoituksen aikana, vanha sisältö on jo
menetetty.
JSON verkosta
Verkosta luettaessa vastaus on tiedosto-olion kaltainen, joten sen voi
antaa suoraan json.load-funktiolle. Kirjasto on urllib.request;
moni projekti käyttää sen sijaan helppokäyttöisempää requests-kirjastoa.
import json
import urllib.request
with urllib.request.urlopen("https://esimerkki.example/data.json") as vastaus:
data = json.load(vastaus)
Verkkokutsu voi epäonnistua monella tavalla — palvelin ei vastaa, osoite on väärä, vastaus ei ole JSONia — joten kääri se poikkeuskäsittelyyn:
from urllib.error import URLError
try:
with urllib.request.urlopen(osoite, timeout=10) as vastaus:
data = json.load(vastaus)
except URLError as virhe:
print("Yhteys epäonnistui:", virhe)
except json.JSONDecodeError:
print("Vastaus ei ollut kelvollista JSONia")
Esitysmuotojen koko
#!/usr/bin/env python3
import base64
import json
import pickle
import urllib.parse
taulukko = [
[0, 1, 0, 1, 0, 1, 0, 1],
[0, 1, 5, 1, 0, 1, 0, 1],
[0, 1, 4, 1, 0, 1, 0, 1],
[0, 1, 3, 1, 0, 1, 0, 1],
[0, 1, 0, 1, 2, 1, 0, 1],
[0, 1, 0, 1, 7, 1, 0, 1],
[0, 1, 0, 1, 10, 1, 0, 1],
[0, 1, 0, 1, 0, 1, 0, 1],
]
# JSON sellaisenaan. Soveltuu lomakkeen kentän arvoksi.
json_versio = json.dumps(taulukko, indent=None, separators=(",", ":"))
print("json", len(json_versio), json_versio, sep="\n")
# JSON url-koodattuna. Soveltuu linkkeihin.
url_versio = urllib.parse.quote_plus(json_versio)
print("json+url-koodaus", len(url_versio), url_versio, sep="\n")
# JSON base64-koodattuna. Soveltuu linkkeihin ja on lyhyempi kuin url-koodaus.
b64_versio = base64.urlsafe_b64encode(
json_versio.encode("utf-8")
).decode("ascii")
print("json+base64", len(b64_versio), b64_versio, sep="\n")
# Pickle base64-koodattuna. Yllättäen pisin vaihtoehto.
pickle_versio = base64.urlsafe_b64encode(pickle.dumps(taulukko)).decode("ascii")
print("pickle+base64", len(pickle_versio), pickle_versio, sep="\n")
Ohjelman tuloste:
$ ./serialisointi.py
json
146
[[0,1,0,1,0,1,0,1],[0,1,5,1,0,1,0,1],[0,1,4,1,0,1,0,1],[0,1,3,1,0,1,0,1],[0,1,0,1,2,1,0,1],[0,1,0,1,7,1,0,1],[0,1,0,1,10,1,0,1],[0,1,0,1,0,1,0,1]]
json+url-koodaus
308
%5B%5B0%2C1%2C0%2C1%2C0%2C1%2C0%2C1%5D%2C ...
json+base64
196
W1swLDEsMCwxLDAsMSwwLDFdLFswLDEsNSwxLDAsMSwwLDFdLFswLDEsNCwxLDAs ...
pickle+base64
236
gASVpQAAAAAAAABdlChdlChLAEsBSwBLAUsASwFLAEsBZV2UKEsASwFLBUsBSwBL ...
Base64-muoto on tässä selvästi tiiviimpi kuin url-koodattu JSON, mutta ero riippuu tallennettavasta rakenteesta. Kannattaa siis miettiä jo alkuvaiheessa, millainen rakenne on tiivis. Pickle-tuloksen pituus riippuu käytetystä protokollaversiosta, joten se voi vaihdella Python-versioiden välillä.
22. Liite: Python 2 ja Python 3
Python 2:n tuki päättyi 1.1.2020, eikä sitä käytetä tällä kurssilla. Verkosta löytyy kuitenkin runsaasti vanhoja esimerkkejä, joten seuraavat erot on hyvä tunnistaa.
| Asia | Python 3 | Python 2 (vanhentunut) |
|---|---|---|
| Tulostus | print("foo") on funktio |
print "foo" oli lause |
| Jakolasku | 1 / 2 on 0.5 |
1 / 2 oli 0 |
| Merkkijonot | aina Unicodea; tavut erillinen bytes-tyyppi |
tyypit str ja unicode,
u"teksti" |
| Lukualueet | range palauttaa laiskan olion |
range palautti listan, xrange oli
laiska |
| Merkistön määrittely | UTF-8 on oletus | # -*- coding: utf-8 -*- tarvittiin |
23. Lisätietoa
- The Python Tutorial — virallinen aloitusopas
- Python Standard Library — vakiokirjaston hakuteos
- PEP 8 — Style Guide for Python Code
- Python FAQ
- Pythonin perusteita (TIM)
- Pythonin luokista (TIM)
Käyttäjien kommentit