Python vs. PHP: Miten koodi tiivistyy

Thursday, November 19th 2009 at 23:32 in Technology, Python

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.

21 Comments
Tommi Forsström 20.11.2009 08:51:36

Hehe, hyvä haasto ja paljon erittäin valideja pointteja. En nyt lähde aamutuimaan näihin tarkemmin pureutumaan, mutta ei tuo PHP-koodisi kyllä ihan sieltä optimaalisimmasta päästä ole.

Lisäksi, kuten siellä edellisessä kommenttiketjussa totesimme, kyse on paikoin makuasioista. Esimerkiksi tuossa list comprehension kohdassa, tuo sama PHP stilisoituna olisi mielestäni luettavampi kuin väkisin yhdelle riville tungettu setti.

Minua ainakin ahdistaa nykyään suunnattomasti tuollaiset useita eri sulkuvariantteja sisältävät yhden rivin turbolauseet. Niistä tulee vähän sama fiilis, kuin lukisi tekstiä, jota ei ole osattu jakaa kappaleisiin.

Jussi Vaihia 20.11.2009 11:41:30

Tässä testaamattomia toteutuksia PHP:lle:

  1. array(array_reduce(function($x,$y){ return $x+$y}, array_map(function($article){ return ($article->published)?$a:null; }), $articles)));

  2. function createArticle($fields) { $fields = array_merge(array('title', 'body'=>'', ...), $fields);

  3. PHP:llä vastaava toteutetaan "magic methodsien" avulla.

Ensimmäisestä py-esimerkistä puuttunee [... if article.published] sekä str-muunnos.

Kennu 20.11.2009 15:16:10

@Jussi: Totta, Python-esimerkistä jäi se ehto pois vahingossa. Tuo sinun PHP-esimerkki käyttää anonyymejä funktioita, joten se toimii vasta PHP 5.3:ssa. Saa nähdä, miten yleiseksi tuon tyyppinen ohjelmointi muodostuu. Minusta se ei ole ihan yhtä selkeää kuin Pythonin comprehensionit. (List comprehensionit ovat aika yleinen ohjelmointisyntaksi funktionaalisissa kielissä, kun taas PHP:ssä tuo on jälleen kerran kikkailua array_xxx-funktioilla.)

Kakkoskohtasi ei ilmeisesti ota siihen kantaa, että Pythonin keyword-argumentit ovat tosiaan monessa suhteessa kätevämpiä ja selkeämpiä kuin assosiatiivisten arrayden kanssa kikkailu.

Kolmoskohdasta herää kysymys, voiko PHP:llä ihan oikeasti toteuttaa vastaavan "deskriptoriluokan", joka proxyaa toisen luokan attribuutit täydellisesti? Onko se helppoa ja luotettavaa, ja jos on, miksei sitä käytetä enemmän?

Ossi 20.11.2009 16:30:10

Selvyyden puolesta PHP-esimerkkikoodisi on kyllä tavattoman paljon helmpi ymmärtää, kuin tuo set(reduce... Pythonissa.

Miten se kuuluisi sisentää, jos siitä haluaisi ymmärrettävän? Seuraavastiko:

set( reduce( lambda x,y: x+y, [ [ c.lower() for c in article.categories ] for article in articles if article.published ] ) )

?

Kennu 20.11.2009 16:31:11

@Tommi: "Eri sulkuvariantteja sisältävät turbolauseet" -- Olen juuri lueskellut David Pollakin Beginning Scala -kirjaa, jonka ensimmäiset lauseet ovat:

"Ouch! That hurts my brain! Stop making me think differently. Oh wait.. it hurts less now. I get it. This different way of solving the problem has some benefits."

Itseänikin vielä sattuu vähän aivoihin funktionaalinen ohjelmointi, mutta siitä tuntuu tulevan koko ajan yhä luonnollisempaa, kun ei koko ajan sorru perinteisiin for-looppeihin vaan yrittää rohkeasti tehdä asioita vähän eri tavalla.

Ossi 20.11.2009 16:32:48

Jaahah, tämä blogihärpätys strippaa spacet pois rivien aluista. Kokeillaanpa korostuksia eri tavalla:

set( reduce( lambda x,y: x+y, [ [ c.lower() for c in article.categories ] for article in articles if article.published ] __ ) )

Ossi 20.11.2009 16:35:42

Nyt kun tuon koodin näkee sisennettynä, niin seuraava kysymys onkin, miten ihmeessä nuo silmukat toimivat? "for c in article.categories" tai "for article in articles if article.published" eivät näytä sisältävän mitään tehtävää silmukan sisällä.

Miksi tuossa käytetään eri sulkuja eri tarkoituksiin? Vai käytetäänkö?

Kennu 20.11.2009 16:36:51

@Ossi: Sori spaceista, se on HTML:n ominaisuus että ne katoavat :-) Mitä noihin sisennyksiin tulee, en usko että ne auttavat sinua ymmärtämään mitä set(), reduce(), lambda ja list comprehensionit tarkoittavat. Funktionaalisesta ohjelmoinnista ei voi tehdä proseduraalista ohjelmointia lisäämällä sisennyksiä :-)

Ossi 20.11.2009 16:42:18

En kieltämättä ole harrastanut funktionaalista ohjelmointia. Mikä siinä on sitten niin hienoa? Koodin tekeminen vaikealukuiseksi?

Minusta noin yleisesti on kyllä suurempi ongelma kirjoittaa koodia, jota muut eivät ymmärrä, kuin riskeerata kirjoitusvirheet ym. kirjoittamalla liian paljon koodia. :-)

Kennu 20.11.2009 16:43:42

Ihan lyhyenä introna voisi tiivistää näin:

[article.title for article in article_list] tarkoittaa, että article_list-listasta muodostetaan uusi lista, jonka jäseniksi otetaan kunkin artikkelin title. Tässä ei siis luupata mitään koodinpätkää, vaan tuloksena syntyy ainoastaan tuo uusi lista. Tuo article.title voi olla mikä tahansa lauseke.

reduce(func(x,y), list) taas tarkoittaa, että listan jäsenille suoritetaan peräkkäin funktio func(x,y), niin että x on funktion edellinen tulos ja y on listan seuraava elementti. Omassa esimerkissäni käytin sitä yhdistämään listan sisältämät "alilistat" yhdeksi isoksi listaksi, eli [[1, 2, 3], [4, 5, 6]] => [1, 2, 3, 4, 5, 6].

Jussi Vaihia 20.11.2009 16:44:45

@Kennu Tarkoitus oli esittää vastaavanlaista PHP-koodia näyttäen ettei asiat niin työläitä PHP:lla ole. Koodia toki kertyy enemmän verrattaessa ominaisuuksiin, joita kieli ei natiivisti tue.

Kwargsit ovat näppäriä. Mitä kaikkea kolmoskohdasta jää suoralla PHP-kopiolla mäkeen?

class LazyProfileDescriptor(object) { """Class for lazy loading of request.profile.""" function construct(request) { $this->_lazy_request = $request; $this->_lazy_profile = null; function get($name) { """Load profile when descriptor is first accessed.""" if (strstr('lazy', $name)) return $this->_lazy_request->$name; if ($this->_lazy_profile and $this->_lazy_request) $this->_lazy_profile = $this->._lazy_request->user->get_profile() return $this->_lazy_profile->name;

$request->profile = new LazyProfileDescriptor($request);

Kennu 20.11.2009 16:47:12

@Ossi: Muiden koodaajien täytyy tietenkin ymmärtää funktionaalista ohjelmointia, jos sitä käytetään sovelluskehityksessä. Pythonissa tämä on oletus: se on olennainen osa kyseistä ohjelmointikieltä ja koodaajien voi olettaa osaavan nämä ominaisuudet.

Esimerkiksi PHP:ssä taas on hyvinkin validi pointti, että nuo Jussin kirjoittamat esimerkit eivät ole välttämättä kovin tuttuja monille PHP-koodaajille, ja niiden käyttäminen voi olla riskialtista. Varsinkin kun puhutaan PHP 5.3- tai PHP 6-ominaisuuksista.

Jussi Vaihia 20.11.2009 16:52:16

Kolmoskohdan PHP-kopio ei siksi kyllä venynyt nopealla hutasulla, alkp. obju meni lapsiveden mukana. Sinnepäin kuitenkin. :) Perjantai-ilta kutsuu!

Jussi Vaihia 21.11.2009 14:12:59

Päivitys eiliseen (http://pastebin.com/m2d3a8080). Metaclassit eivät ole tuttua minulle, joten arvostan suuresti jos näytät tämän ominaisuuden suloja. PHP:ssa ei ole metaclasseja, mutta laiskuutta koodiin saa kyllä call()/get() avulla esim. juuri Django tyyppisen laiskan ORM:in toteutusta varten.

Bro 23.11.2009 23:03:38

SQL:n koostaminen tolleen stringeistä on tosi virhealtista, esimerkiksi PHP:n postgres-extensio tarjoaa tähän tarkoitukseen pg_query_params()-funkkarin, jolla välttyy turhilta injektioilta. http://fi.php.net/manual/en/function.pg-query-params.php

Ja mitä nyt näihin tulee:

if (!array_key_exists('published', $fields)) $fields['published'] = true; if (!array_key_exists('body', $fields)) $fields['body'] = '';

voi ne hoitaa ihan vaan castaamalla:

settype($fields['published'], 'bool'); settype($fields['body'], 'string');

Anonyymeja funktioita on muuten PHP:ssa voinut käyttää jo versiosta 4 lähtien, http://fi.php.net/manual/en/function.create-function.php

Ton lazy loadingin voi tosiaan toteuttaa PHP:n magic-funkkareilla, mutta niiden dokumentoiminen silleen, että IDEt ymmärtäis sen on jokseenkin mahdotonta, joten itse kannatan JavaBeans-tyylisiä gettereitä ja settereitä, joilla koodi pysyy suht yhdenmukaisena kautta linjan.

PS. Vammaisen pieni tää kommentointi-textarea.

Kennu 23.11.2009 23:09:01

Pahoittelen Djangon default-kommentoinnin spartalaisuutta, tämä kaipaisi muutakin kuten "remember me" -optiota. Tosin kannattaa käyttää WebKit-pohjaista selainta, niin voi itse venyttää textareoita vapaasti :-)

Mielestäni create_functionia ei kyllä lasketa miksikään oikeaksi lambda-funktioksi, sillehän annetaan koodi stringinä eikä sitä voi kääntää etukäteen bytecodeksi jne. Juuri sellaista nihkeyttä, jonka takia PHP:llä on hankala ohjelmoida monia Pythonissa helppoja asioita ja mieluummin vaan tekee "perinteisesti".

Mikko Rantalainen 26.11.2009 12:46:01

[Ja vielä sama kommentti varmuuden vuoksi toisen kerran, kun ensimmäisen kommentin tuloksena tuli tyhjä valkoinen sivu. Epäselväksi jäi, oliko näin tarkoitus tapahtua, vai kaatuiko palvelimen softa...]

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." --Brian Kernighan

Joskus kannattaa kirjoittaa pidempää koodia, jos se on helpompaa ymmärtää.

Mielestäni näin on erityisesti ensimmäisessä tapauksessa. PHP-koodista on hyvin paljon helpompi ymmärtää, mitä ollaan tekemässä. Väittäisin, että jos PHP ja Python -versioissa olisi jokin looginen bugi, jonka seurauksena tulos olisi melkein oikein, niin ongelma olisi helpompi korjata PHP-versiossa.

Katso myös: http://stackoverflow.com/questions/1103299/help-me-understand-this-brian-kernighan-quote

Kennu 26.11.2009 13:13:19

@Mikko: Oli näemmä yhteysongelmia spämmifiltteripalveluun ja muutamia kommentteja jäi tulematta läpi, sorry.

Minun argumenttini siihen, että PHP:ssä tehtyjä bugeja on helpompi korjata kuin Pythonissa, on tämä: Python-versiossa bugeja on ylipäätään mahdollista tehdä merkittävästi vähemmän.

Toisin sanoen: Jos PHP:llä tehdään koodi, jossa on 5 potentiaalista bugikohtaa ja Pythonissa vain 1, niin mielestäni Python on pienen lisäopiskelun arvoinen.

Tavoitteenahan tulisi kuitenkin olla tehdä alusta lähtien mahdollisimman bugitonta koodia, eikä niinkään tehdä ohjelmointia ja debuggaamista vähän helpommaksi sen kustannuksella, että bugeja syntyy sitten huomattavasti enemmän. Mielestäni IT-ala on kärsinyt tästä asenteesta jo pitkään, jatkuvien softapäivitysten tulva käyttiksiin ja selaimiin osoittaa tämän.

Bloggailen myöhemmin lisää siitä, miten funktionaalinen ohjelmointi voi konkreettisesti vähentää bugien määrää sekä yksinkertaistaa ohjelmakoodia ja lisätä sen tehokkuutta.

Ossi 26.11.2009 16:10:53

Olisi todellakin kiva kuulla argumentteja tämän funktionaalisen ohjelmoinnin autuudesta. Toistaiseksi vaikuttaa siltä, että se on lähinnä korkealle vietyä abstraktiota.

Esimerkiksi tämä muodostuslause [article.title for article in article_list] on todella epäselvä. Tämä tosin saattaa olla Pythonin omaperäisestä syntaksista kiinni?

Minä saan aina näppylöitä, kun koodaajat alkavat intoilla abstraktiokerroksista ja "yleiskäyttöisistä" funktioista / widgeteistä / kirjastoista / whatever. Moiset ovat ylipäätään todella vaikeita ymmärtää kenellekään muulle, kuin alkuperäiselle koodaajalle. Ja tämä on erityisen paha ongelma jos & kun tiimissä on useita koodaajia.

Kennu 26.11.2009 17:54:25

Heitäpä Ossi esimerkki jostain selkeämmästä tavasta ilmaista käsite: "Lista tämän artikkelilistan artikkelien otsikoista"?

Minusta Pythonin syntaksi on selkeä ja helppo ymmärtää. Esim Scalassa on jo pikkasen enemmän hahmotettavaa yield-avainsanan myötä:

for (article <- articles) yield article.title

Pythonissa lauseen ympärillä oleva [ .. ] auttaa hahmottamaan, että siinä muodostetaan lista.

Ossi 27.11.2009 14:32:02

Okei... siis hakasulut lauseen itsensä ympärillä ovat merkittävä tekijä? Minä jotenkin olen tottunut sellaiseen ajatteluun, että suluilla ympäröidään silmukka- tai funktiorakenne ja että nämä alkavat vasta avainsanan jälkeen.


You can use Markdown to format your comment:

  • > quoted text
  • *italic* text
  • **bold** text
  • `code block` (multi-line is ok, whitespace is preserved)
  • [link text](http://www.google.com "link title")

Separate paragraphs in your text with two newlines