!!8. Keerulisemad Pythoni võimalused
Käesolev Pythoni kursus teeb hea sissejuhatuse Pythoniga programmeerimisse, aga puhtalt aine läbimisega ennast võluriks kutsuda ei saa. Selle jaoks peab läbima selle viimase silmaringimaterjalide peatüki. 

Siin peatükis vaatame, kuidas teha lihtsaid asju keerulisemalt, et vähendada üleliigse koodi kirjutamist. Õpime keerulisemaid Pythoni võimalusi, et teha elu lihtsamaks. 

!!!Ettevalmistus
Selle peatüki läbimiseks piisab Pythoni installatsioonist: midagi paigaldama ei pea. 

Et peatükist täiesti aru saada, peab olema läbitud õpiku [[https://progeopik.cs.ut.ee/osa1.html | esimene]] ja [[https://progeopik.cs.ut.ee/osa2.html | teine]] osa. 

!!!Lühem if-lause
Vahepeal on vaja millegi väga lihtsa jaoks kasutada if-lauset. Näiteks, sõna kääne muutub osastavaks, kui see viitab mitmele esemele: ostukorvis on 1 '''toode''', aga ostukorvis võib olla mitu '''toodet'''.

Sellises olukorras peame kasutama if-lauset:

(:codestart python gutter='false':)
if toodete_arv == 1:
    print("Ostukorvis on {} toode".format(toodete_arv))
else:
    print("Ostukorvis on {} toodet".format(toodete_arv))
(:codeend:)

Et vähendada kirjutatud koodi, saab if-lause kirjutada ühele reale:

(:codestart python gutter='false':)
print("Ostukorvis on {} {}".format(toodete_arv, "toode" if toodete_arv == 1 else "toodet"))
(:codeend:)

Lühema if-lause valem:

(:codestart python gutter='false':)
väärtus_kui_tõene if tingimus else väärtus_kui_väär
(:codeend:)

Näited:

(:codestart python gutter='false':)
>>> "tõene" if True else "väär"
'tõene'
>>> "tõene" if False else "väär"
'väär'
>>> "paaris" if 20 % 2 == 0 else "paaritu"
'paaris'
>>> "paaris" if 21 % 2 == 0 else "paaritu"
'paaritu'
>>> "tühi" if len([]) == 0 else "täis"
'tühi'
>>> "tühi" if len([5]) == 0 else "täis"
'täis'
(:codeend:)

Proovi kirjutada lühike tingimuslause, mis annab väärtuseks @@"fizz"@@, kui arv jagub kolmega ning arvu ise, kui see ei jagu kolmega. 

Meil on järjend, mis sisaldab esemeid, mida on keelatud lennukile kaasa võtta keelatud. 

(:codestart python gutter='false':)
keelatud = ["veepudel", "kahvel", "žilett", "ilutulestikud", "elevant"]
(:codeend:)

Kirjuta programm, mis küsib kasutajalt eset ning väljastab, kas see on keelatud või mitte nii, et programmil ei ole ühtegi taanet. 

(:codestart python gutter='false':)
Sisesta ese: telefon
telefon ei ole keelatud lennukile kaasa võtmiseks
(:codeend:)

(:codestart python gutter='false':)
Sisesta ese: veepudel
veepudel on keelatud lennukile kaasa võtmiseks
(:codeend:)

!!!Järjendi hõlmamine
Kui tahame luua uut järjendit ja täita seda mingite andmetega, oleme siiamaani loonud uue järjendi ning seejärel for-tsükliga sinna elemente juurde lisanud. 

Näiteks, meil on järjend sõnadest ja me tahame saada järjendit nende pikkustest. 

(:codestart python gutter='false':)
sõned = ["Python", "on", "üldotstarbeline", "interpreteeritav", "programmeerimiskeel"]

pikkused = []
for sõne in sõned:
        pikkused.append(len(sõne))

print(pikkused)  # [6, 2, 15, 16, 19]
(:codeend:)

Sama asja saab kirja panna vähema koodiga:

(:codestart python gutter='false':)
sõned = ["Python", "on", "üldotstarbeline", "interpreteeritav", "programmeerimiskeel"]

pikkused = [len(sõne) for sõne in sõned]

print(pikkused)  # [6, 2, 15, 16, 19]
(:codeend:)

Seda võtet nimetatakse järjendi hõlmamiseks (ingl. k. ''list comprehension''). 

Veel üks näide: tahame saada kahe astmeid. Pikemalt saab teha nii:

(:codestart python gutter='false':)
kahe_astmed = []
for arv in range(10):
        kahe_astmed.append(2**arv)

print(kahe_astmed)  # [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
(:codeend:)

Järjendi hõlmamisega:

(:codestart python gutter='false':)
kahe_astmed = [2**arv for arv in range(10)]

print(kahe_astmed)  # [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
(:codeend:)

Ülesanne võib olla natuke keerulisem. Näiteks soovime kahe astmeid ainult siis, kui need lõppevad neljaga. 

(:codestart python gutter='false':)
kahe_astmed_neljaga = []

for arv in range(23):
    if 2**arv % 10 == 4:
        kahe_astmed_neljaga.append(2**arv)

print(kahe_astmed_neljaga)  # [4, 64, 1024, 16384, 262144, 4194304]
(:codeend:)

Ka lisatingimusi saame panna järjendi hõlmamisse:

(:codestart python gutter='false':)
kahe_astmed_neljaga = [2**arv for arv in range(23) if 2**arv % 10 == 4]

print(kahe_astmed_neljaga)  # [4, 64, 1024, 16384, 262144, 4194304]
(:codeend:)

Proovi järjendi hõlmamisega luua järjend, mis sisaldab ainult arve, mis ei jagu arvudega 3 ega 5:

(:codestart python gutter='false':)
[1, 2, 4, 7, 8, 11, 13, 14, 16, 17, 19, ...]
(:codeend:)

Meil on järjend, mis sisaldab kõige sagedasemaid sõnade põhivorme (lemmasid) koos nende järkudega:

(:codestart python gutter='false':)
[(1, "olema"), (2, "ja"), (3, "see"), (4, "tema"), (5, "mina"), (6, "ei"), (7, "et"), (8, "kui"), (9, "mis"), (10, "ka")]
(:codeend:)

Proovi järjendi hõlmamisega luua järjend, mis jätab alles ainult sõnad:

(:codestart python gutter='false':)
["olema", "ja", "see", "tema", "mina", "ei", "et", "kui", "mis", "ka"]
(:codeend:)

Proovi hõlmamist ka kuhjadega, sõnastikega ja ennikutega. 

!!!Funktsioonide kasutamine parameetrites
!!!!Sorteerimine
Kui me tahame sorteerida arvude järjendit, siis saab kasutada funktsiooni @@sorted@@ või meetodit @@sort@@. 

(:codestart python gutter='false':)
>>> arvud = [1, 5, 9, 2, 6, 5]
>>> sorted(arvud)  # ei sorteeri muutujat, tagastab sorteeritud järjendi
[1, 2, 5, 5, 6, 9]
>>> arvud
[1, 5, 9, 2, 6, 5]
>>> arvud.sort()  # sorteerib muutuja
>>> arvud
[1, 2, 5, 5, 6, 9]
(:codeend:)

Mis siis, kui meil on vaja midagi keerulisemat sorteerida? Näiteks meil on järjendisse salvestatud ennikud, mille üks element on arv. Kuidas öelda sorteerimisfunktsioonile, et sorteerida arvu järgi?

(:codestart python gutter='false':)
maletajad = [("Ding Liren", 3), ("Nepomnjaštši", 4), ("Caruana", 2), ("Carlsen", 1)]
(:codeend:)

Sorteerimisfunktsioonidel on parameeter @@key@@, millele saab panna väärtuseks funktsioone. See funktsioon jooksutatakse iga järjendi elemendi peal läbi nii, et parameetriks pannakse element ning järjend sorteeritakse selle funktsiooni tagastuste põhjal. Meie järjendi puhul saaksime teha funktsiooni, mis tagastab enniku teise liikme.

(:codestart python gutter='false':)
def teine_liige(ennik):
        return ennik[1]
(:codeend:)

Nüüd peab meelde tuletama, et funktsioonid on ka muutujad, lihtsalt neid saab välja kutsuda. Seega meie funktsioon on salvestatud muutujasse @@teine_liige@@. Sorteerime järjendi nii, et @@key@@ parameetri väärtuseks läheb see funktsioon. 

(:codestart python gutter='false':)
>>> maletajad
[('Ding Liren', 3), ('Nepomniachtchi', 4), ('Caruana', 2), ('Carlsen', 1)]
>>> maletajad.sort(key=teine_liige)
>>> maletajad
[('Carlsen', 1), ('Caruana', 2), ('Ding Liren', 3), ('Nepomniachtchi', 4)]
(:codeend:)

Töötas! Proovime veel. Kui meil on sõnede järjend, sorteeritakse neid tavaliselt tähestiku järjekorras:

(:codestart python gutter='false':)
>>> sõned = ["aaaa", "b", "ccc", "ddddd", "ee"]
>>> sorted(sõned)
['aaaa', 'b', 'ccc', 'ddddd', 'ee']
(:codeend:)

Aga mis siis, kui tahame neid sorteerida sõnepikkuse järjekorras? Peaksime tegema funktsiooni, mis tagastab sõne pikkuse.

(:codestart python gutter='false':)
def sõne_pikkus(sõne):
        return len(sõne)
(:codeend:)

Oota, selline funktsioon on juba olemas ja seda kasutasime isegi siin funktsioonis: @@len@@. Paneme sorteerimisel parameetri @@key@@ väärtuseks lihtsalt selle!

(:codestart python gutter='false':)
>>> sorted(sõned, key=len)
['b', 'ee', 'ccc', 'aaaa', 'ddddd']
(:codeend:)

Saime isegi meeldiva ja loetava koodi. 

Kui funktsiooni pole veel kirjutatud, peame selle ise defineerima, nagu tegime esimeses näites. Üsna tüütu on nii lihtsat funktsiooni ise defineerida. Võiks ju olla lihtsam viis funktsiooni defineerimiseks ilma, et peab mitu rida koodi juurde kirjutama. 

!!!!Lambda-funktsioonid
Et vähendada koodi kirjutamist, saame kasutada lambda-funktsioone. Need on funktsioonid, mida saab kirjutada lühidalt ja kasutada anonüümselt ehk neid ei pea muutujasse salvestama. 

Vaatame funktsiooni, mida me enne kirjutasime: 

(:codestart python gutter='false':)
def teine_liige(ennik):
        return ennik[1]
(:codeend:)

Seda saab samaväärselt kirjutada lambdana: 

(:codestart python gutter='false':)
teine_liige = lambda ennik: ennik[1]
(:codeend:)

Defineerisime just funktsiooni @@teine_liige@@, mis võtab parameetriks muutuja @@ennik@@ ning tagastab selle teise liikme. 

Saame neid samamoodi kasutada teiste funktsioonide parameetrites:

(:codestart python gutter='false':)
>>> maletajad
[('Ding Liren', 3), ('Nepomniachtchi', 4), ('Caruana', 2), ('Carlsen', 1)]
>>> maletajad.sort(key=lambda ennik: ennik[1])
>>> maletajad
[('Carlsen', 1), ('Caruana', 2), ('Ding Liren', 3), ('Nepomniachtchi', 4)]
(:codeend:)

Tegelikult on [[https://docs.python.org/3/howto/sorting.html#operator-module-functions | sellised funktsioonid]] ka juba Pythoni standardteegis olemas, aga nende kasutamiseks peab ühe mooduli importima ning nendega sai teha hea sissejuhatuse lambdadesse.

Vaatame veel funktsioone, mis nõuavad parameetritena teisi funktsioone.

!!!!Funktsioon map
Üks asi, mis võib tihti ette tulla, on kogu järjendi elementidele mingi funktsiooni rakendamine. Näiteks on vaja arvusõnede järjend muuta arvude järjendiks. Selle jaoks peaks need for-tsükliga läbi käima ja kuskile uude järjendisse lisama.

(:codestart python gutter='false':)
sõned = ["3", "1", "4", "1", "5", "9"]

arvud = []
for sõne in sõned:
        arvud.append(int(sõne))

print(arvud)  # [3, 1, 4, 1, 5, 9]
(:codeend:)

See on päris palju koodi nii lihtsa asja jaoks. Et säästa koodi kirjutamist, on Pythonis funktsioon @@map@@. Sellele saab parameetriteks panna ühe funktsiooni ning järjendi, et jooksutada seda funktsiooni kõikide järjendi elementide peal. See tagastab objekti, mida saab läbida for-tsükliga või muuta järjendiks funktsiooniga @@list@@. Eelmine kood uuesti, kasutades funktsiooni @@map@@:

(:codestart python gutter='false':)
sõned = ["3", "1", "4", "1", "5", "9"]

arvud = list(map(int, sõned))

print(arvud)  # [3, 1, 4, 1, 5, 9]
(:codeend:)

Tegime palju lühema koodiga sama asja. 

Võimalik, et funktsioon nõuab mitut parameetrit, näiteks @@round@@, mille esimene parameeter on ümardatav arv ja teine on kohtade arv, mitmeni peaks ümardama.

(:codestart python gutter='false':)
>>> round(3.14159265, 2)
3.14
(:codeend:)

Sel juhul saab @@map@@ parameetriteks panna 2 järjendit: esimesed parameetrid ja teised parameetrid. Ümardame näiteks ujukomaarvude järjendi arvud vastavalt teisele järjendile.

(:codestart python gutter='false':)
arvud = [3.14159, 2.71828, 1.41421, 6.28318, 1.61803]
kohtadeni = [2, 3, 4, 3, 2]

ümardatud = list(map(round, arvud, kohtadeni))
print(ümardatud)  # [3.14, 2.718, 1.4142, 6.283, 1.62]
(:codeend:)

Proovi @@map@@ funktsiooniga kõik järjendi sõned suurtähestada (sõne meetod @@upper@@). 

!!!!Funktsioon filter
Vahepeal on vaja järjendeid filtreerida. Näiteks tahame järjendist jätta alles ainult positiivsed arvud. Võib jälle teha for-tsükli:

(:codestart python gutter='false':)
arvud = [3, -6, 1, -2, 4, -8, 1, -3]
positiivsed = []

for arv in arvud:
    if arv > 0:
        positiivsed.append(arv)

print(positiivsed)  # [3, 1, 4, 1]
(:codeend:)

Seda saab lühemalt teha funktsiooniga @@filter@@. See funktsioon nõuab parameetritesse funktsiooni ning järjendit. Tagastatud järjendisse jäetakse alles ainult need elemendid, mille peal funktsioon tagastab @@True@@. Eelmine koodijupp funktsiooniga @@filter@@:  

(:codestart python gutter='false':)
arvud = [3, -6, 1, -2, 4, -8, 1, -3]
            
positiivsed = list(filter(lambda arv: arv > 0, arvud))

print(positiivsed)  # [3, 1, 4, 1]
(:codeend:)

Seda saab ka kasutada sõnede peal. Näiteks saame alles jätta kõik tähed ja tühikud tekstis ilma muude sümboliteta:

(:codestart python gutter='false':)
lause = "„Funktsionaalprogrammeerimine on lihtne,” ütles mitte keegi."

print("".join(filter(lambda x: x.isalpha() or x == " ", lause)))
# Funktsionaalprogrammeerimine on lihtne ütles mitte keegi
(:codeend:)

Kuna @@filter@@ tagastab järjendilaadse objekti, peab sõne saamiseks selle kokku liitma sõne meetodiga @@join@@.

Kasuta @@filter@@ funktsiooni, et saada kätte salajane sõnum järgnevast tekstist:

(:codestart python gutter='false':)
XXXSXXXXXXAXXXXXXLXXXXXXAXXXXXXJXXXXXXAXXXXXXNXXXXXXEXXXXXX XXXXXXSXXXXXXÕXXXXXXNXXXXXXUXXXXXXMXXX
(:codeend:)

Veel üks huvitav funktsioon on [[https://www.geeksforgeeks.org/reduce-in-python/ | reduce]].

!!!Funktsioon zip
Kui meil on mitu järjendit sarnaste andmetega ja me tahame neid kokku pakkida, siis peaksime need for-tsükliga indekseerima:

(:codestart python gutter='false':)
eesnimed = ["Jüri", "Mailis", "Tõnis", "Tanel", "Urmas"]
perenimed = ["Ratas", "Reps", "Lukas", "Kiik", "Reinsalu"]

ministrid = []

for i in range(len(eesnimed)):
        ministrid.append((eesnimed[i], perenimed[i]))

print(ministrid)
# [('Jüri', 'Ratas'), ('Mailis', 'Reps'), ('Tõnis', 'Lukas'), ('Tanel', 'Kiik'), ('Urmas', 'Reinsalu')]
(:codeend:)

Funktsioon @@zip@@ lubab seda palju lihtsamalt teha:

(:codestart python gutter='false':)
eesnimed = ["Jüri", "Mailis", "Tõnis", "Tanel", "Urmas"]
perenimed = ["Ratas", "Reps", "Lukas", "Kiik", "Reinsalu"]

print(list(zip(eesnimed, perenimed)))
# [('Jüri', 'Ratas'), ('Mailis', 'Reps'), ('Tõnis', 'Lukas'), ('Tanel', 'Kiik'), ('Urmas', 'Reinsalu')]
(:codeend:)

See on kasulik, kui meil on näiteks erinevaid andmeid samade asjade kohta ja me tahame neid kokku liita. 

Funktsioonile zip võib sisse sööta lõpmatu arvu parameetreid:

(:codestart python gutter='false':)
>>> list(zip([1], [2], [3], [4], [5]))
[(1, 2, 3, 4, 5)]
(:codeend:)

Seega, kui me sisestame saadud paarid @@zip@@ funktsiooni parameetritesse, siis saame tagasi eesnimede ja perenimede järjendid:

(:codestart python gutter='false':)
>>> list(zip(('Jüri', 'Ratas'), ('Mailis', 'Reps'), ('Tõnis', 'Lukas'), ('Tanel', 'Kiik'), ('Urmas', 'Reinsalu')))
[('Jüri', 'Mailis', 'Tõnis', 'Tanel', 'Urmas'), ('Ratas', 'Reps', 'Lukas', 'Kiik', 'Reinsalu')]
(:codeend:)

Aga kui meil on paarid järjendi sees, siis kuidas kõiki elemente funktsiooni parameetritesse saada?

Et järjendit funktsiooni parameetritena kasutada, tuleb järjend panna parameetriks ning selle ette kirjutada tärn. Proovime algul lihtsama näitega: paneme funktsiooni round parameetriks kaheliikmelise järjendi.

(:codestart python gutter='false':)
>>> round(*[3.14159265368, 2])
3.14
(:codeend:)

Proovime sama võtet @@zip@@ funktsiooni peal:

(:codestart python gutter='false':)
>>> ministrid = [('Jüri', 'Ratas'), ('Mailis', 'Reps'), ('Tõnis', 'Lukas'), ('Tanel', 'Kiik'), ('Urmas', 'Reinsalu')]
>>> list(zip(*ministrid))
[('Jüri', 'Mailis', 'Tõnis', 'Tanel', 'Urmas'), ('Ratas', 'Reps', 'Lukas', 'Kiik', 'Reinsalu')]
(:codeend:)

Töötab! Oleme edukalt järjendid kokku ja lahti pakkinud.

!!!Kokkuvõte
Kasutasime erinevaid võtteid, et vähendada üleliigset koodi kirjutamist. Võibolla tundub tõesti, et see teeb lihtsaid asju keerulisemaks, aga piisava harjutamisega muutuvad need lihtsamaks ning pikema koodi kirjutamise soov kaob ära. 

Kuigi õpitud võtted lubavad lahendada peaaegu kõiki ülesandeid ühe reaga, peab meelde tuletama, et [[https://www.python.org/dev/peps/pep-0008/#maximum-line-length | Pythoni ametlik stiiliõpetus]] nõuab, et read on maksimaalselt 79 tähemärgi pikkused. Päris programmides tuleks kood mõistlikult paigutada mitmele reale. 

Paljud siin peatükis rakendatud funktsioonid on seotud funktsionaalprogrammeerimisega. Sellesse teemasse süveneb aine "Programmeerimiskeeled" ([[https://ois2.ut.ee/#/courses/MTAT.03.006/details | MTAT.03.006]]) keeltega Haskell ja Scala.

!!!Enesekontrolliküsimused
(:includeurl https://courses.cs.ut.ee/LTAT.03.001/2020_fall/uploads/Main/silmaring8_1.html width=100% height="230px" border="0" :)
(:includeurl https://courses.cs.ut.ee/LTAT.03.001/2020_fall/uploads/Main/silmaring8_2.html width=100% height="230px" border="0" :)
(:includeurl https://courses.cs.ut.ee/LTAT.03.001/2020_fall/uploads/Main/silmaring8_3.html width=100% height="260px" border="0" :)

!!!Ülesanded
1. Antud on järjendid nimedest, matriklinumbritest ja keskmistest hinnetest. 

(:codestart python gutter='false':)
nimed = ["Jaan", "Martin", "Katrin", "Margus", "Tiiu", "Jüri", "Anna", "Sirje", "Ülle", "Kristjan", "Anne", "Julia", "Andres", "Marina", "Rein", "Aivar", "Tiina", "Urmas", "Toomas", "Maria"]
matriklinumbrid = ["B69310", "B28761", "B79826", "B14207", "B16122", "B61619", "B14708", "B59695", "B50264", "B32270", "B88961", "B73302", "B29125", "B87856", "B48386", "B22124", "B52814", "B80444", "B56290", "B57742"]
hinded = [4.15, 3.61, 3.59, 3.28, 4.5, 4.6, 4.16, 3.83, 4.97, 4.26, 4.92, 3.93, 3.64, 4.12, 4.03, 4.75, 4.38, 4.65, 3.09, 4.04]
(:codeend:)

Kirjuta '''ühe reaga''' funktsioon, mis tagastab ennikud nendest, kelle keskmine hinne on vähemalt 4.6, sorteeritud keskmise hinde järgi kahanevalt. 

(:codestart python gutter='false':)
>>> stipisaajad(nimed, matriklinumbrid, hinded)
[('Ülle', 'B50264', 4.97), ('Anne', 'B88961', 4.92), ('Aivar', 'B22124', 4.75), ('Urmas', 'B80444', 4.65), ('Jüri', 'B61619', 4.6)]
(:codeend:)

2. Tekstifaili @@evolutsioon.txt@@ on salvestatud tekst imelikul kujul: tekst algab ülevalt paremalt ja liigub alla. Kirjuta '''ühe reaga''' funktsioon @@korrasta@@, mis võtab parameetrina failinime ning tagastab õiges järjekorras loetud teksti. Lahendamiseks piisab siin peatükis kasutatud funktsioonidest, v.a faili sisselugemine. Faili sulgemine siin ülesandes pole tähtis.

(:codestart python gutter='false':)
nne mE
g,raon
, fnsd
 audtl
enl  e
vd mbs
o hoes
laasa
vrvtuf
eee to
d  wir
.bbofm
 eenus
 iedl
(:codeend:)

(:codestart python gutter='false':)
>>> with open("ül1.py") as f: print(len(f.readlines()))
1
>>> %Run 'ül1.py'
>>> korrasta("evolutsioon.txt")
'Endless forms most beautiful and most wonderful have been, and are being, evolved.  '
(:codeend:)
