Category: Python

Posted on in Technology, Python, Ruby on Rails

Moniperintään liittyy useita ongelmia, joiden vuoksi esimerkiksi Java ja monet muut ohjelmointikielet eivät tue sitä. Javassa käytetään interfaceja simuloimaan moniperintää. Interfaceissa ei voi kuitenkaan olla ohjelmakoodia, joten ohjelmoijan pitää itse toteuttaa kaikki interfaceen liittyvät toiminnot omassa luokassaan. Seurauksena on sitä ikävää turhaa bloattia, josta Java-ohjelmat ovat tunnettuja.

Rubyn mixinit

Rubyssa moniperintä on ratkaistu mixineillä, jotka ovat oikein kaunis ja yksinkertainen tapa lisätä valmiita toimintoja omiin luokkiin. Tämä on kätevää pitkälti sen vuoksi, että kielessä on dynaaminen tyypitys. Mixin on käytännössä valmiita apumetodeita sisältävä moduuli. Nämä apumetodit voi lisätä "lennossa" omaan olioonsa extend-kutsulla:

module Sayable
  def say
    puts @message
  end
end

class Hello
  def initialize
    @message = 'Hello, World'
  end
end

hello = Hello.new
hello.extend Sayable
hello.say

Jos apumetodit haluaa lisätä pysyvästi mukaan omaan luokkaansa, sen voi tehdä luokkamäärittelyssä include-kutsulla:

class Hello
  def initialize
    @message = 'Hello, World'
  end
  include Sayable
end

Pythonin mixinit

Pythonissa on moniperintä, mutta sen käyttö saattaa olla tarpeettoman hankalaa omissa projekteissa. Esimerkiksi konstruktorien kutsuminen oikealla tavalla on hieman epäintuitiivista ja menee heti rikki, jos hierarkian yksikin luokka jättää kutsumatta super-konstruktoria.

Monessa tapauksessa voikin olla järkevämpää simuloida mixinejä käyttämällä moniperintää pelkkien apumetodien määrittelyyn. Tässä lähestymistavassa mixin-luokassa ei ole konstruktoria eikä omia instanssimuutujia. Se vain olettaa, että mixiniä käyttävässä luokassa tietyt instanssimuuttujat on jo määritelty:

class Sayable(object):
    def say(self):
        print(self.message)

class Hello(Sayable):
    def __init__(self):
        self.message = 'Hello, World'

hello = Hello()
hello.say()

Oma Hello-luokka voi periytyä jostain muusta luokasta. Silloin ohjelmoijan pitää huomioida super-konstruktorin kutsuminen oman koodinsa osalta, mutta mixin-luokista ei tarvitse välittää:

class Sayable(object):
    def say(self):
        print(self.message)

class ExampleBase(object):
    def __init__(self):
        pass

class Hello(ExampleBase, Sayable):
    def __init__(self):
        super(Hello, self).__init__()
        self.message = 'Hello, World'

hello = Hello()
hello.say()

Tämä malli toimii parhaiten, jos isäntäluokka on aina ensimmäisenä perimäluettelossa, ja mixin-luokat tulevat sen jälkeen. Pythonin MRO (method resolution order) kulkee nimittäin vasemmalta oikealle, ja super-kutsut kohdistuvat ensimmäisenä vasemmanpuoleisimpaan isäntäluokkaan.

Posted on in Django, Technology, Python, Ruby on Rails

Olen käyttänyt viime aikoina paljon energiaa sen pohtimiseen, kumpi on parempi alusta kehittää web-sovelluksia: Python-pohjainen Django vai Ruby on Rails.

Ruby on Rails

Kypsä ja elegantti, mutta kärsii suorituskyvyn rajallisuudesta. Ruby on Railsilla on vaivatonta tehdä siistejä sovelluksia, joissa kaikilla komponenteilla on vakioitu paikkansa. Toisen kehittäjän tai alihankkijan on helppo omaksua projektin rakenne. Rubyssä jopa koodin sisennys on vakioitu aina 2 spaceen.

Python & Django

"Hakkerihenkinen", helposti muokattava, erittäin suorituskykyinen. Ongelmana jokaisen projektin erilaisuus, kun sovellusten komponentteja toteutetaan eri tavoilla ja sijoitellaan sinne tänne. Ei esimerkiksi vakioitua paikkaa layout-tason templateille, omille apukirjastoille, cronjob-skripteille ja vastaaville osioille. Yrityksen pitää itse määritellä policyt näille.

Kumpi tärkeämpää, eleganttius vai suorituskyky?

Tähän se valinta kilpistyy. Pythonilla on isoja valtteja reaalimaailmassa, kuten esimerkiksi Googlen App Engine -tuki sekä hiljattain ilmestynyt FriendFeedin/Facebookin Tornado-webbipalvelin. Ruby on Rails puolestaan tuntuu olevan vähän pienempien pelurien alusta, mutta toisaalta kuitenkin Twitter käyttää sitä.

Kumman sinä valitsisit?

Posted on in Technology, Python, Ruby on Rails

Tämä on ehkä hieman epäortodoksinen vertailu, mutta mittailin huvikseni Facebookin hiljattain julkaiseman Tornado 0.1:n sekä myöskin uusimman Ruby on Rails 2.3.4:n suorituskykyä.

Framework Language Server Request/s
Tornado 0.1 (git) Python 2.6.2 Nginx 0.6.35 1961.11
Ruby on Rails 2.3.4 Ruby 1.9.1 Apache 2.2.11/Passenger 2.2.5 421.94
Ruby on Rails 2.3.4 Ruby 1.9.1 Nginx 0.7.61/Passenger 2.2.5 470.47
Plain HTML serving (none) Nginx 0.7.61 6317.71

Mittaukset on tehty samalla koneella kuin tämä aiempi vertailu, mutta käyttiksenä oli nyt Ubuntu 9.04. Testiohjelmana "ab -n 100000 -c 25". Testattu sovellus oli yksinkertainen "hello world", eli tässä mitattiin lähinnä kattorajaa frameworkien suorituskyvyille. Mukana myös vertailun vuoksi staattisen HTML:n jakelu Nginxillä.

Tornado näyttäisi siis oikein suorituskykyiseltä, kun se konfiguroidaan ohjeiden mukaan. Ajoin testissä 4 rinnakkaista Tornado-prosessia (1 per CPU-core) ja niiden edessä Nginxiä. Muita kokemuksia kyseisestä frameworkista ja sen sopivuudesta web-sovelluskehitykseen ei vielä oikein ole.

Ruby on Rails ei myöskään ole ihan huonoimmasta päästä. Ruby 1.9:ssä on tiettävästi uusi tehokkaampi VM ja production-moodissa Rails pitää kaikki luokat cachetettuna muistissa. Nähdäkseni RoR yltää suurin piirtein samoihin tuloksiin kuin PHP:n nopeimmat MVC-frameworkit. Passenger puolestaan tekee RoR-sovellusten deployaamisesta varsin helppoa sekä Apachella että Nginxillä.

Pythoniin verrattuna Ruby kuitenkin vaikuttaa olevan konsistentisti aina vähintään puolet hitaampi. Djangolla sain aiemmin samalla koneella noin 1000 req/s ja Tornado vielä tuplasi sen.

Posted on in Technology, Python

Vaikka olen ohjelmoinut Pythonilla jo jonkin aikaa, opin tämän vasta nyt kantapään kautta:

class Pizza(object):
  tyyppi = 'tavallinen' # tästä tulee luokkamuuttuja
  
  def __init__(self):
    self.juusto = 'tupla' # tästä tulee instanssimuuttuja

Oleellinen ero on siinä, että luokkamuuttujan muuttaminen vaikuttaa kaikkiin luokan instansseihin:

capricciosa = Pizza()
mexicana = Pizza()
# molemmat ovat tyyppiä 'tavallinen'

Pizza.tyyppi = 'pannu'
# nyt molemmat ovat tyyppiä 'pannu'

En tajunnut tätä heti, koska luokkamuuttujien määrittely näyttää Pythonissa samalta kuin instanssimuuttujien määrittely PHP/Java/C#-tyyppisissä kielissä. Ne kuitenkin vastaavat näiden kielten "static"-muuttujia.

Tätä juttua ei välttämättä huomaa ohjelmoidessa, kunnes jossain vaiheessa asettaa esimerkin mukaisesti Pizza.tyyppi = 'jotain' koko luokalle, ja silloin kaikki objektit muuttuvat kerralla. Luokkamuuttujan voi myös huomaamattaan peittää samannimisellä instanssimuuttujalla:

capricciosa = Pizza()
capricciosa.tyyppi = 'tavallinen'
mexicana = Pizza()
# molemmat ovat tyyppiä 'tavallinen'

Pizza.tyyppi = 'pannu'
# nyt ainoastaan mexicana on tyyppiä 'pannu'

Kyseinen ominaisuus on hyödyllinen ja noudattaa Pythonin logiikkaa, mutta sen kanssa pitää olla tarkkana.

Posted on in Technology, Python, Django

Ryhdyin tänään toteuttamaan unit-testausta erääseen Django-pohjaiseen web-projektiin. Ohjelmointikielenä on siis Python. Olen erittäin tyytyväinen siihen, miten kätevästi koko projektin voi testata yhdistelemällä sopivasti unittest- ja doctest-testejä.

Django tukee molempia frameworkkeja automaattisesti. Testipatteriston voi ajaa läpi yhdellä komennolla, joka näyttää onnistuessaan suunnilleen tältä:

$ ./manage.py test

.................
--------------
Ran 17 tests in 2.780s

OK
Destroying test database...

Aloitin testien rakentamisen luomalla joukon luokkia, jotka perivät unittest.TestCasen. Ne tyhjentävät tietokannan ja luovat sinne esimerkkidataa jokaista testiä varten. Näissä luokissa suoritetaan sellaiset laajemmat testit, jotka eivät liity pelkästään yksittäisiin funktioihin.

class SiteTestCase(TestCase):
    def setUp(self):
        cleanup()
        create_sites(self)
    def tearDown(self):
        cleanup()
    def testXxx(self):
        ...

Käytin globaaleja funktioita (cleanup, create_sites) tietokannan käsittelyyn, koska silloin niitä on kätevä hyödyntää myös doctesteissä. Doctestejä on puolestaan mukava ripotella suoraan funktioiden yhteyteen tuotantokoodiin. Ne ovat normaalikäytössä ikäänkuin kommentteja, eivätkä häiritse tuotantoa.

class Site(CompatibleModel):
    objects = CustomManager('site')
    name = SlugField(max_length=254)
    title = CharField(max_length=254)
    secret = CharField(max_length=254)
    notify_url = CharField(max_length=254)

    @classmethod
    def get_by_name(cls, name):
        """
        >>> from djangoapp import tests
        >>> tests.cleanup()
        >>> data = tests.create_sites()
        >>> site = Site.get_by_name('testsite1')
        >>> site.name
        u'testsite1'
        """
        return cls.objects.get(name__exact=name)

Ylläolevaan esimerkkiin on upotettu doctest-testi. Se siivoaa ensin tietokannan, luo esimerkkidatan, ja varmistaa sitten, että esimerkkidata löytyy tietokannasta kyseisellä funktiolla. Doctestin syntaksi on sama kuin interaktiivisessa Python-tulkissa. Tässä viimeinen rivi kertoo, mitä aiemman rivin pitää tulostaa, jotta testi menee läpi. Vastaavia ehtoja voi olla useita.

Doctestien selkeänä etuna on se, että ohjelmoija näkee koko ajan koodista, onko kaikkiin funktioihin liitetty tarvittavat testitapaukset. Niiden ohella voi vielä käyttää python-coverage -työkalua. Se mittaa testien ajamisen yhteydessä, montako prosenttia koodin kokonaismäärästä käytiin läpi.

Posted on in Technology, Python, Django

Disclaimer: I'm still learning Django so there may be smarter ways of doing the things I describe here. I also haven't tested this much though the principle seems to work. The example code has been edited and may not be fully correct.

Background

By default, Django 1.0 supports only a single, hardcoded database connection, defined by the settings.DATABASE_XXX variables. All models use the same connection.

A scalable website needs several different database connections when MySQL is used. Models might be stored in separate databases, and sometimes a read-only replica of a database might be used for querying. For sharding purposes, several different databases might be used for the same model.

Using a custom Manager to choose the database

The "objects" attribute of Django models can be replaced with a custom Manager class. This allows you to override the get_query_set() method, which chooses the database connection. For example:

from django import db
import new

class CustomSettings:
    DATABASE_HOST = '...'

CUSTOM_SETTINGS = CustomSettings()

class CustomManager(db.models.Manager):
    def custom_conn(self):
        conn = db.backend.DatabaseWrapper()
        cursor = lambda mgr: mgr._cursor(CUSTOM_SETTINGS)
        conn.cursor = new.instancemethod(cursor, conn,
            db.backend.DatabaseWrapper)

    def get_query_set(self):
        query = db.models.sql.Query(self.custom_conn())
        return CustomQuerySet(self.model, query)

class MyModel(db.models.Model):
    objects = CustomManager()

Managing transaction commits

Unfortunately, Django still uses a hard-coded connection object to commit transactions after saving models. This must be overridden or nothing will happen. First the INSERT:

class CustomManager(...):
    ...

    def _insert(self, values, **kwargs):
        return self.insert_query(self.model, values, **kwargs)

    def insert_query(self, model, values,
            return_id=False, raw_values=False):
        conn = self.custom_conn()
        query = db.sql.InsertQuery(model, conn)
        query.insert_values(values, raw_values)
        rv = query.execute_sql(return_id)
        # Need to commit here
        conn._commit();
        return rv

The UPDATE can't be committed in CustomManager, because it's called directly in the QuerySet object. This means you have to use an custom QuerySet object like this:

class CustomQuerySet(db.models.query.QuerySet):
    def _update(self, values, **kw):
        rv = super(CustomQuerySet, self)._update(values, **kw)
        # Need to commit here
        self.query.connection._commit()
        return rv

Choosing the shard in queries

When sharding is used, the database connection depends not only on the model, but also some parameter that needs to be passed with the query. Choosing a readonly connection is very similar. This can be done by adding a method to the custom Manager class:

class CustomManager(...):
    ...
    def get_query_set(self, shard_id=None, readonly=False):
        # Use the parameters to choose the connection
        ....

    def shard(self, shard_id):
        return self.get_query_set(shard_id)

    def readonly(self):
        return self.get_query_set(None, True)

# To query the model from a specific shard
MyModel.objects.shard(42).get(id=76)

# To use a read-only connection
MyModel.objects.readonly().get(id=76)

Conclusion

Multiple database and sharding are possible, but they require overriding some Django internals. This might break in future versions of Django.

Apparently work is already in progress for Django support multiple databases by default. Hopefully all the cases described in this article will be supported (multiple databases, read-only databases and sharded databases).

Update:

The DELETE operation was actually quite difficult to implement, because Django uses a global module-level delete_objects() function instead of going through the Manager and/or QuerySet. My current solution is to override the Model's delete() and manually delete by the primary key from the table.

ForeignKey references in models also cause problems when the field is not really a foreign key but resides in another database. I had to change them into IntegerFields and handle them manually in application code.

It would be really nice if Django had some generic database query context, which could be set up when querying or inserting/updating/deleting models. This context could be used to carry information about what kind of database connection is needed. The global module-level connection object could then be replaced with a call to some overrideable function that would see the context and could decide which connection to return.