Opus Polyglotis II: Python
- Tuesday, February 21st, 2012
- Skriv en kommentar

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!”.
Analyse av løsningen
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.
Kategorier: Polyglot.
RSS feed for kommentarene.
Tilbaketråkk.



February 21st, 2012 at 8:38 am
Gleder meg til å følge med videre på denne serien! :)
Spørsmål: Jeg ser du bruker map og lambda en del. Hva tenker du om sånn her kode:
[h.replace(' ', '_') for h in headers]
versus:
map(lambda h: h.replace(‘ ‘, ‘_’), headers)
February 21st, 2012 at 8:50 am
Jostein: Generator expressions heter det vel i Python-verden, og er vel i praksis mye det samme som list comprehensions, som jeg har snakket om flere ganger tidligere. Min preferanse er map og lignende høyereordens funksjoner, fordi det er mer universelt mellom ulike språk. Jeg føler også det er litt mere fleksibelt, uten at jeg kan argumentere helt for det enda eller komme med noen gode eksempler. Men jeg ser at generator expressions nok er enklere å forstå for en nybegynner, og har også brukt endel comprehensions i CoffeeScript, Erlang, Clojure m.fl. Se for eksempel artiklene om Boo, Nemerle og CoffeScript fra julekalenderen 2011.
Jeg husker jeg vurderte å bruke generator expressions for å løse oppgaven, men den tanken var forsvunnet da jeg satte meg ned for å kode. Jeg tenker rett og slett i map/filter/reduce når jeg skal gjøre datamanipulasjon, og da ble det som det ble.
Jeg tror begge teknikkene er noe man bør beherske, og så må man etterhvert finne ut selv hvor man skal velge det ene over det andre. Men mitt budskap er i alle fall at map er en av de viktigste funksjonene å lære seg å bruke, om man bruker et språk som støtter høyereordens funksjoner.
February 22nd, 2012 at 2:32 pm
[...] I forrige post så du en Python-løsning som var grei nok, men den hadde et par problem. Koden manglet struktur – det var bare en rekke funksjoner i toppnivået. Og for å legge til flere formater, en endring man må kunne forvente, måtte man modifisere dispatch-koden. Det er noe man generelt bør unngå. [...]
February 29th, 2012 at 2:21 pm
[...] docs.python.org – litt overrasket over at denne kom så langt opp, men kodet jo litt Python for litt siden [...]
March 1st, 2012 at 2:49 pm
[...] har presentert 3 ulike programmer som gjør det samme – i Python, Ruby og Rebol – og her kommer den aller siste implementasjonen for denne [...]
March 2nd, 2012 at 11:04 am
Leste akkurat din post om Ruby, der du brukte ein dict for å unngå å endre på dispatch-metoden.
Python har ein metode som gir deg alle metoder i scope.
Så dispatchen din kan vere slik:
globals()['format'+outformat](csvReader, csvReader.next())
Då trenger du ikkje å endre dispatchen når du legger til nye formater. Det er mulig du må trikse litt for å få den case-insensitive dog. ;)