Pięć setek szaleństwa

Wtem!

— Leży!

Produkcja? Ale tutaj?

Traceback (most recent call last):
  File "/var/www/foo/catalogue/views.py", line 252, in get
    product = Product.objects.get(upc=tokens[0])
  File "/usr/lib/python2.6/site-packages/django/db/models/manager.py", line 132, in get
    return self.get_query_set().get(*args, **kwargs)
  File "/usr/lib/python2.6/site-packages/django/db/models/query.py", line 351, in get
    % (self.model._meta.object_name, num, kwargs))
TypeError: 'DoesNotExist' object is not callable

Odśwież. Działa. Ki czort? Odśwież. Leży. Działa — działa — leży — działa. Wynik zdaje się zależeć od procesu, który akurat obsłuży zapytanie.

No dobrze, zobaczmy, co…

        raise self.model.MultipleObjectsReturned("get() returned more than one %s -- it returned %s! Lookup parameters were %s"
                % (self.model._meta.object_name, num, kwargs))

Wyjątek. W trakcie rzucania wyjątku. Wbudowanego w klasę modelu.

Wdech, wydech. Drugi worker dostał. Dziwacznie poskręcane fragmenty jego wnętrzności zasypują mi skrzynkę.

Wdech, wydech, grep.

Zanim się przedarłem, straciliśmy jeszcze trzech. W sumie połowa zwijała się tam i charczała wewnętrznymi błędami. Wytrzymajcie, jeszcze tylko kilka plików. I jest.

        try:
            product = Product.objects.get(upc=product_upc)
        except Product.DoesNotExist, Product.MultipleObjectsReturned:
            # …

Zasadzka — sztuk trzy w projekcie — ładnie nas urządzili. Jeden koniec kodu powoli kopie dołek pod drugim. Pułapki udało się usunąć, ale tamtym już nikt nie pomoże. Przysłanie na produkcję posiłków zajmie minimum trzy dni. Pozostaje tylko patrzeć, jak jeden po drugim toną w tym dziwacznym morzu bezradności, serwując skomplikowane dokumenty, jednak dławiąc się na najprostszych rzeczach. Zostali sami. Oni i pięć setek szaleństwa.

Kurtyna. Niestosowne słowa cisną mi się na usta, ubliżać chcę składni, albowiem łaknę Pythona trzeciej inkarnacji.

Django: TemplateResponse

Przyszło Django 1.3, pojawiła się paskudna implementacja class-based views, direct_to_template zostało oznaczone jako przestarzałe. Ale pojawił się też godny następca, choć z class-based views nie ma nic wspólnego.

Nie piszemy więc tak:

from django.shortcuts import render_to_response
from django.template import RequestContext

def foo(request):
    # ...
    return render_to_response('foo.html', {'bar': baz},
            context_instance=RequestContext(request))

Nie piszemy też tak:

from django.views.generic.simple import direct_to_template

def foo(request):
    # ...
    return direct_to_template(request, 'foo.html', {'bar': baz})

Ani ­— tym bardziej — tak:

from django.views.generic import TemplateView

def foo(request):
    # ...
    return TemplateView.as_view(template_name='foo.html')(request, bar=baz)

Drodzy państwo, zamiast powyższych używamy:

from django.template.response import TemplateResponse

def foo(request):
    # ...
    return TemplateResponse(request, 'foo.html', {'bar': baz})

Django, except IntegrityError

Jeden mały, niewinny szczeniaczek o maślanych oczkach zostaje gdzieś na świecie żywcem przemielony na karmę dla kotów za każdym razem, kiedy piszesz coś takiego:

try:
    # ...
except IntegrityError:
    return None

Łapanie IntegrityError jako metoda wykrywania problemów jest równie dobrym pomysłem, co wymiana czujników dymu na urządzenie wykrywające odgłos syreny nadjeżdżającej straży pożarnej.

Błąd na poziomie bazy danych jest nieodwracalny. Nie można nic naprawić. To jak pójść pośmiertnie do lekarza. Połknięcie wyjątku bez dalszej propagacji to tylko dodatkowe punkty za styl. Dzięki temu wszystkie otwarte transakcje (z izolującą włącznie) właśnie stały się bezużyteczne, a najbliższa operacja na bazie danych spowoduje spektakularny zgon całej aplikacji.

Wersja obrazowa

Twój kod próbuje dywanikiem z kibla gasić ogień w domu, w którym dawno złuszczyły się tapety i zniknęły meble. W domu, który sam podpalił. Niezbyt zadowolony z rezultatów wychodzi więc przed budynek i dusi strażaków wężem. Po wszystkim, gwiżdżąc, z rękoma w kieszeniach, odchodzi w stronę zachodzącego słońca, po drodze mijając sąsiada — powoli spopielającego się we własnym ogródku.

See no eval, hear no eval, speak no eval

Choć sam konta w Książeczce Maryja nie posiadam, przyszło mi ostatnio ścierać się z ichnim API, coby dla klienta wdrożenia poczynić. Po przejrzeniu dostępnych bibliotek (w tym dość żałosnego python-sdk), stanęło na dość popularnym projekcie pyfacebook.

Oryginalne repozytorium było wybrakowane pod względem funkcjonalności, wybraliśmy więc fork, który z pewnością stworzył fan Władcy Pierścieni. Jak się miało później okazać, „jeden, by wszystkie zgromadzić i w ciemności związać” dość wiernie oddaje uzyskany efekt.

Człowiek głupi, to przy pierwszym problemie zajrzał w bebechy ofiary. Po tym stanął jak wryty i całe Satchmo przeleciało mu przed oczami. Na początek tradycyjny problem — przejście się w glanach po separacji warstw:

from threading import local

_thread_locals = local()
def get_facebook_client():
    """
    Get the current Facebook object for the calling thread.

    """
    try:
        return _thread_locals.facebook
    except AttributeError:
        raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
class FacebookMiddleware(object):
    """
    Middleware that attaches a Facebook object to every incoming request.
    The Facebook object created can also be accessed from models for the
    current thread by using get_facebook_client().

    callback_path can be a string or a callable.  Using a callable lets us
    pass in something like lambda reverse('our_canvas_view') so we can follow
    the DRY principle.
    """
    # ...

    def process_request(self, request):
        # ...
        _thread_locals.facebook = request.facebook = Facebook(self.api_key,
                self.secret_key, app_name=self.app_name,
                callback_path=callback_path, internal=self.internal,
                proxy=self.proxy, app_id=self.app_id, oauth2=self.oauth2)

Przerażające konstrukcje zaczęły się jednak później:

# generate the Facebook proxies
def __generate_proxies():
    for namespace in METHODS:
        methods = {}

        for method, param_data in METHODS[namespace].iteritems():
            methods[method] = __generate_facebook_method(namespace, method, param_data)

        proxy = type('%sProxy' % namespace.title(), (Proxy,), methods)

        globals()[proxy.__name__] = proxy

__generate_proxies()
class Facebook(object):
    """
    Provides access to the Facebook API.

    ...
    """

    def __init__(self, api_key, secret_key, auth_token=None, app_name=None,
                 callback_path=None, internal=None, proxy=None,
                 facebook_url=None, facebook_secure_url=None,
                 generate_session_secret=0, app_id=None, oauth2=False):
        # ...
        for namespace in METHODS:
            self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace))

Skończyło się na własnym forku i refaktoryzacji tych i wielu innych fragmentów kodu. Poprawioną wersję można znaleźć na GitHubie.

Na koniec stare powiedzenie ludowe:

Gdy bowiem zoczysz, iż jest coś narzeczy, a za cwanego masz się i uważasz __globals__ i eval() za sprawy rozwiązanie, mylisz się wielce, przeto idź przypudrować nos¹.

¹ Ciało, 2003

Python: Nie będziesz używał **kwargs nadaremnie

Prawidłowe użycie:

def foo(bar, *args, **kwargs):
    frobnicate(bar)
    baz(*args, **kwargs)

Nigdy tak¹:

def foo(bar, *args, **kwargs):
    frobnicate(bar)
    baz()

¹ Z wyjątkiem kilku API, które wyraźnie rezerwują sobie możliwość wprowadzenia dodatkowych parametrów. Działają tak np. wbudowane sygnały w Django.

Ciche połykanie śmieci w parametrach w malowniczy sposób zemści się przy pierwszej literówce w kodzie. I będzie ci się należało.

Jeśli wydaje ci się, że w ten sposób skracasz sobie kod, to przypomnij sobie starą zasadę: utrzymywanie kodu wymaga dwa razy więcej inteligencji i sprytu, niż jego napisanie. Jeśli cały swój spryt włożysz w stworzenie kodu, to z definicji jesteś za głupi, by go potem debugować.

To jest mój wątek i nie oddam go nikomu

Bezstanowość protokołu HTTP jest faktem. Niezależnie od tego, czego chciałby autor danej aplikacji. Na przykład nie jest prawdą, że jeden proces lub wątek serwera jest przypisany jednemu, konkretnemu odwiedzającemu stronę. Wybaczcie zatem łzy, które zakręciły mi się w oczach na widok tego:

def _get_taxprocessor(request=None):
    taxprocessor = get_thread_variable('taxer', None)
    if not taxprocessor:
        if request:
            user = request.user
            if user.is_authenticated():
                user = user
            else:
                user = None
        else:
            user = get_current_user()

        taxprocessor = get_tax_processor(user=user)
        set_thread_variable('taxer', taxprocessor)

    return taxprocessor

Dalej był już tylko płacz:

def get_current_user():
    user = get_thread_variable('user', None)
    if user != None: return user
    req = get_current_request()
    if req == None: return None
    return req.user

I zgrzytanie zębów:

from threading import local

_threadlocals = local()

def set_current_user(user):
    set_thread_variable('user', user)

def set_thread_variable(key, var):
    setattr(_threadlocals, key, var)

def get_thread_variable(key, default=None):
    return getattr(_threadlocals, key, default)

def get_current_request():
    return get_thread_variable('request', None)
class ThreadLocalMiddleware(object):
    """Middleware that gets various objects from the
    request object and saves them in thread local storage."""

    def process_request(self, request):
        set_thread_variable('request', request)
        set_current_user(request.user)

Pytanie-zagadka: co się stanie z podatkiem, gdy ten sam wątek serwera obsłuży innego użytkownika? Pytanie pomocnicze: skąd może pochodzić ów kod?

Otrzymujesz k3 Punkty Obłędu. Jeśli całkowita liczba zebranych punktów wynosi 6 lub więcej, rozpatrz test nabycia trwałej choroby psychicznej zgodnie z procedurą opisaną w rozdziale Obłęd podręcznika.

Być jak Satchmo

Z pewnością naczytałeś się już, jakie to Satchmo nie jest doskonałe, zazdrościsz i chciałbyś stworzyć coś równie wspaniałego. Dość jednak nieprzespanych nocy, albowiem przygotowałem krótki poradnik, który w kilku krokach pozwoli ci dorównać mistrzom.

Sięgaj tam, gdzie import nie sięga

Tak jest, zacznij od stworzenia modułu z myślnikiem w nazwie. Niestety, oczywista nazwa email-auth.py została już zajęta — musisz się bardziej wysilić. Znajomym oczy wypadną z zazdrości, gdy tylko pierwszy raz zobaczą:

hahaha = __import__('pokonalem-was', globals(), locals(), [], -1)

Uatrakcyjniaj pętle

Od dawna wiadomo już, że przedwczesna optymalizacja jest złem, naszą odpowiedzią będzie zatem przedwczesna dezoptymalizacja! Oto przykład atrakcyjnego wyświetlenia listy:

{% for product in products %}
    {% if forloop.first %} <ul>  {% endif %}
        <li>{% thumbnail product.main_image.picture 85x85 as image %}
        <a href="{{ product.get_absolute_url }}"><img src="{{ image }}" width="{{ image.width }}" height="{{ image.height }}" /></a>
        <a href="{{ product.get_absolute_url }}">{{ product.translated_name }}</a></li>
    {% if forloop.last %} </ul> {% endif %}
{% endfor %}

Tylko wyobraź sobie ich miny! Jeśli chcesz przeskoczyć mistrza, spróbuj przenieść kod do Pythona:

for counter, product in enumerate(products):
    if counter == 0:
        print '<ul>'
    # ...i tak dalej

Nie daj się zaszufladkować

Nie łudźmy się — przestrzenie nazw są dla frajerów. Bez ceregieli pakuj wszystko w ścieżkę Pythona i upewnij się, że tak właśnie importujesz swoje moduły. Pokaż, że jesteś ważniakiem i twórz moduły o jak najogólniejszych nazwach. Naucz fajansów pokory, tych kilka dodatkowych wpisów w PYTHONPATH ich nie zabije. Mogą to zrobić konflikty, ale jeśli chcą używać czegoś ponad twój framework, to sami są sobie winni i zasłużyli na karę.

export PYTHONPATH=~/web/satchmo/satchmo/apps

Unikaj biurokracji

Po co męczyć się z formularzami, gdy do wszystkiego sięgnąć można już w widoku? Niezwykle istotne jest tu unikanie request.REQUEST.

if request.method=="POST":
    reqdata = request.POST
else:
    reqdata = request.GET

if reqdata.has_key('quantity'):
    try:
        quantity = round_decimal(reqdata['quantity'], places=2, roundfactor=.25)
    except RoundedDecimalError:
        quantity = Decimal('1.0')
        log.warn("Could not parse a decimal from '%s', returning '1.0'", reqdata['quantity'])
else:
    quantity = Decimal('1.0')

Jeśli już musisz użyć formularza, upewnij się, że upakujesz wszystkie, niezwiązane ze sobą grupy pól w jednej dużej klasie. Dzięki temu zaoszczędzisz sobie kilka wywołań is_valid() i jednocześnie udaremnisz wszelkie próby innego wykorzystania poszczególnych części przez te niedorozwoje, które mają czelność importować twoje klasy.

Wyznaczaj nowe trendy

Przez takich jak oni, programowanie obiektowe stoi praktycznie w miejscu. Pokaż im nowe sztuczki, takie jak zastąpienie rozszerzania klas nadpisywaniem funkcji w locie¹:

def confirm_info(request, template='shop/checkout/protx/confirm.html', extra_context={}):
    payment_module = config_get_group('PAYMENT_PROTX')
    controller = confirm.ConfirmController(request, payment_module)
    controller.templates['CONFIRM'] = template
    controller.extra_context = extra_context
    controller.onForm = secure3d_form_handler
    controller.confirm()
    return controller.response

¹ W rzeczywistości ConfirmController.onForm jest w konstruktorze klasy ustawiany na ConfirmController._onForm, co można uznać za architekturę po dwakroć lepszą.

Parametry dobieraj z rozmachem

Piękno tkwi w różnorodności. Upewnij się zatem, że wyczerpiesz wszelkie metody osiągnięcia tego samego celu.

class ProductImage(models.Model):
    # ...
    picture = ImageWithThumbnailField(verbose_name=_('Picture'),
        upload_to="__DYNAMIC__",
        name_field="_filename",
        max_length=200)
    # ...

class ImageWithThumbnailField(ImageField):
    # ...
    def __init__(self, verbose_name=None, name=None,
                 width_field=None, height_field=None,
                 auto_rename=NOTSET, name_field=None,
                 upload_to=upload_dir, **kwargs):
        # ...
        if upload_to == "__DYNAMIC__":
            upload_to = upload_dir
        # ...

Bądź elastyczny

Teraz twój sklep znajduje się w Polsce, ale kto wie, co będzie po obiedzie? Upewnij się, że cała konfiguracja może być edytowana w locie. Zwłaszcza te jej fragmenty, które wymagają restartu aplikacji. To jedna z wielu sztuczek, które zapewnią ci popularność w branży. Co prawda dawni przyjaciele zazdroszczą ci już do tego stopnia, że przestali się do ciebie odzywać, ale i tak nie tęsknisz po tych prostakach. Od teraz twoim jedynym przyjacielem jest aplikacja django-livesettings. Na innych nie masz szans, bo przyjaciół poznaje się w biedzie, a ty przecież właśnie zyskałeś umiejętności, dzięki którym praktycznie już jesteś bogaty.

Python: finanse

Nie dalej jak wczoraj kolega podesłał mi łatkę do mojej biblioteki do słownego zapisu liczb i kwot. Nie zdradzę od kogo, by chronić niewinnego. Grunt, że łatka wyglądała tak:

--- to_words_pl.py	(upstream)
+++ to_words_pl.py	(working copy)
@@ -82,7 +82,7 @@
         iteration += 1
     if unit:
         result.append(unit)
-    result.append(u'%d/100' % int(remainder * 100))
+    result.append(u'%d/100' % int(round(remainder * 100)))
     result = ' '.join(result)
     return result

Zdziwiłem się bardzo, bo zwykłem kwoty odpowiednio zaokrąglać do dwóch miejsc po przecinku. Co się jednak okazało? 0.48 zamieniało się w 0.47. A dokładniej? W 0.47999999999999998. Tuś mi, ptaszku.

Patrząc na 0.48 tak naprawdę w głowie widziałem decimal.Decimal('0.48'). Jak się jednak okazuje, niektórzy próbują operacje finansowe przeprowadzać na liczbach zmiennopozycyjnych. Nie używamy typu float do operacji finansowych. Dlaczego?

>>> 0.48
0.47999999999999998
>>> 0.82
0.81999999999999995

Do operacji na liczbach o znanej precyzji używamy typu decimal.Decimal i jego kontrolowanego (i konfigurowalnego!) mechanizmu zaokrąglania:

>>> from decimal import Decimal
>>> Decimal('0.48') + Decimal('0.12')
Decimal('0.60')
>>> vat = Decimal('0.48') * Decimal('0.22')
>>> vat.quantize(Decimal('0.01'))
Decimal('0.11')

Python: wyjątkowo głupi pomysł

Nigdy nie róbcie tak:

try:
    # ...
except FooException, e:
    # ...
    raise e

Wyjątki przepuszcza się tak:

try:
    # ...
except FooException:
    # ...
    raise

Istotna różnica polega na niezniszczeniu całego stosu wywołań. Z góry dziękuję.

Django: abstrakcji ciąg dalszy

Tym razem inne podejście, naturalne dziedziczenie abstrakcyjnych modeli z dwoma dodatkowymi metodami.

W przeciwieństwie do poprzedniego przykładu, pozwala używać super() w abstrakcyjnych klasach pośrednich.

from django.db import models

class AbstractMixin(object):
    _classcache = {}

    @classmethod
    def contribute(cls):
        return {}

    @classmethod
    def construct(cls, *args, **kwargs):
        attrs = cls.contribute(*args, **kwargs)
        attrs.update({
            '__module__': cls.__module__,
            'Meta': type('Meta', (), {'abstract': True}),
        })
        key = (args, tuple(kwargs.items()))
        if not key in cls._classcache:
            clsname = (('%s%x' % (cls.__name__, hash(key)))
                       .replace('-', '_'))
            cls._classcache[key] = type(clsname, (cls, ), attrs)
        return cls._classcache[key]

Przykład użycia:

class CategoryFactory(models.Model, AbstractMixin):
    name = models.CharField(max_length=100)

    class Meta:
        abstract = True

    def __unicode__(self):
        return self.name

class ProductFactory(models.Model, AbstractMixin):
    name = models.CharField(max_length=100)

    class Meta:
        abstract = True

    @classmethod
    def contribute(cls, category):
        return {'category': models.ForeignKey(category)}

    def __unicode__(self):
        return u'%s / %s' % (self.category, self.name)

Konkretyzacja klas:

class MyCategory(CategoryFactory.construct()):
    pass

class MyProduct(ProductFactory.construct(category=MyCategory)):
    pass

Działa również nasz test:

from . import models

c = models.MyCategory(name='cat')
p = models.MyProduct(name='prod', category=c)
print p # 'cat / prod'