El Blog de Trespams

Blog personal sobre tecnologia, gestió de projectes i coses que se me passen pel cap

Multiples constructors a Python

Aprofit que vull provar la beta del notebook de l'iPython per redactar aquest petit apunt. Sí, com ho llegiu, això està escrit des d'un navegador fent servir com a editor IP[y]Notebook, un entorn que ens permet documentar, escriure code, fer gràfiques, etc i guardar-ho tot dins el que anomena notebook, de manera que es pot distribuir fàcilment el que hem fet, documentat, amb el codi, etc. Podeu trobar la darrera versió a

https://github.com/ipython/ipython

Com que el notebook ens permet escriure en format Markdown, posar codi Python i després esportar-ho a html o RestructuredText (entre d'altres formats), és ideal per articles tècnics que tenguin Python com a llenguatge de programació. De fet s'està convertint ràpidament en un medi ideal per a compartir articles i treballs en el món científic, ja que és un format obert, on el codi es visible i es pot veure fàcilment el que s'està executant, i com és Python el llenguatge de programació és bo d'entendre.

Però bé, me n'estic anant per les bardisses. Del que jo us volia parlar avui és de com amb Python podem crear fàcilment múltiples constructors per la nostra classe.

Imagina-vos la següent situació: voleu crear un objecte a partir d'una cadena de text que ben bé pot ser un json, un csv, un xml, etc. El nostre objecte tindrà molts atributs i presumiblement també es podrà fer servir encara que les dades no venguin derectament d'un d'aquests formats.

Per exemple, suposem que tenim la class

class Blogger(object):
def __init__(self, nom, llinatge, url_blog, edat, periodicitat, idioma):
self.nom = nom
self.llinatge = llinatge
self.url_blog = url_blog
self.edat = edat
self.periodicitat = periodicitat
self.idioma = idioma

def __str__(self):
return "{} {}".format(self.nom, self.llinatge)

blogger = Blogger('Antoni', 'Aloy', 'http://trespams.com', 45, 'pse', 'ca')

print (blogger)

>>Antoni Aloy

Ara suposem que enlloc de constuir l'objecte d'aquesta manera el que volem és fer-ho des d'un format xml

<blogger>                                                                                                                                                                                                                                                                          <nom>Antoni</nom>                                                                                                                                                                                                                        <llinatge>Aloy</llinatge>                                                                                                                                                                                                                  <url_blog>http://trespams.com</url_blog>                                                                                                                                                                              <edat>45</edat>                                                                                                                                                                                                                                    <periodicitat>pse</peridodicitat>                                                                                                                                                                                                <idioma>ca</idioma>                                                                                                                                                                                                                             </blogger>

Podem anar obtenint cada un dels atributs com

xml = """ <blogger> <nom>Antoni</nom> <llinatge>Aloy</llinatge> <url_blog>http://trespams.com</url_blog> <edat>45</edat> <periodicitat>pse</periodicitat> <idioma>ca</idioma> </blogger>""" 

from lxml import etree 
root = etree.fromstring(xml)
print (root.xpath('/blogger/nom')[0].text)

 >>Antoni

Així doncs el que podríem fer és anar obtenint els atributs i posar-los dins una variable i d'aquesta manera tendríem tot el que necesissitam per a construir la nostra classe. Això funcionaria, però (sí hi ha un emperò) que és mal de llegir i fa que els constructor de la nostra classe tengui molts atributs i sigui mal de manejar.

Quan ens trobam amb una situació així, és a dir, la d'una classes amb molts atributs pens que hem de donar prioritat a la claretat del codi i a l'autodocumentació. En el nostre cas el que voldríem és poder construir la nostra classe a partir de l'xml, que també ho poguem fer sense aquest xml i que tot quedi el més documentat i lligat possible.

Aquí ens trobam amb la capacitat que té Python de tenir múltiples constructors, de fet __init__ sols seria un constructor més, el constructor per defecte

class Blogger(object):
def __init__(self):
self.nom = None
self.llinatge = None
self.url_blog = None
self.edat = None
self.periodicitat = None
self.idioma = None

@classmethod
def from_xml(cls, xml_string):
obj = cls()
root = etree.fromstring(xml)
obj.nom = root.xpath('/blogger/nom')[0].text
obj.llinatge = root.xpath('/blogger/llinatge')[0].text
obj.url_blog = root.xpath('/blogger/url_blog')[0].text
obj.edat = root.xpath('/blogger/edat')[0].text
obj.periodicitat = root.xpath('/blogger/periodicitat')[0].text
obj.idioma = root.xpath('/blogger/idioma')[0].text
return obj

def __str__(self):
return "{} {}".format(self.nom, self.llinatge)

blogger = Blogger.from_xml(xml)

print(blogger)

>>Antoni Aloy

De fet d'aquesta manera podem tenir constructors per diferents formats d'entrada i també jugar amb els paràmetres obligatoris que volem que hi hagi a l'__init__. A l'exemple he fet que el constructor per defecte no tengui paràmetres per a que es vegés millor com podem fer servir un mètode de classe @classmethod però de fet podem fer tot tipus de combinacions segons ens quedi el codi més entenedor i s'adapti a les regles de negoci que volem modelar.

Per exemple podríem fer també:

class Blogger(object):
def __init__(self):
self.nom = None
self.llinatge = None
self.url_blog = None
self.edat = None
self.periodicitat = None
self.idioma = None

@classmethod
def from_xml(cls, xml_string):
obj = cls()
atributs = [i for i in obj.__dict__.keys() if i[:1] != '_']
root = etree.fromstring(xml)
for atribut in atributs:
setattr(obj, atribut, root.xpath('/blogger/{}'.format(atribut))[0].text)
return obj

def __str__(self):
return "{} {}".format(self.nom, self.llinatge)

blogger = Blogger.from_xml(xml)

print(blogger)

Antoni Aloy

No m'acaba d'agradar pel fet de que estam fent un poc de "màgia" amb l'instrospecció i a més suposam que hem definit els atributs (això no funcionaria si els definim com a slot), però si asumim que podem anar per convenció seria un mètode prou senzill, és més també podem escriure

class Blogger(object):
def __init__(self, **kwargs):
self.nom = kwargs.get('nom', None)
self.llinatge = kwargs.get('llinatge', None)
self.url_blog = kwargs.get('url_blog', None)
self.edat = kwargs.get('edat', None)
self.periodicitat = kwargs.get('periodicitat', None)
self.idioma = kwargs.get('idioma', None)

@classmethod
def from_xml(cls, xml_string):
obj = cls()
atributs = [i for i in obj.__dict__.keys() if i[:1] != '_']
root = etree.fromstring(xml)
for atribut in atributs:
setattr(obj, atribut, root.xpath('/blogger/{}'.format(atribut))[0].text)
return obj

def __str__(self):
return "{} {}".format(self.nom, self.llinatge)

blogger = Blogger.from_xml(xml)

print("From xml {}".format(blogger))

blogger = Blogger(nom='Antoni', llinatge='Aloy', edat=45)

print("From __init__ {}".format(blogger))

dades = {'nom': 'Antoni', 'llinatge': 'Aloy', 'url_blog': 'http://trespams.com', 'edat': 45, 'periodicitat': 'eps',
'idiioma': 'ca'}
blogger = Blogger(**dades)

print("From dict {}".format(blogger)) 

>>From xml Antoni Aloy >>From __init__ Antoni Aloy >>From dict Antoni Aloy

Hem aconseguit tenir dos constructors, poder passar un nombre variable de paràmetres a la incialització, ja sigui directament o bé mitjançant un diccionari (per Python és el mateix de fet com podem veure) o construir l'objecte a partir d'un XML.

La gràcia de tot això és que tot queda prou documentat, d'una ullada podem veure els atributs que espera la classe. Potser podríem fer més màgia, assignar els atributs direcctament a l'__init__ d'una manera semblant a com ho estam fent quan cream l'objecte a partir de l'xml, però en aquest cas perdem la documentació que ens dona llistar cada un dels atributs, hauríem d'anar a revisar l'entrada de l'xml o la documentació del que hem de parsejar. Mala cosa, aquesta optimització que ara ens pareix tan clara i que ens fa sentir uns putos hackers, el que farà és que d'aquí uns mesos estem cercant per la bústia de correu a veure si trobam la documentació de l'xml.

El codi ha de ser informatiu per ell mateix, ens ha de contar el que fa. En el moment que no sigui així, millor deixar-se de trucs i deixar l'ego programming de banda. Els constructor múltiples ja ens permeten estalviar-nos molta feina sense afectar a la claretat del codi, això és el que hem d'aprofitar.

Nota: Per si teniu curiositat pel format original d'aquest post ho teniu al Dropbox

blog comments powered by Disqus