Decorators

Dette afsnit henvender sig til dig, som har kendskab til klasser og objekt orienteret programmering i Python.

Decorators er forsimplet sagt funktioner, som gør andre funktioners funktionalitet endnu mere funktionelle. De forventer at andre funktioner bruges som parametrer. Man kalder også denne form for calling higher order functions.

Hvor man oftest arbejder med funktioner uden helt at tage stilling til, hvad de egentlig er, så er det i arbejdet med decorators anderledes. Her kræver det forståelse for at alt i Python er objekter.

Vi kommer til at tænke mere abstrakt, når vi arbejder med decorators. Bemærk at det kan være meget svært at få greb om konceptet i starten, og at man skal øve sig rigtig rigtig rigtig mange gange inden det giver mening. Logikken er i høj grad en disciplin som man ikke kan læse sig til, da det kræver træning, træning, træning og træning.

Eksempel 1

Lad os allerførst vænne os stille og roligt til decorators og tildeling af funktions-objekter til variabler.

# Funktion tildeles objektet b. b får a's funktionalitet.
def a():
    print("a")
    
b = a

a() # a
b() # b

print()

# funktionen
def f():                                                                                                  
    print("function")                                                                                       

# decorator                                                                                                          
def decoratoren(func):                                                                                          
    def g():                                                                                            
        print("Decorator henter ", end="")                                                             
        func()                                                                                              
    return g                                                                                            
# Main program                                                                                                            
                                                                              
x = decoratoren(f)                                                                                    
x()
    
print()

# Simpel decorator
def velkomst(deko):                                                                                            
    def indhold():                                                                                            
        print("Hej Pythons!")                                                                                     
        deko()    # Henter dekoration()
        print("ny tekst 1")
        deko() # Henter dekoration()
        print("ny tekst 2")  
        deko()  # Henter dekoration()                                                                                      
    return indhold                                                                                           
                                                                                                            
def dekoration():                                                                                                 
    print("#"*10)                                                                                          
                                                                                                            
                                                                                                            
start = velkomst(dekoration)                                                                                          
start()
print()

I eksemplerne tildeler vi funktioner til vores variabler. Prøv at læse dem igennem nogle gange og lav dem i din editor. Læg mærke til at vi ikke kalder på en funktion, men henter funktionsobjektet. Forskellen er, at når vi kalder på en funktion bruger vi parenteser, mens vi ikke bruger parenteser, når vi tildeler objekter.

Vi skriver altså:

# korrekt
a = funktionen

og IKKE

# forkert
a = funktionen()

Funktionen er som sagt et objekt. Og vi ønsker at a skal tildeles objektets egenskaber. Vi ønsker ikke at a skal kalde på funktionen.

Eksempel 2

Lad mig her eksemplificere dette yderligere.

Vi definerer først funktionen start. Vi giver den en a=”start” som standard-værdi inde i parentesen.

I start()-funktionen tildeler vi to nye funktioner. En som hedder versal() og en anden som hedder minuskel(). Versal returnerer HEJ! og minuskel returnerer hej…

if-statementet returnerer HEJ! hvis standard-værdien(“start”) ikke ændres.

def start(a="start"):

    # Funktionerne defineres som først versal og derefter minuskel
    def versal(x="hej"): #<- x defineres
        return x.upper()+"!"

    def minuskel(x="hej"):
        return x.lower()+"...";

    # Herefter returnerer vi een af dem
    if a == "start":
        # Vi bruger ikke parenteser da vi ikke kalder funktionen
        # vi returnerer funktionen som objektet
        return versal
    else:
        return minuskel

Lad os nu tildele variblen v funktionens objektet start.

# Tildel start-funktionen til en variabel: v
v = start()      

v er nu objektet for start og vi kan se at start er blevet et funktionsobjekt.

# Tildel start-funktionen til en variabel: v
v = start()      

Printer vi nu objektet og kalder på funktionen, får vi et HEJ! da if-statementet returnerer versal()-funktionen, når start er indsat.

print(v())
#HEJ!

Og lad os så prøve at ændre standarden “start” til noget andet. Fx “slut”. Vi ser at den printer hej…. Det er altså vores if-statement, der har returneret minuskel()-funktionen til os.

print(start("slut")())
#hej...
Tænk på en sandwich når du eksperimenterer med decorators. Der er tale om funktioner, som kan hente andre ind i funktionen.

Lad os tildele en ny funktion. Vi kalder den wrap(). Inde i wrap()-funktionen tildeler vi parametret, fyld. wrap viser os dermed fyld(et).

def wrap(fyld): 
    print("\nHer skriver jeg noget inden jeg henter funktionen du tildeler...")
    print(fyld())

Vi kan nu printe wrap ud. Så vi gør præcis som tidligere. Men læg her mærke til, at det er igennem wrap, at vi får vores fyld ud.

wrap(start)
#Her skriver jeg noget inden jeg henter funktionen du tildeler...
# <function start.<locals>.versal at 0x7f82c4053950>
wrap(v)
#Her skriver jeg noget inden jeg henter funktionen du tildeler...
#HEJ!
wrap(start("Et eller andet"))
#Her skriver jeg noget inden jeg henter funktionen du tildeler...
#hej...

Eksempel 3

Jeg vil i dette eksempel ikke dissekere koderne som tidligere. I stedet vil jeg lægge den ud i dens helhed. Kan du se hvad der sker? Læs den igennem et par gange og skriv den i din editor.

# Her er vores decorator
def indpakning(fyld):

    # Vi definerer nu en ny funktion til (fyld)et som indpakningen henter
    def wrapper():

        # Koden som eksekveres før funktionen
        print("Før funktionen kører lægger vi underbrødet")

        # Kald på indhold.
        fyld()

        # Koden som eksekveres efter den originale funktion. Koden i decorator.
        print("Efter funktionen køres kommer vi overbrødet på")

    # funktionen har endnu ikke kører
    # vi returnerer vores wrapper-funktion.
    # wrapperen eksekverer nu både koden inden og efter funktionen, samt funktionen.
    return wrapper

# Primær funktion
def indhold():
    print("Funktionen som står inde i mellem wrapperen tildeler vi salat og kylling, og tilsætter salt og pebber")

indhold() 
#Funktionen som står inde i mellem wrapperen tildeler vi salat og kylling, og tilsætter salt og pebber

# Vi dekorerer indholdet inde i wrapper()-funktionen i indpakningen.
# Indpakningen wrapper den altså helt automatisk

indhold_decorate = indpakning(indhold)
indhold_decorate()
#Før funktionen kører lægger vi underbrødet
#Funktionen som står inde i mellem wrapperen tildeler vi salat og kylling, og tilsætter salt og pebber
#Efter funktionen køres kommer vi overbrødet på

Vi definerer først vores indpakning(fyld). Inde i den laver vi en wrapper() som kalder på indpakningens parameter, fyld.

Mellem fyldet skriver vi to print-funtioner. En som kører inden fyld() og en som kører efter.

Til sidst returnerer vi wrapperen.

Selve funktionen, som skal dekoreres, definerer vi indhold(). Denne printer følgende: Funktionen som står inde i mellem wrapperen tildeler vi salat og kylling, og tilsætter salt og pebber.

Herefter kalder vi på indhold:

indhold()

Og så tildeler vi variablen indhold_decorate funktionsobjektet indpakning, som nu får indhold indsat som parameter til fyld.

indhold_decorate = indpakning(indhold)
indhold_decorate()

Koden eksekveres til sidst:

Før funktionen kører lægger vi underbrødet

Funktionen som står inde i mellem wrapperen tildeler vi salat og kylling, og tilsætter salt og pebber

Efter funktionen køres kommer vi overbrødet på

@ – shortcut

Det smarte ved en decorator er, at vi kan bruge shortcuts. I stedet for at skrive en variabel og tildele den et funktions-objekt, kan vi skrive @<funktion>.

Lad os tage udgangspunkt i vores kode. Vi definerer endnu en funktion. Vi kalder funktionen nyt_indhold(). Den skriver “vegetar indhold”.

@indpakning
def nyt_indhold():
    print("Vegetar indhold")

nyt_indhold()  

Programmet eksekveres nu:

Før funktionen kører lægger vi underbrødet på

Vegetar indhold

Efter funktionen køres kommer vi overbrødet på

Velbekommen!

Eksempel 4

I dette eksempel ser vi på, at rækkefølgen ikke er uvæsentlig når man arbejder med decorators. I den første menu er burgeren med brød uden om bøffen. I det næste eksempel er menuen med tomater og salat uden om brøddet og bøffen.

# EKSEMPEL 4
def broed(fyld):
    def delene():
        print("-------".center(10))
        fyld()
        print("/"*10)
    return delene

def grontsager(fyld):
    def fyldet():
        print("tomater")
        fyld()
        print("salat")
    return fyldet

def koed(koed="bøf"):
    print(koed)

koed()
#--bøf--
print() # luft
print("Menu 1".center(20))
print("-"*20)
koed = broed(grontsager(koed))
koed()

print()
print("Menu 2".center(20))
print("-"*20)
# Med shortcuts

@grontsager
@broed
def salat_burger(koed="--bøf--"):
    print(koed)

salat_burger()

Alle eksemplerne findes på GitHub

Træning

Decorators kræver mere fokus end hvad vi nok er vant til i Python. Derfor så træn i det enkle og byg op. Prøv fx at lave om på dette eksempel og byg videre på det. Skab dine egne små eksempler.

I sidste ende handler din træning om at bryde koden ned så du til sidst forstå den. Forstår du den ikke første gang, så forstår du den måske i dit næste, 10. eller 20. forsøg.

def decorator(funktion):
    def wrapper():
        print("Inden funktionen")
        funktion()
        print("Efter funktionen")
    return wrapper

@decorator
def f():
    print("Dette er min funktion")
f()

# ny funktion
def fx():
    print("Dette er min anden funktion")
fx = decorator(fx)
print("*"*10)
fx()