V oglate oklepaje zapiramo (izpeljane ali naštevne) sezname, v zavite zapiramo množice in slovarje. V okrogle pa ... terke? Lahko naredimo "izpeljano terko"? Ne, tega ne bi potrebovali velikokrat. Pač pa nas čaka - in bo pogosto v okroglih oklepajih - še nekaj bolj nenavadnega in imenitnejšega: generatorji.
g = (x ** 2 for x in range(4))Tale g, kot se hitro prepričamo, ni terka, temveč nekaj
drugega.
g<generator object <genexpr> at 0x1067263b0>
Kaj je generator, čemu služi in kako ga uporabljamo? Na prvi dve vprašanji lahko odgovorimo naenkrat: generator je nekaj, kar generira objekte. Vsakič, ko bomo od njega zahtevali nov objekt, bo vrnil novo število - najprej 0, potem 1, potem 4, potem 9. pač kvadrate naravnih števil od 0 do (vključno) 3.
Kako pa "zahtevamo" nov objekt. Tega ne počnemo s funkcijo
next.
next(g)0
next(g)1
next(g)4
next(g)9
next(g)---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
/var/folders/f9/lgq5ysmn24j3lkss6zkgmtr00000gn/T/ipykernel_96026/4253931490.py in <module>
----> 1 next(g)
StopIteration:
Na koncu, ko smo zahtevali že peti objekt - g pa je
zmožen generirati le štiri, se je pač pritožil, javil napako.
Sem se zgoraj zatipkal, ko sem napisal, da generatorjev ne
uporabljamo s funkcijo next, potem pa točno to počel? Ne,
nisem se zatipkal. V resnici ne bomo. Funkcijo next sem jo
pokazal samo zato, da bi razumeli, kaj počnejo generatorji. V praksi pa
klicanje next-a prepustimo for-u.
g = (x ** 2 for x in range(4))
for i in g:
print(i)0
1
4
9
Če to poskusimo ponoviti, ne gre: generator je že "iztrošen". Kar je imel zgenerirati, je zgeneriral.
for i in g:
print(i)Lahko zavrtimo generator nazaj? Obstaja prev? Ali
rewind, restart. Ne, to bi bila prehuda
zahteva, kot bomo videli v nadaljevanju, ko bomo spoznavali, kako so
narejeni in česa vsega so v resnici zmožni. Pa tudi uporabno in smiselno
ne bi bilo.
Generator je torej videti kot izpeljani seznam, le da namesto oglatih oklepajev uporabimo okrogle. Kadar ga pošljemo kot edini argument funkciji, smemo oklepaje celo izpustiti. Napišimo funkcijo, ki kot argument dobi seznam števil in vrne prvo število, ki je večje od 50.
def nad50(s):
for e in s:
if e > 50:
return ePreskusimo jo, trikrat:
nad50([5, 1, 40, 1, 82, 12, 6])82
nad50([x ** 2 for x in range(10)])64
# Dodatni presledki so neumni in so tu samo zaradi pregledanosti primera
nad50( (x ** 2 for x in range(10)) )64
V prvem primeru je dobila najobičajnejši seznam. Tako smo si
predstavljali, ko smo jo pisali. V drugem primeru smo ji dali seznam
kvadratov števil do 10 in vrnila je prvi kvadrat, ki je večji od 50. V
tretjem klicu pa ji sploh nismo dali seznama, temveč generator, ki je
dajal kvadrate - natančno isto reč kot gornji g. Funkcija
je čez generator pognala zanko for natančno tako, kot jo
sicer poganja čez sezname.
V čem je prednost generatorjev pred seznami? V tem, da ne sestavijo
seznama. Če želimo izračunati vsoto kvadratov prvih stotih števil in za
to uporabimo generator, na ta način ne sestavimo seznama teh števil (kot
bi ga, če bi rekli sum([x**2 for x in range(100)]), temveč
števila, namreč kvadrate, generiramo sproti (tako, da pokličemo
sum((x**2 for x in range(100))). Hm, pa to v resnici
deluje? No, seveda. Funkcija sum bi lahko bila napisana
takole
def sum(s):
vsota = 0
for e in s:
vsota += e
return vsotaZanka for lahko gre prek generatorja, torej ona reč deluje.
Že, že, poreče pozornejši študent: kaj pa range(100)?
Mar ta ne sestavi seznama stotih števil? Smo res kaj dosti pridobili -
namesto seznama kvadratov števil do 100 imamo pač števila do 100 - je to
res tak napredek? Tu lahko študenta pohvalimo za budnost, vendar se
moti.
range(100)range(0, 100)
V resnici range ne sestavi seznama, temveč generator.
Ampak temu se bomo posvetili malo kasneje, ko bomo o generatorjih vedeli
še malo več.
Tu si raje oglejmo še en lušten detalj. Spomnimo se, da okrog
elementov terke ni potrebno pisati oklepajev; to smo uporabili, recimo,
pri vračanju rezultatov z return x, y ali pri menjavi
vrednosti spremenljivk a, b = b, a. Podobno tudi okrog
generatorjev ni potrebno pisati oklepajev, kadar so edini argument
funkcije. Zadnji klic funkcije nad_50, ko smo podali
generator, je imel precej oklepajev - da bi se znašli med njimi, smo
dodali celo nepotrebne presledke.
nad50( (x ** 2 for x in range(10)) )Ker je tale generator edini argument funkcije, smemo oklepaje izpustiti in pisati kar
nad50(x ** 2 for x in range(10))64
Vsoto kvadratov števil od 0 do 9 lahko izračunamo tako.
sum(x ** 2 for x in range(10))285
Tako bodemo počeli poslej.
Še nekoliko naprednejša snov: Bi znali napisati generator Fibonaccijevih števil, tako kot smo napisali generator kvadratov? Da, vendar bo preprostejša nekoliko drugačna pot. Napisali bomo funkcijo, ki ne vrača le enega rezultata temveč "generira" rezultate. Torej, nekaj takšnega:
def fibonacci(n):
# Tole ne deluje!
a = b = 1
for i in range(n):
return a
a, b = b, a+bTale funkcija ne deluje, napisali smo jo le, da ilustriramo idejo:
radi bi naredili zanko. Ko funkcijo prvič pokličemo, bi
return vrnil prvo Fibonaccijevo število. Ko jo pokličemo
naslednjič, se funkcija ne bi izvajala od začetka, temveč od tam, kjer
smo nazadnje vrnili rezultat, se pravi z vrstico, ki sledi
returnu. No, v resnici naredimo natanko tako, le namesto
returna moramo uporabiti yield:
def fibonacci(n):
a = b = 1
for i in range(n):
yield a
a, b = b, a + bPreskusimo.
f = fibonacci(10)
f<generator object fibonacci at 0x1068bf840>
Rezultat je torej - generator! Generatorje lahko napišemo v takšni
obliki, kot smo jih spoznali sprva, lahko pa v obliki "funkcije".
Mehanika je v resnici takšna, da klic te funkcije vrne generator - kot
smo poklicali fibonacci(10), smo kot rezultat dobili
generator.
next(f)1
next(f)1
next(f)2
next(f)3
next(f)5
Kako to deluje, bomo najlažje videli, če v funkcijo nasujemo nekaj
print-ov.
def fibonacci(n):
print("Spet bo treba računati")
a = b = 1
for i in range(n):
print("Zdaj bom vrnil člen", a)
yield a
print("In potem bomo nadaljevali")
a, b = b, a + bPokličimo funkcijo, shranimo generator.
f = fibonacci(10)Se je kaj izpisalo? Nič!!! "Funkcija" se sploh še ni začela izvajati!
Funkcija steče vsakič, ko pokličemo next in "zamrzne" ob
yield-u.
next(f)Spet bo treba računati
Zdaj bom vrnil člen 1
1
Poglejte print-e in izpis, pa boste točno videli, kaj se
je izvedlo.
In potem glejte, kako teče funkcija naprej vsakič, ko pokličemo
next.
next(f)In potem bomo nadaljevali
Zdaj bom vrnil člen 1
1
next(f)In potem bomo nadaljevali
Zdaj bom vrnil člen 2
2
next(f)In potem bomo nadaljevali
Zdaj bom vrnil člen 3
3
Funkcija vsakič nadaljuje od tam, kjer se je prejšnjič ustavila, se
pravi od yield-a.
Seveda tudi tega generatorja ne bomo klicali z next,
temveč s for.
for x in fibonacci(10):
print(x)Napišemo lahko celo funkcijo, ki vrne (no, generira) vsa Fibonaccijeva števila.
def fibonacci():
a = b = 1
while True:
yield a
a, b = b, a+bNeskončno zanko, while True, smo že videli, vendar je
bil v njej vedno break, ki jo je nekoč prekinil. Kdo pa
prekine to zanko? Če nismo previdni, nihče.
for i in fibonacci():
print(i)se vidimo onstran večnosti. Pač pa lahko poiščemo, recimo, prvo Fibonaccijevo število, ki je večje od 50.
for i in fibonacci():
if i > 50:
print(i)
break55
Ah, saj imamo že funkcijo za to reč, nad50. Naj kar ta
pove, katero je prvo Fibonaccijevo število večje od 50!
nad50(fibonacci())55
Še en zanimiv primer je generator, ki vrne vse delitelje podanega števila.
def delitelji(n):
for i in range(1, n + 1):
if n % i == 0:
yield i
list(delitelji(42))[1, 2, 3, 6, 7, 14, 21, 42]
Opazimo lahko, da z enim deliteljem dobimo dva: če je i
delitelj n-ja, je tudi n // i delitelj
n-ja. Če je tako, zadošča, da gremo do korena iz
n in vrnemo po dva delitelja.
from math import sqrt
def delitelji(n):
for i in range(1, int(sqrt(n) + 1)):
if n % i == 0:
yield i
yield n // i
list(delitelji(24))[1, 24, 2, 12, 3, 8, 4, 6]
Koren iz n moramo spremeniti v celo število, ker
range ne mara necelih.
Pazite, tole sta dva yielda. Funkcija se izvaja tako, da
vrne najprej eno število, in ko zahtevamo naslednje, se izvede naslednji
yield.
Funkcija je zdaj bistveno hitrejša (pri velikih številih bi se to
utegnilo kar poznati - namesto, da gre do milijona, bo šla le do 1000.
Vendar žal ne dela pravilno. Če je n, recimo,
25, bo funkcija dvakrat vrnila 5. A tega se znamo hitro
znebiti.
def delitelji(n):
for i in range(1, int(sqrt(n) + 1)):
if n % i == 0:
yield i
if i ** 2 != n:
yield n // iZdaj nas morda moti le še to, da števila ne prihajajo v pravem
vrstnem redu. Kot delitelje 42 bi namesto 1, 2, 3, 6, 7, 14, 21, 42
dobili 1, 42, 2, 21, 3, 14, 6, 7. To lahko popravimo tako, da
n // i ne vračamo sproti, temveč jih le shranjujemo in jih
vračamo kasneje.
def delitelji(n):
ostali = []
for i in range(1, int(sqrt(n) + 1)):
if n % i == 0:
yield i
if i ** 2 != n:
ostali.append(n // i)
for e in ostali:
yield eNismo še čisto zmagali. Zdaj imamo 1, 2, 3, 6, 41, 21, 14, 7 - prva
polovica je iz prvega yielda, druga (od 41 naprej) iz
drugega. Te, druge, vrača v enakem vrstnem redu, v katerem jih je
vstavljal v seznam, saj append pač vstavlja na konec.
Pa vstavljajmo raje na začetek! Uporabimo insert.
def delitelji(n):
ostali = []
for i in range(1, int(sqrt(n) + 1)):
if n % i == 0:
yield i
if i ** 2 != n:
ostali.insert(0, n // i)
for e in ostali:
yield eŽal se je insertu modro izogibati. Za to, da vstavi
element na prvo mesto, mora enega za drugim premakniti vse ostale. V
teoriji (ki se jo boste učili drugo leto) je ta program enako počasen
kot bi bil, če bi prvo zanko spustili prek range(1, n + 1).
S tem, ko smo zamenjali append z insert, smo,
vsaj v teoriji, zapravili ves prihranek.
To je preprosto urediti. Vstavljali bomo na konec, z
append. Pač pa bomo seznam nato prehodili v obratnem
vrstnem redu. To se da narediti z indeksiranjem
(for i in range(-1, -n - n, -1): yield ostali[i], vendar si
bomo raje pomagali s priročno funkcijo reversed, ki obrne
seznam.
def delitelji(n):
ostali = []
for i in range(1, int(sqrt(n) + 1)):
if n % i == 0:
yield i
if i ** 2 != n:
ostali.append(n // i)
for e in reversed(ostali):
yield eČe nam je pomnilnika žal bolj kot časa, pa lahko naredimo drugače: gremo do korena in vračamo delitelji, nato pa od korena nazaj dol in vračamo njihove pare. Z drugimi besedami: to kar v gornjem programu shranjujemo, v spodnjem ponovno zgeneriramo.
from math import sqrt, ceil
def delitelji(n):
ostali = []
for i in range(1, int(ceil(sqrt(n)))):
if n % i == 0:
yield i
for i in range(int(sqrt(n)), 0, -1):
if n % i == 0:
yield n // iPrepričajmo se, da deluje tudi v zoprnih robnih primerih.
list(delitelji(42))[1, 2, 3, 6, 7, 14, 21, 42]
list(delitelji(25))[1, 5, 25]
list(delitelji(4))[1, 2, 4]
list(delitelji(3))[1, 3]
list(delitelji(2))[1, 2]
list(delitelji(1))[1]
Generatorji so samo posebna zvrst iteratorjev. Kaj pa so iteratorji?
Uvedimo jih tako, da najprej posplošimo nekaj, kar že poznamo.
Ob slovarjih smo izvedeli, da kot ključ ne moremo uporabiti ravno
poljubnega objekta, temveč le objekte, ki so nespremenljivi
immutable. To je samo približno res. Uporabiti smemo objekte,
ki so hashable. Objekti morajo imeti določeno (bolj interno)
metodo __hash__, ki jo slovarji (in množice) potrebujejo
zato, da se bodo odločili, kam shraniti določen ključ (oz. element). Če
podatkovni tip to metodo ima, je hashable; če ne, ne in ne more
služiti kot ključ slovarja (in biti elemnet množice). Metode
__hash__ nikoli ne kličemo neposredno, temveč pokličemo
funkcijo hash(obj), ki pokliče
obj.__hash__.
hash("Berta")-3551166701062947909
"Berta".__hash__() # tega ne počnemo, temveč kličemo `hash`-3551166701062947909
(Ta ovinek je pogost. Tudi funkcija len(obj) v resnici
pokliče obj.__len__ in vrne njen rezultat. Metode
__len__ nikoli ne kličemo neposredno. Kot tudi pišemo
s[i] in ne s.__getitem__(i). Ker nismo
neumni.)
Zdaj pa se vrnimo h generatorjem. Tudi gornja metoda
next je samo ovinek: next pokliče metodo
__next__. Podatkovni tipi, ki imajo metodo
__next__ so iterable. Konkretno, takim objektom
rečemo iteratorji.
Generatorji so vrsta iteratorjev. Niso pa generatorji edini
iteratorji. V Pythonu je kup priložnostnih iteratorjev. Iterator dobimo
s funkcijo iter, ki ji podamo objekt, prek katerega bi radi
iterirali. Če imamo seznam s in pokličemo
iter(s), bomo dobili iterator prek tega seznama.
s = ["Ana", "Berta", "Cilka"]
t = iter(s)
t<list_iterator at 0x1068569e0>
next(t)'Ana'
next(t)'Berta'
next(t)'Cilka'
Funkcija iter je sicer enaka prevara kot
hash, len ali iter: seznami imajo
metodo __iter__. Funkcija iter torej le
pokliče __iter__ in ta vrne iterator, torej objekt, ki ima
__next__ in ta vrača zaporedne elemente seznama.
Prav tako imajo metodo __iter__ tudi niz (vrne iterator,
ki vrača zaporedne znake niza), množica in terka (vračata zaporedne
elemente) in slovar (vrača ključe v nekem kakršnemkoliže vrstnem
redu).
d = {"Ana": 12, "Berta": 20, "Cilka": 15}
t = iter(d)
t<dict_keyiterator at 0x1068a6520>
next(t)'Ana'
next(t)'Berta'
next(t)'Cilka'
Z zanko for lahko gremo čez tiste in natančno
tiste reči, ki so iterabilne - torej, ki imajo metodo
iter.
Zanka for je zelo preprosta reč. Če imamo zanko
for spremenljivka in nekaj:
bo poklicala g = iter(nekaj), potem pa izvajala
spremenljivka = next(g), dokler next(g) ne
vrne napake StopIteration. Napako bo seveda skrila, saj v
resnici ne gre za napako, temveč le za signal, da je delo opravljeno.
Pokažimo kar primer. Vzemimo
s = ["Ana", "Berta", "Cilka"]Najprej ga prehodimo s for.
for x in s:
print(x)Ana
Berta
Cilka
Kar se v resnici dogaja zgoraj, je to:
g = iter(s)
while True:
x = next(g)
print(x)Ana
Berta
Cilka
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
/var/folders/f9/lgq5ysmn24j3lkss6zkgmtr00000gn/T/ipykernel_96026/2251081137.py in <module>
1 g = iter(s)
2 while True:
----> 3 x = next(g)
4 print(x)
StopIteration:
No, skoraj to. Naš while se konča z napako,
for pa to napako prestreže. Tega še ne znamo, a pokažimo,
saj bodo mnogi itak razumeli.
g = iter(s)
while True:
try:
x = next(g)
print(x)
except StopIteration:
breakAna
Berta
Cilka
To je to. To v resnici dela zanka for.
Na doslejšnjih predavanjih smo govorili stvari kot "z zanko
for lahko gremo prek seznamov", "zanka for zna iti tudi
prek množic" ... Figo. Zanka for ne zna ničesar. Vse delo
opravijo iteratorji. Zanka for od objekta zahteva iterator
in ga kliče.
Naslov zveni malo bedasto. Gre le zato, da imajo iteratorji metodo
iter in če jo pokličemo, iteratorji vrnejo kar samega sebe.
Zato lahko for-u damo seznam ali generator, in
for lahko v vsakem primeru pokliče iter.
t = iter([])
t<list_iterator at 0x106855450>
iter(t)<list_iterator at 0x106855450>
Številka v obliki 0x...nekaj, pove, kje v pomnilniku se
nahaja ta reč. Na ta način lahko prepoznamo, ali gre za isto stvar ali
ne. Tu je številka enaka, torej iter(t) vrne kar sam
t. Kot sem rekel.
Nekoč smo imeli
imena = ["Ana", "Berta", "Cilka", "Dani", "Ema"]
teze = [72, 65, 75, 68, 63]in ko sem povedal za zip(imena, teze), sem rekel, da se
vede, kot da bi vrnil seznam parov imen in tež. Vendar nisem napisal
zip(imena, teze)temveč
list(zip(imena, teze))[('Ana', 72), ('Berta', 65), ('Cilka', 75), ('Dani', 68), ('Ema', 63)]
in rekel, da spreglejmo tisti list.
zip je v resnici generator. Ko ga podamo
list-u, le da z nekakšno zanko for pobere vse
njegove elemente in jih zloži v novi seznam. Kasneje smo
zip uporabljali le v zanki for, ki ji je
vseeno, ali dobi seznam ali generator. Zato sem lahko govoril, da se
zip vede, kot da bi vrnil seznam.
Najprej preverimo, da je res, kar govorim.
t = zip(imena, teze)
t<zip at 0x1068fc380>
next(t)('Ana', 72)
next(t)('Berta', 65)
next(t)('Cilka', 75)
Res je generator. Ker po današnjem predavanju kar pokamo od pameti,
pa napišimo svoj zip. Če torej zip-a še ne bi
bilo, bi si ga sami sprogramirali približno tako.
def my_zip(s, t):
for i in range(len(s)):
yield s[i], t[i]
t = my_zip(imena, teze)
print(next(t))
print(next(t))('Ana', 72)
('Berta', 65)
for ime, teza in my_zip(imena, teze):
print(ime, teza)Ana 72
Berta 65
Cilka 75
Dani 68
Ema 63
Takle zip bi delal samo, če mu podamo stvari, ki jih je
možno indeksirati (recimo sezname), pri čemer mora biti za prvo stvar
(s) možno poklicati len, poleg tega pa prva
stvar ne sme biti daljša od druge. In, končno, tale zip
sprejme samo dva argumenta, pravi zip pa jih poljubno. A za
zdaj bodi. Boljše naredimo kasneje.
Kaj pa enumerate. Isto, seveda. Tudi generator.
t = enumerate(imena)
t<enumerate at 0x1068ef040>
next(t)(0, 'Ana')
next(t)(1, 'Berta')
Tudi enumerate bi si znali narediti sami.
def my_enumerate(s):
for i in range(len(s)):
yield i, s[i]for i, ime in my_enumerate(imena):
print(i, ime)0 Ana
1 Berta
2 Cilka
3 Dani
4 Ema
range? Isto. Tule je preprostejša različica
range-a, ki zahteva zgornjo in spodnjo mejo, korak pa je
vedno 1.
def my_range(start, end):
i = start
while i < end:
yield i
i += 1
for i in my_range(5, 8):
print(i)5
6
7
Od Pythona 3 naprej vse tovrstne funkcije vračajo iteratorje. Tudi
metode slovarjev, values, items in, hm,
keys, vrnejo iteratorje.
Lepota iteratorjev je, da nikoli ne ustvarijo celotnih seznamov. To je hitrejše in prijaznejše do pomnilnika. Obenem uporaba iteratorjev vodi k malo drugačnemu pogledu na potek programa - predstavljamo si ga lahko kot tok podatkov. V Pythonu je to malo manj izrazito, ker ni ravno idealen jezik za tak način programiranja. A vendar se ga da prikazati tudi z njim.
Najbolj frajerski so iteratorji, ki generirajo neskončna zaporedja. V
itertools imamo iterator count(n), ki šteje od
n do neskončno. Če argument n izpustimo, bo
štel od 0.
from itertools import count
for i in count(5):
if i == 10:
break
print(i)5
6
7
8
9
Če ne bi imeli enumerate, bi namesto
enumerate(s) pisali zip(count(), s). Iterator
zip bi šel pač hkrati prek iteratorja, ki ga vrne
count in prek s.
Tudi count ni vesoljska znanost, ta pa res ne.
def my_count(n=0):
while True:
yield n
n += 1
for i in my_count(5):
if i == 10:
break
print(i)5
6
7
8
9
count() izgleda relativno neuporaben. Ni. Sam ga imam
zelo rad in ga pogosto uporabim. V določenih situacijah (katerih opis pa
tule izpustimo) ga dejansko uporabljam namesto enumerate.
Prav pa pride tudi vedno, kadar nekaj štejemo, iščemo ... in ne vemo, do
kod bo potrebno šteti.
Katero je prvo število, katerega kvadrat presega 1000?
for i in count():
if i ** 2 > 1000:
break
print(i)32
Seveda lahko sestavimo tudi kvadrate vseh naravnih števil.
kvadrati = (x ** 2 for x in count())In potem spustimo zanko čeznje.
for x in kvadrati:
if x > 1000:
break
print(i)32
Katero pa je prvo popolno število? Se pravi, prvo število, ki je enako vsoti svojih deliteljev?
for n in count(1): # šteti začnemo pri 1, sicer bi dobili 0
if n == sum(x for x in range(1, n) if n % x == 0):
break
print(n)6
Še bolj imenitno: s count si lahko pripravimo generator
vseh popolnih števil!
popolna = (n for n in count(1) if n == sum(x for x in range(1, n) if n % x == 0))next(popolna)6
next(popolna)28
next(popolna)496
next(popolna)8128
for n in count(1) poskrbi, da bo šel n od 1
do neskončno, if pa izloča vsa nepopolna števila. Ko
pokličemo next, bo zanka gnala n toliko časa,
da bo if zadovoljen in generator bo "izgeneriral"
n. Ko naslednjič pokličemo next, teče zanka
naprej. In tako vsakič, ko zahtevamo naslednje število. Prva tri je
našel hitro, na 8128 pa je bilo potrebno že malo počakati.
Če razumete tale, zadnji generator, vam je jasno vse.
zip,
range, enumerateZgornje funkcije so bile malo poenostavljene. Resnične so sprogramirane v C-ju. Če bi jih hoteli narediti v Pythonu, pa tudi ne bi bile bistveno daljše od gornjih približkov, le malo bolj previdno se jih moramo lotiti.
def my_zip(*args):
args = [iter(arg) for arg in args]
try:
while True:
yield tuple([next(arg) for arg in args])
except StopIteration:
passFunkcija dobi poljubno število argumentov, zato
*args.
V prvi vrstici takoj pokličemo iter za vsak argument in
to zložimo kar nazaj v args. Tako zagotovimo, da imamo same iteratorjev,
se pravi, da nam je vseeno, ali je nek argument (že) iterator ali pa
(še) seznam ali kaj podobnega.
Nato bomo v neskončnost ponavljali
tuple([next(arg) for arg in args]). Z
[next(arg) for arg in args] sestavimo seznam vsega, kar
vračajo posamični iteratorji, in to pretvorimo v terko, ker
zip pač vrača terke. (Zakaj ne kar
tuple(next(arg) for arg in args)? Daljša zgodba. PEP-479.)
try-except prestrežeta
StopIteration in končata delo.
Preverimo, da res deluje: pokličimo ga s tremi argumenti, pri čemer je en (neskončen) generator, ostala dva pa sta tudi različno dolga.
for i, ime, teza in my_zip(count(), imena, teze[:-2]):
print(i, ime, teza)0 Ana 72
1 Berta 65
2 Cilka 75
Kaj pa range?
def my_range(start, end=None, step=1):
if end == None:
end = start
start = 0
while start < end if step > 0 else start > end:
yield start
start += steplist(my_range(5))[0, 1, 2, 3, 4]
list(my_range(5, 10))[5, 6, 7, 8, 9]
list(my_range(5, 15, 2))[5, 7, 9, 11, 13]
list(my_range(15, 5, -2))[15, 13, 11, 9, 7]
Pravi range zna še marsikaj, ampak "generatorski" del
smo podelali.
enumerate je v primerjavi z njima res preprost.
def my_enumerate(s, start=0):
s = iter(s)
try:
while True:
yield start, next(s)
start += 1
except StopIteration:
passlist(my_enumerate(imena, start=5))[(5, 'Ana'), (6, 'Berta'), (7, 'Cilka'), (8, 'Dani'), (9, 'Ema')]
Enkrat se bo treba ustaviti. V zvezi z vsemi temi rečmi bi bilo mogoče povedati še zelo veliko. Tole je ena najbolj kul tem v Pythonu. Pravzaprav treba priznati, da to niti ni tema iz Pythona. V ozadju tega, kar počnemo tule, je poseben slog programiranja, funkcijsko programiranje. Python ga omogoča in med "normalnimi" jeziki je za takšen slog pravzaprav eden boljših. Obstajajo pa jeziki, ki so posebej narejeni za takšno programiranje. Če je bilo komu tole, kar smo počeli doslej, všeč naj si nujno ogleda kak SML ali Haskell, morda pa ga bo zabaval tudi Racket (dialekt Lispa).
V Pythonu pa si bo ta, ki so mu bile te reči všeč, dobro ogledal module functools, iterools in operator.