顯示具有 python 標籤的文章。 顯示所有文章
顯示具有 python 標籤的文章。 顯示所有文章

2015年8月17日 星期一

為什麼需要 async programming 以及相關技術的演進

幾種使用 async programming 的情境:

GUI: 像是Web、iOS、Android apps。在 event-driven 的架構下用 callback 滿直覺的, 但是為了避免卡住 UI thread 而需要連續多個 non-blocking I/O 才能完成一件事時,寫起來就不直覺了。這時新增 thread 用 blocking I/O 寫起來比較直覺,但是不容易處理 thread-safety。

server: 像是 Web server、API server、reverse proxy。要能同時承受上千筆連線,必須用 non-blocking I/O。遇到 CPU 吃重的工作時,可以轉交給其它專門的 server (如向資料庫讀資料或用 Lucene 處理 text search)。

其它像是一些會用到多筆 http request 取資料的小程式,例如用 Gmail API 取出所有信件的附件,或是 stress test 的工具,或是 Web crawler,這些也會用 non-blocking I/O 加速程式 (或用 multi-thread/multi-process + blocking I/O 也行,最近才寫了個小工具 prunner.py)。

共通需求

  • 用 non-blocking I/O 避免卡住服務或縮短完成時間。
  • 用 single thread 降低實作複雜度。

為了滿足上述需求,主流的解法是用 single-thread event loop。但遇到 CPU bound 時還是得開新 thread。

注意這個共通需求對 Web page 和 server 是必要需求,但對其它情境只是「優先希望的作法」,Android、iOS 或 crawler 還是可以選擇新增 thread/process 使用 blocking I/O,然後自行承擔 multi-thread/multi-process 衍生的問題。

non-blocking I/O 的問題

single-thread event loop 只有幫忙監聽所有 non-blocking I/O 的動靜,用 callback 的方式通知結果。用 callback 處理 GUI 反應很自然,但是需要依序發數個 http request 的時候,在 callback 裡跳來跳去就頭大了,很難看出程式的流程。

換句話說,我們需要以下功能:

  • 用 synchronous API 來使用 non-blocking I/O。方便看出控制流程。
  • 執行中可以得到 call stack,方便除錯。

提供底層使用 non-blocking I/O 的 synchronous API

如何在不增加新的語法下提供這樣的功能?換句話說,我們還是想新增 thread 用「看起來像 blocking I/O 但底層是 non-blocking I/O」的 API。Python 的 gevent 提供一個不錯的解法:

  • 以 event loop 為底,實作 coroutine (由 greenlet 完成)
  • 提供 synchronous API + non-blocking I/O 實作 (用 yield 主動讓出執行權)
  • task scheduler 會在 I/O 完成後再回到當初 yield 處,讓使用 API 的人用起來像 blocking I/O 一般
剩下的唯一的問題是:所有程式都要用 gevent 提供的 I/O API。實務上這很難作到,所以 gevent 另外提供 monkey patch 處理沒有用 gevent API 的 third-party library。去除 monkey patch 的不穩因素 (比方說 lxml.etree.parse(URL) 會用 native code 從網路讀資料而 block main thread),效果意外地不錯。除了 Python 以外,Go 也有 goroutine,不過我沒研究也沒相關使用經驗。

值得一提的是,coroutine 是 non-preemptive multitasking,比使用 native thread 的 multi-thread (preemptive multitasking) 容易避免 race condition,不過代價是要小心不要寫出 busy loop 卡住整個 process。

新語法:async/await

相對於 Python 陣營有龐大使用 blocking I/O third-party library 的問題,JavaScript 陣營 (Node.js / Browser) 天生就使用 non-blocking I/O (長年以來只有一個 thread 的自然結果),這點很有優勢。

JavaScript 不用 coroutine 而是發展出兩種方式提供 synchronous API:

  • Promise: 看起來像 synchronous API,只是囉嗦了點,還有可能會搞混執行順序。
  • async/await: 基於 Promise 定義新的語法,簡化使用並突顯使用 async 的程式。

詳情見The long road to Async/Await in JavaScript。另外 What Is Asynchronous Programming? 和 Managing Asynchronous Code - Callbacks, Promises & Async/Await 也值得一看。

另外,Python 也會在 3.5 加入新語法 async/await,用它們明確地定義 coroutine,藉此區分 generator。async/await 去掉 thread 的概念,應該會比 coroutine 適合。但對習慣 multi-thread 的人來說,可能 coroutine 比較親切吧?

整體來說,未來繼續用 Node.js 或 Python 寫 server 程式還是不錯的選擇,語法簡單、用戶眾多又有廣大的 third-party library 可用 (目前各用 gevent 和 Node.js 寫過一次上線的服務)。

雜談

話說大家都用 synchronous API 提供 non-blocking I/O 後,還能稱這為 async programming 嗎?不過語法裡有 async/await啦。具體來說,async programming 的定義到底是什麼......

2015年8月14日 星期五

prunner.py: 連猴子都會用的加速框架

問題

寫 python script 時有時會遇到 I/O bound (例如需發出大量 HTTP request) 或 CPU bound, 這時可用 multiprocessing 加速。但是自己寫 multi-process 有點麻煩:

  • 要留意 API 使用細節。
  • 容易發生 race condition 或 dead lock。
  • 程式較複雜時會切成多個階段作業, 有時會閒置部份 processes 而浪費了加速的機會。
  • 辛苦寫好的平行架構不易重覆使用 (我至少重寫三次以上...泣)。

解法

prunner.py 是我寫好的框架, 中心思想是透過 task queue 提供容易重覆使用的架構, 還有協助減少閒置的 process。

以計算 sum( (i+1) * 2 ) 為例, 下面的程式會用很蠢的方式透過 10 個 processes 平行計算出結果:

import prunner

def begin():
    prunner.get_dict()['sum'] = 0
    prunner.post_task(init, range(2000))

def init(numbers):
    for i in numbers:
        prunner.post_task(add_one, i)

def add_one(n):
    prunner.post_task(double, n + 1)

def double(n):
    prunner.post_task(sum_up, n)
    prunner.post_task(sum_up, n)

def sum_up(n):
    with prunner.global_lock():
        prunner.get_dict()['sum'] += n

def end():
    print prunner.get_dict()['sum']

prunner.init(10, False, begin, end)
prunner.start()

每個 function call 會在一個 process 上執行, 藉此增加使用到的 CPU。也可以盡情地用 blocking I/O, 反正有很多 processes 會平行執行程式。

若不習慣 message loop 的寫法, 也可以用較 low level 的 API, 直接繼承 ParallelTaskRunner 然後覆寫 begin(), run(), end():

import prunner

TASK_INIT = 'task_init'
TASK_ADD_ONE = 'task_add_one'
TASK_DOUBLE = 'task_double'
TASK_SUM = 'task_sum'

class MyRunner(prunner.ParallelTaskRunner):
    def begin(self, options):
        self.dict_['sum'] = 0
        self.queue.put(prunner.Task(TASK_INIT, range(2000)))

    def run(self, task):
        if task.label == TASK_INIT:
            for i in task.data:
                self.queue.put(prunner.Task(TASK_ADD_ONE, i))
            return

        if task.label == TASK_ADD_ONE:
            self.queue.put(prunner.Task(TASK_DOUBLE, task.data + 1))
            return

        if task.label == TASK_DOUBLE:
            self.queue.put(prunner.Task(TASK_SUM, task.data))
            self.queue.put(prunner.Task(TASK_SUM, task.data))
            return

        if task.label == TASK_SUM:
            with prunner.ScopeLock(self.lock):
                self.dict_['sum'] += task.data

    def end(self):
        print self.dict_['sum']


runner = MyRunner(10, False, None)
runner.start()

和前面一樣, 同時有多個 processes 執行 run(), 每次呼叫帶有不同的資料。

雜談

1. 因為底層是 multiprocessing, prunner.py 可以處理 CPU bound 和 I/O bound, 直接用 blocking I/O 也沒問題, 方便配合 third-party library。

不過 I/O 需求量極高時, 還是用 gevent 較適當, 但是要找合 gevent 的 third-party library 或用 gevent monkey patch。

2. 這個架構中比較需要巧思的部份是判斷程式的中止條件並且提供有效率的實作。中止條件是「所有 process 閒置且 queue 是空的」。

各個 process 有一個獨立的 flag 表示閒置與否, 這樣比用一個共用的計數器有效率。在我的電腦上執行 prunner_example.py 的時候, 獨立 flag 和計數器的時間分別是 2.5s 和 3.6s。

3. 雖然可以用 post_task() 執行 instance method, 不過會隱含 serialize/deserialize instance 以及傳到其它 process 的成本。這是用框架的缺點, 用起來方便, 但不小心會造成不必要的 overhead。我寫了兩個例子比較: prunner_example3.py 用 instance method, pyrunner_example2.py 用 function。執行時間分別是 2.8s 和 2.6s。

4. 試過各種架構後, 覺得 task queue (或稱 message queue) 還是最直覺的運作方式。有想過 producer-consumer 或 MapReduce 的架構, 不過還是 task queue 最易使用。這種單機層級的運算, 用 task queue 應該綽綽有餘。若要跨機器執行, 太過彈性可能會增加實作的複雜度。那時應該用別人作好的成熟工具。

2015年8月10日 星期一

用 grequests 作 stress test

作了一個 C10K 的 server 後, 還要作 stress test 才能驗證 server 真的挺得住 C10K 的連線。試了幾套工具, 最後覺得 grequests 最好用 (用 gevent monkey patch 的 requests), 用法就這麼簡單:

import grequests

def exception_handler(request, exception):
    print "Request failed: %s" % exception

url = 'SERVER_URL'
rs = [grequests.post(url, data={...})
      for i in range(10000)]
rss = grequests.map(rs, size=1000, exception_handler=exception_handler)

這樣就寫好一個會同時產生 1k 個連線, 總共產生 10k 個連線的 client。因為是直接寫程式, 可以自訂各式各樣的內容, 測得出真正的效果, 不會被簡單的 cache 消掉絕大多數的 requeests。若要改用 get 就用 grequests.get(url) 產生 request, 詳細用法見 requests。

幾個測試情境和作法:

  • Connection bound: 提升 size 數, 或多跑幾個 client
  • CPU bound: 同上, 故意使用耗 CPU 的 API
  • Memory bound: 依 API 的性質, 產生 1MB 或更大的資料, 由 data=... 傳過去

grequests.map() 不設 size 的話, 會盡可能產生最多連線, client 有可能自己先爆掉。依自己的網路環境和 server 狀況, 可試看看不同的值。

執行前記得先用 ulimit -n 提升 open file number, 不過要用 root 才能提升, 作法如下:

$ sudo bash
$ ulimit -n 50000
$ su YOUR_ID

這樣可以用一般帳號測試, 但保有 max open file number = 50k。

以下是我試過但不管用的方法, 憑印象寫下放棄使用的原因 (不見得是工具不好):

  • ab: 很久以前用過覺得不夠順手, 查到文章推薦用 httperf, 這回我就跳過 ab 了。
  • httperf: 雖然有 --wsesslog 可以讀文字檔產生不同內容, 還是不夠彈性。另外有遇到 open too many files 的問題, 似乎要編程式才能提升 limit, 就懶得再試了
  • loadtest: 看起來不錯, 可用 -R 指定 script自訂程式, 由 loadtest 幫忙處理其它事, 不過沒有試成功, 就試別套了
  • nodeload: 還不錯用, 可以自訂程式, 執行後還會自動產生報表, 附一個 local http server 跑在 port 8000 可以即時看報表變化。不過量大了以後會炸, 有些 bug, 作者又說沒在維護了, 就放棄它了。
  • Btw, 用這些工具的時候, 都要自己設好 http header 才可以使用 POST, 不如 requests 直覺。

2015年7月14日 星期二

Python, JavaScript, C++, Java 和 Unicode 以及 UTF-8 等編碼

基本觀念

Unicode 是一張表,定義每個文字對應的數字是什麼,比方說 'A' 是 65, '我'是 25105。

UTF-8, UTF-16, UTF-32, UCS-2, UCS-4 等則是 Unicode 的各種編碼。所謂的編碼,是某種定好的儲存格式 (或說 serialization/deserialization format 也許比較好懂?), 方便讓不同的應用程式之間傳遞資料。

比方說應用程式 A 寫入文字到檔案裡,應用程式 B 從檔案讀出文字,兩者要說好用的編碼格式 (或能從資料辨別使用的編碼格式), 才有辦法互通資料。舉例來說, 網路傳輸常用 HTTP 作為溝通協定, HTTP 有提供 client 或 server 指定傳輸內容編碼的方法 (Accept-Charset 和 Content-Type), 可以指明是用 UTF-8 或其它編碼。

寫程式的時候,基本上是假設外部讀來的資料是某種 Unicode 編碼 (最常用 UTF-8 ), 內部使用 Unicode 處理,要傳出去 (寫入硬碟/網路傳輸) 前再轉回 Unicode 編碼。用 Unicode 的好處是,可以正確計算字串長度, 這影響到使用 length, index, 取出 substring, 比對 substring (包含 regexp) 等字串 API。

這裡有以前寫的介紹,有多提一點相關的事。

Python

Python 2 就有定義 type unicode 和 type str。type str 可看成一般性的 binary, 以 byte 為單位。Python 2 的字串 '...' 的型別是 str, 要特別用 u'...' 才會改用 unicode。

有了 type unicode, 處理 unicode 比較輕鬆:

  1. 外部讀進來的字 (UTF-8) 存在 type str, 馬上轉成 unicode
  2. 函式的輸入輸出都用 unicode (len(), [] 的行為正確)
  3. 要輸出到螢幕、檔案、網路時再轉成 UTF-8。

參考 All About Python and Unicode 和 Unicode in Python: Common Pitfalls 了解細節。

另外, Python 可能用 UCS-2 或 UCS-4 實作 Unicode, 可從 sys.maxunicode 的值得知, 65535 表示用 UCS-2。因為 UCS-2 / UCS-4 每個字分別占 2 / 4 bytes, 對使用的記憶體的量有很大的影響,不同作業系統會有不同考量, 像 Mac OS X 用 UCS-2, Ubuntu 12.04 用 UCS-4。然後在 Python 3.3 後都支援 UCS-4 了 (因為有實作 PEP 0393 -- Flexible String Representation)。

例如 u'𝌆' 會被當成兩個字 [*1]:

In [1]: u'我'
Out[1]: u'\u6211'

In [2]: len(u'我')
Out[2]: 1

In [3]: u'𝌆'
Out[3]: u'\U0001d306'

In [4]: len(u'𝌆')
Out[4]: 2

Btw, Python 3 改變字串預設的型別: '...' 是 unicode, 要用 b'...' 才會用 str,用意是希望大家預設就用 Unicode 避免混亂,不過卻造成 2 升 3 的一些問題。直接引用以前碎碎唸的心得:

在 python2.7 踩到 python3 unicode 的雷,import unicode_literals 會讓 'abc' 自動變成 u'abc' ( 原本的行為是 ‘abc' == b'abc') ,難怪怎麼看都不太對勁,third-party code 寫 'abc',別的 third-party package 預期收到 raw string 卻收到 unicode string, 於是回報 TypeError: 'unicode' does not have the buffer interface

用 ipdb 除錯時覺得相當奇怪,一樣的程式碼 (如 s = 'abc'),在 ipdb 內執行得到的結果是 raw string, 但原本的程式執行時卻變 unicode ....

搜了一下才看到也有不少人反應這點: Proposal to not use unicode_literals #22 https://github.com/PythonCharmers/python-future/issues/22

JavaScript

JavaScript’s internal character encoding: UCS-2 or UTF-16?: 詳細到不行的介紹,沒耐性就直接看文中的 conclusion 吧。

結論是 JavaScript 直接用 UCS-2 表示字元, 不像 Python 分成 str 和 unicode。開發者幾乎不用操心 Unicode、UTF-8 之類的事, 反正在 JavaScript 內都是用 UCS-2 表示。缺點是超出 65535 的 Unicode 不會被正確處理:

> '我'.length
1
> '𝌆'.length
2

不過網頁還是可以正確顯示字的外觀,大概是因為瀏覽器內部用 UTF-16 處理,字的 Unicode 仍然是對的,畫字時有選對 font glyph。但是受限於 JavaScript 的語言規範,只能斷成多個字表示。

C++

C++ 沒有定義型別 Unicode, 要自己用 std::string, std::wstring 或其它型別表示。

查到幾種轉換方法

  • 用 C++11
  • 用 Boost
  • 用 icu
  • 用 utf8-cpp

最後一個作法最簡單, include 幾個 header 檔即可,不像其它方法要改 compiler 參數或引入一大包函式庫。所以我後來是用 utf8-cpp, 待要非得用到其它方法時再來研究為啥需要其它方法。stackoverflow 裡有很多討論 (由此可見 C++ 使用 Unicde 的方法有多混亂....), 這是其中一篇。

Java

Java 的基本單位 char 一樣用 UCS-2 實作, 但 String 提供另外的 API 可正確存取 Unicode。直接引用別人的結論:

Han Guokai wrote on 20th January 2012 at 18:28:

In Java5, ‘char’ means code unit (16-bit), not character (code point). One character uses one or two chars. String’s length method return the char count. And there are some additional methods based on code points, i.e. codePointCount. See: http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html

比方說 Java String 提供 charAt 和 codePointAt。後者是正確的 Unicode 字元,而前者是 char, 遇到 UCS-2 外的字一樣會出錯。

結論

總算弄清楚過去常用的四種語言實作 Unicode 和 Unicode 編碼的方法,結果四種語言有四套作法, 真是出乎意料出外...。推薦閱讀JavaScript’s internal character encoding: UCS-2 or UTF-16? 了解 BMP, supplementary planes 和 surrogate pairs, 會比較清楚 JavaScript 和 Java 使用 UCS-2 作為字元定義遇到的問題 (為了省記憶體, 是必要的 trade-off)。

備註

1. Blogspots 無法正式顯示這個字, 可從這裡看到測試用的字。https://codepoints.net/ 可找到更多範例。

2015/07/19 更新

經 Scott 指正, 更新內文對 Python Unicode 實作的描述。

2015年6月19日 星期五

server push by long polling

程式碼

沒興趣讀落落長心得的人, 這裡有用 Tornado 實作的程式和測試碼。

心得

server push 是指從 server 主動送訊息給 client, 這裡有圖解一些達成 server push 的方式。其中我個人偏好 long polling 的作法,運作方式就和字面一樣: client 先發出一個連線, server 不要立即回應。等 server 需要主動通知 client 時, 再回傳資料。

這個作法的好處是:

  • 各種 client 都適用, 只要能用 http 連線即可, 不是瀏覽器也OK, 現在各個平台都有好用的 http 函式庫。
  • 不用擔心 client 網路環境問題。client 能主動建立往 server 的連線, 反過來就很難說了。

另一方面, long polling 的缺點是 server 會有許多閒置的連線占資源。像 apache 這類每個 client 用獨立的 thread 處理連線的作法, 就不適合處理 long polling。假設一個 thread 占據 10MB 空間, 1k 個不作事的連線, 就占掉 10G 記憶體了。另外大量 thread 之間的 context switch 也是可觀的時間負擔。

但是改用 epoll 之類的 API 寫成 event-driven 的架構, 可用 single process single thread 的方式同時處理大量連線, 就沒有浪費記憶體和 context switch 的時間負擔。現在有不少現成的工具使用 epoll 包成 green thread, 使用上和 multi-thread 一樣容易上手 (而且還不用擔心 race condition, 因為實際上只有一個 native thread),降低實作門檻。其中 Tornado 是 Python 寫成的 web framework 並內建可上線用的 web server。我作了簡單的 benchmark, 的確可以快速反應一秒內同時擁入的一千個連線。用 Tornado 省去不少工夫 (若不想用 web server 而是自己從頭作 server, 也可用 gevent)。

實作 long polling 的另一個問題是: 如何在察覺資料更新時, 能立即通知 client? 想像 client 透過連線 S 連往 server, server 先不作回應。接著 server 在其它地方取得新資料, 再透過 S 回傳資料。處理連線 S 的程式要怎麼暫停? 之後要怎麼返回執行? 這個作法可以 scale 嗎?

對於 single process 程式來說, 直覺且低成本的作法是: 在 thread A 讀資料, 發覺沒資料時改用 condition 變數 (搭配 lock) 停住程式, 然後在 thread B 收到新資料時, 用同一個 condition 變數通知 thread A 可以繼續讀資料。對應到 Tornnado 的寫法是用 locks.Condition:

condition = locks.Condition()

@gen.coroutine
def waiter():
    print("I'll wait right here")
    yield condition.wait()  # Yield a Future.
    print("I'm done waiting")

@gen.coroutine
def notifier():
    print("About to notify")
    condition.notify()
    print("Done notifying")

@gen.coroutine
def runner():
    # Yield two Futures; wait for waiter() and notifier() to finish.
    yield [waiter(), notifier()]

io_loop.run_sync(runner)

有了 locks.Condition, 要實作一個完整可上線使用的 long polling 只是小事一椿, 下面的程式碼實作兩個 API /get 和 /set: 提供基本的 "get value by key" 和 "set value for key"。/get 沒資料時會先停住不回應, 等到 /set 取得資料後再立即返回。這只是示意用程式, 沒防錯也沒控管記憶體的用量:

class Entry(object):
   ...

g_cache = {}

class GetHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def post(self):
        global g_cache

        key = self.get_argument("key", None)
        entry = g_cache.get(key, None)
        if not entry:
            entry = g_cache[key] = Entry()

        value = entry.get_value()
        if value is not None:
            self.write(json.dumps({
                'error_code': ERROR_OK,
                'value': value,
            }))
            return

        # Wait the data to be ready.
        timeout = self.get_argument("timeout", DEFAULT_TIMEOUT)
        now = tornado.ioloop.IOLoop.current().time()
        timeout = now + float(timeout)
        condition = entry.get_condition()
        try:
            yield condition.wait(timeout=timeout)  # Yield a Future.
        except Exception, e:
            logging.exception('condition timeout? timeout=%s' % str(timeout))
            self.write(json.dumps({'error_code': ERROR_TIMEOUT}))
            return

        value = entry.get_value()
        self.write(json.dumps({
            'error_code': ERROR_OK,
            'value': value,
        }))


class SetHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def post(self):
        global g_cache

        key = self.get_argument("key", None)
        value = self.get_argument("value", None)
        entry = g_cache.get(key, None)
        if not entry:
            entry = g_cache[key] = Entry()
        entry.set_value(value)
        condition = entry.get_condition()
        condition.notify_all()
        self.write(json.dumps({'error_code': ERROR_OK}))

application = tornado.web.Application([
    (r"/get", GetHandler),
    (r"/set", SetHandler),
])

完整的程式見開頭的連結。

2015年2月6日 星期五

跳掉檔案開頭數 bytes 的作法

假設有個大檔案 (e.g. 500MB) 在開頭存有 meta data, 後面存有內容, 要怎麼跳掉開頭的 meta data, 只留下內容呢?

命令列的作法

從這裡看到兩個有效率的作法:

使用 subshell: 先用一個 dd 跳掉開頭 1131 bytes (block size=1131, read 1 block), 再用第二個 dd 用正常的 block size 讀寫檔, 這樣效率就和直接用 dd 複製檔案差不多了。

$ ( dd bs=1131 count=1 of=dev_null && dd bs=4K of=out.mp3 ) < 100827_MR029_LobbyControl.mp3

使用 pipe 配合 a group of commands: 概念差不多, 只是改用 pipe。

$ dd if=filtered.dump bs=512k | { dd bs=1131 count=1 of=/dev/null; dd bs=512k of=trimmed.dump; }

Python 不建立新檔的作法

產生新檔案的時間成本有點高, 可能會花到數秒。如果是 Python 程式內要用的話, 可以直接用 file object 然後想辦法跳掉開頭資料。

偷懶的作法是直接用 file object, 然後先呼叫 file.read(N) 跳掉開頭 N bytes。缺點是若 file object 會傳入 third-party lib 使用, 其它程式碼可能會用 seek() 回頭讀開頭的內容。

保險的作法是自訂一個 file object wrapper。這個 wrapper 行為和內建 file object 一模一樣, 只是讀不到開頭數個 bytes。這樣效率和一般的 file object 幾乎沒差。 程式碼見這裡, 另外這裡是測試碼。關鍵是覆寫和移動檔案位置有關的 seek 和 tell。然後用 Python 提供的 __getattr__(self, attr) 實作剩下沒改到的 method。

那麼, 要怎麼知道該覆寫那些 method 呢? 用 ipython 建一個 file object, 然後利用補字功能, 再一個個看 method 說明, 很快就知道該覆寫那些了。

以下是示意的操作過程:

$ ipython

In [1]: f = open('/etc/fstab')

In [2]: f.<TAB>
f.close       f.encoding    f.fileno      f.isatty      f.name        f.next        f.readinto    f.readlines   f.softspace   f.truncate    f.writelines
f.closed      f.errors      f.flush       f.mode        f.newlines    f.read        f.readline    f.seek        f.tell        f.write       f.xreadlines

In [2]: f.close?
Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method close of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
close() -> None or (perhaps) an integer.  Close the file.

Sets data attribute .closed to True.  A closed file cannot be used for
further I/O operations.  close() may be called more than once without
error.  Some kinds of file objects (for example, opened by popen())
may return an exit status upon closing.

In [3]: f.encoding?

...

In [21]: f.seek?

Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method seek of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
seek(offset[, whence]) -> None.  Move to new file position.

Argument offset is a byte count.  Optional argument whence defaults to
0 (offset from start of file, offset should be >= 0); other values are 1
(move relative to current position, positive or negative), and 2 (move
relative to end of file, usually negative, although many platforms allow
seeking beyond the end of a file).  If the file is opened in text mode,
only offsets returned by tell() are legal.  Use of other offsets causes
undefined behavior.
Note that not all file objects are seekable.

...

2015年1月25日 星期日

The Python Standard Library By Example

看到不錯的書: The Python Standard Library By Example, Table of Contents 有不少有用的關鍵字,知道要做什麼事的時候,可以用什麼內建模組。

另外, 這裡有書上附的範例碼, 玩玩小程式比看文件快上手。

抓網站內容和使用 lxml.html 讀取 DOM 內容

lxml 功能強大, 不過提供太多 API, 不太容易在官網找資料 (或是我太沒耐性吧...)。記錄一下抓網站取內容常用的 code snippet:

下載網頁內容轉成 lxml.html 的 Element object

import sys

import requests
import lxml.html

DEBUG = False

def get_web_content(link):
    if not link:
        return None, lxml.html.fromstring(u'<html></html>')
    try:
        r = requests.get(link)
        try:
            content = r.content.decode('UTF-8')
        except UnicodeDecodeError, ude:
            if DEBUG:
                msg = (
                    'The content is not in UTF-8 (ude=%s). '
                    'Try ISO-8859-1 instead.\n' % ude                )
                sys.stderr.write(msg)
            # Try another encoding. If fail, just let it fail.
            content = r.content.decode('ISO-8859-1')

        if DEBUG:
            sys.stderr.write('Get content of link %s: %s\n' % (link, content[:50]))
        return r.status_code, lxml.html.fromstring(content)
    except Exception, e:
        if DEBUG:
            sys.stderr.write('Fail to get content of link: %s (e=%s)\n' % (link, e))
        return None, lxml.html.fromstring(u'<html></html>')

使用 cssselect 取值

取得 lxml.html Element 後, 用 cssselect() 和 CSS path 可輕鬆取值。剩下的就是在 ipython 上試 API, 看怎麼操作 Element 物件, 這樣比看文件快。

python multiprocessing 小記

因為 CPython 有 GIL 的緣故, 需要提升 CPU 效率時, 不會用 multi-thread, 會改用 multi-process。內建模組 multiprocessing 提供許多好東西, 實作 multi-process 簡單許多。習慣用 multiprocessing 後,有時不是 CPU bound 而是 I/O bound, 我還是用 multiprocessing。反正....不是 CPU bound 的話, 多耗 CPU 也無所謂。

之前寫 crawling 的程式,為了增加同時抓網頁的數量,就用 multiprocessing 加速 [*1]。以下是兩個例子:

從例子裡找片段程式碼來用比看文件快。這裡記錄一下相關心得:

  • 若要讓不同 process 共享資料,需要用 multiprocessing 訂的物件, 有含 list, dict 等,單一物件 (如 int, float, str) 則是用 Value 指定 typecode, 如: shared_integer = multiprocessing.Value('i', 0)
  • 使用 shared data 會有 race condition, 要用 Lock 保護,不然就直接用 Queue 或 Pipe。
  • 還有 Event 和 Semaphore 等物件,要作比較複雜的機制時,可以拿來用。
  • 有提供跨機器的 multi-processing, 有需要再來細看。
  • 程式架構照 producer-consumer 的方式寫比較容易, 也就是各個 process 共用 Queue, 然後 producer 用 blocking put, consumer 用 blocking get, 另有一個 main process 監控情況, 確保程式會結束 (這大概是最麻煩的地方)。
  • 注意量大的時候 put 會因 queue full 卡住,所以不能在 main process 用 blocking put。即使很容易填入 Queue 的初始資料,用另外的 process 填入初始資料比較保險, 因為在初始資料太多時,可能因 queue full 卡住不動了。不然就是先產生 consumer process 再初始化 Queue, 避免因為沒有 consumer 而在 queue full 時卡死。我偏好用另外的 process 初始化 Queue 的資料, 因為新增 process 成本不高 (只有一次), 程式邏輯比較單純。
  • 雖然有 non-blocking put/get, 但是使用 non-blocking 操作, 要多記操作是否失敗, 失敗要另排時間重試, 還是新增 process (or thread) 用 blocking 操作易寫易懂。
  • 各種意外都有可能發生,process 因 exception 掛掉的話,可能會讓 main process 搞錯情況而不會結束,process 執行的函式最上層要加個 try-catch 確保發生意外時能重置管理 process 的狀態,確保 main process 會結束。crawl_ratings.py 分成 _parse_url() 和 _do_parse_url() 就是要處理這個問題。
  • multiprocessing 有提供 log, 可開啟輸出到 stderr, 方便追蹤各 process 情況。用法是: multiprocessing.log_to_stderr(logging.INFO)

備註

*1 遇到 I/O bound 時, 除 multi-thread, multi-process 外,還有一個選擇是用 single thread event-based 的解法。Python 有 gevent 可用。

但是 gevent 的缺點是,一但用了 gevent, 全部 I/O 都要用 gevent, 不然 main thread 就被卡住了。於是 third-party 程式也要找使用 gevent 的版本。比方說 requests 很好用,但用了 gevent 後要用 gevent 版的 requests, 用其它公司提供的 SDK, 也要自己 patch 成 gevent 版本, 所以使用前要三思。不是要作到 C10K 等級的 daemon, 還是用 multiprocessing 省事。之前作一個上線的服務是用 gevent, 為此費了一些工夫作到全盤使用 gevent。

相關資料

2015-09-01 更新

懶得研究這麼多可以考慮用 prunner.py。

2015年1月10日 星期六

Python 處理 UTF-16 的 CSV

Python 內建的 csv 模組預設無法處理 UTF-16, 直接讀的話會出現

Error: line contains NULL byte

自行轉換編碼成 UTF-8 即可,作法如下:

import codecs
import csv

# 用 codecs 解碼 utf-16 為 unicode object
f = codecs.open(filename, 'rU', 'utf-16') 

# 再轉回 utf-8 給 csv.reader 用
cr = csv.reader(line.encode('utf-8') for line in lines)
for row in cr:
   # row 的內容就是 CSV parse 完的結果

或是先用 iconv 轉檔, 再用 Python csv 模組處理亦可:

$ iconv -f utf-16 -t utf-8 file_in_utf16 > file_in_utf8

備註: Python csv 模組不接受 unicode 的輸入,若來源不是檔案,也要記得轉成 utf-8 再傳入 csv.reader()

2014年8月31日 星期日

ph: python HTML generator without using any template syntax

偶而需要用 python 產生 HTML 報表, 自己手刻 HTML 頗麻煩的。寫個小 script 不會像寫網頁那頁使用 MVC 將資料和顯示分離, 不方便套 template (況且學 template 的語法也有點麻煩)。

找了一下發現 pyh, 語法滿直覺的, 大致符合我的需求。於是自己重做了一個 ph, 改成更合自己習慣的語法, 順便練習自己設計一套簡單的 DSL, 寫起來像這樣:

from ph import *
doc = HTML()
doc.body() << p('Hello, world!') << (p('What a ') << strong('wonderful') << ' world!')
print unicode(doc)
輸出像這樣 (有手動排版過):
<!DOCTYPE html>
<html>
<head><title>No Title</title></head>
<body>
<p>Hello, world!</p>
<p>What a <strong>wonderful</strong> world!</p>
</body>
</html>

以前沒想過覆寫 operator, 覺得這種作法容易讓人困惑。但是以 HTML generator 的例子來說, 用 << 組合 HTML 元素挺方便的。

2014年8月5日 星期二

在 vim 內 indent JSON

太容易忘了,乾脆寫篇文章加強記憶:

:%!python -m json.tool

是的,其實就是呼叫 python module 來處理

2013年12月29日 星期日

在 Ubuntu 下用 gdb debug CPython

用法和其它 library 稍微不同, 安裝 python-dbg 後, 是提供另一個含有 debug symbol 的 python 執行檔 python2.7-dbg, 而不是 gdb 自己另外載入 python 的 debug symbol。

可從 dpkg -L python-dbg 的結果得知這事:

$ dpkg -L python2.7-dbg | grep bin/python
/usr/lib/debug/usr/bin/python2.7-gdb.py
/usr/lib/debug/usr/bin/python2.7
/usr/bin/python2.7-dbg
/usr/bin/python2.7-dbg-config
/usr/lib/debug/usr/bin/python2.7-dbg-gdb.py
$ nm /usr/bin/python2.7
nm: /usr/bin/python2.7: no symbols
$ nm /usr/bin/python2.7-dbg | head
                 U ASN1_INTEGER_get@@OPENSSL_1.0.0
                 U ASN1_STRING_data@@OPENSSL_1.0.0
                 U ASN1_STRING_length@@OPENSSL_1.0.0
                 U ASN1_STRING_to_UTF8@@OPENSSL_1.0.0
                 U ASN1_TIME_print@@OPENSSL_1.0.0
                 U ASN1_item_d2i@@OPENSSL_1.0.0
00000000008857e0 d AST_type
0000000000967588 b Add_singleton
00000000009675e8 b Add_type
0000000000967560 b And_singleton

實際執行的結果:

$ gdb --args python2.7-dbg -c 'import time; time.sleep(10)'
GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...
Reading symbols from /usr/bin/python2.7-dbg...done.
(gdb) r
Starting program: /usr/bin/python2.7-dbg -c import\ time\;\ time.sleep\(10\)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
^C
Program received signal SIGINT, Interrupt.
0x00007ffff69ad003 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:82
82      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) bt
#0   __select_nocancel at ../sysdeps/unix/syscall-template.S:82
#1   floatsleep at ../Modules/timemodule.c:943
#2   time_sleep at ../Modules/timemodule.c:206
#3   PyCFunction_Call at ../Objects/methodobject.c:81
#4   call_function at ../Python/ceval.c:4021
#5   PyEval_EvalFrameEx at ../Python/ceval.c:2666
#6   PyEval_EvalCodeEx at ../Python/ceval.c:3253
#7   PyEval_EvalCode at ../Python/ceval.c:667
#8   run_mod at ../Python/pythonrun.c:1353
#9   PyRun_StringFlags at ../Python/pythonrun.c:1316
#10  PyRun_SimpleStringFlags at ../Python/pythonrun.c:969
#11  Py_Main at ../Modules/main.c:583
#12  main at ../Modules/python.c:23

順便備忘一下相關的文章: Debugging C/C++ and CPython using GDB 7′s new Python extension support 提到如何在 gdb 內使用 python extension 來 debug python code, 可以從 gdb 內看到 C 函式對回去 python script 的檔名和行數。

2013年9月18日 星期三

python 快速開發: 使用 ipython 撰寫 python 程式

一但習慣某個東西的好處後, 久了就會忘了它的重要性。今天重操舊業寫了久違的 python, 覺得能用 python 寫程式已很爽了, 還能用 ipython 寫 python, 更是爽上加爽!特此記錄一下, 分享給還未試過的人。

ipython 是 python 的互動式 shell, 功能相當強大, 而且預設設定就相當好用。如今 ipython 已發展到令人難以想像的地步, 以下只是基本的使用情境。

試用別人的模組

$ ipython
Python 2.7.3 (default, Aug  1 2012, 05:14:39)
Type "copyright", "credits" or "license" for more information.

IPython 0.13.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: import os

In [2]: os.pa<TAB>
os.pardir          os.pathconf        os.pathsep
os.path            os.pathconf_names

In [2]: os.path.join?
Type:       function
String Form:<function join at 0x7f67f9c09ed8>
File:       /usr/lib/python2.7/posixpath.py
Definition: os.path.join(a, *p)
Docstring:
Join two or more pathname components, inserting '/' as needed.
If any component is an absolute path, all previous path components
will be discarded.

In [3]: os.path.join('a', 'b')
Out[3]: 'a/b'

In [4]: os.path.join??
Type:       function
String Form:<function join at 0x7f67f9c09ed8>
File:       /usr/lib/python2.7/posixpath.py
Definition: os.path.join(a, *p)
Source:
def join(a, *p):
    """Join two or more pathname components, inserting '/' as needed.
    If any component is an absolute path, all previous path components
    will be discarded."""
    path = a
    for b in p:
        if b.startswith('/'):
            path = b
        elif path == '' or path.endswith('/'):
            path +=  b
        else:
            path += '/' + b
    return path

首先我先 import os, 第二行打了 os.pa<TAB> 看看有那些名稱開頭為 "pa" 的子物件或函式可用。也可以用 os.<TAB> 看 os 下的全部物件和函式。

接著找到 os.path.join, 在它後面加上 "?", 表示想知道 os.path.join 的 python doc。了解用法後就在第三行試試, ipython 預設會輸出 expression 回傳的結果 (而且是用 pretty-print 的方式呈現)。

最後第四行用 "??" 查詢 os.path.join 的實作, 有時沒有文件或看不懂文件的話, 直接看原始碼也滿方便的。另外讀別人程式發覺 import module 順序太複雜時, 也可以先用 ipython import 進來, 再用 "??" 看最後 import 到的是那一份實作。也可從裡面提到的檔名再用編輯器打開來看。

有了 <TAB>、? 和 ?? 後, 不太需要從外部找參考手冊, 直接在 ipython 裡試比較快。

開發片段小程式

試了一些片段小程式, 了解怎麼使用別人的模組後, 再來會想拼湊一些小程式。這時可用 edit 進入編輯器 (會選用環境變數 EDITOR 設的值):

In [1]: edit
IPython will make a temporary file named: /tmp/ipython_edit_bAOgl9.py
Editing... done. Executing edited code...
Out[1]: 'class Rect(object):\n    def __init__(self, x, y, w, h):\n        self.x = x\n        self.y = y\n        self.w = w\n        self.h = h\n'

In [2]: r = Rect(0, 0, 100, 50)

In [3]: r.w
Out[3]: 100

In [4]: r.area()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-83a71df0c49c> in <module>()
----> 1 r.area()

AttributeError: 'Rect' object has no attribute 'area'


In [5]: edit -p
IPython will make a temporary file named: /tmp/ipython_edit_stFwGi.py
Editing... done. Executing edited code...
Out[5]: 'class Rect(object):\n    def __init__(self, x, y, w, h):\n        self.x = x\n        self.y = y\n        self.w = w\n        self.h = h\n\n    def area(self):\n        return self.w * self.h\n'

In [6]: r2 = Rect(0, 0, 100, 50)

In [7]: r2.area()
Out[7]: 5000

在第一行按 edit 後, 畫面會切到編輯器, 寫好內容存檔離開後, ipython 會載入剛才打的內容到 ipython 裡, 也就是第一行輸出 'class Rect(object):\n def __init__ ...' 那一串。這裡我定義了 class Rect, 然後試一下 Rect 看看有沒有寫錯。接著第五行 edit -p 表示回去編輯上一次的內容, 執行後一樣會進入編輯器, 不過內容會先有上一次打過的東西。這回我多補了 area() 函式, 於是新增的 Rect 物件就有這函式了。

拼湊差不多後, 可用 edit -p 取得內容, 另外存到檔案, 或是打 hist 看打過的內容, 手動剪貼到檔案裡。

開發完整程式

想法明確時, 直接用編輯器編寫, 再輔以 ipython 測試更有效率。

首先, 終端機A的編輯器畫面如下所示, 寫了一個階乘函數:

# foo.py
def factorial(n):
    s = 1
    for i in range(n):
        s *= i
    return s

然後在終端機B的 ipython 測試:

In [1]: import foo

In [2]: foo.factorial(3)
Out[2]: 0

In [3]: reload(foo)
Out[3]: <module 'foo' from 'foo.py'>

In [4]: foo.factorial(3)
Out[4]: 6

第二行測完發覺結果不對, 於是回終端機A修改, 將 range(n) 改為 range(1, n + 1), 再回來終端機B執行 reload(foo) 重新載入 foo。接著第四行重測就得到正確的結果了。

當然, 發覺結果不對時, 也可以在 ipython 測測 range() 的用法, 或用 range? 看看說明, 很快就會找到答案。

結語

以上只是基本的 ipython 用途, 相信開發速度已可以比單用編輯器寫 python 快上兩倍以上。強烈建議有寫 python 的人一定要用看看 ipython。

2013年4月19日 星期五

python BaseHTTPServer 速度緩慢的原因

在用 Bottle 的開發模式時, 發覺有時候速度會異常的慢。等到受不了以後, 按下 ctrl+c 看到以下的 backtrace:

Exception happened during processing of request from ('xxx.xxx.xxx.xxx', 37515)
Traceback (most recent call last):
File "/usr/lib/python2.7/SocketServer.py", line 284, in _handle_request_noblock
self.process_request(request, client_address)
File "/usr/lib/python2.7/SocketServer.py", line 310, in process_request
self.finish_request(request, client_address)
File "/usr/lib/python2.7/SocketServer.py", line 323, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/usr/lib/python2.7/SocketServer.py", line 638, in __init__
self.handle()
File "/usr/lib/python2.7/wsgiref/simple_server.py", line 121, in handle
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
File "/usr/lib/python2.7/wsgiref/simple_server.py", line 85, in get_environ
host = self.address_string()
File "/usr/lib/python2.7/BaseHTTPServer.py", line 498, in address_string
return socket.getfqdn(host)
File "/usr/lib/python2.7/socket.py", line 137, in getfqdn
hostname, aliases, ipaddrs = gethostbyaddr(name)
KeyboardInterrupt

依照過去的經驗, 這個 gethostbyaddr 相當可疑。

作個簡單測試證實問題:

$ python -c 'import socket; print socket.gethostbyaddr("xxx.xxx.xxx.xxx")'
Traceback (most recent call last):
File "<string>", line 1, in <module>
socket.herror: [Errno 2] Host name lookup failure

再來用老招 ltrace 確認問題源頭:

$ ltrace  python -c 'import socket; print 
socket.gethostbyaddr("xxx.xxx.xxx.xxx")' 2>&1 | grep gethost
( ... 略 ... )
gethostbyaddr_r(0x7fffa5bbb834, 4, 2, 0x7fffa5bb77f0, 0x7fffa5bb7830) = 0

由 man gethostbyaddr 得知它會查 /etc/hosts, 最後在 /etc/hosts 加入一筆 "xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx", 就立即有結果了:

$ python -c 'import socket; print socket.gethostbyaddr("xxx.xxx.xxx.xxx")'
('xxx.xxx.xxx.xxx', [], ['xxx.xxx.xxx.xxx'])

2013年3月20日 星期三

python memory leak

由於 CPython 使用 reference counting 管理記憶體, reference count 到 0 時會立即釋放記憶體, 遇到 memory leak 時, 至少可以用 del 稍微自救一番。

import sys

a = []
print sys.getrefcount(a)  # 2
b = a
print sys.getrefcount(a)  # 3
del b
print sys.getrefcount(a)  # 2

如上所示, 可在程式中用 sys.getrefcount() 檢查 referece count 是否符合預期, 盡量在不用以後立即呼叫 del, 當 reference count 變為 0 時, 會提前釋放記憶體。

注意 python 的 while、for 沒有形成新的 scope, 有可能因此誤判一些物件的生命週期:

for i in range(10):
    pass
print i  # 9, 並非 NameError

備註

附上 CPython 2.7.3 Include/object.h 內關於減 reference 相關的程式:

769 #define Py_DECREF(op)                                   \
770     do {                                                \
771         if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
772         --((PyObject*)(op))->ob_refcnt != 0)            \
773             _Py_CHECK_REFCNT(op)                        \
774         else                                            \
775         _Py_Dealloc((PyObject *)(op));                  \
776     } while (0)

可看出在 reference count 為 0 時, 確實會呼叫 deallocator。

今日休閒時間已用盡, 改天再來看看為什麼初始數值為 2。

2012年1月16日 星期一

Python debug 的方法

有別於 C/C++, Python 通常會有原始碼, 除非套件提供者無腦地用 egg 包裝, 不然應該滿好改程式碼的 (我討厭 egg!! 其實這有雙關, 不過 ...)。再加上使用 virtualenv 擁有自己安裝的函式庫, 這樣和別人共用 server 時, 不用擔心改函式庫的程式會影響到別人, 相當方便。

在這樣方便改程式的情況下, 通常我會採下列三種方法之一來觀察程式 (或除錯):

使用 IPython

在要觀察程式的地方寫入

import IPython
IPython.Shell.IPShellEmbed(argv=[])()  # 舊版
IPython.embed()                        # 新版
我甚至寫了個 vim 巨集展開這段。

使用 pdb

import pdb; pdb.set_trace()
效果類以上一個, 不過應該是 ipython 更易操作。或許也可考慮用 IPython pdb。習慣用 gdb 看 backtrace 的話, 用 pdb 滿直覺的。

使用 logger

配合 decorator 的語法, python 要觀察特定函式容易許多, 如這篇提到的做法, 寫個 decorator "log", 配合 lazy initialization 的方式設定 logging。之後只要在有興趣的幾個函式上加個 @log 即可。

這個作法應用的範圍更大, 像在 WSGI 沒 console 可用, 也不能輸出到 STDOUT, 用這招就沒問題。還有, 這個作法也可在 production 環境記錄關鍵函式的執行速度, 協助 profiling。

題外話

若是要除錯的話, 視情況用 unit test 會更好, 原因見《為什麼要寫 unit test?為什麼要先寫測試?》。在見識過 C/C++ 的困難處後, 覺得在 Python 的環境裡寫 test 實在是太輕鬆寫意了, 雖說寫 test 確實還是有它的難處, 不過該寫的東西之前寫得差不多了, 有興趣的人請翻翻舊文。

2011年12月25日 星期日

善用 strace、debugger 從執行期間找出問題根源

最近被迫在短時間內學會 strace、gdb 這些之前一直用不到的重兵器, 都還不熟練就是了。剛好使用 hgsubversion 時有些困擾, 雖說它和 svn 整合得很好, 無縫接好 pull / push, 但它不會顯示 mercurial 對應到的 svn 版本, 平時看其它和 svn 整合的工具 (如 issue tracking) 會很困擾, 用得都是 svn 版號。

剛剛想到可以學 Strace -- The Sysadmin's Microscope 的做法, 用 strace 找出關聯的程式, 再來看怎麼修正它。

我推測 hg 一定有記錄 svn 相關版本的方式, 不然無法和 svn server 同步資料。於是挑個會讀到 svn 資料的指令來試:

strace -o trace.log -s 512 -e read,open hg svn info

用顯示的 svn 版號 123 來翻 trace.log, 發現這兩行:

open("/path/to/project/.hg/svn/lastpulled", O_RDONLY) = 3
read(3, "123\n", 4096)                 = 5

於是找到 meta data 存在 .hg/svn/ 下。 到該目錄下找到 .hg/svn/rev_map 這個檔案, 裡面存 hg 和 svn 的版號對應表。至少這樣就有足夠的材料寫個 script 來轉換 hg、svn 的版號。

不過若能直接加到 hg 裡, 應該會更方便也較可攜。要做這點相對容易, 可以到 hgsubversion 原始碼目錄下用 ack 找相關程式。

先用 ack rev_map 找到存 meta data 的物件 revmap, 再用 ack revmap 找到 wrappers.py 是換掉 hg 指令的程式。

再來用 pdb 觀察 revmap 如何被使用。先在 wrappers.py 裡設中斷點, 然後執行 pdb /usr/local/bin/hg parents --svn 找出 meta data 如何被讀出來。於是明白可在函式 parents 的部份塞入幾行顯示 svn 版本:

--- a/hgsubversion/wrappers.py        2011-12-25 00:34:39.170606104 +0800
+++ b/hgsubversion/wrappers.py        2011-12-25 00:33:04.161800527 +0800
@@ -57,6 +57,9 @@
         raise hgutil.Abort('No parent svn revision!')
     displayer = cmdutil.show_changeset(ui, repo, opts, buffered=False)
     displayer.show(ha)
+    # patch svn revision
+    print '\033[1;32msvn revision: %d\033[0m' % hashes[ha.node()][0]
+    print
     return 0

這樣打 hg parents --svn 就會多輸出一行 svn 的版號。雖說顯示在 hg log 會更方便, 不過 wrappers.py 裡沒有 log, 之後有再找時間看看怎麼加。

附帶一提, 剛用 pdb 時還不太習慣, 忘了 python 是執行期載入程式, 不能像 gdb 那樣在執行前就指定檔名指定行數設中斷點。而是要在 script 裡直接塞

import pdb; pdb.set_trace()

2011-12-25 更新

如留言裡的討論, 上述的修正沒有實質幫助, 就當作練 debugger 吧。最後覺得另外寫個 shell script 最省事效果也最好, 寫好的東西放在這裡。

2011年12月3日 星期六

CPython 的 garbage collection

來消化之前有讀沒時間寫的東西。

長期使用 python 後, 會很納悶為啥記憶體一直漲, 明明已沒用到了, 卻沒有減少。這篇提到 CPython 的 gc 機制採用 reference counting, 另有備案的 mark and sweep, 用來解決 circular reference。btw, 強烈推薦 Back to basic: Series on dynamic memory management, 淺顯地介紹各種 gc 運作的方式。

照 Scott 的說法, reference counting 算是苦工的半自動化管理記憶體, 因為實作細節交給實作者處理, 用 Python API 寫 CPython 的 extension 時, 開發者得自己管理物件的 reference count, 是件很辛苦的事。好處是在 reference count 為 0 的時候, 會立即回收記憶體。但缺點是, 若 a、b 兩物件互相參照到對方, 卻沒被任何人用到的話, a 和 b 都無法被回收, 因為 reference count 永遠會是 1。

為解決這個問題, 只好多提供 gc.collect 喚起 mark and sweep 的演算法清掉 a、b。但 mark and sweep 也有自己的問題, 主要有兩點:

  • 不像 reference counting 會在沒用到的第一時間點立即回收, 而是 mark and sweep 執行後才回收。
  • 執行 mark and sweep 的時候為確保沒有算錯 reference graph, 必須暫停目前所有執行中的 python process/thread, 結束後才可繼續執行。可想而知, 記憶體用量大後, 這個暫停時間也會太久, 不適合需要不斷有回應的程式 (如 GUI、Web)。

所以又有了 generational garbage collection, 關鍵的想法是: 觀察到大部份物件都很早死 (英年早逝啊~), 所以只要回收年輕的物件即可回收大部份的記憶體。於是將使用到的物件分不同「年代」, 預設只回收最年輕的一代, 沒回收到的物件就放到下一代。下回要再回收的時候, 就不動這個舊一代的物件, 減少要建立的 reference graph。

回想平時寫程式的結構, 可以想見, 像頻繁使用的 local variable 仍會被這個方法回收, 偶而用到的 global variable 只有第一次會算到, 之後被當作舊一代的物件後, 就不會再檢查。這樣只要有個方式確保不會漏查舊一代指向新一代的物件, 就可確保不會誤刪仍用到的物件。至於明明可回收卻沒回收到的物件, 相較於省下的時間來說, 算是可接受的取捨。generational garbage collection 值得一看, 有提到一些巧思, 取得計算時間和空間的平衡。

可想而知, CPython 用的是 generational gc, 而 JVM 也是。看來好東西大家都會一起用。

Btw, 即使了解了這些仍無法解釋為啥 CPython 有一堆沒有歸還一堆沒在用的記憶體, 實際的情況比想像中還複雜, CPython 有一些「絕對不會歸還」的記憶體, 像是 small integer pool, 和重覆回收使用的 PyIntObject, 或是有 circular reference 且這之中有物件實作 __del__。

2011-12-04 Update

看到 Thinker 針對本篇的補充, 貼過來備忘: CPython 的 GC 二、三事

2011年11月23日 星期三

超輕量級的 python web framework: Bottle

寫完上篇冗長的 Django 心得後, 改來介紹極端相反的 web framework: Bottle。

有一則關於 Python 的笑話是這麼說的:

Python is the only language with more Web frameworks than keywords.

在初評估 web framework 時, 對照一大串 web framework 看到這則笑話, 實在是令我哭笑不得。不過在有不同需求後, 到覺得這樣也是有好處啦。

當初要做 ego-post 的時候, 想找個很方便部署的框架, 比較方便推廣, 最終目的是讓一般人也能用 (後來發覺寫成 Chrome App 更適合)。我的主要功能用 javascript 和 html5 (用到 localStorage) 實作, web 後端只是用來存長期資料而已。一開始是試 Flask, 但裝 Flask 需要另外裝 Werkzeug 和 Jinja 2, 所以最後改用 Bottle。

Bottle 有多小呢? 全部就一個 bottle.py 而已, 所以懶得要求使用者另外裝 Bottle 的話, 將 bottle.py 加到目前專案就結束了。而且它不到 3000 行, 有興趣了解最基本的 web 應用需要那些功能, 看 bottle.py 應該可以學到不少東西。像 http response code 418 I'm a teapot 這種蠢東西, 就是從 bottle.py 裡學到的。

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...