Deep Dive inn i Python Decorators

Oversikt

Python-dekoratører er en av mine favoritt Python-funksjoner. De er den mest brukervennlige * og * utvikler-vennlig implementering av aspektorientert programmering som jeg har sett på et hvilket som helst programmeringsspråk. 

En dekoratør lar deg forstørre, modifisere eller helt erstatte logikken til en funksjon eller metode. Denne tørre beskrivelsen gjør ikke dekoratører rettferdighet. Når du begynner å bruke dem, vil du oppdage et helt univers av pene applikasjoner som bidrar til å holde koden stram og ren og flytte viktige "administrative" oppgaver ut av hovedstrømmen av koden din og til en dekoratør. 

Før vi hopper inn i noen kule eksempler, hvis du vil utforske opprinnelsen til dekoratører litt mer, så fungere dekoratører først i Python 2.4. Se PEP-0318 for en interessant diskusjon om historie, begrunnelse og valg av navnet 'decorator'. Klasse dekoratører dukket opp først i Python 3.0. Se PEP-3129, som er ganske kort og bygger på alle konseptene og ideene til funksjonskunstnere.

Eksempler på Cool Decorators

Det er så mange eksempler at jeg er hard presset til å velge. Mitt mål her er å åpne tankene dine for mulighetene og introdusere deg til super nyttig funksjonalitet du kan legge til koden din ved å bokstavelig talt annotere dine funksjoner med en liner.

De klassiske eksemplene er de innebygde @staticmethod og @classmetod decorators. Disse dekoratørene gjør en klassemetode tilsvarende en statisk metode (det er ikke gitt et eget første argument) eller en klassemetode (første argument er klassen og ikke forekomsten).

Den klassiske dekoratøren

klasse A (objekt): @classmetod def foo (cls): skriv ut cls .__ name__ @staticmethod def bar (): print 'Jeg har ingen bruk for forekomsten eller klassen' A.foo () A.bar () 

Produksjon:

A Jeg har ingen bruk for forekomsten eller klassen

Statiske og klassemetoder er nyttige når du ikke har en forekomst i hånden. De brukes mye, og det var veldig tungvint å bruke dem uten dekoratørens syntaks.

Memoization

@Memoize decoratoren husker resultatet av den første påkallingen av en funksjon for et bestemt sett med parametere og caches det. Senere påkallinger med samme parametere returnerer det cachelagrede resultatet. 

Dette kan være en stor ytelsesbooster for funksjoner som gjør dyre prosessering (for eksempel å nå ut til en ekstern database eller ringe flere REST-APIer) og kalles ofte med de samme parameterne.

@memoize def fetch_data (items): "" "Gjør noe seriøst arbeid her" "" resultat = [hent_item_data (i) for jeg i elementer] returresultat 

Kontraktsbasert programmering

Hva med et par dekoratører som heter @precondition og @postcondition for å validere inngangsargumentet samt resultatet? Vurder følgende enkle funksjon:

def add_small ints (a, b): "" "Legg til to ints hvis summen fortsatt er en int" "" retur a + b

Hvis noen kaller det med store heltall eller lengder eller strenger, vil det lykkes tydelig, men det vil bryte kontrakten at resultatet må være en int. Hvis noen kaller det med feilaktige datatyper, får du en generell runtime-feil. Du kan legge til følgende kode i funksjonen:

def add_small ints (a, b): "" "Legg til to ints i hvis sum er fortsatt en int" "" påstand (isinstance (a, int), 'a must be an int') ), 'b må være et int') resultat = a + b assert (isinstance (resultat, int), 'argumentene er for store, sum er ikke et int') returresultat 

Vår fin en-linje add_small_ints () Funksjonen ble bare et uhyggelig kvist med gale påstander. I en real-world-funksjon kan det være veldig vanskelig å se på et øyeblikk hva det egentlig gjør. Med dekoratører kan pre- og postforholdene bevege seg ut av funksjonslegemet:

@precondition (isinstance (a, int), 'a must be an int') @precondition (isinstance (b, int), 'b må være en int') @postcondition (isinstance (retval, int), 'argumentene er for stor. sum er ikke en int ') def add_small ints (a, b): "" "Legg til to ints i hvis sum er fortsatt en int" "" retur a + b 

Autorisasjon

Anta at du har en klasse som krever autorisasjon via en hemmelighet for alle sine mange metoder. Å være den fullstendige Python-utvikleren, ville du sannsynligvis velge en @authorized metodeutsmyker som i:

klassen SuperSecret (objekt): @authorized def f_1 (* args, hemmelig): "" "" "" autorisert def f_2 (* args, hemmelig): "" "" "" ... @authorized def f_100 (* args, secret ): "" "" ""

Det er definitivt en god tilnærming, men det er litt irriterende å gjenta det, spesielt hvis du har mange slike klasser. 

Mer kritisk, hvis noen legger til en ny metode og glemmer å legge til @ autorisert dekorasjon, har du et sikkerhetsproblem på hendene. Ha ingen frykt. Python 3 klassen dekoratører har fått ryggen din. Følgende syntaks vil tillate deg (med den riktige klassen dekoratørdefinisjonen) å autorisere hver metode for målklassene automatisk:


@ authorized klasse SuperSecret (objekt): def f_1 (* args, hemmelig): "" ff (* args, hemmelig): "" " "" ""

Alt du trenger å gjøre er å dekorere selve klassen. Merk at dekoratøren kan være smart og ignorere en spesiell metode som __i det__() eller kan konfigureres til å gjelde for en bestemt delmengde hvis det er nødvendig. Himmelen (eller fantasien din) er grensen.

Flere eksempler

Hvis du vil forfølge flere eksempler, sjekk ut PythonDecoratorLibrary. 

Hva er en dekorator?

Nå som du har sett noen eksempler i gang, er det på tide å avdekke den magiske. Den formelle definisjonen er at en dekoratør er en callable som aksepterer en callable (målet) og returnerer en callable (the decorated) som aksepterer de samme argumentene som det opprinnelige målet. 

Woah! det er mange ord som er stablet på hverandre uforståelig. Først, hva er en callable? En callable er bare et Python objekt som har a __anrop__() metode. De er vanligvis funksjoner, metoder og klasser, men du kan implementere en __anrop__() metode på en av klassene dine, og så vil dine klassiske tilfeller bli callables også. For å sjekke om en Python-objekt kan kalles, kan du bruke funksjonen callable ():


callable (len) True callable ('123') False

Legg merke til at innløsbar () funksjonen ble fjernet fra Python 3.0 og brakt tilbake i Python 3.2, så hvis du av en eller annen grunn bruker Python 3.0 eller 3.1, må du sjekke for eksistensen av __anrop__ attributt som i hasattr (len, '__call__').

Når du tar en slik dekorator og bruker den ved hjelp av @ syntaksen til noen callable, blir den opprinnelige callable erstattet med callable returnert fra dekoratøren. Dette kan være litt vanskelig å forstå, så la oss illustrere det ved å se på gutten til noen enkle dekoratører.

Funksjon Dekorere

En funksjonskunstner er en dekoratør som brukes til å dekorere en funksjon eller en metode. Anta at vi vil skrive ut strengen "Ja, det virker!" hver gang en dekorert funksjon eller metode kalles før du faktisk påkaller den opprinnelige funksjonen. Her er en non-decorator måte å oppnå det på. Her er funksjonen foo () som skriver ut "foo () her":

def foo (): print 'foo () her' foo () Output: foo () her 

Her er den stygge måten å oppnå ønsket resultat:

original_foo = foo def decorated_foo (): print 'Ja, det fungerer!' original_foo () foo = decorated_foo foo () Output: Ja, det virker! foo () her

Det er flere problemer med denne tilnærmingen:

  • Det er mye arbeid.
  • Du forurenser navneområdet med mellomnavn som original_foo () og decorated_foo ().
  • Du må gjenta det for hver annen funksjon du vil dekorere med samme evne.

En dekoratør som oppnår det samme resultatet og er også gjenbrukbart og kompositt ser slik ut:

def yeah_it_works (f): def dekorert (* args, ** kwargs): print 'Ja, det fungerer' return f (* args, ** kwargs) returnert dekorert

Merk at yeah_it_works () er en funksjon (derfor kallbar) som aksepterer en callable ** f ** som et argument, og returnerer en callable (den nestede funksjonen ** dekorert **) som aksepterer et hvilket som helst antall og typer argumenter.

Nå kan vi bruke den til enhver funksjon:


@yeah_it_works def f1 () print 'f1 () her' @yeah_it_works def f2 () print 'f3 () her' @yeah_it_works def f3 () print 'f3 () her' f1 () f2 () f3 Ja, det fungerer f1 () her Ja, det fungerer f2 () her Ja, det fungerer f3 () her

Hvordan virker det? Den opprinnelige f1, f2 og f3 Funksjoner ble erstattet av den dekorerte nestede funksjonen returnert av yeah_it_works. For hver enkelt funksjon, fanget f callable er den opprinnelige funksjonen ( f1f2 eller f3), så den innredede funksjonen er forskjellig og gjør det rette, som er skrevet ut "Ja, det virker!" og deretter påkalle den opprinnelige funksjonen f.

Klasse dekoratører

Klasse dekorere operere på et høyere nivå og dekorere en hel klasse. Deres effekt finner sted i klassetidspunktet. Du kan bruke dem til å legge til eller fjerne metoder for enhver dekorert klasse eller til og med å bruke funksjonskunstnere til et helt sett med metoder. 

Anta at vi vil holde oversikt over alle unntak som er oppvokst fra en bestemt klasse i en klassetributt. La oss anta at vi allerede har en funksjonskunstner kalt track_exceptions_decorator som utfører denne funksjonaliteten. Uten en klassen dekorator, kan du manuelt bruke den på alle måter eller ty til metaklasses. For eksempel:


klasse A (objekt): @track_exceptions_decorator def f1 (): ... @track_exceptions_decorator def f2 (): ... @track_exceptions_decorator def f100 (): ... 

En klassen dekoratør som oppnår det samme resultatet er:


def track_exception (cls): # Få alle callable attributter av klassen callable_attributes = k: v for k, v i cls .__ dict __. items () hvis callable (v) # Dekorer hvert kallbart attributt til til inngangsklassen for navn , func i callable_attributes.items (): decorated = track_exceptions_decorator (func) setattr (kls, navn, dekorert) retur kls @track_exceptions klasse A: def f1 (selv): print ('1') def f2 '2')

Konklusjon

Python er kjent for sin fleksibilitet. Dekoratører tar det til neste nivå. Du kan pakke tverrgående bekymringer i gjenbrukbare dekoratører og bruke dem til funksjoner, metoder og hele klasser. Jeg anbefaler at alle seriøse Python-utviklere blir kjent med dekoratører og dra full nytte av fordelene sine.