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:

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

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

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ä.

Usein tarvittavat merkkijonometodit
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:

Listojen metodit dokumentaatiossa

7. Operaattorit

Pythonin tärkeimmät 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:

== 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:

#!/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 tryexcept-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 tryexcept 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.

json-moduulin funktiot
FunktioSuuntaKohde
json.dumps(obj)Python → JSONmerkkijono
json.loads(s)JSON → Pythonmerkkijono
json.dump(obj, f)Python → JSONtiedosto-olio
json.load(f)JSON → Pythontiedosto-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.

Tavallisimmat erot vanhoja esimerkkejä luettaessa
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

Käyttäjien kommentit

Kommentoi Lisää kommentti