Hoe u uw eigen gegevensstructuur in Python implementeert

Python biedt volwaardige ondersteuning voor het implementeren van uw eigen gegevensstructuur met behulp van klassen en aangepaste operators. In deze zelfstudie implementeert u een aangepaste pijplijngegevensstructuur die willekeurige bewerkingen op de gegevens kan uitvoeren. We zullen Python 3 gebruiken.

De pijpleidingdatastructuur

De gegevensstructuur van de pipeline is interessant omdat deze erg flexibel is. Het bestaat uit een lijst met willekeurige functies die op een verzameling objecten kunnen worden toegepast en een lijst met resultaten produceren. Ik zal profiteren van de uitbreidbaarheid van Python en het pipe-karakter ("|") gebruiken om de pijplijn te construeren.

Live voorbeeld

Voordat we in alle details duiken, laten we een zeer eenvoudige pijplijn in actie zien:

x = bereik (5) | Pipeline () | dubbel | Ω afdrukken (x) [0, 2, 4, 6, 8] 

Wat is hier aan de hand? Laten we het stap voor stap afbreken. Het eerste element bereik (5) maakt een lijst met gehele getallen [0, 1, 2, 3, 4]. De gehele getallen worden ingevoerd in een lege pijplijn aangeduid met Pijpleiding(). Vervolgens wordt een "dubbele" functie toegevoegd aan de pijplijn en tenslotte de coole Ω functie beëindigt de pijplijn en zorgt ervoor dat deze zichzelf evalueert. 

De evaluatie bestaat uit het nemen van de input en het toepassen van alle functies in de pijplijn (in dit geval alleen de dubbele functie). Ten slotte slaan we het resultaat op in een variabele met de naam x en drukken we deze af.

Python-lessen

Python ondersteunt klassen en heeft een zeer geavanceerd objectgericht model met meerdere overervingen, mixins en dynamische overbelasting. Een __in het__() function dient als een constructor die nieuwe instanties maakt. Python ondersteunt ook een geavanceerd meta-programmeermodel, waar we in dit artikel niet op ingaan. 

Hier is een eenvoudige klasse met een __in het__() constructor die een optioneel argument nodig heeft X (standaard 5) en slaat het op in a self.x attribuut. Het heeft ook een foo () methode die de self.x attribuut vermenigvuldigd met 3:

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

Hier is hoe het te instantiëren met en zonder een expliciet x-argument:

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

Aangepaste operatoren

Met Python kunt u aangepaste operators voor uw klassen gebruiken voor een mooiere syntaxis. Er zijn speciale methoden bekend als "dunder" -methoden. De "dunder" betekent "dubbele onderstrepingsteken". Deze methoden zoals "__eq__", "__gt__" en "__or__" stellen je in staat om operatoren zoals "==", ">" en "|" te gebruiken met uw klasseninstanties (objecten). Laten we eens kijken hoe ze werken met de klasse A.

Als u twee verschillende exemplaren van A met elkaar probeert te vergelijken, is het resultaat altijd Onwaar, ongeacht de waarde van x:

>>> afdrukken (A () == A ()) Fout 

Dit komt omdat Python de geheugenadressen van objecten standaard vergelijkt. Laten we zeggen dat we de waarde van x willen vergelijken. We kunnen een speciale "__eq__" -operator toevoegen die twee argumenten "zelf" en "ander" gebruikt en hun x-kenmerk vergelijkt:

 def __eq __ (zelf, ander): return self.x == other.x 

Laten we het verifiëren:

>>> afdrukken (A () == A ()) Waar >>> afdrukken (A (4) == A (6)) False 

De pipeline implementeren als een Python-klasse

Nu we de basisbeginselen van klassen en aangepaste operatoren in Python hebben behandeld, laten we deze gebruiken om onze pijplijn te implementeren. De __in het__() constructor neemt drie argumenten: functies, invoer en terminals. Het argument "functions" is een of meer functies. Deze functies zijn de fasen in de pijplijn die werken op de invoergegevens. 

Het argument "invoer" is de lijst met objecten waarop de pijplijn zal werken. Elk item van de invoer wordt verwerkt door alle pipelinefuncties. Het argument "terminals" is een lijst met functies en wanneer een ervan wordt gevonden, evalueert de pipeline zichzelf en retourneert het resultaat. De terminals zijn standaard alleen de printfunctie (in Python 3 is "print" een functie). 

Merk op dat in de constructor een mysterieuze "Ω" aan de klemmen wordt toegevoegd. Ik zal dat hierna uitleggen. 

The Pipeline Constructor

Hier is de klassendefinitie en de __in het__() constructor:

class Pipeline: def __init __ (self, functions = (), input = (), terminals = (print,)): if hasattr (functions, '__call__'): self.functions = [functions] else: self.functions = lijst (functies) self.input = invoer self.terminals = [Ω] + lijst (terminals) 

Python 3 ondersteunt Unicode volledig in identificatienamen. Dit betekent dat we koele symbolen zoals "Ω" kunnen gebruiken voor variabelen- en functienamen. Hier heb ik een identiteitsfunctie met de naam "Ω" opgegeven, die als een terminalfunctie dient: Ω = lambda x: x

Ik had ook de traditionele syntaxis kunnen gebruiken:

def Ω (x): retourneer x 

De "__or__" en "__ror__" Operators

Hier komt de kern van de Pipeline-klasse. Om de "|" te gebruiken (pijpsymbool), moeten we een aantal operators overschrijven. De "|" symbool wordt door Python voor bitsgewijs of van gehele getallen gebruikt. In ons geval willen we het overschrijven om het ketenen van functies te implementeren en de invoer aan het begin van de pijplijn te voeden. Dat zijn twee afzonderlijke operaties.

De operator "__ror__" wordt aangeroepen wanneer de tweede operand een Pipeline-instantie is zolang de eerste operand dat niet is. Het beschouwt de eerste operand als de invoer en slaat deze op in de self.input attribuut en stuurt de Pipeline-instantie terug (het zelf). Hierdoor kunnen later meer functies worden gekoppeld.

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

Hier is een voorbeeld waar de __ror __ () operator zou worden ingeroepen: 'Hallo daar' | Pijpleiding()

De operator "__or__" wordt aangeroepen wanneer de eerste operand een pipeline is (zelfs als de tweede operand ook een pipeline is). Het accepteert de operand als een opvraagbare functie en het beweert dat de "func" -operand inderdaad opvraagbaar is. 

Vervolgens wordt de functie toegevoegd aan de self.functions attribuut en controleert of de functie een van de terminalfuncties is. Als het een terminal is, wordt de hele pijplijn geëvalueerd en wordt het resultaat geretourneerd. Als het geen terminal is, wordt de pijplijn zelf geretourneerd.

def __or __ (self, func): assert (hasattr (func, '__call__')) self.functionctions.append (func) if func in self.terminals: return self.eval () retour zelf 

Evaluatie van de pijplijn

Naarmate u meer en meer niet-terminalfuncties aan de pijplijn toevoegt, gebeurt er niets. De eigenlijke evaluatie wordt uitgesteld tot de eval () methode wordt genoemd. Dit kan gebeuren door een eindfunctie toe te voegen aan de pijplijn of door te bellen eval () direct. 

De evaluatie bestaat uit het itereren over alle functies in de pijplijn (inclusief de terminalfunctie als die er is) en ze in volgorde uitvoeren op de uitvoer van de vorige functie. De eerste functie in de pijplijn ontvangt een invoerelement.

def eval (self): result = [] voor x in self.input: voor f in self.functions: x = f (x) result.append (x) retourresultaat 

Pijpleiding effectief gebruiken

Een van de beste manieren om een ​​pijplijn te gebruiken, is deze toe te passen op meerdere invoerreeksen. In het volgende voorbeeld is een pijplijn zonder ingangen en geen terminalfuncties gedefinieerd. Het heeft twee functies: de beruchte dubbele functie die we eerder hebben gedefinieerd en de standaard math.floor

Vervolgens bieden we drie verschillende ingangen. In de binnenste lus voegen we de Ω terminalfunctie wanneer we deze gebruiken om de resultaten te verzamelen voordat ze worden afgedrukt:

p = Pipeline () | dubbel | math.floor voor invoer in ((0.5, 1.2, 3.1), (11.5, 21.2, -6.7, 34.7), (5, 8, 10.9)): result = input | p | Ω afdruk (resultaat) [1, 2, 6] [23, 42, -14, 69] [10, 16, 21] 

Je zou de kunnen gebruiken afdrukken terminalfunctie direct, maar dan zal elk item op een andere regel worden afgedrukt:

keep_palindromes = lambda x: (p voor p in x als p [:: - 1] == p) keep_longer_than_3 = lambda x: (p voor p in x als len (p)> 3) p = Pipeline () | keep_palindromen | keep_longer_than_3 | list (('aba', 'abba', 'abcdef'),) | p | print ['abba'] 

Toekomstige verbeteringen

Er zijn enkele verbeteringen die de pipeline nuttiger kunnen maken:

  • Voeg streaming toe zodat het kan werken op oneindige stromen van objecten (bijvoorbeeld lezen van bestanden of netwerkgebeurtenissen).
  • Geef een evaluatiemodus op waarbij de volledige invoer als een enkel object wordt aangeboden om de omslachtige oplossing voor het verzamelen van één item te voorkomen.
  • Voeg verschillende nuttige pipeline-functies toe.

Conclusie

Python is een zeer expressieve taal en is goed uitgerust voor het ontwerpen van uw eigen gegevensstructuur en aangepaste typen. Het vermogen om standaard operatoren te negeren is zeer krachtig wanneer de semantiek zich leent voor een dergelijke notatie. Het pijpsymbool ("|") is bijvoorbeeld heel natuurlijk voor een pijplijn. 

Veel van de Python-ontwikkelaars genieten van de ingebouwde datastructuren van Python zoals tuples, lijsten en woordenboeken. Het ontwerpen en implementeren van uw eigen gegevensstructuur kan uw systeem echter eenvoudiger en gemakkelijker maken om ermee te werken door het abstractieniveau te verhogen en de interne details van gebruikers te verbergen. Probeer het eens.