Category: Python
Linkkien esikatselu Djangossa
Leikin hieman python-webkit2png:llä ja tein kotisivujeni Shared Links -osioon esikatselun. Niistä linkeistä, joille esikatselukuva on generoitu, pitäisi kyseisen kuvan tulla näkyviin, kun vie hiiren linkin päälle.
Python-webkit2png käyttää Qt-kirjastoa ja WebKit-moottoria esikatselukuvan renderöintiin. Omat asetukseni generoivat kuvia 800x600 pikselin kokoisella virtuaaliselaimella, jossa ei ole Flashia eikä JavaScriptiä. Sen jälkeen kuvat pienennetään kokoon 400x300, mikä tekee niistä tällä hetkellä hieman kökön näköisiä.
Djangossa tämä operaatio ajetaan cronissa management commandina. Se käy läpi ne linkit, joilla ei vielä ole esikatselukuvaa, ja tallentaa uudet kuvat kunkin linkin ImageField-kenttään. Joillekin linkeille tämä epäonnistuu, koska asetin renderöinnin maksimiajaksi 60 sekuntia, ja maailmalta löytyy edelleen kovin raskaita saitteja, jotka eivät siinä ajassa kerkeä latautua kokonaan.
Python vs. PHP: Miten koodi tiivistyy
Olen käynyt aiemmassa blogikirjoituksessa keskustelua PHP:n ja Pythonin eroista koodin tiiviyden suhteen. Siellä heittämäni esimerkit eivät olleet parhaita mahdollisia, joten ajattelin kirjoittaa tähän erikseen muutamista eroista, joiden uskon oikeasti tekevän Pythonista PHP:tä paremman kielen. Kirjoitan otsikot englanniksi, kun en oikein tunne näiden käsitteiden suomenkielisiä nimiä.
List comprehensionit
Web-sovellusohjelmoinnissa on aika yleinen tilanne luupata läpi taulukko, suorittaa taulukon jokaiselle jäsenelle jokin operaatio, ja tallentaa tulokset uuteen taulukkoon. PHP:ssä voisi ottaa esimerkin, jossa artikkeleista kerätään lista niiden kategorioista pieninä kirjaimina:
$categories = array(); foreach ($articles as $article) { if ($article->published) { foreach ($article->categories as $category) { $categories[] = strtolower($category)]; } } } $categories = array_unique($categories);
Pythonissa list comprehensioneita käyttäen yhdellä koodirivillä esimerkiksi näin [Puuttuva ehto ja lower() lisätty]:
set(reduce(lambda x,y: x+y, [[c.lower() for c in article.categories] for article in articles if article.published]))
Itseäni tässä Python-versiossa viehättää eniten se, että rakenne on funktionaalinen eikä imperatiivinen. Tietorakenteita ei tökitä ja sörkitä vaan ne muodostetaan tietyllä säännöllä. Tämä tarkoittaa, että koodista poistuu useita potentiaalisia bugiriskejä taulukkojen alustamisessa, luuppaamisessa ja muuttamisessa. Mitä enemmän koodia, sitä helpommin näihin tulee kirjoitusvirheitä tai muutetaan vahingossa väärää taulukkoa.
Tommi F. varmaan näyttää, miten tuo esimerkki tehdään PHP:llä oikein? ;-)
Keyword-argumentit
Pythonin keyword-argumentit ovat parhaimmillaan kun suunnitellaan API-rajapintoja, joissa pitää välittää iso määrä erilaisia datakenttiä, joista osa on vapaaehtoisia. PHP:ssä tämä pitää usein tehdä assosiatiivisilla taulukoilla tähän tapaan:
function createArticle($fields) { if (!array_key_exists('published', $fields)) $fields['published'] = true; if (!array_key_exists('body', $fields)) $fields['body'] = ''; if (!array_key_exists('author', $fields)) $fields['author'] = ''; INSERT INTO ... $fields['published'], $fields['title'], $fields['body'], $fields['author'] } createArticle(array('title' => 'Otsikko', 'author' => 'kennu'));
Pythonissa rajapinnasta saadaan paljon selkeämmin määritelty:
def create_article(title, body='', published=True, author=''): INSERT INTO ... published, title, body, author create_article('Otsikko', author='kennu')
Keyword-argumentteja voi hyödyntää vielä monilla muillakin tavoilla. Erityisen mukavaa on, että niitä voi muokata ja välittää eteenpäin toisille funktioille:
def create_article(title, body='', published=True, author='', **kwargs): del(kwargs['unwanted_field']) handle_extra_fields(**kwargs)
Näin pystyy rakentamaan hyvin joustavia mekanismeja jatkuvasti muuttuvien tietorakenteiden käsittelyyn, ja toisaalta on helppo kutsua olemassaolevia funktioita muuttamalla argumentteja lennossa tarpeen mukaan.
Meta-propertyt
Halusin määritellä eräässä webbiprojektissa muuttujan nimeltä request.profile, joka palauttaa käyttäjän profiiliobjektin. Halusin kuitenkin, ettei profiilia ladata turhaan tietokannasta, jos kyseistä muuttujaa ei koskaan käytetä missään. Tämä tilanne toteutuu silloin, jos webbisivun kaikki ne osat tulevat välimuistista, joissa profiilia olisi tarvittu. (Sivunmuodostus on hyvin modulaarinen, osa voi tulla välimuistista ja osa ei.)
Ratkaisuni oli suurin piirtein tällainen:
class LazyProfileDescriptor(object): """Class for lazy loading of request.profile.""" def __init__(self, request): self._lazy_request = request self._lazy_profile = None def __getattribute__(self, name): """Load profile when descriptor is first accessed.""" if name.startswith('_lazy_'): return object.__getattribute__(self, name) if self._lazy_profile is None and self._lazy_request is not None: self._lazy_profile = self._lazy_request.user.get_profile() return getattr(self._lazy_profile, name) request.profile = LazyProfileDescriptor(request)
On mahdollista, että koodi vielä sievenisi tuosta, mutta tämä mielestäni kuitenkin demonstroi Pythonin meta-ohjelmoinnin tarjoamia mahdollisuuksia. Tällaisen määrittelyn jälkeen kaikkialla muualla koodissa voi huoletta kutsua request.profile-muuttujaa normaalisti, ja se latautuu sitten tietokannasta vain jos on tarvis. LazyProfileDescriptorista voisi myös melko helposti tehdä geneerisen luokan, joka osaa ladata mitä tahansa tietokannasta tarvittaessa. Latauslogiikka annettaisiin sille konstruktorissa lambda-funktiona.
Metaohjelmointia voisi käsitellä vielä laajemmin meta-classien ja deskriptorien osalta, mutta niistä alkaa olla jo vaikea keksiä lyhyitä havainnollisia esimerkkejä. Django on itsessään loistava esimerkki siitä, miten näitä ominaisuuksia hyödyntäen on luotu kirjasto, jota sovellusohjelmoijan on yksinkertaista käyttää, mutta jossa on hyvin monipuolinen toimintalogiikka taustalla.
Uusien kommenttien sähköposti-ilmoitukset
Koodasin tähän Django-pohjaiseen blogisysteemiini tuen sähköposti-ilmoituksille uusista kommenteista. Testailen niitä tämän artikkelin kommenteissa.
Pythonin and-or-lausekkeet
[UPDATE: Katso kommenteista parempi tapa tehdä tämä.]
Tämä on uusi lempilausekkeeni Pythonissa:
result = condition and option1 or option2 # Esim: name = user.is_authenticated() and user.username or u'anonymous'
Tuo vastaa täydellisesti C:stä ja Javasta tuttua ?:-ehtolauseketta:
name = user.is_authenticated() ? user.username : "anonymous";
Nyt ei tarvitse enää pitkiä if-else-lauseita, kun Pythonin boolean-logiikka hoitaa homman noin.
Varnish käytössä blogissani
Nopeutin blogini toimintaa hiukan ottamalla käyttöön Varnishin. Se on HTTP-reverse-proxy, joka pitää webbisivuja välimuistissa ja palvelee niitä tehokkaammin kuin Apache ja Django pystyvät niitä tuottamaan. Pikaisesti testattuna tämä vaatimaton virtuaalikoneeni pystyy palvelemaan nyt noin 400 hakupyyntöä sekunnissa, kun se ilman Varnishia jäi alle sadan.
Varnish on siitä mukava, että se osaa cachettaa sivuja, vaikkei niissä olisi "virallisia" Cache-Control-headereita. VCL-kielellä voi rakentaa oman logiikan sille, mitä cachetetaan ja mitä ei. Oletuksena sellaiset sivut jätetään pois cachesta, joissa on käytetty cookieita tai authorization-headereita, jotta käyttäjäkohtainen data pysyisi erillään.
Varnishissa on myös näpsäkkä CLI-rajapinta, jolla voi esimerkiksi tyhjentää haluttuja sivuja cachesta. Tein omaa blogiani varten tällaisen apufunktion, joka tyhjentää koko välimuistin aina silloin, kun joku kirjoittaa uuden kommentin tai itse kirjoitan uuden artikkelin:
def clear_varnish(): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 6082)) s.sendall('url.purge .*\n') s.recv(4096) s.close() except: pass
Tämä on toki aika tehotonta, jos saitilla on paljon liikennettä ja cache tyhjenee vähän väliä. Silloin on fiksumpaa käyttää jotain muuta ratkaisua.
Näin varmuuskopioit Google Docsit omalle koneelle
Olen alkanut käyttää Google Docsia yhä useampaan tarkoitukseen henkilökohtaisessa tietojenkäsittelyssä. En viitsi enää kirjoitella omia dokumenttejani ja taulukoitani Officella tai OpenOfficella tiedostoiksi, kun ne on helpompi laittaa Google Docsiin. Omat vaatimukseni ovat sen verran yksinkertaisia, että Google Docs riittää mainiosti.
Samalla on tullut tarve saada nämä dokumentit varmuuskopioitua Google Docsista omalle koneelle siltä varalta, että netti katkeaa pitemmäksi ajaksi tai Google menee vaikka konkurssiin.
Pienellä googlailulla löytyikin heti tähän tarkoitukseen työkalu nimeltä gdatacopier. Se koostuu kahdesta Python-skriptistä, joita on tarkoitus ajaa komentoriviltä:
- gls.py listaa kaikki Google Docsiin tallentamasi dokumentit
- gcp.py kopioi kaikki dokumentit Google Docsista omalle koneelle ODF-muotoon
Google Docsista on siis helppo varmuuskopioida kaikki dokumentit säännöllisesti ajastamalla gcp.py vaikka kerran päivässä pyörähtäväksi cron-jobiksi. Parametrillä -u se vieläpä kopioi ainoastaan muuttuneet tiedostot. Ainoa pieni hankaluus on salasana, joka pitää antaa ohjelmalle selkokielisenä parametrinä.
Jos sattuu olemaan Mac-käyttäjä, niin tähänkin on onneksi valmis ratkaisu. Keychain Access -työkalulla voi tallentaa salasanansa turvallisesti Mac OS X:n uumeniin. Sieltä sen voi myöhemmin kaivaa esiin komentoriviltä security-komennolla. Lopullinen backup-komentorivi saattaisi näyttää tällaiselta:
#!/bin/sh gcp.py -u -o -p \ `security find-generic-password \ -g -a mygoogleaccount \ -s GoogleDocsBackup 2>&1 1>/dev/null \ |sed -e 's/password: "\(.*\)"/\1/'` \ 'mygoogleaccount:/' \ "$HOME/Backups/gdocs"
Security-komento kysyy ensimmäisellä kerralla lupaa avata tarvittava keychain. Sille voi antaa pysyvän luvan lukea tämä yksi salasana joka kerta kyselemättä.
Jinja2 sucks with Django i18n
Until now, Jinja2 was my favorite template engine over Django 1.1's default templates. It provides more flexibility and performs much better.
However, if the website requires language translation support, Jinja2 sucks. Django has a nice automatic feature to collect all translations for a project by simply running:
django-admin.py makemessages -a
Then it's easy to edit the translation files of each language in locale/(language)/LC_MESSAGES/django.po and finally compile them with one command:
django-admin.py compilemessages
This is very simple and automatic, and everything is centralized in one file per language. But with Jinja2, it doesn't work any more, because Django's makemessages doesn't detect the Jinja2 templates. Apparently it's caused by Jinja2 choosing to use different template tags for translations than Django does.
There are some other problems with Jinja2 templates, too. They could pretty easily support most of Django's template tags, but for some reason choose not to. So you have to change {% blocktrans %} to {% trans %} and {% url proj.home.views.index %} into some custom tag you have to implement yourself, and so on. Close, but no cigar.
Jinja2 would be much more interesting if they were (even just 99%) backwards compatible with Django. That would allow an upgrade path from projects initially developed with Django templates and later needing a performance boost from Jinja2.