Izpeljani seznami, slovarji, množice
Contents
13. Izpeljani seznami, slovarji, množice#
Matematiki si lahko privoščijo opisati množico dvakratnikov vseh naravnih števil, katerih kvadrat je manjši od 120, takole:
Mi tudi.
13.1. Izpeljani seznami#
Začnimo s tistim, kar najbolj poznamo: s seznami. Doslej smo jih zapisali tako, da smo v oglatih oklepajih naštevali njihove elemente,
k = [9, 16, 81, 25, 196]
Oglate oklepaje bomo uporabljali tudi poslej, le da znotraj njih ne bomo našteli elementov, temveč napisali izraz, ki jih sestavi. Recimo, da bi hotel seznam korenov števil iz gornjega seznama (po nekem čednem naključju gornji seznam vsebuje ravno same kvadrate). Naredil bi tako:
from math import sqrt
[sqrt(x) for x in k]
Torej: napisal sem oglate oklepaje, kot vedno, kadar definiram seznam, a
namesto, da bi naštel elemente, sem povedal, kakšni naj bodo. Rekel sem,
naj bodo elementi mojega novega seznama koren iz x, pri čemer so x
elementi seznama k
. Kaj pa, če bi hotel namesto tega imeti kvadrate
števil? Isti šmorn:
[x ** 2 for x in k]
Oblika tega, kar pišemo, je vedno [izraz for spremenljivka in nekaj]
, kjer je spremenljivka ime neke spremenljivke (npr. x
, izraz nek izraz, ki (običajno, ne pa čisto nujno) vsebuje to spremenljivko (npr. sqrt(x)
ali x ** 2
), nekaj pa je nekaj, čez kar je možno spustiti zanko for
, torej seznam, slovar, množica ali kaj, česar še ne poznamo, vendar bomo spoznali še danes.
Ko sestavljamo tak izpeljan seznam, moramo torej razmišljati:
kaj hočemo v tem seznamu? S kakšnim izrazom opišem (izračunem) element?
prek česa (katerega seznama,
range
-a…) moram iti, da bom dobil te elemente.
Poglejmo še nekaj primerov.
Imam seznam imen, recimo,
imena = ["Ana", "Berta", "Cilka", "Dani", "Ema"]
Kako bi izračunal poprečno dolžino imena? Imam funkcijo sum
, ki zna
sešteti števila v seznamu. Potrebujem torej le še seznam dolžin (namesto
seznama imen). Pa poglejmo gornji “točki”. Kaj želim v seznamu? Dolžine imen. Kako jih dobim? Očitno z nekim len(ime)
, pri čemer je ime
pač vsako ime iz seznama imena
. V tem stavku smo odgovorili na obe vprašanji – kaj bom računal (len(ime)
) in čez kaj bom spustil zanko (čez imena
). Torej vemo: potrebujemo
[len(ime) for ime in imena]
Zdaj pa tole le še seštejem in delim z dolžino seznama.
sum([len(ime) for ime in imena]) / len(imena)
Poprečna dolžina imena ni nič posebej zanimivega. Računajmo raje
poprečno težo in, da nam ne bo dolgčas, jo bomo računali iz teh
troj (teža, ime, je_ženska)
:
podatki = [
(74, "Anže", False),
(82, "Benjamin", False),
(58, "Cilka", True),
(66, "Dani", False),
(61, "Eva", True),
(84, "Franc", False)]
Takole nam je storiti: z zanko se zapeljemo čez podatke, izračunamo vsoto prvih elementov in jo delimo z dolžino seznama.
Najprej poglejmo, kako bi to počeli po starem. Lepo prosim, ne tako:
vsota = 0
for i in range(len(podatki)):
vsota += podatki[i][0]
print(vsota / len(podatki))
Tole je že boljše:
vsota = 0
for podatek in podatki:
vsota += podatek[0]
print(vsota / len(podatki))
Če hočete, da vas vaš profesor ohrani v trajnem lepem spominu, pa boste sprogrmirali tako:
vsota = 0
for teza, ime, zenska in podatki:
vsota += teza
print(vsota / len(podatki))
Zdaj pa naredimo tako, kot smo se naučili danes. Naredimo lahko
vsota = sum([podatek[0] for podatek in podatki])
print(vsota / len(podatki))
ali, po boljši, zadnji različici
vsota = sum([teza for teza, ime, zenska in podatki])
print(vsota / len(podatki))
13.1.1. Izpeljani seznami in pogoji#
Pri izpeljevanju seznamov lahko dodamo še pogoj, s katerim izbiramo elemente, ki naj se dodajo.
Kako bi sestavili seznam kvadratov vseh lihih števil do 100? Seznam kvadratov vseh števil do 100 je trivialen, napisati nam je treba le:
[x ** 2 for x in range(10)]
Če želimo pobrati samo liha števila, lahko v izpeljavo seznama dodamo še pogoj.
[x ** 2 for x in range(10) if x % 2 == 1]
Tole pravzaprav ni čisto potrebno, lahko bi rekli preprosto
s = [x ** 2 for x in range(1, 10, 2)]
Kaj pa, če bi hoteli seznam vseh števil do 30, ki niso deljiva s 7 in ne vsebujejo števke 7?
[x for x in range(30) if x % 7 != 0 and "7" not in str(x)]
[1,
2,
3,
4,
5,
6,
8,
9,
10,
11,
12,
13,
15,
16,
18,
19,
20,
22,
23,
24,
25,
26,
29]
Prvi del pogoja poskrbi, da število ni deljivo s 7 (ostanek po deljenju s 7 mora biti različen od 0). Drugi del poskrbi, da število ne vsebuje števke 7: število preprosto pretvorimo v niz in preverimo, da ne vsebuje sedmice.
Kako bi izračunali vsoto tež moških v gornjem seznamu podatki
? Za zanko dodamo pogoj. Za začetek sestavimo le seznam imen vseh moških.
[ime for teza, ime, zenska in podatki if not zenska]
['Anže', 'Benjamin', 'Dani', 'Franc']
V definicijo našega seznama lepo na konec, za for
, dodamo še if
in
pogoj, ki mu mora zadoščati element, da nas zanima.
Na enak način naberemo tudi teže.
[teza for teza, ime, zenska in podatki if not zenska]
[74, 82, 66, 84]
Elemente tega seznama seštejemo s sum
, pa smo.
sum([teza for teza, ime, zenska in podatki if not zenska])
306
Zdaj veste, zakaj sem tako utrujal, da ne uporabljajte
for i in range(len(s))
: zato, ker z njim namesto lepega, kratkega
preglednega [teza for teza, ime, zenska in podatki if not zenska]
dobimo
[podatki[i][0] for i in range(len(podatki)) if not podatki[i][2]]
.
Seveda lahko delate tudi na ta, drugi način, če vam je v užitek. V svobodi živimo; noben zakon ne prepoveduje mazohizma.
Sestavimo seznam vseh deliteljev danega števila n
. Vzemimo, recimo
n = 60
. Seznam deliteljev je tedaj seznam vseh x
, pri čemer x
prihaja z intervala 1 ≤ x ≤ n, za katere velja, da je ostanek po
deljenju n
z x
enak 0 (torej x
deli n
).
def delitelji(n):
return [x for x in range(1, n + 1) if n % x == 0]
Zdaj lahko hitro ugotovimo, ali je dano število popolno: popolna števila so (se še spomnimo?) števila, ki so enaka vsoti svojih deliteljev (brez sebe samega).
def popolno(n):
return n == sum(delitelji(n)) - n
Še bolj imenitna števila so praštevila, števila brez deliteljev. Se pravi tista, ki so deljiva le z 1 in s samim seboj.
def prastevilo(n):
return delitelji(n) == [1, n]
Če funkcije delitelji
še ne bi imeli - nič hudega. Ali je število
praštevilo, bi še vedno dognali v eni sami vrstici. Poleg tega bi lahko
šli le od 2 do n (brez n); tako bi izpustili 1 in n ter število
razglasili za praštevilo, če je je deljivo le z 1 in s samim seboj.
def prastevilo(n):
return [x for x in range(1, n + 1) if n % x == 0] == [1, n]
Seveda je preprosteje, če spremenimo meje v range
tako, da ne vključujejo 1
in n
.
def prastevilo(n):
return [x for x in range(2, n) if n % x == 0] == []
Še boljše pa je, če se spomnimo, da so prazni seznami neresnični.
def prastevilo(n):
return not [x for x in range(2, n) if n % x == 0]
Za bis dodajmo še nekaj, česar ne bomo razložili.
def prastevilo(n):
return all(n % x for x in range(2, n))
S to funkcijo - v katerikoliže obliki - hitro sestavimo seznam vseh praštevil med 2 in 100:
[n for n in range(2, 101) if prastevilo(n)]
[2,
3,
5,
7,
11,
13,
17,
19,
23,
29,
31,
37,
41,
43,
47,
53,
59,
61,
67,
71,
73,
79,
83,
89,
97]
Znamo pa, jasno, tudi brez funkcije; tisto, kar dela funkcija, pač kar prepišemo v pogoj.
[n for n in range(2, 101) if not [x for x in range(2, n) if n % x == 0]]
[2,
3,
5,
7,
11,
13,
17,
19,
23,
29,
31,
37,
41,
43,
47,
53,
59,
61,
67,
71,
73,
79,
83,
89,
97]
Takšnih reči sicer raje ne pišemo, saj hitro postanejo nepregledne.
13.1.2. Splošni vzorec#
V splošnem: vsak kos kode, ki izgleda takole
r = []
for e in s:
if pogoj(e):
r.append(izraz)
lahko prepišemo v
r = [izraz for e in s if pogoj(e)]
13.2. Izpeljane množice#
Množice sestavljamo natančno tako kot sezname, le da namesto oglatih oklepajev uporabimo zavite. Takole dobimo vse delitelje 60
{i for i in range(1, 61) if 60 % i == 0}
{1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60}
13.3. Izpeljani slovarji#
Ista reč. Če hočemo narediti slovar, ki bo kot ključe vseboval števila do 10, kot vrednosti pa njihove kvadrate, napišemo
{i: i ** 2 for i in range(11)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
13.4. Nekaj uporabnih funkcij#
Te stvari res zaživijo, če jih uporabljamo v kombinaciji z nekaj funkcijami. Nekatere že poznamo, nekatere bodo postale smiselne šele danes.
13.4.1. enumerate
#
Doslej sem sitnaril, da jo uporabljajmo, kadar potrebujemo tako indeks elementa kot njegovo vrednost. Zdaj bo še toliko uporabnejša, saj bo bistveno izboljšala preglednost izpeljanih seznamov, množic in slovarjev.
Zdaj, ko smo ravno tako pametni, vemo tudi, kako bi napisali enumerate
, če le-tega še ne bi bilo.
def enumerate(s):
i = 0
for x in s:
yield i, x
i += 1
list(enumerate("Ana"))
[(0, 'A'), (1, 'n'), (2, 'a')]
13.4.2. zip
#
Tudi za zip
že vemo: podamo ji nekaj seznamov in vrne nov seznam - seznam terk z elementi seznamov, ki smo jih poslali kot argumente.
No, od danes vemo: v resnici vrne generator parov, ne seznama parov.
zip("Benjamin", "Margareta")
<zip at 0x7ff2830931e0>
Ignorirajte tiste 0x10193c308
(ali karkoli že se prikaže) - izpis je povedal, da smo naredili zip
in ne seznama. Seveda lahko iz njega “izgeneriramo” seznam.
list(zip("Benjamin", "Margareta"))
[('B', 'M'),
('e', 'a'),
('n', 'r'),
('j', 'g'),
('a', 'a'),
('m', 'r'),
('i', 'e'),
('n', 't')]
Napišimo funkcijo, ki pove, v koliko črkah se ujemata dani besedi. Tako se, recimo, MELODIJA in DELODAJALEC ujemata v šestih črkah.
MELODIJA
DELODAJALEC
ELOD JA
Da bo naše delo lažje, se najprej spomnimo, da se True
vede, kot da bi bil 1
in False
, kot da bi bil 0.
True + 7
8
Vzemimo torehj besedi MELODIJA in DELODAJALEC.
b1 = "MELODIJA"
b2 = "DELODAJALEC"
Združimo ju v seznam parov črk (list
pa dodamo samo v izpisu, da vidimo, kaj smo v resnici setavili).
list(zip(b1, b2))
[('M', 'D'),
('E', 'E'),
('L', 'L'),
('O', 'O'),
('D', 'D'),
('I', 'A'),
('J', 'J'),
('A', 'A')]
Poženimo zanko čez seznam parov črk in sestavimo nov seznam, ki bo vseboval True
, če sta črki enaki in False
, če različni.
[c == d for c, d in zip(b1, b2)]
[False, True, True, True, True, False, True, True]
Zdaj pa preštejmo, koliko je True
-jev. Kako? Če je False
isto kot 0 in True
isto kot 1, potem preprosto izračunamo vsoto elementov seznama.
sum(c == d for c, d in zip(b1, b2))
6
Če bi torej hoteli napisati funkcijo, ki pove, v koliko črkah se besedi ujemata, bi rekli kar
def ujemanj(b1, b2):
return sum(c==d for c, d in zip(b1, b2))
Napišimo funkcijo, ki izračuna Evklidsko razdaljo med dvema točkama, katerih koordinate so predstavljene s seznamom. Imejmo
a = [2, 1, -3]
b = [3, 5, 4]
Nalogo bi včasih rešili takole:
def euclid(a, b):
s = 0
for x, y in zip(a, b):
s += (x - y) ** 2
return sqrt(s)
Zdaj znamo veliko veliko preprosteje. Znamo sestaviti seznam parov? Znamo.
[(x, y) for x, y in zip(a, b)]
[(2, 3), (1, 5), (-3, 4)]
Znamo sestaviti seznam razlik parov? Znamo.
[x - y for x, y in zip(a, b)]
[-1, -4, -7]
Pravzaprav potrebujemo seznam kvadratov razlik.
[(x - y) ** 2 for x, y in zip(a, b)]
In zdaj moramo vse to le še sešteti.
sum((x - y) ** 2 for x, y in zip(a, b))
66
Funkcija, ki izračuna Evklidsko razdaljo, je torej
def euclid(a, b):
return sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
Te reči so, ko se jih navadiš, prav imenitno berljive. Program je skoraj dobesedno prepisan opis v naravnem jeziku: evklidska razdalja je koren vsote kvadratov razlik elementov, ki jih “vzporedno” jemljemo iz dveh seznamov.
13.4.3. all
in any
#
Funkciji all
in any
sta videti trivialno, v resnici
pa sta prav imenitni in uporabni. Prva, all
, prejme generator (ali, seveda, seznam, množico, niz, slovar…) in vrne True
, če le-ta generira same resnične reči (recimo same True
).
Število n
je praštevilo, če so vsi ostanki po deljenju z i
(za vsak i
od 2
do n
) različni od 0. Z all
se tole prevede skoraj dobesedno v Python.
def prastevilo(n):
return all(n % i != 0 for i in range(2, n))
Funkcija any
vrne True
, če tisto, kar podamo kot argument, vsebuje vsaj kakšno resnično vrednost. Kako deluje, pokažimo kar na obrnjenih praštevilih: število je sestavljeno, če je ostanek po deljenju s kakim drugim številom enak 0.
def sestavljeno(n):
return any(n % i == 0 for i in range(2, n))
Mimogrede opazimo, da sta all
in any
povezana prek de Morganovega pravila, ki se bi ga morali spomniti iz matematike. Število je praštevilo, če ni sestavljeno,
def prastevilo(n):
return not sestavljeno(n)
Če namesto, da bi poklicali sestavljeno
, vstavimo kar kodo funkcije sestavljeno
, dobimo
def prastevilo(n):
return not any(n % i == 0 for i in range(2, n))
Če le obrnemo pogoj, vidimo, da je
not any(n % i == 0 for i in range(2, n))
isto kot
all(not n % i == 0 for i in range(2, n))
13.4.4. itertools.count
#
Še več cukra se nahaja v modulu itertools
.
Funkcija count
iz modula itertools
je kot range
, ki ji podamo kvečjemu spodnjo mejo ali pa še te ne. count()
generira vsa števila od 0 do neskončno, count(n)
pa vsa števila od n
naprej.
Sam jo najpogosteje uporabljam v zvezi z zip
. zip(count(), s)
je isto kot enumerate(s)
. To sicer ni prav uporabno - očitno je enumerate(s)
preprosteje in jasneje kot zip(count(), s)
. Pač pa se dogaja, da greš z zip
čez tri sezname hkrati, poleg tega pa potrebuješ še indekse. To se da pisati kot
for i, (x, y, z) in enumerate(zip(s, t, u)):
lahko pa pišemo kar
for i, x, y, z in zip(count(), s, t, u)
Pa še kje vam bo prišel prav, če se spomnite nanj.
13.4.5. itertools.chain
#
Če imamo več generatorjev (ali seznamov, slovarjev…) in bi radi šli z eno zanko čez vse, torej čez enega za drugim, jih sestavimo s chain
.
from itertools import chain
for c in chain("Ana", "Berta", "Cilka"):
print(c)
A
n
a
B
e
r
t
a
C
i
l
k
a
Seveda je to isto kot for c in "Ana" + "Berta" + "Cilka":
, vendar je lepota v tem, da nizov nismo sešteli, temveč je chain
. To je lepo predvsem, kadar ne verižimo nizov, temveč kaj, česar se ne da seštevati.
13.4.6. Itertools#
Vsi, ki vam je bilo tole všeč, bodo uživali tudi v ostalih dobrotah iz modula itertools
.