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 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.
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 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
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
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.
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
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
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
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']
Er zijn enkele verbeteringen die de pipeline nuttiger kunnen maken:
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.