Kirjautuminen

Haku

Tehtävät

Koodit: Python: Funktioiden koristimet välimuistina tai suoritusajan mittaajana

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.

Kommentit

Antti Laaksonen [09.07.2015 16:51:01]

#

Hyvä vinkki! Python vaikuttaakin hyvältä kieleltä dynaamisen ohjelmoinnin toteuttamiseen.

ZcMander [16.07.2015 22:21:09]

#

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

Chiman [17.07.2015 10:12:55]

#

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__)

jalski [22.07.2015 16:54:39]

#

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ä.

Chiman [22.07.2015 20:26:10]

#

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

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta