Hlavní navigace

Roury mezi objekty v Pythonu

27. 1. 2012 9:28 (aktualizováno) Pavel Císař

Líbí se mi koncept specializovaných nástrojů propojovaných rourou, a tak jsem přemýšlel, jak bych ho dostal i do PowerConsole. Včera večer jsem nastřelil jednoduchý koncept, jak v Pythonu pospojovat objekty pomocí „roury“ s využitím přetěžování operátorů.

Výchozí myšlenkou bylo použití co nejjednoduššího zápisu co nejvíce podobného zápisu roury v shellu, tedy: A | B | C atd. Protože znak „|“ slouží v Pythonu pro zápis operace OR, lze využít možnosti redefinovat implementaci tohoto operátoru na přenos dat mezi objekty. A protože se jedná o operaci OR, je vyhodnocování prováděno postupně hezky zleva doprava, tak jako u propojování příkazů rourou v shellu. Zbývá už jen dořešit, jak dostat prvotní nebo zpracovaná dat z objektu. Protože se bude jednat v podstatě o sekvenci nějakých dat (řádky textu, sled objektů, cokoliv), nabízí se jako nejjednodušší řešení použití iteračního protokolu.

Následující třída demonstruje možnou implementaci.

 class Pipe(object):
Data = None
def __init__(self,rows=0):
self.rows = rows
def next(self):
if self.__row < self.rows:
self.__row += 1
return "(%i) Data from (%s)" % (self.__row,self.__class__.__name__)
else:
raise StopIteration()
def processRecord(self,record):
if self.Data is None:
self.Data = []
self.Data.append("(%s) Processed %s" % (self.__class__.__name__,record))
def __or__(self,other):
for data in self:
other.processRecord(data)
return other
def __iter__(self):
if self.Data is None:
self.__row = 0
return self
else:
return (x for x in self.Data)

Pro testování si nadefinujeme i tři jednoduché potomky, kteří budou představovat elementární příkazy (jako je ls, sort, awk, re apod.):

 class A(Pipe):
pass

class B(Pipe):
pass

class C(Pipe):
pass

Pokud si vše výše uvedené uložíte do souboru, např. s názvem pipe.py, pak v shellu Pythonu stačí zadat

 from pipe import *

a máte k dispozici tři elementární příkazy: A(), B() a C(). Zápis závorek je bohužel nezbytný, ale s tím se dá žít. Nyní můžete použít konstrukci jako

 A(rows=5) | B() | C()

která vygeneruje 5 řádek dat pomocí A, pošle je ke zpracování do B, která je pošle ke zpracování do C, a výsledek zpřístupní jako instanci třídy C. Odtud je můžete dostat pomocí iterace, např. příkazem:

 for x in A(rows=5) | B() | C():
prin x

Jak to funguje? Parametry zpracování definujete přímo pro konstruktor __init__. Pro tuto jednoduchou ukázku je definován pouze parametr rows, který slouží k vygenerování zkušebních dat. V reálu by to bylo spíše něco jako:

 class ls(Pipe):
def __init__(self,path='.',full=False,attr='XYZ'):
self.path = path
self.full = full
self.attr = attr

Metoda __init__ žádné zpracování dat neprovádí, protože zatím ještě není jasné, co a jak se bude dělat. Slouží pouze pro definici akceptovatelných parametrů příkazu a jejich uložení pro pozdější použití.

Pro získání dat dle parametrů slouží metoda next. Ta je prováděna tehdy, pokud je příkaz požádán o data (voláním metody __iter__), a žádná data zatím nemá (self.Data = None).

Pro zpracování dat přijatých z roury slouží metoda processRecord. Ta je automaticky volána pro každý blok dat na vstupu, a jejím úkolem je provést zpracování dat dle parametrů předaných __init__, a uložení dat do seznamu self.Data.

Pro předání dat dalšímu objektu slouží metoda __or__. Ta využívá iterace objektu přes sebe sama k získání dat a jejich postupnému předání dalšímu objektu.

Metoda __iter__ vrací iterátor na data objektu. Pokud objekt data zatím nemá, vrací odkaz na samotný objekt, takže je volána metoda next; jinak vrací generátor který předá zpracovaná data uložená v self.Data.

Třída Pipe obsahuje pomocný kód pro potřeby prototypu, a který v konečné třídě není potřeba. Pokud se vám tento koncept líbí, a chcete ho použít pro vlastní objekty, použijte spíše následující „holou“ verzi třídy Pipe.

 class Pipe(object):
Data = None
def next(self):
raise StopIteration()
def processRecord(self,record):
if self.Data is None:
self.Data = []
self.Data.append(record)
def __or__(self,other):
for data in self:
other.processRecord(data)
return other
def __iter__(self):
if self.Data is None:
return self
else:
return (x for x in self.Data)

V potomcích pak stačí definovat __init__ a přepsat next a processRecord dle vašich potřeb.

Sdílet