優雅的 Python 介面設計
今天跟ofollow,noindex" target="_blank">@hulucc 日常寫碼吹比, 講到了選第三方庫的原則說: “我其實發現我現在選庫不太 care 他原始碼是怎麼實現的, 但是我非常喜歡那些 api 設計得巨科學的庫。”
科學的 API
API 設計的科學大概是什麼樣的呢? 比如舉一個有名的例子就是requests 這個庫。
Requests is one of the most downloaded Python packages of all time, pulling in over 11,000,000 downloads every month.
這個庫的 API 用起來大概是這樣的:
>>> response = requests.get('https://api.github.com/user', auth=('user', 'pass')) >>> response.status_code 200 >>> response.headers['content-type'] 'application/json; charset=utf8' >>> response.encoding 'utf-8' >>> response.raise_for_status()
這裡設計的所有 Python 程式語言都是見文知意的英文人類語言,requests.get
中的requests
不僅是包名,
還化身成了程式碼語義的一部分。
返回的response
就是一個典型的 HTTP 協議物件,
只要對 HTTP 協議有一定了解的程式設計師,
基本上不用看文件都能猜到它的主要屬性和相關作用。
對應還有便捷的.raise_for_status()
和.json()
這樣的常用方法。
這就是科學的 API 給我的感受。
當然,庫的作者(也就是那個帥哥Kenneth Reitz )也清楚自己的程式碼介面優雅, 他的個人簽名也是這麼說的:
I wrote @requests: HTTP for Humans. The only thing I really care about is interface design. – Kenneth Reitz
不科學的 API
大部分開源高星專案的介面都是比較優雅的, 那麼不科學的 API 大概是什麼樣子呢? 唔,我的話,翻一翻自己兩三年前的程式碼, 就滿是不科學的 API 實現了。
最早接觸**kwargs
這個東西的時候,
我非常喜歡用這個語法,
比如我常常會寫這麼一種函式:
class Record: def create(**kwargs): now = kwargs.get('now', datetime.datetime.now()) key = kwargs.get('key') value = kwargs.get('value') ...
這樣寫的好處是看起來靈活的一比,實現起來爽。
以後假如要加引數,
往往只要在record.create
裡面加一個新的kwargs.get
就行了。
然而在大部分情況,這樣的實現只會把引數給隱式化:
記不住引數呼叫record.create
的時候還得進函式看實現;
而且萬一把value
拼錯成了valeu
,
函式是會像某些語言一樣正常執行的!
然後會在後面某個地方報錯,
這樣就很難方便找出根源了。
後來我大部分情況會這麼寫:
class Record: def create(now=None, key=None, value=None): if now is None: now = datetime.datetime.now() ...
這樣的顯式呼叫強制要求引數的正確性, 雖然實現起來要寫的引數多了, 但是呼叫和閱讀的時候更加明確。
>>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
後來我看到The Zen of Python
的這句Explicit is better than implicit
總會想到這個例子。
(關於 Python 介面引數設計的,有一篇我覺得說的很好的知乎文章:《Python函式介面的一些設計心得 - 靈劍》
)
例子
The Zen of Python
裡還有非常多珠璣可以挖掘。
比如在做的一個專案hutils
,
想著把公司裡各種 Python Web 中常用到的函式抽出來做個基礎庫,
結果寫的時候 80% 的時間都在想怎麼讓 API 變的更科學。
比如我們寫後端的時候, 經常會遇到要轉化框架錯誤類的情況:
def service_call(...): try: external_service.call() except ExternalServiceError as ex: log_error(ex) raise APIError('Error calling external service')
對應的,我們會有個這樣的裝飾器來封裝錯誤處理:
@contextlib.contextmanager def catches(*exceptions, raise_to: BaseException = None, raise_from: Callable[[Exception], BaseException] = None, log=False, ignore=False): try: yield except exceptions as ex: if log: log_error(ex) if not ignore: if raise_from: raise raise_from(ex) else: raise raise_to# pylint: disable=raising-bad-type
有了封裝的裝飾器以後, 簡單的錯誤轉化就可以跟業務程式碼相分離:
@catches(ExternalServiceError, raise_to=APIError('Error calling external service'), log=True) def service_call(...): external_service.call()
但是這樣的裝飾器實現會在 Code Review 階段就會被像@hulucc 這樣的鐵血隊友錘回來, 這樣的 API 實現有幾個不夠科學的地方:
-
raise_to
和raise_from
有重疊之處, 而且呼叫者不注意的話會觸發raise None
的問題, 連pylint
都注意到了。 應當使用型別判斷來合併引數。 -
這樣錯誤轉化,原錯誤類的堆疊資訊會丟失。
應當使用
raise ... from ...
的語法來保留堆疊資訊。 -
transfer
/ignore
/retry
其實是相對獨立的邏輯, 混合處理當然可以, 不過最好的情況是邏輯拆分,獨立處理。
一波討論以後,
順帶順手支援catches(Exception, raises=raise_api_error)
的快捷寫法,
裝飾器的實現就改成了這樣子。
@contextlib.contextmanager def catches(*exceptions, raises: Union[BaseException, Callable[[Exception], BaseException]], log=False): exceptions = exceptions or (Exception,) try: yield except exceptions as ex: if callable(raises): raises = raises(ex) if log: log_error(__name__, raises) raise raises from ex
感覺更加優雅了呢。
結語
Python 因為語法及其靈活,
所以其實介面的設計是全看程式設計師的設計水平的。
但往往科學又優雅的實現就像There should be one-- and preferably only one --obvious way to do it
這句話說的一樣,
是萬中取一的。
不僅要實現功能, 還要優雅,不要汙。 看來寫程式的確是要想得多, 怪不得程式設計師會頭髮少呀 :)
(完)