Cum să implementați structura de date proprie în Python

Python oferă suport complet pentru implementarea structurii de date proprii folosind clase și operatori personalizați. În acest tutorial veți implementa o structură de date personalizată a conductei care poate efectua operații arbitrare asupra datelor sale. Vom folosi Python 3.

Structura datelor din conducte

Structura datelor din conducte este interesantă, deoarece este foarte flexibilă. Este alcătuită dintr-o listă de funcții arbitrare care poate fi aplicată unei colecții de obiecte și poate produce o listă de rezultate. Voi profita de extensibilitatea lui Python și voi folosi caracterul de țeavă ("|") pentru a construi conducta.

Exemplu live

Înainte de a scufunda în toate detaliile, să vedem o conductă foarte simplă în acțiune:

x = interval (5) Conductă () dublu | Ω print (x) [0, 2, 4, 6, 8] 

Ce se petrece aici? Să o despărțim pas cu pas. Primul element interval (5) creează o listă de numere întregi [0, 1, 2, 3, 4]. Integerii sunt alimentați într - o conductă goală desemnată de Pipeline (). Apoi se adaugă o funcție "dublă" la conducte, iar în cele din urmă se răcește Ω funcția termină conducta și îi determină să se evalueze singură. 

Evaluarea constă în luarea de intrare și aplicarea tuturor funcțiilor din conductă (în acest caz doar funcția dublă). În final, stocăm rezultatul într-o variabilă numită x și tipăream.

Clasele Python

Python susține clasele și are un model sofisticat orientat pe obiecte, incluzând mai multe moșteniri, mixuri și suprasolicitare dinamică. Un __ metodei __init () funcția servește ca un constructor care creează noi instanțe. Python susține, de asemenea, un model avansat de meta-programare, la care nu vom intra în acest articol. 

Aici este o clasă simplă care are un __ metodei __init () constructor care are un argument opțional X (implicit la 5) și o stochează într-un self.x atribut. De asemenea, are a foo () care returnează self.x atribut înmulțit cu 3:

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

Iată cum să-i instanțiăm cu și fără un argument explicit x:

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

Operatori personalizați

Cu Python, puteți utiliza operatori personalizați pentru clasele dvs. pentru o sintaxă mai frumoasă. Există metode speciale cunoscute sub numele de "dunder". "Dunder" înseamnă "dublu subliniere". Aceste metode precum "__eq__", "__gt__" și "__or__" vă permit să utilizați operatori precum "==", ">" și "|" cu instanțele de clasă (obiecte). Să vedem cum funcționează cu clasa A..

Dacă încercați să comparați două instanțe diferite ale lui A la fiecare altul, rezultatul va fi întotdeauna Fals indiferent de valoarea lui x:

>>> print (A () == A ()) Fals 

Acest lucru se datorează faptului că Python compară în mod implicit adresele de memorie ale obiectelor. Să presupunem că vrem să comparăm valoarea lui x. Putem adăuga un operator special "__eq__" care are două argumente, "self" și "other", și compară atributul lor x:

 def __eq __ (auto, altul): return self.x == other.x 

Să verificăm:

>>> print (A () == A ()) Adevărat >>> print (A (4) == A (6)) Fals 

Implementarea conductei ca o clasă Python

Acum, că am acoperit elementele de bază ale clasei și operatorii personalizați din Python, să o folosim pentru a implementa conducta noastră. __ metodei __init () constructorul are trei argumente: funcții, intrări și terminale. Argumentul "funcții" este una sau mai multe funcții. Aceste funcții sunt etapele din conducte care funcționează pe datele de intrare. 

Argumentul "input" este lista de obiecte la care va funcționa conducta. Fiecare element al intrării va fi procesat de toate funcțiile conductei. Argumentul "terminale" este o listă de funcții, iar atunci când una dintre ele este întâlnită, conducta se evaluează și returnează rezultatul. Terminalele sunt implicit doar funcția de tipărire (în Python 3, "print" este o funcție). 

Rețineți că în interiorul constructorului se adaugă un terminal "Ω" misterios la terminale. O să explic asta în continuare. 

Constructorul de conducte

Iată definiția clasei și __ metodei __init () constructor:

(de exemplu, funcții, '__call__'): self.functions = [funcții] altceva: self.functions = list (funcții) auto.input = intrări auto.terminale = [Ω] + listă (terminale) 

Python 3 suportă pe deplin Unicode în numele identificatorilor. Aceasta înseamnă că putem utiliza simboluri reci ca "Ω" pentru nume variabile și funcții. Aici, am declarat o funcție de identitate numită "Ω", care servește ca o funcție terminală: Ω = lambda x: x

Aș fi putut folosi și sintaxa tradițională:

def Ω (x): întoarcere x 

Operatorii "__or__" și "__ror__"

Aici vine nucleul clasei Pipeline. Pentru a utiliza "|" (simbolul conductei), trebuie să suprascrieți un număr de operatori. "|" simbolul este utilizat de Python pentru biți sau pentru întregi. În cazul nostru, dorim să o override pentru a implementa înlănțuirea funcțiilor, precum și alimentarea intrărilor la începutul conductei. Acestea sunt două operațiuni separate.

Operatorul "__ror__" este invocat atunci când cel de-al doilea operand este o instanță Pipeline atâta timp cât primul operand nu este. Consideră primul operand drept input și îl stochează în self.input atributul și returnează instanța conductei înapoi (auto). Aceasta permite legarea mai multor funcții mai târziu.

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

Iată un exemplu în care __ror __ () operatorul va fi invocat: "salut acolo" Pipeline ()

Operatorul "__or__" este invocat atunci când primul operand este un conducte (chiar dacă al doilea operand este de asemenea un conducte). Acceptă ca operandul să fie o funcție care poate fi apelat și afirmă că operandul "func" este într-adevăr callabil. 

Apoi, el adaugă funcția la self.functions atribuie și verifică dacă funcția este una dintre funcțiile terminalului. Dacă este un terminal, atunci întregul conducte este evaluat și rezultatul este returnat. Dacă nu este un terminal, conducta însăși este returnată.

(func, '__call__')) self.functions.append (func) dacă func în self.terminals: return self.eval () return self 

Evaluarea conductei

Pe măsură ce adăugați din ce în ce mai multe funcții non-terminale la conducte, nu se întâmplă nimic. Evaluarea efectivă este amânată până la data de eval () se numește metoda. Acest lucru se poate întâmpla fie prin adăugarea unei funcții terminale la conductă, fie prin apelarea eval () direct. 

Evaluarea constă în iterarea peste toate funcțiile din conductă (inclusiv funcția terminalului dacă există una) și executarea acestora în ordinea ieșirii funcției anterioare. Prima funcție din conductă primește un element de intrare.

Definiți valoarea eval (self): result = [] pentru x în auto.input: pentru f în self.functions: x = f (x) result.append (x) 

Utilizarea eficientă a conductei

Una dintre cele mai bune moduri de a utiliza o conductă este să o aplicați mai multor seturi de intrări. În următorul exemplu, este definită o conductă fără intrări și fără funcții terminale. Are două funcții: infamul dubla funcția pe care am definit-o mai devreme și standardul Math.floor

Apoi, oferim trei intrări diferite. În bucla interioară, adăugăm Ω terminale atunci când o invocăm pentru a colecta rezultatele înainte de a le tipări:

p = conductă () dublu | math.floor pentru intrare în ((0.5, 1.2, 3.1), (11.5, 21.2, -6.7, 34.7), (5, 8, 10.9)): rezultat = intrări | p | Ω print (rezultat) [1, 2, 6] [23, 42, -14, 69] [10, 16, 21] 

Ai putea folosi imprimare terminale, dar apoi fiecare element va fi imprimat pe o linie diferită:

(p pentru p în x dacă p (p) = 3) p = conducte () | p = p (x) | păstrați_palindromuri | keep_longer_than_3 | listă ("aba", "abba", "abcdef"), p | print ['abba'] 

Îmbunătățiri viitoare

Există câteva îmbunătățiri care pot face conducerea mai utilă:

  • Adăugați streaming pentru a putea funcționa pe fluxuri infinite de obiecte (de ex., Citirea din fișiere sau evenimente din rețea).
  • Furnizați un mod de evaluare în care întreaga intrare este furnizată ca un singur obiect pentru a evita soluția greoaie de a furniza o colecție de un singur element.
  • Adăugați diferite funcții de conducte utile.

Concluzie

Python este un limbaj foarte expresiv și este bine echipat pentru a vă proiecta propria structură de date și tipuri personalizate. Abilitatea de a suprascrie operatorii standard este foarte puternică atunci când semantica se pretează la o astfel de notație. De exemplu, simbolul țevii ("|") este foarte natural pentru o conductă. 

Mulți dezvoltatori de Python se bucură de structuri de date construite de Python, cum ar fi tupluri, liste și dicționare. Cu toate acestea, proiectarea și implementarea structurii de date proprii poate face sistemul dvs. mai simplu și mai ușor de utilizat prin ridicarea nivelului de abstractizare și ascunderea detaliilor interne de la utilizatori. Incearca.

Cod