Težave z nizi

Po tem, ko smo se poučili o tem, kako so shranjeni nizi in kaj vse se še vsebujejo poleg vidnih znakov, se vrnimo k vremenu v Radovljici, podatkom z dražbe in izboru koles.

for vrstica in open("temperature.txt"):
    print(vrstica)
24

18

15

16

18

Vidimo prazne vrstice? Ki jih ne bi bilo, če bi izpisovali številke, ne nizov?

for vrstica in open("temperature.txt"):
    temp_c = int(vrstica)
    print(temp_c)
24
18
15
16
18

To se zgodi, ker vsaka vrstica, prebrana iz datoteke, vsebuje tudi znak za konec vrstice. Tako je, recimo, videti zadnja.

vrstica
'18\n'

To nas potem pesti pri nalogi z vaj, kjer je potrebno prešteti, kolikokrat se je nek lastnik štirih koles peljal s katerim izmed njih, recimo Cubom.

cube = 0
for vrstica in open("../vaje/kolesa.txt"):
    if vrstica == "Cube":
        cube += 1

cube
0

Že če pogledamo zadnjo vrstico, vidimo, da se je vozil tudi s Cubom, vendar ima vrstica na koncu, seveda, \n, zato primerjava vrstica == "Cube" vrne False.

vrstica
'Cube\n'
vrstica == "Cube\n"
True

Seveda lahko pišemo

cube = 0
for vrstica in open("../vaje/kolesa.txt"):
    if vrstica == "Cube\n":
        cube += 1

cube
46

in dobimo pravi rezultat, vendar nam gre (no, vsaj meni) to na živce. Menda ne obstaja kak način, da se znebimo tega trapastega \n?

Še hujše: kaj, če je pri zapisovanju nekonsistenten in včasih piše "Cube", včasih "cube" in včasih "CUBE"? Obstaja kakšna oblika primerjanja, ki ignorira razliko med velikimi in malimi črkami?

Aja, pa, zdi se mi, da še nisem omenil: njegova tipkovnica je nekaj pokvarjena in včasih dela dvojne n-je, tako da se v njegovi datoteki včasih pojavi tudi Stevenns, NNakamura in Cannyon (in Canyonn). Je mogoče na kak preprost način zamenjati vse dvojne n-je v nizu z enojnimi?

Kam gremo? V funkcije za delo z nizi. Pravzaprav v nekaj splošnejšega.

Metode nizov

Doslej smo spoznali le prgišče Pythonovih funkcij - le toliko, da smo se jih naučili klicati in da smo lahko kaj drobnega sprogramirali.

Python 3.12 ima v resnici samo 71 funkcij (med katerimi je kakšnih 20 tipov). Še več, izmed teh 71 funkcij sem tako, na hitro, naštel 20 funkcij, ki jih dejansko kolikor toliko redno uporabljam. Ostale so bolj kot ne eksotika. Ostale, uporabne funkcije - in Python jih ima na tisoče - so pospravljene po škatlicah.

Kakšnih škatlicah? Takole. Vzemimo nek niz, recimo

vrstica = "NNakamura\n"

Tako kot številke smo tudi nize na tablo narisali kot škatlice, ki vsebujejo nek podatek. Poleg tega vsebujejo te škatlice tudi funkcije za delo s tem podatkom. Kot smo omenili v vaji "Kolesa" se lahko odvečnega \n na koncu niza znebimo tako, da napišemo vrstica = vrstica.strip(). Ta strip je funkcija, ki se nahaja "znotraj" niza vrstica. In do tistega, kar je "znotraj", pridemo tako, da napišemo vrstica, nato piko (ki v vseh kontekstih v Pythonu - še nekaj jih bo - pomeni "znotraj") in potem ime tistega, kar je "znotraj", torej strip. Ker je to funkcija, jo pokličemo. In ker zanjo (zdajle) nimamo posebnih argumentov, pustimo oklepaja prazna.

vrstica
'NNakamura\n'
vrstica.strip()
'NNakamura'

Takšnim funkcijam znotraj škatlic rečemo metode, po angleško method. (Ime je, priznajmo, malo čudno. Zgodovinsko. Vseeno boljše od tega, kako so jih klicali v začetku: sporočilo, message. V kontekstu takratnega razmišljanja, opisovanja tega, kar se dogaja ob klicanju metod, je bilo to ime smiselno, danes pa niti med programerji ni veliko takšnih, ki bi vedeli za ta izraz.)

strip

strip je torej metoda nizov. Vsi nizi imajo strip (vsak svojega, drugega? kakor se vzame.) in njeno delo je, da vrne niz brez belega prostora na začetku in na koncu. Beli prostor so presledki, znak za novo vrstico in tabulatorji.

s = "    niz s   preveč presledki. \n  Res.   \n\n  "

s.strip()
'niz s   preveč presledki. \n  Res.'

Vidimo: odstranil je vse presledke in nove vrstice na začetku in na koncu, tiste vmes pa je pustil pri miru.

Obstajata različici lstrip in rstrip, ki odstranita le presledke na začetku in na koncu. Kateri je kateri, si boste že zapomnili, saj znate angleško.

Metodi strip lahko podamo tudi argument, če želimo odstranjevati kaj drugega kot presledke. To bomo počeli redko, a vseeno pokažimo:

".....čemu služijo te pike?!...".strip(".")
'čemu služijo te pike?!'

Mimogrede smo videli, da lahko metode kličemo tudi na literalih za nize ("dobesedno" navedenem nizu) ne le na nizih, ki so že poimenovani, kot je bil gornji s.

Tako se bomo torej znebili \n na koncu vrstice s kolesom.

vrstica.strip()
'NNakamura'

Nobena metoda niza ne spreminja niza, temveč kvečjemu vrne nov niz. Nizi so nespremenljivi. Niz, na katerega se nanaša vrstica, je še vedno tak, kot je bil.

vrstica
'NNakamura\n'

Metoda strip je zgolj vrnila nov niz in če hočemo, da niz, na katerega se nanaša vrstica, ne bo več vseboval \n, moramo imenu vrstica prirediti vrednost, ki jo vrne vrstica.strip().

vrstica = vrstica.strip()

vrstica
'NNakamura'

Velike in male črke

Naš naslednji kamen spotike je bilo primerjanje, pri katerem zanemarjamo razliko med malimi in velikimi črkami. Takšno torej, po katerem so nizi "canyon", "Canyon" in "CANYON" enaki.

Priznati moram, da funkcije, namenjene takšnemu primerjanju, nisem še nikoli uporabil, sem pa slutil, kje jo najti in jo tam ob zapisovanju teh zapiskov tudi hitro našel. Za tiste, ki bi jo utegnili potrebovati: imenuje se locale.strcoll. Kako, da tak postaran programer, kot sem jaz, ne ve zanjo? Preprosto: ta problem rešim po preprostejši bližnjici. locale.strcoll bi moral uporabiti le za urejanje nizov po abecedi, za ugotavljanje enakosti pa oba niza preprosto pretvorim v male (ali velike) črke.

vrstica.lower()
'nnakamura'
vrstica.upper()
'NNAKAMURA'

Ali celo

vrstica.capitalize()
'Nnakamura'

Spet: nobena od teh funkcij ne spreminja niza, vse zgolj vrnejo nov niz. Če hočem, da bo vrstica vsebovala niz, zapisan s samimi malimi črkami, potrebujem

vrstica = vrstica.lower()
vrstica
'nnakamura'

Zavoljo popolnosti povejmo še za eno metodo: casefold. Ta je podobna lower, vendar naredi še nekateri druge pretvorbe, recimo zamenja ß z ss, saj je "Straße" za tiste, ki znajo nemško, isto kot "strasse". Poleg tega, recimo, poskrbi, da so vsi šumniki zapisani na enak način (zapisati jih je mogoče z enim znakom ali kot kombinacijo c/s/z s strešico).

"Straße".casefold()
'strasse'

Zamenjava podnizov

Tudi metoda replace ima nerodno ime, saj ne spreminja niza, temveč vrne niz, v katerem je nek znak ali podniz zamenjan z drugim.

vrstica.replace("a", "e")
'nnekemure'
vrstica.replace("mu", "mjav")
'nnakamjavra'
vrstica.replace("am", "")
'nnakura'

Kar potrebujemo mi, je zamenjava dveh nn-jev z enim:

vrstica.replace("nn", "n")
'nakamura'

To dela tudi za Canyona:

"cannyon".replace("nn", "n")
'canyon'
"canyonn".replace("nn", "n")
'canyon'
"cannyonn".replace("nn", "n")
'canyon'

Vse skupaj

Ker vsaka od teh metod vrne nov niz, jih lahko "verižimo".

vrstica = "NNakamura\n"
vrstica.strip().lower().replace("nn", "n")
'nakamura'

In zdaj lahko končno preštejemo te bicikle:

cube = stevens = nakamura = canyon = 0
for vrstica in open("../vaje/kolesa.txt"):
    vrstica = vrstica.strip().lower().replace("nn", "n")
    if vrstica == "cube":
        cube += 1
    if vrstica == "stevens":
        stevens += 1
    if vrstica == "nakamura":
        nakamura += 1
    if vrstica == "canyon":
        canyon += 1

print("Cube:", cube)
print("Stevens:", stevens)
print("Nakamura:", nakamura)
print("Canyon:", canyon)
Cube: 46
Stevens: 1
Nakamura: 23
Canyon: 30

Začetek in konec

Nizi imajo še par ducatov metod, vendar večine bodisi še ne moremo razumeti, bodisi niso prav splošno uporabne. Tu bomo navrgli le še dve, potem pa posvetili daljši razdelek zvezdi današnjega predavanja, metodi split.

Preprosti, a pogosto uporabni metodi sta startswith in endswith. Podamo jima niz in vrneta True ali False.

ime = "Benjamin"
ime.startswith("Ben")
True
ime.startswith("B")
True
ime.startswith("")
True
ime.startswith("tisto zgoraj je malo hecno")
False
ime.endswith("min")
True
ime.endswith("max")
False

Obe metodi bomo - kot vse, ki vračajo False ali True - seveda najpogosteje uporabljali v pogojih v if (in while, ko ga bomo poznali).

Razbijanje niza

Hja. V resnici je v direktoriju vaje samo precej poenostavljena različica datoteke s kolesarsko statistiko. V resnici je videti tako:

Cube,51,1216
Canyon,104,444
Cube,74,1508
Cube,79,936
Nakamura,17,95
(in tako naprej)

V vsaki vrstici so trije podatki, ločeni z vejicami: najprej ime kolesa, ki ga je imel neznani kolesar nek dan za izlet, nato prevožena razdalja in potem višinski metri. Kako prebrati to?

Za začetek vzemimo le eno vrstico.

vrstica = "Cube,51,1216"

Doslej zamolčana metoda nizov je split. Ta razbije nize na več nizov; kot argument ji podamo ločilo - v našem primeru vejico.

vrstica.split(",")
['Cube', '51', '1216']

Metoda je vrnila tri nize. Kaj pomenijo tisti oglati oklepaji, ni današnja tema. Motili nas ne bodo. Ker imamo tri nize, jih bomo priredili trem spremenljivkam.

kolo, razdalja, visina = vrstica.split(",")

Razumemo? Desno od enačaja imamo tri stvari, torej levo od njega potrebujemo tri spremenljivke.

Njihove vrednosti so, kot morajo biti.

kolo
'Cube'
razdalja
'51'
visina
'1216'

Pa ugotovimo, kako daleč se je možakar peljal in kakšna je skupna višina njegovih izletov.

skupna_razdalja = 0
skupna_visina = 0
for vrstica in open("kolesa.txt"):
    kolo, razdalja, visina = vrstica.split(",")
    skupna_razdalja += int(razdalja)
    skupna_visina += int(visina)
    
print("Skupna razdalja:", skupna_razdalja)
print("Skupna višina:", skupna_visina)
Skupna razdalja: 6359
Skupna višina: 98076

Meni se zdi to kar imenitno. Kar zapletene datoteke znamo že brati.

V zvezi s split je potrebno povedati še nekaj. Datoteke s podatki, ločene z vejicami, pogosto dobimo iz kakega Excela. Včasih pa so podatki ločeni s tabulatorji ali celo le s presledki - se pravi, belim prostorom. V tem primeru pokličemo split brez argumentov.

"Cube 45 1024".split()
['Cube', '45', '1024']

To je podobno, vendar ne popolnoma isto, kot če kot argument podamo presledek.

"Cube 45 1024".split(" ")
['Cube', '45', '1024']

Razlika je v tem, kako split obravnava zaporedne presledke v nizu.

"Cube   45     1024".split()
['Cube', '45', '1024']
"Cube   45     1024".split(" ")
['Cube', '', '', '45', '', '', '', '', '1024']

V prvem primeru upošteva zaporedne presledke kot en sam presledek, v drugem primeru, ko smo podali argument, pa je pet zaporednih presledkov obravnaval kot pet ločil - med njimi pa so bili prazni nizi. Tega navadno nočemo. Kadar vas torej zasrbi, da bi poklicali split(" "), se obrzdajte in pokličite split().