Slik implementerer du din egen datastruktur i Python

Python gir fullverdig støtte for å implementere din egen datastruktur ved hjelp av klasser og tilpassede operatører. I denne opplæringen vil du implementere en tilpasset pipeline datastruktur som kan utføre vilkårlig operasjon på dataene sine. Vi vil bruke Python 3.

Pipeline Data Structure

Rørledningsdatastrukturen er interessant fordi den er veldig fleksibel. Den består av en liste over vilkårlig funksjoner som kan brukes til en samling objekter og produsere en liste over resultater. Jeg vil dra nytte av Pythons uttrekkbarhet og bruke rørkarakteren ("|") for å konstruere rørledningen.

Live eksempel

Før du drar i alle detaljer, la oss se en veldig enkel rørledning i aksjon:

x = rekkevidde (5) | Pipeline () | dobbelt | Ω utskrift (x) [0, 2, 4, 6, 8] 

Hva foregår her? La oss slå det ned trinnvis. Det første elementet område (5) lager en liste over heltall [0, 1, 2, 3, 4]. Heltallene mates inn i en tom rørledning utpekt av Rørledning (). Deretter legges en "dobbel" -funksjon til rørledningen, og til slutt den kule Ω funksjonen avslutter rørledningen og får den til å evaluere seg selv. 

Evalueringen består av å ta inn inngangen og bruke alle funksjonene i rørledningen (i dette tilfellet bare dobbeltfunksjonen). Til slutt lagrer vi resultatet i en variabel som heter x og skriver den ut.

Python-klasser

Python støtter klasser og har en svært sofistikert objektorientert modell, inkludert flere arv, mixins og dynamisk overbelastning. en __i det__() funksjonen fungerer som en konstruktør som skaper nye forekomster. Python støtter også en avansert metaprogrammeringsmodell, som vi ikke kommer inn i denne artikkelen. 

Her er en enkel klasse som har en __i det__() konstruktør som tar et valgfritt argument x (standard til 5) og lagrer den i a self.x Egenskap. Den har også a foo () metode som returnerer self.x attributt multiplisert med 3:

klasse A: def __init __ (selv, x = 5): self.x = x def foo (selv): returner selv.x * 3 

Her er hvordan du instantierer det med og uten et eksplisitt x-argument:

>>> a = A (2) >>> print (a.foo ()) 6 a = A () print (a.foo ()) 15 

Tilpassede operatører

Med Python kan du bruke egendefinerte operatører til klassene dine for bedre syntaks. Det finnes spesielle metoder som kalles "dunder" -metoder. "Dunder" betyr "dobbelt understrek". Disse metodene som "__eq__", "__gt__" og "__or__" lar deg bruke operatører som "==", ">" og "|" med klassens forekomster (objekter). La oss se hvordan de jobber med A-klassen.

Hvis du prøver å sammenligne to forskjellige forekomster av A til hverandre, vil resultatet alltid være False uavhengig av verdien av x:

>>> print (A () == A ()) False 

Dette skyldes at Python sammenligner minnetadressene til objekter som standard. La oss si at vi vil sammenligne verdien av x. Vi kan legge til en spesiell "__eq__" operatør som tar to argumenter, "selv" og "andre", og sammenligner deres x-attributt:

 def __eq __ (selv, andre): return self.x == other.x 

La oss bekrefte:

>>> print (A () == A ()) True >>> print (A (4) == A (6)) False 

Gjennomføring av rørledningen som en Python klasse

Nå som vi har dekket grunnleggende klasser og tilpassede operatører i Python, la oss bruke den til å implementere vår rørledning. De __i det__() Konstruktøren tar tre argumenter: funksjoner, inngang og terminaler. Funksjonsargumentet er en eller flere funksjoner. Disse funksjonene er trinnene i rørledningen som opererer på inngangsdataene. 

Argumentet "input" er listen over objekter som rørledningen skal operere på. Hvert element av inngangen vil bli behandlet av alle rørledningsfunksjonene. "Terminaler" -argumentet er en liste over funksjoner, og når en av dem oppstår, vurderer rørledningen seg selv og returnerer resultatet. Terminaler er som standard bare utskriftsfunksjonen (i Python 3, er "utskrift" en funksjon). 

Merk at inne i konstruktøren er det lagt til en mystisk "Ω" til terminaler. Jeg skal forklare det neste. 

Rørledningskonstruksjonen

Her er klassen definisjonen og __i det__() konstruktør:

klassen Pipeline: def __init __ (selv, funksjoner = (), input = (), terminaler = (print)): hvis hasattr (funksjoner, '__call__'): self.functions = [funksjoner] ellers: self.functions = liste (funksjoner) self.input = input self.terminals = [Ω] + liste (terminaler) 

Python 3 støtter fullt ut Unicode i identifikasjonsnavn. Dette betyr at vi kan bruke kule symboler som "Ω" for variabel- og funksjonsnavn. Her erklærte jeg en identitetsfunksjon kalt "Ω", som fungerer som en terminalfunksjon: Ω = lambda x: x

Jeg kunne også ha brukt den tradisjonelle syntaksen:

def Ω (x): return x 

Operatørene "__or__" og "__ror__"

Her kommer kjernen i rørledningen. For å bruke "|" (rør symbol), må vi overstyre et par operatører. "|" Symbolet brukes av Python for bitvis eller heltall. I vårt tilfelle ønsker vi å tilsidesette det for å implementere kjetting av funksjoner, samt å mate innspillet i begynnelsen av rørledningen. Det er to separate operasjoner.

Operatøren "__ror__" er påkalt når den andre operand er en pipeline-forekomst så lenge den første operand ikke er. Den betrakter den første operand som inngang og lagrer den i self.input attributt, og returnerer Pipeline-forekomsten tilbake (selvet). Dette tillater kjetting av flere funksjoner senere.

def __ror __ (selv, input): self.input = input return self 

Her er et eksempel hvor __ror __ () operatør ville bli påkalt: 'hallo der' | Rørledning ()

Operatøren "__or__" er påkalt når den første operand er en rørledning (selv om den andre operand er også en rørledning). Den aksepterer operanden som en kallbar funksjon, og det hevder at "func" operanden faktisk kan kalles. 

Deretter legger den funksjonen til self.functions Tilordne og sjekke om funksjonen er en av terminalfunksjonene. Hvis det er en terminal, vurderes hele rørledningen og resultatet blir returnert. Hvis det ikke er en terminal, returneres rørledningen selv.

def __or __ (selv, func): assert (hasattr (func, '__call__')) self.functions.append (func) hvis func i self.terminals: return self.eval () return self 

Evaluering av rørledningen

Når du legger til flere og flere ikke-terminale funksjoner til rørledningen, skjer ingenting. Den faktiske evalueringen blir utsatt til eval () Metoden kalles. Dette kan skje enten ved å legge til en terminalfunksjon på rørledningen eller ved å ringe eval () direkte. 

Evalueringen består av iterating over alle funksjonene i rørledningen (inkludert terminalfunksjonen hvis det er en) og kjører dem i rekkefølge på utgangen av forrige funksjon. Den første funksjonen i rørledningen mottar et inngangselement.

def eval (self): result = [] for x i self.input: for f i self.functions: x = f (x) result.append (x) returresultat 

Bruke rørledningen effektivt

En av de beste måtene å bruke en rørledning er å bruke den på flere sett med inngang. I det følgende eksemplet er det definert en rørledning uten innganger og ingen terminalfunksjoner. Den har to funksjoner: den beryktede dobbelt funksjonen vi definerte tidligere og standarden math.floor

Deretter gir vi det tre forskjellige innganger. I innerløkken legger vi til Ω terminalfunksjon når vi påberoper den for å samle resultatene før du skriver ut dem:

p = rørledning () | dobbelt | math.floor for input i ((0.5, 1.2, 3.1), (11.5, 21.2, -6.7, 34.7), (5, 8, 10.9)): resultat = input | p | Ω utskrift (resultat) [1, 2, 6] [23, 42, -14, 69] [10, 16, 21] 

Du kan bruke skrive ut terminal-funksjonen direkte, men deretter blir hvert element skrevet ut på en annen linje:

keep_palindromes = lambda x: (p for p i x hvis p [:: - 1] == p) keep_longer_than_3 = lambda x: (p for p i x hvis len (p)> 3) p = Pipeline () | keep_palindromes | keep_longer_than_3 | liste (('aba', 'abba', 'abcdef'),) | p | skriv ut ['abba'] 

Fremtidige forbedringer

Det er noen forbedringer som kan gjøre rørledningen mer nyttig:

  • Legg til streaming, så det kan fungere på uendelige strømmer av objekter (for eksempel lesing fra filer eller nettverkshendelser).
  • Gi en evalueringsmodus der hele inngangen er gitt som et enkelt objekt for å unngå den besværlige løsningen ved å gi en samling av ett element.
  • Legg til forskjellige nyttige rørledningsfunksjoner.

Konklusjon

Python er et veldig uttrykksfulle språk og er godt rustet til å designe din egen datastruktur og tilpassede typer. Evnen til å overstyre standardoperatører er svært kraftig når semantikkene gir seg til en slik notasjon. For eksempel er rørsymbolet ("|") veldig naturlig for en rørledning. 

Mange Python-utviklere nyter Pythons innebygde datastrukturer som tuples, lister og ordbøker. Men utforming og implementering av din egen datastruktur kan gjøre systemet enklere og enklere å jobbe med ved å heve nivået av abstraksjon og skjule interne detaljer fra brukere. Gi det et forsøk.