Kirjoittaja: Chiman
Kirjoitettu: 07.07.2015 – 17.07.2015
Tagit: ohjelmointitavat, koodi näytille, vinkki
Pythonissa koristimilla (engl. decorators) saadaan helposti ja kootusti säädeltyä funktion tai luokan toiminnallisuutta. Tässä vinkissä esitellään funktioiden koristamista.
Koristin ympäröi funktion toiminnallisesti niin, että sekä kutsut että paluuarvot kulkevat ympäröivän funktion kautta. Tämä mahdollistaa mm. argumenttien esikäsittelyn, paluuarvon muokkaamisen ja portinvartijana toimimisen eli koko kutsun estämisen.
Argumentteihin ja paluuarvoihin puuttumattomat koristimet voivat esimerkiksi pitää kirjaa funktiokutsujen lukumäärästä tai suoritusajasta.
Aluksi esitellään lähes yksinkertaisin mahdollinen koristin:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- def koristaja(koristettava): def korvaaja(): print('Nyt mennään funktioon') koristettava() print('Funktiosta palattiin') return korvaaja @koristaja def tervehdi(): print('Hei kaikki!') @koristaja def alle_kympin_parittomat(): print([x for x in range(10) if x % 2]) tervehdi() alle_kympin_parittomat()
Tuossa argumenttina annettu "koristettava" on korvattava funktio, jonka tilalle tulee "korvaaja". Koristettava funktio suoritetaan ylimääräisten tulostusten välissä. Koristinta käytetään muussa koodissa @-merkin avulla. Yllä oleva koodi tulostaa:
Nyt mennään funktioon Hei kaikki! Funktiosta palattiin Nyt mennään funktioon [1, 3, 5, 7, 9] Funktiosta palattiin
Yllä oleva koristin ei sallinut argumentteja funktiolle. Seuraavaksi näytetään pari hyödyllisempää ja yleiskäyttöisempää koristinta. Funktion suoritusaikaa mittaava koristin näyttää tältä:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import time from functools import wraps def mittaa_suoritusaika(f): '''koristin funktion suoritusajan mittaamiseen''' @wraps(f) # kopioi koristeltavan funktion ominaisuuksia ympäröivään funktioon def ymparoiva_funktio(*arg, **nimiarg): aloitusaika = time.time() tulos = f(*arg, **nimiarg) # kutsu varsinaista funktiota alkuperäisillä argumenteilla lopetusaika = time.time() kesto = lopetusaika - aloitusaika print('Funktion "%s" suoritus kesti %.6f s' % (f.__name__, kesto)) return tulos # palauta alkuperäisen funktion palauttama arvo return ymparoiva_funktio # palauta funktio ympäröitynä ajanmittauksella @mittaa_suoritusaika def neliosumma(n): return sum(x ** 2 for x in range(n + 1)) @mittaa_suoritusaika def fibonacci(n): a, b = 0, 1 for i in range(n): a, b = b, a + b return a def main(): print('Neliösumma = %d' % neliosumma(10 ** 6)) print('Fibonaccin tuhannes termi on %d numeroa pitkä' % len(str(fibonacci(1000)))) if __name__ == '__main__': main()
Huomaa miten argumentit välitetään koristellulle funktiolle. Suoritettuna yllä olevan koodin tulostus näyttää esim. tältä:
Funktion "neliosumma" suoritus kesti 0.751645 s Neliösumma = 333333833333500000 Funktion "fibonacci" suoritus kesti 0.000170 s Fibonaccin tuhannes termi on 209 numeroa pitkä
@wraps(f):n käyttö kopioi mm. funktion nimen ja ohjemerkkijonon koristeltavasta funktiosta korvaavaan funktioon. Tämä on hyvä käytäntö yleisesti, vaikkei sen merkitys tule tässä esimerkissä ilmi.
Toinen hyödyllinen koristin on välimuisti funktioille. Jos tehdään raskasta laskentaa ja funktio palauttaa aina saman arvon samoilla argumenteilla, funktiolle voidaan toteuttaa argumenttipohjainen välimuisti koristimella:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- from functools import wraps def valimuisti(f): '''Funktion välimuistikoristin Jos koristettavaa funktiota kutsutaan toistamiseen samoilla argumenteilla, toistuvan kutsun sijasta välimuistista palautetaan sama arvo kuin ensimmäisellä kutsukerralla. ''' muisti = {} @wraps(f) def ymparoiva_funktio(*arg, **nimiarg): # Luo argumenteista välimuisti-sanakirjan avaimeksi muuttumaton tuple. # nimiarg on nimettyjen argumenttien sanakirja avain = (arg, tuple(sorted(nimiarg.items()))) try: # haetaan muistista aiemman funktiokutsun tulos argumenttien perusteella tulos = muisti[avain] except KeyError: # aiempaa tulosta ei löytynyt, suorita funktio ja tallenna paluuarvo muistiin tulos = f(*arg, **nimiarg) muisti[avain] = tulos return tulos return ymparoiva_funktio # palauta funktio välimuistilla varustettuna @valimuisti def reitteja(x, y): '''Palauta mahdollisten reittien määrä x*y -ruudukossa Lähtö tapahtuu ruudukon kulmasta ja edetään kohti vastakkaista kulmaa joko vaaka- tai pystysuoraan. ''' if x < 0 or y < 0: # ollaan ruudukon ulkopuolella, ei reittejä return 0 elif x == y == 0: # ollaan maalissa return 1 else: return reitteja(x - 1, y) + reitteja(x, y - 1) def main(): x, y = 15, 10 print('%d x %d -ruudukossa on %d reittiä' % (x, y, reitteja(x, y))) if __name__ == '__main__': main()
Suoritettuna yllä olevan koodin tulostus näyttää tältä:
15 x 10 -ruudukossa on 3268760 reittiä
Ilman välimuistin käyttöä 15x10-ruudukon reittien laskeminen kestää eräällä koneella noin 8 sekuntia. Välimuistikoristinta käyttäen suoritusaika on noin 0,02 sekuntia. Välimuistin avulla ruudukon jokaisesta risteyksestä ei tarvitse laskea reittiä uudelleen loppuun asti vaan vain aiemmin vierailtuun risteykseen saakka.
Muisti-sanakirjan avaimena käytetään tuplea, joka koostuu sekä nimeämättömistä että nimetyistä argumenteista. Tämä tekee valimuisti-koristimesta yleiskäyttöisen.
Muisti-sanakirja alustetaan koristinta kiinnitettäessä ja sen sisältö säilyy koristellun funktion kutsujen välillä. Useamman rinnakkaisesti koristellun funktion välimuistit eivät sekoitu keskenään.
Koodiesimerkit toimivat Pythonin 2- ja 3-versioilla.
Hyvä vinkki! Python vaikuttaakin hyvältä kieleltä dynaamisen ohjelmoinnin toteuttamiseen.
Tätä tulisi käyttää, jotta tietyt magic-funktiot toimii, kuten dokumentoinnissa käytettävä __doc__:
https://docs.python.org/2/library/functools.html#functools.wraps
ZcMander kirjoitti:
Tätä tulisi käyttää, jotta tietyt magic-funktiot toimii, kuten dokumentoinnissa käytettävä __doc__:
https://docs.python.org/2/library/functools.html#functools.wraps
Tuon käyttö on järkevää, vaikkei sen merkitys näy tämän vinkin koodeissa. Lisäsin.
Vaikutusta voi havainnollistaa suorittamalla ylläoleva välimuistikoodi ilman @wraps(f):ää ja sen kanssa, kun main-funktioon on lisätty kaksi riviä:
def main(): x, y = 15, 10 print('%d x %d ruudukossa on %d reittiä' % (x, y, reitteja(x, y))) print('funktion nimi: %s' % reitteja.__name__) print('funktion ohje: %s' % reitteja.__doc__)
Nopealla silmäyksellä, näyttääpi tavalta kompensoida Pythonin staattisten muuttujien puute aliohjelmissa ja funktioissa (välimuisti koristin). Muukin koristimien tarjoama toiminnallisuus näyttäisi hoituvan esikääntäjän avulla kohtuullisen helposti muissa ohjelmointikielissä.
jalski kirjoitti:
Nopealla silmäyksellä, näyttääpi tavalta kompensoida Pythonin staattisten muuttujien puute aliohjelmissa ja funktioissa (välimuisti koristin).
Ei tuo korvaa staattista muuttujaa, vaan mahdollistaa välimuistin liittämisen erilaisiin funktioihin yhdellä @valimuisti-rivillä.
Staattinen muuttuja muissa kielissä on keino saavuttaa jokin ominaisuus. Luontevin tapa toteuttaa tuo ominaisuus Pythonissa riippuu tilanteesta. Se voi olla esim. sanakirjan käyttö oletusargumenttina *), jolloin sanakirjan sisältö säilyy funktiokutsujen välillä. Toinen vaihtoehto on käyttää generaattoria (funktio, jossa yield-lause) ja tietysti itse määritelty olio on aina varteenotettava tapa säilyttää tila kutsujen välillä.
*) Oletusargumentin käyttö muistina menee näin:
def eromuisti(x, muisti={}): ero_edelliseen = x - muisti.get('x', 0) muisti['x'] = x return ero_edelliseen print(eromuisti(2)) # 2 print(eromuisti(5)) # 3