Opus Polyglotis II: Python

act1

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.

flow_python

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.

6 kommentarer til “Opus Polyglotis II: Python”

  1. Jostein Says:

    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)

  2. Torbjørn Says:

    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.

  3. Opus Polyglotis II: Ruby Says:

    [...] 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å. [...]

  4. Google History Says:

    [...] docs.python.org – litt overrasket over at denne kom så langt opp, men kodet jo litt Python for litt siden [...]

  5. Opus Polyglotis II: Clojure Says:

    [...] har presentert 3 ulike programmer som gjør det samme – i Python, Ruby og Rebol – og her kommer den aller siste implementasjonen for denne [...]

  6. Vidar Lund Says:

    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. ;)

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Alf Kåre Lefdal: Distributed Podcast er også ganske interessant. De tar opp tema som fx. ...

Stian: +1 for 6er til This Developer's Life! Min definitive favoritt. Jeg trengte også...

Torbjørn: Takk for flere tips, Vegard. Deep Fried Bytes ligger på oversikten min fra 2009...

Vegar: Og glemte helt ios: Nsbrief og ideveloper live. Har du hørt på deep fried byt...

Vegar: Mye kjekt her. TDL, hanselminutes og .net rocks ligger i en klasse for seg. Suv...

Torbjørn: Helt enig, arkivet til Software Engineering Radio er en gullgruve om man vet hva...

Einar W. Høst: Jeg synes at det kuleste med se-radio er backloggen av intervjuer... det er noen...

arnab: fantastisk :)...

Olav: Glimrende blogg ! Modellen av hjernens arbeid passer ikke bare på nyskaping: ...

Torbjørn: Ja, flydesign trekkes ofte frem som et eksempel på dette fenomenet. Design av b...

Mulig relaterte linker

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • NodeJS vs. ASP.NET
  • Pulten min..
  • No ifs and buts
  • Community-fiskebolle på ROOTS 2012
  • Kategorier

  • .net ninja (37)
  • Bøker (18)
  • Diverse prosjekter (37)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (78)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (34)
  • NDC (4)
  • NNUG / community (63)
  • O/RM & databaser (10)
  • Off topic (118)
  • OO-design/clean code (31)
  • Podcasts (15)
  • Polyglot (82)
  • Ruby (29)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (24)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (34)
  • WPF (9)
  • WTF (13)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta