Python ger fullfjädrad support för att implementera din egen datastruktur med hjälp av klasser och anpassade operatörer. I denna handledning kommer du att implementera en anpassad pipeline datastruktur som kan utföra godtyckliga operationer på dess data. Vi kommer att använda Python 3.
Rörledningens datastruktur är intressant eftersom den är mycket flexibel. Den består av en lista över godtyckliga funktioner som kan tillämpas på en samling objekt och skapa en lista över resultat. Jag kommer att dra nytta av Pythons extensibility och använda rörteckenet ("|") för att konstruera rörledningen.
Innan du dyker in i alla detaljer, låt oss se en mycket enkel rörledning i åtgärd:
x = intervall (5) | Pipeline () | dubbel | Ω utskrift (x) [0, 2, 4, 6, 8]
Vad händer här? Låt oss bryta ner det steg för steg. Det första elementet intervall (5)
skapar en lista med heltal [0, 1, 2, 3, 4]. Heltalen matas in i en tom pipeline betecknad med Rörledning()
. Sedan läggs en "dubbel" funktion till rörledningen, och slutligen den svala Ω
funktionen avslutar rörledningen och får den att utvärdera sig själv.
Utvärderingen består av att ta in ingången och tillämpa alla funktioner i rörledningen (i detta fall bara dubbelfunktionen). Slutligen lagrar vi resultatet i en variabel som heter x och skriver ut det.
Python stöder klasser och har en mycket sofistikerad objektorienterad modell med flera arv, mixins och dynamisk överbelastning. En __i det__()
funktionen fungerar som en konstruktör som skapar nya instanser. Python stöder också en avancerad meta-programmeringsmodell, som vi inte kommer in i i den här artikeln.
Här är en enkel klass som har en __i det__()
konstruktör som tar ett valfritt argument x
(standard till 5) och lagrar den i en self.x
attribut. Det har också en foo ()
metod som returnerar self.x
attribut multiplicerat med 3:
klass A: def __init __ (själv, x = 5): self.x = x def foo (själv): returnera self.x * 3
Här är hur man instanserar det med och utan ett explicit x-argument:
>>> a = A (2) >>> skriv ut (a.foo ()) 6 a = A () print (a.foo ()) 15
Med Python kan du använda anpassade operatörer för dina klasser för bättre syntax. Det finns speciella metoder som kallas "dunder" -metoder. "Dunder" betyder "dubbel understrykning". Med dessa metoder som "__eq__", "__gt__" och "__or__" kan du använda operatörer som "==", ">" och "|" med dina klassföremål (objekt). Låt oss se hur de arbetar med A-klassen.
Om du försöker jämföra två olika instanser av A till varandra kommer resultatet alltid att vara falskt oavsett värdet av x:
>>> skriv ut (A () == A ()) Falskt
Detta beror på att Python jämför standardobjekten till objekten som standard. Låt oss säga att vi vill jämföra värdet av x. Vi kan lägga till en särskild "__eq__" operatör som tar två argument, "själv" och "annat" och jämför deras x-attribut:
def __eq __ (jag själv, andra): return self.x == other.x
Låt oss verifiera:
>>> skriv ut (A () == A ()) Sann >>> skriv ut (A (4) == A (6)) False
Nu när vi har täckt grunderna för klasser och anpassade operatörer i Python, låt oss använda den för att genomföra vår pipeline. De __i det__()
konstruktören tar tre argument: funktioner, ingång och terminaler. Funktionen "Funktioner" är en eller flera funktioner. Dessa funktioner är de steg i rörledningen som fungerar på ingångsdata.
Inmatningsargumentet är listan över objekt som rörledningen ska fungera på. Varje del av ingången behandlas av alla rörledningsfunktioner. "Terminaler" -argumentet är en lista över funktioner, och när en av dem möter pipelinen utvärderar sig själv och returnerar resultatet. Terminalerna är som standard bara utskriftsfunktionen (i Python 3, "print" är en funktion).
Observera att inuti konstruktören läggs en mystisk "Ω" till terminalerna. Jag ska förklara det nästa.
Här är klassdefinitionen och __i det__()
konstruktör:
klass Pipeline: def __init __ (själv, funktioner = (), ingång = (), terminaler = (skriv ut)): om hasattr (funktioner, '__call__'): self.functions = [functions] else: self.functions = list (funktioner) self.input = inmatning self.terminals = [Ω] + lista (terminaler)
Python 3 stöder helt Unicode i identifieringsnamn. Det betyder att vi kan använda svala symboler som "Ω" för variabla och funktionsnamn. Här förklarade jag en identitetsfunktion som heter "Ω", som fungerar som en terminalfunktion: Ω = lambda x: x
Jag kunde ha använt den traditionella syntaxen också:
def Ω (x): returnera x
Här kommer kärnan i Pipeline-klassen. För att använda "|" (rörsymbol) måste vi överväga ett par operatörer. "|" Symbol används av Python för bitvis eller heltal. I vårt fall vill vi åsidosätta det för att genomföra chaining av funktioner samt mata inmatningen i början av rörledningen. Det är två separata operationer.
Operatören "__ror__" åberopas när den andra operand är en pipeline-instans så länge som den första operand inte är. Den betraktar den första operand som ingång och lagrar den i self.input
attribut och returnerar Pipeline-instansen tillbaka (jaget). Detta möjliggör kedjning av fler funktioner senare.
def __ror __ (self, input): self.input = inmatningsavkastning själv
Här är ett exempel där __ror __ ()
operatören skulle åberopas: "hej där" Rörledning()
Operatören "__or__" åberopas när den första operand är en pipeline (även om den andra operand också är en pipeline). Den accepterar operand som en kallbar funktion och det hävdar att "func" operand verkligen är kallbar.
Sedan lägger den funktionen till self.functions
Anteckna och kontrollera om funktionen är en av terminalfunktionerna. Om det är en terminal utvärderas hela rörledningen och resultatet returneras. Om det inte är en terminal returneras rörledningen själv.
def __or __ (self, func): assert (hasattr (func, '__call__')) self.functions.append (func) om func i self.terminals: return self.eval () returnera själv
När du lägger till fler och fler icke-terminala funktioner till rörledningen sker inget. Den faktiska utvärderingen är uppskjuten till och med eval ()
Metoden heter. Detta kan hända antingen genom att lägga till en terminalfunktion på rörledningen eller genom att ringa eval ()
direkt.
Utvärderingen består av att iterera över alla funktioner i rörledningen (inklusive terminalfunktionen om det finns en) och kör dem i ordning på utgången från föregående funktion. Den första funktionen i rörledningen tar emot ett inmatningselement.
def eval (self): result = [] för x i self.input: för f i self.functions: x = f (x) result.append (x) returresultat
Ett av de bästa sätten att använda en pipeline är att tillämpa den på flera uppsättningar av inmatningar. I det följande exemplet definieras en ledning utan ingångar och inga terminalfunktioner. Den har två funktioner: den ökända dubbel
funktion som vi definierade tidigare och standarden Math.floor
.
Sedan ger vi det tre olika insatser. I innerbandet lägger vi till Ω
terminalfunktionen när vi anropar den för att samla resultaten innan de skrivs ut:
p = pipeline () | dubbel | math.floor för inmatning i ((0.5, 1.2, 3.1), (11.5, 21.2, -6.7, 34.7), (5, 8, 10.9)): result = input | p | Ω utskrift (resultat) [1, 2, 6] [23, 42, -14, 69] [10, 16, 21]
Du kan använda skriva ut
terminalfunktionen direkt, men då skrivs varje artikel på en annan linje:
keep_palindromes = lambda x: (p för p i x om p [:: - 1] == p) keep_longer_than_3 = lambda x: (p för p i x om len (p)> 3) p = Pipeline () | keep_palindromes | keep_longer_than_3 | lista (('aba', 'abba', 'abcdef'),) | p | skriv ut ['abba']
Det finns några förbättringar som kan göra rörledningen mer användbar:
Python är ett mycket uttrycksfullt språk och är välutrustat för att designa din egen datastruktur och anpassade typer. Möjligheten att åsidosätta standardoperatörer är mycket kraftfull när semantiken lånar sig till en sådan notation. Rörsymbolen ("|") är till exempel väldigt naturlig för en rörledning.
Många Python-utvecklare tycker om Pythons inbyggda datastrukturer som tupler, listor och ordböcker. Att utforma och implementera din egen datastruktur kan dock göra ditt system enklare och lättare att arbeta med genom att höja abstraktionsnivån och dölja interna detaljer från användare. Ge det ett försök.