tirsdag 21. februar 2012 Polyglot Python
OPUS POLYGLOTIS II, Act 1
Tagline: "En enkel men litt naiv begynnelse"
På scenen: Python
I første versjon av programmet mitt har jeg laget en litt naiv og "rett frem" implementasjon. Det fungerer, men har litt for lite struktur. Dette vil jeg forbedre i de kommende blogpostene, men da i andre programmeringsspråk.
PS: Gikk du glipp av innledningen til denne bloggserien? Da bør du lese den først for å få nødvendig bakgrunnsinformasjon.
Det er egenlig ganske passende at jeg har valgt Python for den første implementasjonen. Jeg har relativt lite erfaring med Python, og det er et språk som anbefales for nybegynnere. Det er fleksibelt og egner seg også bra til funksjonell programmering, noe jeg benytter meg av i stor grad i koden du skal få se nå.
Så la oss hoppe rett i det...
Det første jeg må gjøre er å importere de modulene jeg kommer til å benytte i koden:
21 import sys # Needed to get command line args 22 import csv # Comma Separated Values module 23 import re # Regular expression module
Og så setter jeg igang med det første output-formatet, som er JSON. Implementasjonen består av funksjonen formatJson, som har to parametre – et array of arrays som inneholder alle dataradene, og et array som inneholder kolonnenavnene. I tillegg trenger jeg to støttefunksjoner og en konstant. Ved hjelp av en del map og join er det ganske enkelt å bygge opp JSON-strengen, men det skjer mye på få linjer kode her, så hold tunga rett i munnen:
27 # Pre-compile regular expressions 28 NUMBER_PATTERN = re.compile(r"\d+.?\d*$") 29 30 def formatJsonValue(v): 31 if NUMBER_PATTERN.match(v): 32 return v 33 return '"%s"' % (v) 34 35 def formatJsonObject(values): 36 fields = map( 37 lambda h, t: '"%s":%s' % (h, t), 38 formatJsonObject.headers, 39 map(formatJsonValue, values)) 40 return "{%s}" % (', '.join(fields)) 41 42 def formatJson(records, headers): 43 # Passing headers via function attribute 44 # (functions are objects you see!) 45 formatJsonObject.headers = headers 46 return "[%s]" % ',\n'.join( 47 map(formatJsonObject, records))
Er du ikke så vandt med funksjoner som map bør du studere denne koden til du har overbevist deg selv om at den genererer det riktige resultatet. Map er noe du finner i de fleste programmeringsspråk i dag, og er veldig nyttig å beherske. Er du ukomfortabel med regulære uttrykk bør du også studere hvordan jeg lager og bruker konstanten NUMBER_PATTERN. Uttrykket sier at et tall består av en eller flere siffer, etterfulgt av null eller ett punktum, etterflulgt av null eller flere siffer.
Når man programmerer forsøker man hele tiden å visualisere for seg selv hvordan data flyter og endrer seg gjennom koden. I figuren under har jeg gjort et best effort på å vise hvordan formatJson fungerer.
Vi fortsetter så med XML-formateringen, som jeg har splittet opp i to funksjoner. Igjen bruker jeg endel map og join, og putter inn endel strategisk plasserte linjeskift og tabulatorer i strengene jeg bygger opp for å formatere XML'en på en fornuftig måte:
51 def formatXmlRecord(values): 52 fields = map( 53 lambda h, t: 54 "<" + h + ">" + t + "</" + h + ">", 55 formatXmlRecord.headers, 56 values) 57 return "\t<record>\n\t\t%s\n\t</record>" % \ 58 ('\n\t\t'.join(fields)) 59 60 def formatXml(records, headers): 61 formatXmlRecord.headers = \ 62 map(lambda h: h.replace(' ', '_'), headers) 63 return "<records>\n%s\n</records>" % '\n'.join( 64 map(formatXmlRecord, records))
Det finnes selvsagt biblotek for generering av både JSON og XML, men da hadde jeg ikke fått vist så mye strenghåndtering som jeg har lyst til. Jeg holder meg til basisbiblotekene, og mekker mine egne formater!
Nå er altså formatene ferdige, og jeg begynner på selve "programmet". Siden jeg bruker Python, og ikke tenker så mye på struktur i denne første implementasjonen, så skriver jeg koden for å lese kommandolinjeargumenter, lese csv-filen, og utføre formateringen, direkte i toppnivået i filen. Jeg pakker det altså ikke inn i en egen main-funksjon.
Her leser jeg de to argumentene, og åpner csv-filen for lesing:
68 outFormat = sys.argv[1] 69 dataFile = open(sys.argv[2], 'r')
Så tar jeg i bruk Python's CSV-modul til å opprette et objekt som kan tolke CSV-filen. Denne modulen sparer meg for ganske mye arbeid:
71 csvReader = csv.reader(dataFile, 72 delimiter=';', 73 quotechar="\"")
Og da gjenstår det bare å velge en formateringsfunksjon basert på outFormat, og så kalle denne. Det første jeg tenkte å bruke var en switch, men så viste det seg at Python overraskende nok ikke har noen slik struktur! I stedet bruker man ofte det man kaller table dispatch basert på en dictionary. De siste linjene i programmet mitt definerer dispatch-tabellen, henter ut den riktige funksjone, kaller den, og skriver resultatet til konsollet:
78 print { 79 'json': formatJson, 80 'xml': formatXml, 81 'yaml': lambda a, b: "YAML support coming soon!" 82 }[outFormat.lower()](csvReader, 83 csvReader.next())
Legg merke til at jeg også la til en dispatch for YAML-formatet – et lambda-uttrykk som bare returnerer "YAML support coming soon!".
Løsningen er enkel, og egentlig ganske ryddig. 60 linjer inkludert noen kommentarer (40 LOC) er mindre enn jeg i utgangspunktet trodde jeg ville bruke.
Men koden har noen svakheter. Programmet skulle legge til rette for å kunne utvides med flere ut-formater, men mangler struktur for å unngå at dette blir rotete og etterhvert uoversiktelig. I tillegg bryter jeg det såkalte open/closed-prinsippet, som i praksis sier at jeg burde kunne gjøre de forventede utvidelsene uten å måtte modifisere eksisterende kode. For jeg kan jo ikke legge til flere formater uten å modifisere dispatch-koden min.
Allerede i den kommende bloggposten vil jeg utbedre disse problemene gjennom å ta i bruk noen kjente design patterns fra objektorientert programmering.