Förklarat (med exempel och användningsfall)

By rik

I Python är dekoratörer ett mycket kraftfullt verktyg. Genom att använda dekoratörer kan vi anpassa en funktions beteende genom att ”vira in” den i en annan funktion. Detta tillåter oss att skriva mer strukturerad kod och återanvända funktionalitet. Denna guide kommer att visa dig inte bara hur du använder dekoratörer, utan även hur du skapar dem från grunden.

Förkunskaper

För att fullt ut förstå konceptet med dekoratörer i Python, är det viktigt att ha vissa grundläggande kunskaper. Nedan följer en lista över begrepp som det är bra att känna till innan du dyker in i den här guiden. Jag har också inkluderat länkar till resurser om du behöver repetera dessa områden.

Grundläggande Python

Det här ämnet räknas som medel- till avancerad nivå. Därför är det viktigt att du har grundläggande kunskaper i Python, som datatyper, funktioner, objekt och klasser innan du börjar lära dig om dekoratörer. Du bör även vara bekant med objektorienterade begrepp som getters, setters och konstruktörer. Om du inte är bekant med programmeringsspråket Python, kan följande resurser vara en bra startpunkt:

Officiella Python-sidan
W3Schools Python-tutorial

Funktioner som ”förstklassiga medborgare”

Utöver grundläggande Python-kunskaper, är det viktigt att förstå att funktioner behandlas som ”förstklassiga medborgare” i Python. Det betyder att funktioner, liksom alla andra element i Python (som integers eller strings), är objekt. Eftersom de är objekt, kan vi göra vissa saker med dem:

  • Vi kan skicka en funktion som ett argument till en annan funktion, precis som med strings eller integers.
  • Funktioner kan returneras av andra funktioner, på samma sätt som du returnerar strings eller integers.
  • Funktioner kan lagras i variabler.

Den enda skillnaden mellan funktionsobjekt och andra objekt är att funktionsobjekt innehåller en magisk metod som heter __call__().

Om du känner dig bekväm med dessa förkunskaper, kan vi nu gå vidare till huvudämnet: dekoratörer.

Vad är en Python-dekoratör?

En Python-dekoratör är, enkelt uttryckt, en funktion som tar en annan funktion som argument och returnerar en modifierad version av den funktionen. Tänk dig att du har en dekoratör ’foo’, som tar funktionen ’bar’ som argument och returnerar en ny funktion, ’baz’. ’Baz’ är en modifierad version av ’bar’, i den meningen att ’baz’ anropar ’bar’, men kan göra andra saker före och efter anropet. Låt oss illustrera detta med lite kod:

# 'foo' är en dekoratör, den tar en funktion, 'bar', som argument.
def foo(bar):

    # Här skapar vi 'baz', en modifierad version av 'bar'.
    # 'baz' kommer att anropa 'bar', men kan göra annat före och efter.
    def baz():

        # Före 'bar' anropas, skriver vi ut något.
        print("Något händer...")

        # Sedan anropar vi 'bar'.
        bar()

        # Sedan skriver vi ut något annat efter 'bar' har körts.
        print("Något annat händer...")

    # Slutligen returnerar 'foo' 'baz', den modifierade versionen av 'bar'.
    return baz

Hur skapar man en dekoratör i Python?

För att visa hur man skapar och använder dekoratörer i Python, ska vi skapa en enkel loggningsdekoratör. Denna dekoratör kommer att logga namnet på funktionen den dekorerar varje gång funktionen körs.

Vi börjar med att skapa dekoratörfunktionen, som tar in ’func’ som argument. ’Func’ är den funktion vi vill dekorera.

def create_logger(func):
    # Funktionens kropp kommer här

Inuti dekoratörfunktionen skapar vi en ny funktion, ’modified_func’, som loggar namnet på ’func’ innan den körs.

# Inuti 'create_logger'
def modified_func():
    print("Anropar: ", func.__name__)
    func()

Slutligen returnerar ’create_logger’ den modifierade funktionen. Hela ’create_logger’-funktionen kommer att se ut så här:

def create_logger(func):
    def modified_func():
        print("Anropar: ", func.__name__)
        func()

    return modified_func

Nu har vi skapat vår första dekoratör. Funktionen ’create_logger’ tar in en funktion (’func’) och returnerar en modifierad version (’modified_func’), som loggar namnet på ’func’ innan den körs.

Hur man använder dekoratörer i Python

För att använda vår dekoratör, använder vi ’@’-syntaxen på följande sätt:

@create_logger
def say_hello():
    print("Hej världen!")

När vi anropar ’say_hello()’ i vårt skript, kommer utskriften att bli:

Anropar:  say_hello
Hej världen!

Vad gör ’@create_logger’ egentligen? Det är ett sätt att applicera dekoratören på vår ’say_hello’-funktion. För att bättre förstå vad som händer, kan vi åstadkomma samma sak som att använda ’@create_logger’ genom följande kod:

def say_hello():
    print("Hej världen!")

say_hello = create_logger(say_hello)

Med andra ord, ett sätt att använda dekoratörer är att uttryckligen anropa dekoratören och skicka in den funktion som ska dekoreras som argument. Det andra, och mer kortfattade sättet, är att använda ’@’-syntaxen.

I detta avsnitt har vi gått igenom hur man skapar Python-dekoratörer.

Lite mer komplexa exempel

Exemplet ovan var ganska enkelt. Det finns mer komplexa situationer, som när funktionen vi dekorerar tar argument. En annan komplex situation är när vi vill dekorera en hel klass. Vi kommer att gå igenom båda dessa scenarier.

När funktionen tar argument

När funktionen du dekorerar tar argument, måste den modifierade funktionen acceptera dessa argument och sedan skicka dem vidare när den anropar den omodifierade funktionen. Låt oss återigen använda ’foo’, ’bar’ och ’baz’ för att illustrera detta. Kom ihåg att ’foo’ är dekoratörfunktionen, ’bar’ är funktionen vi dekorerar och ’baz’ är den modifierade versionen av ’bar’. ’Bar’ tar in argumenten, och skickar dem till ’baz’ när den anropas. Här är ett kodexempel:

def foo(bar):
    def baz(*args, **kwargs):
        # Du kan göra något här
        ...
        # Sedan anropar vi 'bar' och skickar med 'args' och 'kwargs'.
        bar(*args, **kwargs)
        # Du kan också göra något här
        ...

    return baz

Om ’*args’ och ’**kwargs’ ser obekanta ut, är de helt enkelt pekare till positions- respektive nyckelordsargumenten.

Det är viktigt att notera att ’baz’ har tillgång till argumenten, och därför kan utföra viss argumentvalidering innan ’bar’ anropas.

Till exempel, om vi hade en dekoratörfunktion, ’ensure_string’, som säkerställde att argumentet som skickas till en funktion som den dekorerar är en sträng, skulle vi implementera den så här:

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('Argumentet till ' + func.__name__ + ' måste vara en sträng.')
        else:
             func(text)

    return decorated_func

Vi kan dekorera ’say_hello’-funktionen på följande sätt:

@ensure_string
def say_hello(name):
    print('Hej', name)

Sedan kan vi testa koden så här:

say_hello('John') # Detta bör fungera bra
say_hello(3) # Detta bör orsaka ett fel

Och utskriften borde bli:

Hej John
Traceback (most recent call last):
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # should throw an exception
   File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argument to + func._name_ + must be a string.')
TypeError: argumentet till say hello måste vara en sträng. $0

Som förväntat skrev skriptet ut ”Hej John” eftersom ”John” är en sträng. Ett fel uppstod när vi försökte skriva ut ”Hej 3” eftersom ”3” inte är en sträng. Dekoratören ’ensure_string’ kan användas för att validera argument för alla funktioner som kräver en sträng.

Att dekorera en klass

Förutom att dekorera funktioner, kan vi även dekorera klasser. När du lägger till en dekoratör till en klass, ersätter den dekorerade metoden klassens konstruktor/initieringsmetod (__init__).

Om vi återgår till vårt ’foo’-’bar’-scenario, antar vi att ’foo’ är vår dekoratör och ’Bar’ är klassen vi dekorerar. Då kommer ’foo’ att dekorera ’Bar.__init__’. Detta är användbart om vi vill göra något innan objekt av typen ’Bar’ skapas.

Det betyder att följande kod

def foo(func):
    def new_func(*args, **kwargs):
        print('Gör något innan instansieringen')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("I initieraren")

är ekvivalent med

def foo(func):
    def new_func(*args, **kwargs):
        print('Gör något innan instansieringen')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("I initieraren")


Bar.__init__ = foo(Bar.__init__)

Faktum är att instansiering av ett objekt av klassen ’Bar’, med vilken som helst av de två metoderna, ger samma utskrift:

Gör något innan instansieringen
I initieraren

Exempel på dekoratörer i Python

Även om du kan skapa dina egna dekoratörer, finns det några som redan är inbyggda i Python. Här är några av de vanligaste dekoratörerna som du kan stöta på:

@staticmethod

Den statiska metoden används i en klass för att visa att metoden den dekorerar är en statisk metod. Statiska metoder är metoder som kan köras utan att man behöver skapa en instans av klassen. I följande exempel skapar vi en klass ’Dog’ med en statisk metod ’bark’.

class Dog:
    @staticmethod
    def bark():
        print('Vov vov!')

Nu kan ’bark’-metoden nås så här:

Dog.bark()

Och när vi kör koden får vi följande utskrift:

Vov vov!

Som vi nämnde tidigare kan dekoratörer användas på två sätt. ’@’-syntaxen är den mer koncisa. Det andra sättet är att anropa dekoratörfunktionen och skicka in funktionen vi vill dekorera som argument. Det betyder att koden ovan uppnår samma sak som koden nedan:

class Dog:
    def bark():
        print('Vov vov!')

Dog.bark = staticmethod(Dog.bark)

Och vi kan fortfarande använda ’bark’-metoden på samma sätt

Dog.bark()

Vilket ger samma utskrift

Vov vov!

Som du ser är den första metoden renare, och det är mer tydligt att funktionen är statisk redan innan du har läst hela koden. Därför kommer vi att använda den första metoden för de återstående exemplen, men kom ihåg att den andra metoden är ett alternativ.

@classmethod

Den här dekoratören används för att visa att metoden den dekorerar är en klassmetod. Klassmetoder liknar statiska metoder, i det att de båda inte kräver att klassen har instansierats innan de kan anropas.

Den största skillnaden är att klassmetoder har tillgång till klassattribut, medan statiska metoder inte har det. Detta beror på att Python automatiskt skickar klassen som det första argumentet till en klassmetod när den anropas. För att skapa en klassmetod i Python, kan vi använda ’classmethod’-dekoratören.

class Dog:
    @classmethod
    def what_are_you(cls):
        print("Jag är en " + cls.__name__ + "!")

För att köra koden, anropar vi helt enkelt metoden utan att instansiera klassen:

Dog.what_are_you()

Och resultatet blir:

Jag är en Dog!

@property

Egenskapsdekoratören används för att beteckna en metod som en ”getter” för en egenskap. Låt oss återgå till vårt ’Dog’-exempel och skapa en metod som hämtar hundens namn.

class Dog:
    # Skapa en konstruktormetod som tar in hundens namn
    def __init__(self, name):

         # Skapa en privat egenskap 'name'
         # Dubbla understreck gör attributet privat
         self.__name = name

    
    @property
    def name(self):
        return self.__name

Nu kan vi komma åt hundens namn som en vanlig egenskap,

# Skapa en instans av klassen
foo = Dog('foo')

# Komma åt egenskapen 'name'
print("Hundens namn är:", foo.name)

Och resultatet av att köra koden blir

Hundens namn är: foo

@property.setter

Dekoratören ’property.setter’ används för att skapa en ”setter” för våra egenskaper. För att använda ’@property.setter’-dekoratören, ersätter du ’property’ med namnet på den egenskap som du skapar en setter för. Om du till exempel skapar en setter för egenskapen ’foo’, kommer din dekoratör att vara ’@foo.setter’. Här är ett exempel med hundar för att illustrera:

class Dog:
    # Skapa en konstruktormetod som tar in hundens namn
    def __init__(self, name):

         # Skapa en privat egenskap 'name'
         # Dubbla understreck gör attributet privat
         self.__name = name

    
    @property
    def name(self):
        return self.__name

    # Skapa en setter för vår egenskap 'name'
    @name.setter
    def name(self, new_name):
        self.__name = new_name

För att testa settern, kan vi använda följande kod:

# Skapa en ny hund
foo = Dog('foo')

# Ändra hundens namn
foo.name="bar"

# Skriv ut hundens nya namn
print("Hundens nya namn är:", foo.name)

När vi kör koden, får vi följande utskrift:

Hundens nya namn är: bar

Betydelsen av dekoratörer i Python

Nu när vi har gått igenom vad dekoratörer är, och du har sett några exempel, kan vi diskutera varför dekoratörer är viktiga i Python. Det finns flera anledningar till det, och några av dem har jag listat nedan:

  • De möjliggör återanvändning av kod: I loggningsexemplet ovan kan vi använda ’@create_logger’ på vilken funktion som helst. Detta låter oss lägga till loggningsfunktionalitet till alla våra funktioner utan att manuellt skriva den för varje funktion.
  • De låter dig skriva modulär kod: Om du återgår till loggningsexemplet, kan du med dekoratörer separera kärnfunktionaliteten, i det här fallet ’say_hello’, från den extra funktionalitet du behöver, som i det här fallet är loggning.
  • De förbättrar ramverk och bibliotek: Dekoratorer används flitigt i Python-ramverk och bibliotek för att tillhandahålla extra funktionalitet. Till exempel används dekoratörer i webbramverk som Flask eller Django för att definiera rutter, hantera autentisering eller applicera middleware på specifika vyer.

Slutord

Dekoratörer är otroligt användbara; du kan använda dem för att utöka funktionaliteten i en funktion utan att ändra dess kärnfunktion. Detta är användbart när du vill mäta prestanda, logga när en funktion anropas, validera argument innan en funktion anropas eller verifiera behörigheter innan en funktion körs. När du väl förstår dekoratörer, kommer du att kunna skriva kod på ett renare och mer effektivt sätt.

Efter detta kanske du vill läsa våra artiklar om tupler och användning av cURL i Python.