[譯] 設計 Pythonic API
當編寫一個包(庫)的時候,為它提供一個良好的API,幾乎與它的功能本身一樣重要(好吧,至少你想要讓別人使用),但怎么才算一個良好的API呢?在這篇文章中,我將嘗試通過比較Requests和Urllib(Python標準庫的一部分)在一些經典的HTTP場景的使用,從而提供關于這個問題的一些見解,并看看為什么Requests已經成為了Python用戶中的事實上的標準。
- 在我們的探究過程中,我們會使用 Python 3.5 和 Requests 2.10.0 。
** 此博文是我上周的一個本地Python聚會( PywebIL )上的演講的改編。你可以 在這里 找到幻燈片。
requests vs. urllib
用例1:發送一個GET請求
import urllib.request
urllib.request.urlopen('http://python.org/')
<http.client.HTTPResponse at 0x7fdb08b1bba8>
import requests
requests.get('http://python.org/')
<Response [200]>
顯式(API端點)優于隱式
- 注意到requests對于它要做的事更簡潔(因此,更清晰)。
- urllib 被看成隱式發送GET請求,因為它并不接受一個 data 參數
- requests 函數明確表明它要做什么。
有用的對象表示
- 當檢查它的時候, requests 返回了一個帶有請求狀態碼的幫助字符串 (通過實現 __repr__() 方法來完成)。
- urllib 僅僅返回默認的(不清晰的)對象表示
代碼片段
( requests/api.py ):
def request(method, url, **kwargs):
with sessions.Session() as session:
return session.request(method=method, url=url, **kwargs)
def get(url, params=None, **kwargs):
kwargs.setdefault('allow_redirects', True)
return request('get', url, params=params, **kwargs)
def post(url, data=None, json=None, **kwargs):
return request('post', url, data=data, json=json, **kwargs)
- 所有的HTTP動作在發送之前都遵循相同的流程,因此,有一個 request() 主流程函數。
- 為每個調用 request() 的動作實現一個“輔助函數”,啟用我們正在尋找的明確性。
用例2:獲取請求狀態碼
import urllib.request
r = urllib.request.urlopen('http://python.org/')
r.getcode()
import requests
r = requests.get('http://python.org/')
r.status_code
無需getter和setter
- 將對象屬性作為實際屬性訪問(而不是進行方法調用)讓代碼更清晰些。
- 如果你是從其他OOP語言過來的 (嗯…… Java),那么你可能會使用getter和setter,從而允許未來對對象屬性進行改變。在Python中不需要這樣,僅需使用 @property 裝飾器。
代碼片段
class HTTPResponse(io.BufferedIOBase):
# ...
def getcode(self):
return self.status
- urllib (或實際上是 http )使用一個“getter”來返回類屬性。
用例3:編碼、發送和解碼POST請求
import urllib.parse
import urllib.request
import json
url = 'http://www.httpbin.org/post'
values = {'name' : 'Michael Foord'}
data = urllib.parse.urlencode(values).encode()
response = urllib.request.urlopen(url, data)
body = response.read().decode()
json.loads(body)
import requests
url = 'http://www.httpbin.org/post'
data = {'name' : 'Michael Foord'}
response = requests.post(url, data=data)
response.json()
輕松訪問常用功能
- requests 為數據編碼和加載JSON響應提供了一個開箱即用體驗,然而在 urllib 中,你必須自己實現這些部分。
- 在設計你的API時考慮:我的包被用的頻率多高?我可以添加什么插件,從而使得使用更容易?
同時注意, requests 還提供了一種優雅的方式來發送JSON內容:
import requests
url = 'http://www.httpbin.org/post'
data = {'name' : 'Michael Foord'}
response = requests.post(url, json=data)
response.json()
用例4:發送鑒權請求
下面為HTTP請求創建了持久性憑證,然后發送請求:
import urllib.request
gh_url = 'https://api.github.com/user'
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, gh_url, 'user', 'pswd')
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
opener = urllib.request.build_opener(handler)
opener.open(gh_url)
import requests
session = requests.Session()
session.auth = ('user', 'pswd')
session.get('https://api.github.com/user')
但如果我們只是想進行一次HTTP調用呢?我們需要所有的代碼嗎?這里, requests 允許你這樣:
import requests
requests.get('https://api.github.com/user', auth=('user', 'pswd'))
為簡單和高級使用提供可能性
- 當發送單個請求,和為多個請求發送一個更詳細的請求時, requests 允許簡潔使用。
- 當用戶需要一個簡單的用例時,不要讓他經過一個漫長的過程。
比起自己創建一個,更喜歡使用Python數據類型
- requests 對Python數據結構的使用使得它非常容易使用。沒有必要去了解內部的 requests 包。
庫代碼
def prepare_auth(self, auth, url=''):
"""Prepares the given HTTP auth data."""
# ...
if auth:
if isinstance(auth, tuple) and len(auth) == 2:
# special-case basic HTTP auth
auth = HTTPBasicAuth(*auth)
- requests 在內部將 (user,pass) 元組轉換成一個鑒權類。
用例5:處理錯誤
from urllib.request import urlopen
response = urlopen('http://www.httpbin.org/geta')
response.getcode()
---------------------------------------------------------------------------
HTTPError Traceback (most recent call last)
<ipython-input-45-5fba039d189a> in <module>()
1 from urllib.request import urlopen
----> 2 response = urlopen('http://www.httpbin.org/geta')
3 response.getcode()
/usr/lib/python3.5/urllib/request.py in urlopen(url, data, timeout, cafile, capath, cadefault, context)
161 else:
162 opener = _opener
--> 163 return opener.open(url, data, timeout)
164
165 def install_opener(opener):
/usr/lib/python3.5/urllib/request.py in open(self, fullurl, data, timeout)
470 for processor in self.process_response.get(protocol, []):
471 meth = getattr(processor, meth_name)
--> 472 response = meth(req, response)
473
474 return response
/usr/lib/python3.5/urllib/request.py in http_response(self, request, response)
580 if not (200 <= code < 300):
581 response = self.parent.error(
--> 582 'http', request, response, code, msg, hdrs)
583
584 return response
/usr/lib/python3.5/urllib/request.py in error(self, proto, *args)
508 if http_err:
509 args = (dict, 'default', 'http_error_default') + orig_args
--> 510 return self._call_chain(*args)
511
512 # XXX probably also want an abstract factory that knows when it makes
/usr/lib/python3.5/urllib/request.py in _call_chain(self, chain, kind, meth_name, *args)
442 for handler in handlers:
443 func = getattr(handler, meth_name)
--> 444 result = func(*args)
445 if result is not None:
446 return result
/usr/lib/python3.5/urllib/request.py in http_error_default(self, req, fp, code, msg, hdrs)
588 class HTTPDefaultErrorHandler(BaseHandler):
589 def http_error_default(self, req, fp, code, msg, hdrs):
--> 590 raise HTTPError(req.full_url, code, msg, hdrs, fp)
591
592 class HTTPRedirectHandler(BaseHandler):
HTTPError: HTTP Error 404: NOT FOUND
import requests
r = requests.get('http://www.httpbin.org/geta')
r.status_code
讓用戶選擇如何處理錯誤
- 有些程序員喜歡異常,而有些喜歡檢查。
- 在某些情況下,檢查更優雅,而有時正好相反。
- 讓你的用戶根據實際情況選擇使用哪個比較好。
- 默認返回代碼允許這樣,而默認 exceptions 并不會。
使用示例:
from urllib.request import urlopen
from urllib.error import URLError, HTTPError
try:
response = urlopen('http://www.httpbin.org/geta')
except HTTPError as e:
if e.code == 404:
print('Page not found')
else:
print('All good')
Page not found
from requests.exceptions import HTTPError
import requests
r = requests.get('http://www.httpbin.org/posta')
try:
r.raise_for_status()
except HTTPError as e:
if e.response.status_code == 404:
print('Page not found')
Page not found
import requests
r = requests.get('http://www.httpbin.org/geta')
if r.ok:
print('All good')
elif r.status_code == requests.codes.not_found:
print('Page not found')
Page not found
目前就是這樣了。在準備這個演講/文章的過程中,我學到了很多(Ele注,在翻譯的時候我也學到了很多,O(∩_∩)O~),我希望你也讀讀它。我會很高興在下面或者在推ter (@noamelf)上看到你的評論(Ele注:歡迎去原文評論哈)。
更新 (2016年8月8日)
如果你像許多人,包括我自己一樣,最終好奇為什么在Requests和Urllib之間有如此鮮明的差異。Nick Coghlan在 下面的注釋 和下面的一篇博文(標題自解釋): 它解決了什么問題? (Ele注:剛好翻譯了這篇的中文版)中分享了它關于這個問題的廣闊的視角。
來自:https://github.com/ictar/pythondocument/blob/master/Others/設計Pythonic API.md