4.1. Tiedostojen kirjoittaminen ja lukeminen
Esitietokurssilla CSE-A1111 Ohjelmoinnin peruskurssi Y1 tutustuttiin jonkin verran tekstitiedostojen käsittelyyn. Jos perusteet ovat päässeet unohtumaan, voi käydä läpi Y1-kurssin kurssimonisteen luvun 6.2: "Tekstitiedostojen käsittely".
Tässä luvussa perehdymme aiheeseen hieman tarkemmin. Tutustumme myös merkkijonojen muotoiluun, jota tulostuksen yhteydessä usein tarvitaan. Ulkoisena lähdemateriaalina on
- The Python Tutorial: 7. Input and Output
- The Python Standard Library: 6.1.3. Format String Syntax
- The Python Standard Library: 16.2. io — Core tools for working with streams
Nämä lähtee kannattaa pitää mielessä ohjelmointitehtäviä ja omaa projektia tehtäessä.
Tekstitiedoston lukeminen
Tässä luvussa käsittelemme erilaisia tapoja lukea tekstitiedostoja. Aloitamme yksinkertaisimmasta, eli tiedoston lukemisesta rivi kerrallaan. Sen jälkeen tutustumme tietyn merkkimäärän lukemiseen tiedostosta ja koko tiedoston lukemiseen kerrallaan.
Tekstitiedoston lukeminen rivi kerrallaan
Helpoin tapa tekstitiedoston lukemiseen lienee rivi kerrallaan. Yleinen muoto Pythonissa tälle on seuraava:
file = open(path)
for line in file:
... # Tehdään jotain linelle
Käsiteltävä tiedosto on siis ensin avattava Pythonin sisäänrakennetulla funktiolla open, minkä jälkeen voimme
iteroida for
-lauseella tiedoston rivien ylitse. Voisimme myös eksplisiittisesti lukea tiedostosta rivi kerrallaan
seuraavasti:
file = open(path)
line = file.readline()
while line != '':
...
line = file.readline()
Metodi file.readline() palauttaa tyhjän merkkijonon ''
tiedoston päättyessä.
Rivin lopussa on rivinvaihtomerkki!
Tekstitiedoston rivit erotetaan toisistaan rivinvaihtomerkeillä. Readlinen palauttaman rivin lopussa on rivinvaihtomerkki
'\n'
(loppumerkkiä voidaan tarvittaessa säädellä funktion open
parametreilla). Myös iteroitaessa
tiedoston rivien yli for
-lauseella on jokaisen rivin lopussa rivinvaihtomerkki.
Helppo tapa päästä eroon rivinvaihdosta, mikäli sillä ei ole käyttöä, on kutsua metodia str.rstrip().
Jos luettu merkkijono on tyhjä, tuloksena on siis rivi '\n'
, mikä puolestaan on eri kuin tyhjä merkkijono ''
.
Jatkossa kuitenkin käytämme suoraviivaisempaa iterointia tiedon rivien ylitse.
Kokeillaan tehdä jotain tiedostolle bok.txt, jonka sisältö on seuraava:
"If you think education is expensive, try ignorance"
— Derek Bok, former President of Harvard University
Voisimme vaikkapa laskea rivien sekä merkkien määrän tiedostossa:
>>> path = 'bok.txt'
>>> file = open(path)
>>> lineCount = 0
>>> characterCount = 0
>>> for line in file:
... lineCount += 1
... characterCount += len(line)
...
>>> print("File {} has {} characters in {} lines".format(path, characterCount, lineCount))
File bok.txt has 109 characters in 3 lines
Hienoa!
Pari tärkeää asiaa on kuitenkin päässyt unohtumaan. Ensiksi, mitä tapahtuu, jos tiedostoa ei ole?
>>> path = 'otherfile.txt'
>>> file = open(path)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'otherfile.txt'
Funktio open heittää siis poikkeuksen FileNotFoundError mikäli tiedostoa ei ole. The Python Standard Libraryn luvussa 2. Built-in Functions on kuvattu tarkemmin, mitä open tekee eri tilanteissa:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
Open file and return a corresponding file object. If the file cannot be opened, an OSError is raised.
Lienee siis syytä siepata luokan FileNotFoundError yläluokka OSError eikä pelkkää FileNotFoundErroria.
Tämä on muuttunut Pythonin versiossa 3.3
Aiemmissa versioissa poikkeuksena oli IOError
.
Muutetaan siis koodiamme niin, että mahdollinen OSError
napataan:
path = 'bok.txt'
try:
file = open(path)
except OSError:
print("Could not open {}".format(path))
else:
lineCount = 0
characterCount = 0
for line in file:
lineCount += 1
characterCount += len(line)
print("File {} has {} characters in {} lines".format(path, characterCount, lineCount))
Tässä käytimme try-except
-rakenteen else
-osiota, joka siis suoritetaan vain jos try
-osio ei
heitä poikkeusta.
Yksi pieni tekninen ongelma koodissa vielä on. Kun open
avaa tiedoston, varaa se sitä varten
käyttöjärjestelmältä resurssin, niin sanotun tiedostokahvan (engl. file handle). Vaikka olemme lukeneet kaikki rivit
tiedostosta, ei Python tiedä että emme enää tarvitse tiedostoa mihinkään eikä osaa vapauttaa tiedostokahvaa. Jos
lukisimme monia tiedostoja näin, pitäisi Python turhaan varattuna tiedostokahvoja ja ne saattaisivat loppua jossain
vaiheessa kesken.
On siis parasta vapauttaa kahva ja se tapahtuu metodille file.close(). Voisimme laittaa kutsun file.close()
else
-osion
loppuun. Tässä esimerkissä tämä tuskin olisi vaarallista, mutta yleensä emme voi olla varmoja, että else
-osion koodi
ei heitä uusia poikkeuksia, joten on paras laittaa closen kutsu finally
-osioon. Siellä tulee kuitenkin tarkistaa,
että avaaminen on onnistunut. Tämä selviää tarkistamalla onko muuttuja file
sidottu.
Kääräistään lopuksi koko koodi funktion sisälle.
import sys
def print_character_and_line_counts(path):
input_file = None
try:
input_file = open(path)
except OSError:
print("Could not open {}".format(path), file=sys.stderr)
else:
lineCount = 0
characterCount = 0
for line in input_file:
lineCount += 1
characterCount += len(line)
print("File {} has {} characters in {} lines".format(path, characterCount, lineCount))
finally:
if input_file:
input_file.close()
Kokeillaan tätä tiedostoilla 'bok.txt' ja 'otherfile.txt':
>>> print_character_and_line_counts('bok.txt')
File bok.txt has 109 characters in 3 lines
>>> print_character_and_line_counts('otherfile.txt')
Could not open otherfile.txt
Sama kakku siististi valmiissa kääreessä: with
-rakenne
Pythonissa on with
-rakenne, jonka avulla voidaan hallistusti käyttää olioita, jotka edellyttävät siivoustoimenpiteitä käytön
lopussa. Tiedoston avaaminen lukemista varten ja sulkeminen lopuksi hoituu siististi with
-rakenteella:
def print_character_and_line_counts(path):
with open(path) as input_file:
lineCount = 0
characterCount = 0
for line in input_file:
lineCount += 1
characterCount += len(line)
print("File {} has {} characters in {} lines".format(path, characterCount, lineCount))
Tämä rakenne sulkee mahdollisesti avoimen tiedoston tuli poikkeuksia tai ei, mutta poikkeukset pääsevät kuitenkin with
in
ulkopuolelle, joten niihin on tarvittaessa varauduttava, tyyliin:
def print_character_and_line_counts3(path):
try:
with open(path) as input_file:
lineCount = 0
characterCount = 0
for line in input_file:
lineCount += 1
characterCount += len(line)
print("File {} has {} characters in {} lines".format(path, characterCount, lineCount))
except OSError:
print("Could not open {}".format(path), file=sys.stderr)
Tiedoston kaikkien rivien lukeminen kerralla
Joskus haluamme lukea kaikki tiedoston rivit kerralla ja tehdä sitten näille jotain. Tätä varten on tarjolla metodi str.readlines(), joka palauttaa tiedoston rivit listana merkkijonoja.
Esimerkiksi jos haluamme lajitella tiedoston sisältämät rivit aakkosjärjestykseen, luemme kaikki rivit listaksi merkkijonoja ja kutsumme listalle määritettyä metodia list.sort(). Lopuksi vielä muistamme poistaa turhan rivinvaihtomerkin kunkin rivin lopusta ennen tulostusta:
def print_sorted_file(path):
try:
with open(path) as input_file:
lines = input_file.readlines()
lines.sort()
for line in lines:
print(line.rstrip())
except OSError:
print("Could not open {}".format(path), file=sys.stderr)
Tätä voi kokeilla vaikkapa oheiseen tiedostoon exceptions.txt, jossa on Pythonin valmiiden poikkeusluokkien nimet.
Tämä voi vaatia paljon muistitilaa!
Metodia file.readlines() ei kannata käyttää, mikäli emme tarvitse kaikkia rivejä käsittelyyn samalla kertaa, kuten yllä olevassa lajitteluesimerkissä. Muuten varaamme tarpeettomasti muistitilaa, mikäli tiedosto on kovin iso.
Esimerkkitehtävä
Virta eli stream
Tähän asti olemme lukeneet tiedostoa käyttäen funktion open
tuottamaa oliota miettimättä sen enempää, mikä
kyseinen olio oikein on. Katsotaan, mitä Python toteaa oliosta:
>>> file=open('bok.txt')
>>> file
<_io.TextIOWrapper name='bok.txt' mode='r' encoding='UTF-8'>
>>> type(file)
<class '_io.TextIOWrapper'>
File
on siis luokan io.TextIOWrapper
ilmentymä. TextIOWrapper on tekstitiedostojen käsittelyyn tarkoitettu luokka ja sen dokumentaatio on the python standard
libraryn luvussa 16.2. io — Core tools for working with streams, mistä
löytyy tarkempi kuvaus tarjolla olevista luokista ja metodeista.
Tällaisia olioita kutsutaan tekstivirroiksi (engl. text stream). Tekstivirtaa voidaan ajatella peräkkäisenä jonona
merkkejä c0, c1, ..., cn-1 ja positiona i
joka on välillä
0...n
.
Positio i kertoo, mistä kohtaa seuraavaksi luetaan. Kun funktio open
palauttaa avatun virran, on positio
aluksi 0. Lukeminen siirtää positiota eteenpäin; esimerkiksi kun luemme tiedostosta 'bok.txt' ensimmäisen rivin, on
positio 53, kun luemme toisen rivin 54 ja kolmannen rivin 109.
>>> file=open('bok.txt')
>>> file.tell()
0
>>> file.readline()
'"If you think education is expensive, try ignorance"\n'
>>> file.tell()
53
>>> file.readline()
'\n'
>>> file.tell()
54
>>> file.readline()
' --- Derek Bok, former President of Harvard University\n'
>>> file.tell()
109
>>> file.readline()
''
>>> file.tell()
109
>>> file.close()
Tämänhetkinen positio selviää metodilla file.tell(). Positiota voi myös siirtää metodilla file.seek().
>>> file=open('bok.txt')
>>> file.seek(10)
10
>>> file.readline()
'ink education is expensive, try ignorance"\n'
>>> file.close()
Tekstitiedoston lukeminen pienemmissä paloissa
Yllä luimme tekstivirtaa riveittäin tai kaikki rivit yhdellä kertaa. Toisinaan haluamme kuitenkin lukea tekstivirtaa pienemmissä paloissa, jopa vain yksi merkki kerrallaan. Tähän on tarjolla metodi file.read(). Metodille voidaan antaa parametrina lukumäärä, jonka verran merkkejä halutaan lukea kerrallaan. Lukeminen alkaa aina tekstivirran tämänhetkisestä positiosta. Jos merkkejä on jäljellä vähemmän kuin mitä pyysimme, saamme vain jäljellä olevat merkit.
>>> file=open('bok.txt')
>>> file
<_io.TextIOWrapper name='bok.txt' mode='r' encoding='UTF-8'>
>>> file.tell()
0
>>> x=file.read(13)
>>> x
'"If you think'
>>> len(x)
13
>>> file.tell()
13
>>> x=file.read(50)
>>> x
' education is expensive, try ignorance"\n\n --- Dere'
>>> len(x)
50
>>> file.tell()
63
>>> x=file.read(1000)
>>> x
'k Bok, former President of Harvard University\n'
>>> len(x)
46
>>> file.tell()
109
>>> file.close()
Samaten, jos emme anna lainkaan luettavien merkkien lukumäärää tai annamme arvon None tai -1, luetaan kaikki jäljellä olevat merkit.
>>> file=open('bok.txt')
>>> file.read()
'"If you think education is expensive, try ignorance"\n\n --- Derek Bok, former President of Harvard University\n'
>>> file.close()
Lukeminen merkkijonosta
Toisinaan haluamme käsitellä merkkijonoa ikään kuin se olisi tiedosto. Esimerkiksi syötettä lukevaa ohjelmaa testattaessa voi olla näppärää pitää syötteet merkkijonoissa tiedostojen sijaan.
Tätä varten on tarjolla valmis luokka StringIO, ja sitä käytetään seuraavasti:
>>> import io
>>>
>>> f = io.StringIO("""Mielivaltaista tekstiä
... joka voi jatkuva vaikka eri riveille""")
>>>
>>> f.readline()
'Mielivaltaista tekstiä\n'
Formatoitujen tiedostojen lukeminen
Yksi keskeinen ominaisuus lähes joka ohjelmassa on mahdollisuus tallentaa ohjelman toimintaan liittyvää tietoa tiedostoihin sekä ladata ohjelman käyttöön. Tallennettavalla tiedolla on lähes aina jokin selkeä rakenne. Tiedon esitystapaa tiedostoissa kutsutaan tiedostoformaatiksi.
Luvun 4.2 kotitehtävässä opettelemme lukemaan lohkopohjaista tiedostoformaattia. Tällaista käytetään usein esimerkiksi ääntä tai kuvaa käsittelevien ohjelmien syötteessä. Luvun 4.3 kotitehtävässä vastaavasti opimme lukemaan vapaamuotoisempaa tiedostoformaattia, jota ihmisen on helpompi lukea ja kirjoittaa, mutta jota konekin ymmärtää.
Lisämateriaalia: Monimutkaisempaa tekstisyötteen käsittelyä
Seuraavassa tarkastelemme muutamaa monimutkaisempaa tapaa käsitellä tekstisyötettä. Esitetyt tekniikat ovat jossain määrin monimutkaisempia kuin edellä esitetyt, eikä näitä tarvita esimerkiksi kotitehtävien tekemiseen. Nämä esitetään tässä lähinnä mielenkiinnon herättämiseksi ohjelmointi- ja muiden kielten koneelliseen käsittelyyn. Eli ei hätää, vaikka tämä vaikuttaisikin hämärältä.
Tekstisyötteen tokenisointi
Toisinaan haluamme paloitella luetun tekstin merkityksellisiin sanatasoisiin yksiköihin ennen kuin näille tehdään jotain
toimenpiteitä. Esimerkiksi Python-tulkki pilkkoo annetun syötetiedoston Python-kielen kannalta merkityksellisiin
yksiköihin, kuten avainsanoihin (if
, def
, class
, try
jne.), literaaleihin kuten numerot ja
lainausmerkeissä oleviin merkkijonoihin, operaattoreihin kuten +
, -
, >
sekä erottimiin kuten (
, )
,
:
ja =
. Tällaista toimintaa kutsutaan leksikaaliseksi analyysiksi tai tokenisoinniksi. Valitettavasti hyvää
suomenkielistä termiä ei ole. Seuraavassa tutustumme tähän tarkemmin.
Esimerkki: Funktiokutsujen tokenisointi
Tarkastellaan funktiokutsuja, jotka ovat muotoa:
f(x, g(y), 10)
missä f
ja g
ovat funktion nimiä ja x
sekä y
puolestaan muuttujan nimiä.
Miten tehdään ohjelma, joka palauttaa meille syötteessä olevat tokenit, eli muuttujien ja funktioiden nimet, luvut ja ja erottimet? Haluaisimme tulokseksi olioita, joista selviää tokenin tyyppi, mahdollinen arvo sekä muuta mahdollisesti hyödyllistä tietoa. Esimerkiksi yllä esitetystä lausekkeesta haluaisimme saada seuraavan listan tokeneita:
Name(position=0, value=f)
Separator(position=1, value=()
Name(position=2, value=x)
Separator(position=3, value=,)
Name(position=5, value=g)
Separator(position=6, value=()
Name(position=7, value=y)
Separator(position=8, value=))
Separator(position=9, value=,)
Number(position=11, value=10)
Separator(position=13, value=))
Määritetään tätä varten abstrakti luokka Token
ja sille alaluokat Number
, Name
ja
Separator
. Kaikilla tokeneilla on attribuutti position
, joka sisältää sen tiedostoposition, josta token
löytyi sekä attribuutti value
, joka sisältää konkreettisen merkkijonon, joka erottaa samantyyppiset tokenit
toisistaan.
class Token:
def __init__(self, position, value):
self.position = position
self.value = value
def __repr__(self):
return "{}(position={}, value={})".format(type(self).__name__, self.position, self.value)
class Number(Token):
pass
class Name(Token):
pass
class Separator(Token):
pass
Määritimme Tokenille uudelleen metodin __repr__
, joka palauttaa oliota kuvaavan merkkijonon. Käytämme tässä olion
tyyppiä, jonka saamme Pythonin sisäänrakennetulla funktiolla type
ja kaikille luokille määritettyä attribuuttia
__name__
, joka sisältää luokan nimen. Koska kaikilla token-luokilla on samanlaiset attribuutit, ei meidän
tarvitse määrittää uudestaan niille uudestaan luontimetodiakaan.
Määritetään tämän jälkeen Tokenizer
-luokka:
class Tokenizer:
def __init__(self, path):
self.path = path
self.file = open(path) # Jos tiedoston avaaminen ei onnistu, heittää OSErrorin
self.buffer = []
def close(self):
self.file.close()
def maybeReadCharToBuffer(self):
""" Jos buffer on tyhjä, luetaan seuraava merkki filestä bufferiin """
if self.buffer == []:
nextChar = self.file.read(1)
if nextChar == '':
raise EOFError('End of file {}'.format(self.path))
self.buffer.append(nextChar)
def peekChar(self):
""" Palautetaan seuraava merkki, mutta ei siirrytä eteenpäin syötteessä """
self.maybeReadCharToBuffer()
return self.buffer[0]
def getChar(self):
""" Palautetaan seuraava merkki, ja siirrytään eteenpäin syötteessä """
self.maybeReadCharToBuffer()
return self.buffer.pop()
def unGetChar(self, char):
""" Laittaa merkin takaisin bufferiin myöhempää lukemista varten """
self.buffer.insert(0, char)
def skipWhiteSpace(self):
""" Ohitetaan välilyönnit ja muut tyhjät merkit """
while self.peekChar().isspace():
self.getChar()
return self.peekChar()
def getNextToken(self):
""" Palautetaan seuraava token tai None jos syöte on loppu """
try:
nextChar = self.skipWhiteSpace()
startPosition = self.file.tell()-1 # Tiedostopositio on yhden edellä
if nextChar.isdigit():
chars = [ self.getChar() ]
while self.peekChar().isdigit():
chars.append(self.getChar())
return Number(startPosition, int("".join(chars)))
elif nextChar.isidentifier():
chars = [ self.getChar() ]
while self.peekChar().isidentifier():
chars.append(self.getChar())
return Name(startPosition, "".join(chars))
elif nextChar in ['(', ')', ',']:
return Separator(startPosition, self.getChar())
else:
raise SyntaxError('{}:{}: Unexpected character {}'
.format(self.path, startPosition, nextChar))
except EOFError:
return None
def getTokens(self):
""" Luetaan file ja muunnetaan se listaksi tokeneita """
tokens = []
nextToken = self.getNextToken()
while nextToken:
tokens.append(nextToken)
nextToken = self.getNextToken()
self.close()
return tokens
Keskeisiä metodeja syötteen lukemisessa ovat maybeReadCharToBuffer
, peekChar
sekä getChar
. Eräänlaisena
välivarastona merkkien lukemisen ja käytön välissä käytetään listaa nimeltä buffer
. Mikäli buffer on tyhjä lukee
metodi maybeReadCharToBuffer
sinne uuden merkin. Metodi peekChar
palauttaa seuraavan merkin bufferista,
mutta ei varsinaisesti etene syötteessä. Metodi getChar
toimii muuten samoin, mutta se myös etenee syötteessä
sillä se poistaa bufferista ensimmäisen merkin. Tästä seuraa, että buffer tyhjenee ja maybeReadCharToBuffer
joutuu myöhemmin lukemaan sinne uuden merkin.
Metodi skipWhitespace
ohittaa syötteessä kaikki seuraavat välilyönnit, rivinvaihdot ja muut niin sanotut tyhjät
merkit. Tässä se käyttää apuna metodeja peekChar
ja getChar
. Näistä edellisen avulla tarkistetaan, pitääkö
seuraava merkki ohittaa ja jos pitää niin jälkimmäiselle se ohitetaan.
Metodi getNextToken
lukee ja palauttaa seuraavan tokenin syötteestä. Se ohittaa ensin tyhjät merkit ja laittaa
sitten talteen alkuposition (tämä on yhden pienempi kuin mitä file.tell()
antaa, sillä tiedostosta on jo luettu
yksi merkki enemmän metodia peekChar
varten). Sitten metodi tarkistaa, onko seuraava merkki numero (str.isdigit()), nimeen kuuluva kirjain
(str.isidentifier()) tai joku
erotinmerkeistä (
, )
tai ,
. Numero ja nimi luetaan hyvin samaan tapaan kuin miten tyhjät merkit ohitettiin:
metodilla peakChar
tarkistetaan seuraava merkki ja metodilla getChar
siirrytään eteenpäin. Jos seuraava
merkki ei ole mikään odotetuista, heitetään poikkeus.
Metodi getTokens
lukee tiedoston ja palauttaa listan tokeneita.
Metodia unGetChar
ei tässä esimerkissä tarvita, mutta usein tokenisoinnissa siitä on hyötyä. Jos toisin kuin
tässä esimerkissä emme yhden merkin kurkistuksella eteenpäin tiedä, mikä on oikea valinta, on hyvä tarjota
mahdollisuus peruuttaa valinta ja tämän voi tehdä metodilla unGetChar
.
Kokeillaan ohjelmaa tokenize.py syötetiedostoon call.
>>> from tokenizer import Tokenizer
>>> for token in Tokenizer('call').getTokens():
... print(token)
...
Name(position=0, value=f)
Separator(position=1, value=()
Name(position=2, value=x)
Separator(position=3, value=,)
Name(position=5, value=g)
Separator(position=6, value=()
Name(position=7, value=y)
Separator(position=8, value=))
Separator(position=9, value=,)
Number(position=11, value=10)
Separator(position=13, value=))
Parsiminen eli jäsennys
Edellä opimme pilkkomaan syötettä rakenteellisesti merkitseviin sanatason kokonaisuuksiin. Yleensä haluamme myös tunnistaa korkeamman tason rakenteita kuten lauseita. Ohjelmointikielten kääntämisessä ja luonnollisten kielten käsittelyssä jäsennys (engl. parsing) on keskeinen osatehtävä. Jäsennyksessä ajatellaan yleensä, että syöte on valmiiksi tokenisoitu, eli voimme käsitellä sanatason osasia, kuten numeroita, varattuja sanoja jne.
Jäsennykseen on tarjolla monia tekniikoita ja niitä voi opiskella esimerkiksi kurssilla T-106.4200 Johdatus kääntäjätekniikkaan.
Tutustutaan jäsennykseen yllä käyttämämme esimerkkikielen kautta.
Esimerkki: yksinkertainen jäsentäminen
Käyttämämme rakenteet ovat siis muotoa f(x, g(y), 10)
. Voimme esittää tällaisen muodon tarkemmin käyttäen niin
sanottua EBNF (Extended Backus-Naur Form)
-muotoa, jolla usein esitetään ohjelmointikielten kielioppi. Esimerkiksi Pythonin kielioppi löytyy The Python
Language Referencen luvusta 10. Full Grammar specification.
Käsittelemämme rakenteet voidaan esittää seuraavilla EBNF-säännöillä.
expr ::= Number | call
call ::= Name [ '(' [ exprList ] ')' ]
exprList ::= expr ( ',' expr )*
Merkintä ::=
jakaa säännön vasempaan ja oikeaan puoleen. Vasemmalla on aina yksi sana, joka nimeää kieliopillisen
rakenteen. Oikealla puolella on nolla tai useampi vaihtoehtoinen muoto tälle rakenteelle. Vaihtoehdot erotetaan
toisistaan pystyviivalla.
Yllä olevat säännöt luetaan seuraavasti: jokainen lauseke (expr) on joko numero (Number) tai kutsu (call). Kutsu on
nimi (Name), jonka perässä mahdollisesti on kaarisulkujen (
ja )
välissä lausekelista (exprList).
Hakasulkumerkit [
ja ]
tarkoittavat, että niiden välissä oleva sisältö voi esiintyä tai olla
esiintymättä. Lausekelistassa pilkuilla ',' toisistaan erotettuja lausekkeita. Merkintä (
)*
tarkoittaa, että
sulkujen välissä oleva sisältö voi esiintyä mielivaltaisen monta kertaa (myös nolla kertaa). Kirjoitimme tässä
pienillä kirjaimilla ne nimet, jotka määrittävät jonkun kielioppisäännön. Isolla kirjoitimme nimet, jotka kuvaavat
yllä määrittämiämme tokeneita Name ja Number lainausmerkkeihin laitamme erotinmerkit (
, ,
ja )
.
Huomaa, että sääntö call käsittelee myös muuttujan nimet, ei pelkkiä funktiokutsuja. Edellisissä ei ole sulkuja ja niiden välissä parametreja.
Miten tämä määritys muutetaan ohjelmakoodiksi? Käytetään luvussa 2.1 oppimaamme top-down menetelmää.
Muodostetaan metodi kullekin yllä kuvatulle kielioppisäännölle. Tehdään metodin nimet lisäämällä jäsentämistä tarkoittava sana 'parse' nimen alkuun:
def parseExpr(self):
pass
def parseCall(self):
pass
def parseExprList(self):
pass
Metodien rungot muodostetaan käymällä läpi kielioppisäännön oikeaa puolta ja kirjoittamalla koodi, joka tarkistaa
vastaako syöte sääntöä. Jos säännössä vastaan tulee joku token (Name, Number tai erotin (
, ,
, tai )
), pitää
tarkistaa, että syötteessä on vastaava token. Jos vastaan tulee jokun säännön vasemmalla puolella esiintyvä nimi
(expr, call, exprList), lisäämme kutsun vastaavaan metodiin. Toistuvat rakenteet (esim. säännössä exprList) korvaamme
sopivalla silmukalla ja ehdolliset rakenteet (esim. [ exprList ]) korvaamme sopivalla ehdolla.
Tämä ei kaikkinensa ole aivan mekaanista työtä, vaan vaatii jonkin verran soveltamista. Käytämme tässä kahta
apumetodia peekToken
ja getToken
, jotka toimivat samaan tapaan kuin Tokenizerin peekChar ja getChar.
Saamme seuraavat metodit
def parseExpr(self):
if isinstance(self.peekToken(), Number):
return self.getToken().value
elif isinstance(self.peekToken(), Name):
return self.parseCall()
else:
# Virhe
def parseCall(self):
name = self.getToken().value # Nimi talteen
# Jos edessä on '(' kyseessä on oikea funktiokutsu
if isinstance(self.peekToken(), Separator) and self.peekToken().value == '(':
# Ohitetaan '('
self.getToken()
# Jos edessä on ')', on parametrilista tyhjä
if isinstance(self.peekToken(), Separator) and self.peekToken().value == ')':
parameters = list()
else: # Muuten jäsennetään parametrit
parameters = self.parseExprList()
# Nyt pitäisi edessä olla ')'
if isinstance(self.peekToken(), Separator) and self.peekToken().value == ')':
self.getToken()
else:
# Virhe
return ... # Palautetaan olio, joka kuvaa funktiokutsua
else:
return name # Palautetaan name, tämä ei ollutkaan varsinainen kutsu vaan muuttujan nimi
def parseExprList(self):
exprList = [ self.parseExpr() ] # Luetaan ensimmäinen expr
# Niin kauan kun edessä on ',', luetaan seuraava expr
while isinstance(self.peekToken(), Separator) and self.peekToken().value == ',':
self.getToken()
exprList.append(self.parseExpr())
return exprList
Tulostus tekstitiedostoon
Katsotaan seuraavaksi, miten Pythonissa tulostetaan tekstitiedostoon.
Merkkijonojen muotoilu tulostusta varten
Tulostuksen yhteydessä tarvitaan usein muotoiltuja merkkijonoja ja Python tarjoaa useita tapoja niiden tekemiseen.
Suoraviivaisin tapa on liimailla haluttu lopputulos palasista käyttäen +
-operaattoria. Jos käytetään muita kuin
merkkijonoja, pitää ne konvertoida esim. str
-funktiolla merkkijonoiksi. Esimerkiksi näin:
>>> nimi = "Maija Metso"
>>> ika = 37
>>> asema = "Johtaja"
>>>
>>> print(nimi + " " + str(ika) + ", " + asema)
Maija Metso 37, Johtaja
Tämä on toki täysin kelvollinen tapa muotoilla merkkijonoja, mutta +
-merkkejä ja str
-kutsuja tulee helposti
runsaasti. Muotoilulausekkeista on tällöin vaikea ilman ohjelman suorittamista nähdä, millaisia merkkijonoja ollaan
tekemässä.
Parempi on kuitenkin käyttää valmista metodia str.format
, jolla saa hienostuneita ulkoasuja pienellä vaivalla. Käsittelemme sitä tarkemmin
luvussa Merkkijonojen muotoilu metodilla str.format.
file=sys.stderr
saadaan virheilmoitus menemään ns. standard error -tulostusvirtaan. Syöttö- ja tulostusvirroista lisää kohta.