>

programming

快快樂樂學 Python decorator

這篇算是改寫自 A primer on Python decorators,不算是翻譯文,不過例子是跟結構都是從文章中擷取出來的。

引子

在 Python 當中,functions 是 first-class objects。也就是說你可以對 function 做任何跟其他 objects 一樣的事情。舉例來說,你可以把一個 function 指定到一個變數當中:

>>> def addone(n):
...     return n + 1
>>> addone(3)
4
>>> add1 = addone
>>> add1(3)
4

這樣看來可能沒什麼,但是當你可以把某個 function 傳入另外一個 function 之後再回傳時,我們可以發現這會有很多有趣的應用。舉例來說,在 Python 裡面有內建 map 這個 function,你只要傳入一個 function 以及一個 list,map 就會回傳一個新的 list,回傳的 list 內容是把你原本傳入的 list 當做輸入,傳進去 function 之後回傳的結果。以下是個例子:

>>> numbers = [1,2,3,4]
>>> map(addone, numbers)
[2, 3, 4, 5]

這樣可以幹啥?

我們透過了 map 這個簡單的例子來示範這麼做的好處,接下來我們來展示一個更棒的例子。

Fibonacci number 的定義如下:

Fibonacci number

Fibonacci seed

如果用 Python 來寫,你可能會寫出下面的 code:

def fib(n):
    if n in [0,1]:
        return n
    else:
        return fib(n-1) + fib(n-2)

這邊我們可以看到,當計算 fib(10) 的時候,會去計算 fib(9) 和 fib(8)。計算 fib(9) 的時候會計算 fib(8) 和 fib(7),這邊我們可以看到 fib(8) 的值被重複計算到了。所以我們希望把重複算過的值記錄下來,這樣就可以省下很多計算的時間,這個技巧叫做 memoization

要達到這個目的,一個簡單的想法是在 fib function 當中用個 dictionary 把算過的值記下來,如果要求的值已經算過了,就回傳存過的值,不過這方法麻煩的地方就是在於還要修改 fib 的程式碼才能達到這個效果。在 Python 當中,我們可以有更好的方法!既然 function 也可以當做回傳值,我們可以把傳入的 function 修改成會把結果記下來的版本再回傳,下面是這個 function 可能的樣子:

def memoize(fn):
    stored = {}

    def memoized(*args):
        try:
            return stored[args]
        except KeyError:
            result = fn(*args)
            stored[args] = result
            return result

    return memoized

這麼一來,我們要產生有 memoization 效果的 fib function 可以這麼做:

fib = memorize(fib)

這跟 decorator 有啥關係?

由於這種 pattern 常常出現,所以 Python 當中提供了 decorators 這個機制來讓你更簡單的做這種事情。

@memoize
def fib(n):
    if n in [0,1]:
        return n
    else:
        return fib(n-1) + fib(n-2)

在這邊,我們會說 memoize 是個 decorator,它 decorate 了 fib 這個 function。其實這邊做的事情跟上面的例子沒有什麼不同,只是透過了 @ 這個 operator 來減少了 coding 的麻煩。

在 decorator 當中,我們也可以傳入參數。舉例來說,如果我們希望用 memcached 來存已經算好的結果,我們可能會寫成下面的樣子:

@memcached('127.0.0.1:11211')
def fib(n):
    if n in [0,1]:
        return n
    else:
        return fib(n-1) + fib(n-2)

這樣寫其實就等同於

fib = memcached('127.0.0.1:11211')(fib)

結論

這邊很簡單了介紹了 Python 當中的 decorator,相信看完之後對 decorator 應該有些初步的了解。Python 當中 decorator 是個很常見的樣式,在各種 framework 當中處處可見。透過 decorator 的應用,將可以讓你的程式碼語義更加的簡潔易懂。

Django Patterns (4) - Templates

http://agiliq.com/books/djangodesignpatterns/templates.html

Templates

PROJECTS AND APPS.

在 project level 要有個 base.html,在每個 app level 也要有個 base.html。在 app level 中的 base.html 應該要 extends project level 中的 base.html。

 {# Eg Project base.html #}

 <html>
 <head>
 <title>{% block title %}My Super project{% endblock %}</title>
 ...

 {# app base.html #}

 {% extends 'base.html' %}

 {% block title %}{{ block.super }} - My duper app {% endblock %}
 ...


 {# login.html #}

 {% extends 'auth/base.html' %}
 {% block title %}{{ block.super }} - Login {% endblock %}
 ...

LOCATION OF TEMPLATES

一個 app 的 templates 應該要在 appname/template.html。所以 templates 應該要放在

  1. project/templates/app/template.html
  2. project/app/templates/app/template.html

這兩個位置其中之一

讓樣讓兩個 app 可以有同樣的 template name

HANDLING ITERABLES WHICH MAYBE EMPTY

在你的 views 中你會

 posts = BlogPosts.objects.all()
 ...
 payload = {'posts':posts}
 return render_to_response('blog/posts.html', payload, ..)

因為 posts 可能會是 empty,所以在 templates 中我們會有這樣的形式:

 {% if posts %}
     {% for post in posts %}
         ...
 {% endfor %}


 {% else %}

 {% endif %}

Django Patterns (3) - Forms

django logo

繼續 Django Patterns 的心得,之前的心得可以看這裡這裡

Forms

PREFER MODELFORM TO FORM

ModelForm 已經知道對應 models 的 UI widgets,所以在大多數的狀況之下 ModelForm 就夠用了。以下是些常見的狀況:

Hiding some fields from ModelForm which are needed for a DB save.

 #in Forms.py
 class ProfileForm(forms.ModelForm):
     class Meta:
         model = Profile
         exclude = ['user',]

 #In Views:
 form = ProfileForm(request.POST)
 profile = form.save(commit = False)
 profile.user = request.user
 profile.save()

或是:

 #Todo test this
 class ProfileForm(forms.ModelForm):
     class Meta:
         model = Profile
         exclude =['user',]
     def __init__(self, user, *args, **kwargs)
         self.user = user
         super(ProfileForm, self).__init__(*args, **kwargs)

     def save(*args, **kwargs):
         self.instance.user = self.user
         super(ProfileForm, self).save(*args, **kwargs)

Customizing widgets in ModelForm fields

有時候你只需要 override 在你 ModelForm 當中的 field,你可以這麼做:

 from django.contrib.admin.widgets import AdminFileWidget

 class ProfileForm(forms.ModelForm):
     class Meta:
         model = Profile
         exclude = 'user',

     def __init__(self, *args, **kwargs):
         super(ProfileForm, self).__init__(*args, **kwargs)
         # note that self.fields is available just after calling super's __init__
         self.fields['picture'].widget = AdminFileWidget()

Saving multiple Objects in one form

 class ProfileForm(forms.ModelForm):
     class Meta:
         model = Profile
         exclude = ['user',]

 class UserForm(forms.ModelForm):
     class Meta:
         model = User
         exclude = [...]

 #in views.py
 userform = UserForm(request.POST)
 profileform =  ProfileForm(request.POST)
 if userform.is_valid() and profileform.is_valid():
     #Only if both are valid together
     user = userform.save()
     profile = profileform.save(commit = False)
     profile.user = user
     profile.save()

 {# In templates #}
 <form ...>
 {{ userform }}
 {{ profileform }}
 <input type="submit" />
 </form>

FORMS SHOULD KNOW HOW TO SAVE THEMSELVES.

如果你的 forms 是 ModelForm,那麼它已經知道要怎麼樣存 data 了,如果是 forms.Form,那麼應該要有個 .save(),這樣跟 ModelForm 對應,而且讓你可以這麼做:

 #in views.py
 def view_func(request):
     if request.method == 'POST':
         form  = FormClass(request.POST)
         if form.is_valid():
             obj = form.save()
             ...
         ...

而不是:

 if form.is_valid():
     #handle the saving in DB inside of views.

.save() 應該要回傳一個 Model Object

THE FORM SHOULD KNOW WHAT TO DO WITH IT’S DATA

如果你建造了一個類似 contact form 的 form,由於你的 form 的目的是要寄信,因此 logic 應該要保持在 form 當中:

 class ContactForm(forms.Form):
     subject = forms.CharField(...)
     message = forms.TextField(...)
     email = forms.EmailField(...)
     ...

     def save(self):
         mail_admins(self.cleaned_data['subject'], self.cleaned_data['message'])

Django Patterns (2) - Models

django logo

延續上篇的心得,這部份是講 models 相關。

Multiple Managers

一個 Model 可以根據你的需求有多個 Model Managers,假設你不希望顯示任何沒有經過認證的 objects(is_approved=False):

 class ModelClassApprovedOnlyManager(models.Manager):
     def get_query_set():
         self.get_query_set().filter(is_approved = True)

 class ModelClass(models.Model):
     ...
     is_approved = models.BooleanField(default = False)

     all_objects = models.Manager()
     objects = ModelClassApprovedOnlyManager()

如果你用了多個 managers,第一個 managers 要是預設的 manager,這會是被 ModelClass._default_manager 存取到的 manager。將會被 admin 用到

Custom Manager Methods

假設你需要以下的 query: Event.objects.filter(status=’P’).filter(start_date__gte=datetime.datetime.now()).order_by(‘start_date’)

你可能之後還會需要 filter by status 以及 created date。為了避免重複的 code,你可以在你的 default manager 當中加入 custom methods:

 class EventQuerySet(models.query.QuerySet):
     def published(self):
         return self.filter(is_published=True)

     def upcoming(self):
         return self.filter(start_date__gte=datetime.datetime.now())

 class EventManager(models.Manager):
     def get_query_set(self):
         return EventQuerySet(self.model, using=self._db) # note the `using` parameter, new in 1.2

     def published(self):
         return self.get_query_set().published()

     def upcoming(self):
         return self.get_query_set().upcoming()

 class Event(models.Model):
     is_published = models.BooleanField(default=False)
     start_date = models.DateTimeField()
     ...

     objects = EventManager()    # override the default manager

這樣你可以把你的 logic 放在 model 當中,為什麼你需要 custom query set?因為這樣就可以做出以下的 query:

 Event.objects.published().upcoming().order_by('start_date')

HIERARCHICAL RELATIONSHIPS

你會需要有 hierarchical relationships 的時候,最簡單的方法:

 class ModelClass(models.Model):
     ...
     parent = models.ForeignKey('ModelClass')

當 hierarchical 很淺的時候可以這樣用,不過如果樹狀結構比較複雜的時候,你可以用 django-mptt

Tips

  • SINGLETON CLASSES - 當你只希望有一個 object 被建立的時候
  • LOGGING - 記得在 object 被創造、變更或是移除的時候要有 log
  • AUDIT TRAIL AND ROLLBACK - 當一個 object 被更改或刪除時,要能夠回朔
  • DEFINE AN unicode - 記得要在 model 當中定義 unicode 以傳回有意義的名字
  • DEFINE A get_absolute_url() - 在 admin 中會用到

USE @PERMALINK FOR CALCULATING GET_ABSOLUTE_URL

你會希望有唯一的 canonical url 表達,通常都會寫在 urls.py 當中。

如果你有個 class:

 class Customer(models.Model)
     ...

     def get_absolute_url(self):
         return /customer/%s/ % self.slug

你會在兩個地方有同樣的表示,你會比較想要這樣:

 class Customer(models.Model)
     ...

     @permalink
     def get_absolute_url(self):
         return ('customers.detail', self.slug)

WORKING WITH DENORMALIZED FIELDS

你想要追蹤在一個部門當中的 employees:

 class Department(models.Model):
     name = models.CharField(max_length = 100)
     employee_count = models.PositiveIntegerField(default = 0)


 class Employee(models.Model):
     department = models.ForeignKey(Department)

一種方法是 override savedelete:

 class Employee(models.Model):
     ...

     def save(self, *args, **kwargs):
         if not self.id:
             #this is a create, not an update
             self.department.employee_count += 1
             self.department.save()
         super(Employee, self).save(*args, **kwargs)

     def delete(self):
         self.department.employee_count -= 1
         self.department.save()
         super(Employee, self).delete()

另外一個方法則是 attach post_savepost_delete 的 listener:

 from django.db.models import signals

 def increment_employee_count(sender, instance, raw, created, **kwargs):
     if created:
         instance.department.employee_count += 1
         instance.department.save()

 def decrement_employee_count(sender, instance, **kwargs):
     instance.department.employee_count -= 1
     instance.department.save()

 signals.post_save.connect(increment_employee_count, sender=Employee)
 signals.post_delete.connect(decrement_employee_count, sender=Employee)

ABSTRACT CUSTOM QUERIES IN MANAGER METHODS

如果你有些很複雜的 SQL Query 很難透過 Django 的 ORM 表達,你可以自己定 Custom SQL。這些應該抽象化為 Manager method

Django Patterns (1) - URLs

django logo

最近翻了一下 Django Patterns,稍微整理之後記錄一下心得。

PROJECTS AND APPS

在 project level 要有個 urls.py,在 app level 也要有個 urls.py。在 project level 中的 urls.py 要 include 每個 app 的 urls.py

 #project urls.py

 urlpatterns = patterns(
     '',
     (r'^', include('mainpages.urls')),
     (r'^admin/(.*)', admin.site.root),
     (r'^captcha/', include('yacaptcha.urls')),
     .....
 )

 #app urls.py
 urlpatterns = patterns(
     'app.views',
     url(r'^$', 'index'),
     url(r'^what/$', 'what_view')
     .....
 )

NAMING URLS

url patterns 要有 name,for example.

 url(r'^$', 'index', name='main_index'),

這樣可以讓 {% url urlpatternname %} 更好用。

Pattern name 的格式應該要以 appname_viewname 的形式。如果同個 view 被用在不同的 urlpatterns 當中,那麼應該要用 appname_viewname_use 的形式,像是 search_advanced_auth & search_advanced_unauth

 #urls.py for app search
 urlpatterns = patterns(
      'search.views'
      url(r'^advanced_product_search/$', 'advanced', name='search_advanced_product'),
      url(r'^advanced_content_search/$', 'advanced', name='search_advanced_content'),
      ...
 )

在 Google App Engine 中的 unit test

gae logo

乍看之下,在 GAE 當中做 Unit Testing 會是一件麻煩的事情,在沒有把實際的 application 跑起來的狀況下,要怎麼做 datastore or memcache 等等 service 的測試呢?幸好 google 幫我們處理好了這些問題, 讓我們可以在 local 端做 Unit Testing,下面就是介紹在 GAE 當中如何做 Unit Testing 的方法。

在 google app engine 中,提供了一個稱作 testbed 的 module 來建立 GAE 當中的 service stub,testbed 是由 GAE Testbed 這個專案整合進入 SDK 而來的。透過 testbed,你可以建立以下的 service stub:

  • Datastore (init_datastore_v3_stub)
  • Memcache (init_memcache_stub)
  • Task Queue (init_taskqueue_stub)
  • Images (init_images_stub)
  • URL fetch (init_urlfetch_stub)
  • User service (init_user_stub)
  • XMPP (init_xmpp_stub)ww

除了 Testbed 之外,我們還要配合一般常見的的 test framework 來做測試,這邊我選擇 nose。為了要讓 nose 可以認得 GAE 的環境,我們必須另外安裝 NoseGAE 這個 plugin。安裝 nose 還有 NoseGAE 很簡單,可以透過 pip 來安裝。

pip install nose
pip install nosegae

下面用一個例子來看怎麼樣對 Datastore 跟 Memcache 做測試。首先建立一個檔案 models.py,這邊建立了一個 TestModel 的 class,只有兩個 properties。GetEntityViaMemcache 會先查看在 memcache 中是否有資料,如果有的話就直接從 memcache 中取得,否則就從 datastore 中取得再丟進 memcache 中。

from google.appengine.api import memcache
from google.appengine.ext import db

class TestModel(db.Model):
    number = db.IntegerProperty(default=42)
    text = db.StringProperty()  

def GetEntityViaMemcache(entity_key):
    entity = memcache.get(entity_key)
    if entity is not None:
        return entity
    entity = TestModel.get(entity_key)
    if entity is not None:
        memcache.set(entity_key, entity)
    return entity

接著來寫 unit test,在這邊,我們除了要 import 必要的 memcache 以及 db 外,也別忘了 import testbed。

import unittest
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.ext import testbed
import models

class DemoTestCase(unittest.TestCase):

    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.init_datastore_v3_stub()
        self.testbed.init_memcache_stub()

    def tearDown(self):
        self.testbed.deactivate()

    def testGetEntityViaMemcache(self):
        entity_key = str(models.TestModel(number=18).put())
        retrieved_entity = models.GetEntityViaMemcache(entity_key)
        self.assertNotEqual(None, retrieved_entity)
        self.assertEqual(18, retrieved_entity.number)

最後也記得要建立一個 app.yaml,都完成之後,可以直接輸入

nosetests -v --with-gae

如果一切無誤,應該可以看到

testApp.test_index ... ok
testGetEntityViaMemcache (testModels.DemoTestCase) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.146s

OK

這樣就完成了 unitest。

Instance on Google App Engine

gae logo

自從 Google App Engine 從原本用 cpu time 計價改成 instance time 計價之後,很多人抱怨價格變高太多,甚至有看到有些人得比之前多付數十倍之譜。不過與其抱怨變貴,不如在抱怨之前先來看看 GAE 上面一些基本的東西。這邊就是對 instance 的一些基本觀念整理和介紹,大部分的資訊都是來自於官方文件

基本概念

GAE 當中是使用 instance 來做 scaling,一個 instance 提供了所有 application 所需要的東西,包含 language runtime、API、application code 和 memory,每個 instance 之間不會彼此互相影響。instance 可以依照產生的方式分兩種:

  • dynamic instance: GAE 自動產生出來處理 request 的 instance
  • resident instance: 可以讓 user 設定,一直會在開機狀態的 instance。resident instances 存在的目的有二,一個是可以改善 application 的 performance (減少 start-up time),另外就是可以讓 backends 處理比較大的 task

Scaling

基本的概念就是依照 request 的多寡來產生 dynamic instance,這部份每個 application 都會有不同的公式來決定要不要產生新的 instance。大致的概念是這樣的,每個 dynamic instance 都會有自己的 queue 來應付 incoming request,如果 GAE 發現在 queue 裡面的 request 太多,就會產生新的 instance 來處理。當然當loading 減輕時,GAE 也會自動把一些 instance 關閉讓你不用花那麼多錢。在 GAE 當中可以設定 Max Instance 來設定最多產生幾個 instance,雖然會讓 performance 變差,不過可以讓你錢不會無止盡的亂燒…

Loading Request

當 GAE 產生一個新的 instance 的時候,會有 loading time 做初始化。發生在處理這個 instance 的第一個request 的時候,這種 request 叫做 loading request,這種 request 通常都會耗費較長的時間處理。要減少 loading time 可以這麼做:

  1. 只要 load 程式需要的程式碼
  2. 盡量少存取磁碟
  3. 有時候比起 loading 許多分開的檔案,從 .zip 檔做 loading 會快些

除了減少 loading time,GAE 也可以自動送出 warmup request 給新產生的 instance,這樣可以避免讓第一個 request 的 user latency 太長。 預設 warmup request 是關閉的,要開啟可以設定 app.yaml 如下:

inbound_services:
- warmup 

Always on

如果開啟了這個 feature,GAE 會產生 3 個 resident instances,儘管沒有 traffic,這些 instance 還是會在。如果 resident instances 被砍掉,也會馬上生出一個新的 instance 並且送一個 warmup request 給它。不過要注意的是如果瞬間 traffic 太多導致於必須產生很多 dynamic instances,儘管有 always on 也會讓新的 instance 有 loading time。

The Instance View

GAE 當中有提供管理介面可以讓你查看目前 instance 的一些資訊,在 instance view 裡面可以看到的資訊有:

  • Average Queries Per Second (QPS) over the last minute
  • Average Latency over the last minute
  • The number of requests received in the last minute
  • The Age, or how long the instance has been running
  • Current memory usage
  • The instance’s availability, either resident (in other words, Always On) or dynamic.

所以總結一下,如果我們希望不要讓 GAE 開太多 instance 把你的錢燒光,你可以做的事情有:

  • 盡量把你的程式寫好,這是最重要的一點,之後會特地針對這個議題來整理一下在 GAE 上面有關 scalability 該怎麼做。
  • 減少 loading time
  • 設定 max instance

大概看完之後應該對於 GAE 的 instance 有些基本的概念,當發現自己的帳單飆高的時候應該可以看看是不是有哪些該做的事情沒有做。GAE 會幫你處理一些事情,不過不代表你真的只要把程式丟上去之後啥都不管就好。

在 GitHub 當中使用的 work flow

github logo

在 git workflow 的相關文章當中,最出名的就是 git-flow,不過 git-flow 的最大問題就是實在是太複雜了。所以我一直想找個比較簡單的方法, Scott Chacon 這篇文章主要就是講在 GitHub 內部使用的方法,以下是簡單的整理。

GitHub Flow

在 git-flow 當中,是圍繞著 release 的概念所構成,不過在 GitHub 當中其實沒有真正所謂的 release,通常 GitHub 每天都會做 deploy,所以另外搞個 release branch 太麻煩了。在 GitHub 當中, 他們使用 git 的 principle 如下:

  • 所有在 master branch 的程式都是 deployable 的
  • 如果要增加新功能,從 master 當中開一個敘述此新功能的 branch (像是 new-oauth2-scopes 之類的)
  • 在本地端 commit 到這個 branch,並且經常性的 push 到 server 上面同名的 branch
  • 如果需要 feedback、幫助或是你完成這個功能可以 merge 回去的時候的時候,pull request
  • 如果有人 review 過並且簽名之後,就可以 merge 回 master
  • 只要 merge 之後馬上 deploy 程式碼

相對於 git-flow 那一堆有的沒的 branch,GitHub flow 的規則簡單許多,下面分別對每項 principle 再做些說明。

在 master branch 當中的任何東西都是 deployable 的

這是整個 GitHub flow 當中最重要的規定,所有在 master branch 上面的 code 都必須是可以 deploy 上去的,也就是說必須通過測試還有可以 build。所以必須在其他 branch run 過 test 才可以 merge 回 master branch。

在 master branch 中開一個敘述新功能的 branch

當要開始做新功能的時候,就直接從 master branch 開一個新的 branch,branch 名稱就是敘述新功能在幹啥。這有幾點好處,一個就是你可以看到別人在做啥東西,另外就是如果有一陣子沒碰這個 branch,之後回來繼續搞的時候可以很容易的回想起來之前在幹啥。因為在 github 中可以直接在 Branch List 看到目前的所有 branch,所以可以很明確清楚的看到接下來可能會有哪些 features 以及目前的狀況。

經常性的 push 到同名的 branch

既然只要確保 master branch 不要被搞爛就好,那麼 push 到其他的 branch 也不會造成太多的困擾。經常性的 push 除了可以讓你的 source code 有備份之外,也是個和他人保持溝通的管道。

隨時都可以發 pull request

在 GitHub 中,可以用 pull request 來當做 code review system,GitHub 的 pull request 基本上就是拿來溝通用的。

只有在 review 過後才 merge

在 feature branch merge 回 master 前,基本上你的 code 都要讓公司裡面的其他人看過並且簽名。只要 review 過程 ok 而且這個 feature branch 通過 CI,就可以 merge 回 master

Review 過後馬上 deploy 程式碼

在你的 code merge 回 master 後,就馬上 deploy。

結論

相對於 git-flow 來說,GitHub 的 work flow 簡單許多,對於會在一段時間之內做一次 formal release,或是必須對於不同的 branch 做維護的團隊來說,git-flow 會是個很好的選擇。 不過對於經常 testing 以及 deploying 的團隊來說,比較簡單的 GitHub flow 會比較符合需求。