Tviti

Tokrat bo potrebno napisati precej funkcij, vendar bodo tudi precej kratke.

Delali bomo s tviti. Ti so shranjeni v seznamih, ki izgledajo takole.

["sandra: Spet ta dež. #dougcajt",
 "berta: @sandra Delaj domačo za #programiranje1",
 "sandra: @berta Ne maram #programiranje1 #krneki",
 "ana: kdo so te @berta, @cilka, @dani? #krneki",
 "cilka: jst sm pa #luft",
 "benjamin: pogrešam ano #zalosten",
 "ema: @benjamin @ana #split? po dvopičju, za začetek?"]

Zapis tvita se začne z imenom avtorja (brez @), sledi dvopičje, nato pa besedilo tvita.

Kot je priporočeno za daljše teke, se bomo ogrevali kar na prvem kilometru obveznega dela.

Obvezni del

1. Napišite funkcijo unikat(s), ki prejme seznam nekih stvari in kot rezultat vrne nov seznam, v katerem se vsak element pojavi le enkrat. Vrstni red v rezultat naj bo enak vrstnemu redu prvih pojavitev v podanem seznamu. Klic unikat([1, 3, 2, 1, 1, 3, 2]) mora vrniti [1, 3, 2].

Najpreprosteje je sestaviti nov seznam in zlagati vanj. Preden dodamo posamezni element pa preverimo, ali je morda že v seznamu.

def unikati(s):
    t = []
    for i in s:
        if i not in t:
            t.append(i)
    return t

Za nekatere je to prenostavno. Take študente prepoznamo po tem, da hočejo na vsak način brisati ponovljene elemente, pa čeprav naloga eksplicitno zahteva nov seznam. Ne, uprli se bodo: obstoječi seznam bodo skopirali, skupaj z odvečnimi elementi, in te pobrisali naknadno.

def unikati(s):
    t = s[:]
    for i in range(len(t)):
        if t[i] in t[:i]:
            del t[i]
    return t

Potem pa se bodo še čudili, zakaj to ne deluje, temveč izpiše

    if t[i] in t[:i - 1]:
IndexError: list index out of range

Ideja je sicer zvita: za i-ti element (t[i]) preverimo, ali se pojavi med prvimi i elementi.

Razlog, da ne deluje, pa je preprost: z zanko for štejemo od 0 do toliko, kolikor je dolg seznam - se pravi, kolikor je dolg na začetku. Kasneje ga skrajšamo, zanka pa še vedno teče do toliko, kolikor smo določili v začetku.

Problem omilimo tako, da namesto for vzamemo while.

def unikati(s):
    t = s[:]
    i = 0
    while i < len(t):
        if t[i] in t[:i - 1]:
            del t[i]
        i += 1
    return t

Funkcija v tej obliki ne javi napake, deluje pa še vedno ne. Ko pokličemo unikati([1, 2, 1, 1, 3, 2]) vrne [1, 2, 1, 3] namesto [1, 2, 3]. Eno enico preskoči.

Ta past je pa čista klasika: ko brišemo iz seznama, se nam seznam izpodmika. Po del t[i] ne smemo povečati i-ja, saj je na i-to mesto zdaj prišel element t[i+1]. Gora, ki je prišla k Mohamedu, pa te stvari. Torej tako:

def unikati(s):
    t = s[:]
    i = 0
    while i < len(t):
        if t[i] in t[:i]:
            del t[i]
        else:
            i += 1
    return t

V vsakem koraku bodisi pobrišemo i-ti element, bodisi gremo na naslednji element.

Najbrž je kdo odkril tole:

from collections import OrderedDict

def unikati(s):
    return list(OrderedDict.fromkeys(s))

Lepo. Pa tudi razume to, kar je dobil na Googlu? Če ne: kakšen smisel ima to? Se pri tem predmetu učimo programiranja ali googlanja? Če se morate naučiti slednje, ste zgrešili fakulteto; pojdite raje za Bežigrad (pa ne na Pedagoško, tam boste naleteli na istega tipa kot na FRI :).

(V resnici mi je to poslala študentka, ki je sicer sprogramirala drugače. Tole jo je samo zanimalo.)

2. Napišite funkcijo avtor(tvit), ki vrne ime avtorja podanega tvita. Klic avtor("ana: kdo so te @berta, @cilka, @dani? #krneki") vrne "ana".

To bi bilo pa težko zakomplicirati. Razbijemo glede na dvopičje in vrnemo prvi element.

def avtor(tvit):
    return tvit.split(":")[0]

3. Napišite funkcijo vsi_avtorji(tviti), ki prejme seznam tvitov in vrne seznam vseh njihovih avtorje. Vsak naj se v seznamu pojavi le enkrat; vrstni red naj bo enak vrstnemu redu prvih pojavitev. Če funkcijo pokličemo z gornjim seznamom tvitov, mora vrniti ["sandra", "berta", "ana", "cilka", "benjamin", "ema"]. Sandra se pojavi le enkrat, čeprav je napisala dva tvita.

To se pa da zakomplicirati in mnogi so to tudi storili: namesto da bi uporabili funkciji, ki ju že imajo, so ju v bistvu skopirali v novo funkcijo in dodali še eno zanko.

Lepa rešitev je:

def vsi_avtorji(tviti):
    imena = []
    for tvit in tviti:
        imena.append(avtor(tvit))
    return unikati(imena)

ali pa, če se vam zdi pregledneje nekoliko daljše:

def vsi_avtorji(tviti): imena = [] for tvit in tviti: avtor_tvita = avtor(tvit) imena.append(avtor_tvita) imena = unikati(imena) return imena

Zakomplicirana različica je:

def vsi_avtorji(tviti):
    imena = []
    for tvit in tviti:
        avtor_tvita = tvit.split(":")[0]
        if not avtor_tvita in imena:
            imena.append(avtor_tvita)
    return imena

V resnici ni nič tako groznega. Vseeno pa je lepo, če se učite uporabljati funkcije, ki jih imate. Sicer se bodo že naslednje funkcije v tej domači nalogi kar daljšali in daljšale.

Pač pa se bomo kmalu naučili, da gre tudi tako

def vsi_avtorji(tviti):
    return unikati(avtor(tvit) for tvit in tviti)

in celo tako

def vsi_avtorji(tviti):
    return unikati(map(avtor, tviti))

In če hočemo znati take stvari, moramo pač res znati uporabljati tudi funkcije, ki smo jih napisali sami v prejšnji nalogi. :)

4. Napišite funkcijo izloci_besedo(beseda), ki prejme neko besedo in vrne to besedo brez vseh ne-alfanumeričnih znakov (to je, znakov, ki niso črke ali števke) na začetku in koncu. Če pokličemo izloci_besedo("!%$ana---"), mora vrniti "ana". Če pokličemo izloci_besedo("@janez-novak!!!"), vrne "janez-novak" (in **ne "janeznovak"!). Namig: strip() tule morda ne bo preveč uporaben. Pač pa v dokumentaciji Pythona preverite, kaj dela metoda isalnum. Potem nalogo rešite tako, da odstranjujte prvi znak besede, dokler ta ni črka. In potem na enak način še zadnjega. Kako besedi odstranimo znak, pa boste - če se ne boste spomnili sami - izvedeli v zapiskih o indeksiranju.**

Ta vam je pa dala vetra. Nisem pričakoval: včasih vem, kaj je za študente težko in kaj ne, včasih pa zgrešim.

Najpreprostejša rešitev je: odbijamo prvi znak, dokler nam ni všeč. In potem odbijamo zadnji znak, dokler nam ni všeč. Nato vrnemo rezultat.

def izloci_besedo(beseda):
    while beseda and not beseda[0].isalnum():
        beseda = beseda[1:]
    while beseda and not beseda[-1].isalnum():
        beseda = beseda[:-1]
    return beseda

Drug, za računalnik hitrejši, a za nas zamudnejši način je, da najdemo indeks prve in zadnje črke ter vrnemo, kar je vmes.

def izloci_besedo(beseda):
    for prva in range(len(beseda)):
        if beseda[prva].isalnum():
            break
    for zadnja in range(len(beseda), 0, -1):
        if beseda[zadnja-1].isalnum():
            break
    return beseda[prva:zadnja]

Prvi del je lažji: zanko vrtimo, dokler ne pridemo do prve črke. Drugi je podoben, le indeksi so bolj zoprni: šli bomo od dolžine besede do 0 in preverjali en znak pred tem indeksom. Tako se splača zato, ker potem vrnemo vse znake do tega indeksa, se pravi vključno s tistim, ki smo ga preverjali.

Takole pa mi ne bi bilo všeč -- razen če avtor takšne (ali podobne) rešitve ve, kako to v resnici deluje.

import re

def izloci_besedo(beseda):
    return re.search('^[^A-Za-z0-9]*(.*?)[^A-Za-z0-9]*$', beseda).group(1)

5. Napišite funkcijo se_zacne_z(tvit, c), ki prejme nek tvit in nek znak c. Vrniti mora vse tiste besede iz tvita, ki se začnejo s podanim znakom c. Pri tem mora od besed odluščiti vse nealfanumerične znake na začetku in na koncu. Klic se_zacne_z("sandra: @berta Ne maram #programiranje1 #krneki", "#") vrne ["programiranje1", "krneki"].

Sestavimo prazen seznam, gremo čez besede v tvitu, in v seznam zložimo vse, ki se začnejo s podanim znakom. Mimogrede pa pokličemo še izloci_besedo.

def se_zacne_z(tvit, c):
    besede = []
    for beseda in tvit.split():
        if beseda[0] == c:
            besede.append(izloci_besedo(beseda))
    return besede

Nekega lepega dne - o, kako bo lep! celo če bo deževalo - se bomo naučili, da gre tudi tako:

def se_zacne_z(tvit, c):
    return [izloci_besedo(beseda) for beseda in tvit.split() if beseda[0] == c]

6. Napišite funkcijo zberi_se_zacne_z(tviti, c), ki je podobna prejšnji, vendar prejme seznam tvitov in vrne vse besede, ki se pojavijo v njih in se začnejo s podano črko. Poleg tega naj se vsaka beseda pojavi le enkrat. Če pokličemo zberi_se_zacne_z(tviti, "@") (kjer so tviti gornji tviti), vrne ['sandra', 'berta', 'cilka', 'dani', 'benjamin', 'ana']. Vrstni red besed v seznamu je enak vrstnemu redu njihovih pojavitev v tvitih.

To je spet podobno: sestavimo seznam, gremo čez tvite in zlagamo vanj. Edina razlika je, da moramo tokrat uporabiti += (ali extend) namesto append-a, saj lepimo skupaj sezname.

def zberi_se_zacne_z(tviti, c):
    afne = []
    for tvit in tviti:
        afne += se_zacne_z(tvit, c)
    return unikati(afne)

7. Napišite funkcijo vse_afne(tviti), ki vrne vse besede v tvitih, ki se začnejo z @. Če ji podamo gornje tvite, mora vrniti ['sandra', 'berta', 'cilka', 'dani', 'benjamin', 'ana'].

Za to funkcijo pa je vse že pripravljeno.

def vse_afne(tviti):
    return unikati(zberi_se_zacne_z(tviti, "@"))

8. Napišite funkcijo vsi_hashtagi(tviti). Za gornje tvite vrne ['dougcajt', 'programiranje1', 'krneki', 'luft', 'zalosten', 'split'].

In za to prav tako.

def vsi_hashtagi(tviti):
    return unikati(zberi_se_zacne_z(tviti, "#"))

9. Napišite funkcijo vse_osebe(tviti), ki vrne po abecedi urejen seznam vseh oseb, ki nastopajo v tvitih - bodisi kot avtorji, bodisi so omenjene v tvitih. Vsaka oseba naj se pojavi le enkrat. Za gornje tvite funkcija vrne ['ana', 'benjamin', 'berta', 'cilka', 'dani', 'ema', 'sandra'].

Kako lepo je življenje tistih, ki vedo, da se da sezname seštevati.

Funkcija mora vrniti tisto, kar vrneta funkciji vsi_avtorji in vse_afne, le unikate moramo pobrati in vse skupaj urediti.

def vse_osebe(tviti):
    osebe = unikati(vsi_avtorji(tviti) + vse_afne(tviti))
    osebe.sort()
    return osebe

Če vemo (vemo?) za funkcijo sorted, pa gre še hitreje.

def vse_osebe(tviti):
    return sorted(unikati(vsi_avtorji(tviti) + vse_afne(tviti)))

Dodatna naloga

1. Napišite funkcijo custva(tviti, hashtagi), ki prejme seznam tvitov in seznam hashtagov (brez začetnega #). Vrne naj vse avtorje, ki so uporabili vsaj enega od naštetih tagov. Avtorji naj bodo urejeni po abecedi in vsak naj se pojavi le enkrat. Klic custva(tviti, ["dougcajt", "krneki"]) vrne ["ana", "sandra"].

def custva(tviti, hashtagi):
    avtorji = []
    for tvit in tviti:
        if neprazen_presek(se_zacne_z(tvit, "#"), hashtagi):
            avtorji.append(avtor(tvit))
    avtorji.sort()
    return unikati(avtorji)

Za vsak tvit preverim, ali je presek med besedami, ki se začnejo s # in podanimi hashtagi neprazen, če je tako, dodam avtorja tega tvita v seznam avtorjev, ki ga bom posortiral in vrnil.

Ups, aja, nimam funkcije neprazen presek. Jo pač napišemo, ne?

def neprazen_presek(s, t):
    for e in s:
        if e in t:
            return True
    return False

Enkrat (recimo takoj na naslednjih predavanjih) se bomo učili o množicah in izvedeli, da je presek kar set(s) & set(t).

Omenjenega lepega dne pa se bomo nehali hecati in napisali

def custva(tviti, hashtagi):
    return unikati(sorted(avtor(tvit) for tvit in tviti if set(hashtagi) & set(se_zacne_z(tvit, "#"))))

2. Napišite funkcijo se_poznata(tviti, oseba1, oseba2), če je oseba1 v katerem od svojih tvitov omenila osebo oseba2 ali obratno.

Gremo prek vseh tvitov. Za vsakega odkrijemo avtorja in vse, ki so omenjeni. Če je prva oseba pisec, druga pa omenjena ali pa obratno, vrnemo True. Če se to ne zgodi v nobenem tvitu, vrnemo False.

def se_poznata(tviti, oseba1, oseba2):
    for tvit in tviti:
        pisec = avtor(tvit)
        omenjeni = se_zacne_z(tvit, "@")
        if oseba1 == pisec and oseba2 in omenjeni or \
                oseba2 == pisec and oseba1 in omenjeni:
            return True
    return False
Zadnja sprememba: sreda, 19. september 2018, 16.33