編寫軟體包(庫)時,設計良好的 API 與軟體包的功能同樣重要(當然,前提是你想讓別人使用),那麼好的 API 的標準是什嗎?在本文中,筆者將會比較 Requests 庫和 Urllib 庫(屬於 Python 標準庫)在一些典型的 HTTP 使用情境下的差異,並依此發表一些筆者的看法,同時討論一下 Requests 庫為何在 Python 使用者群中成為實際上的標準庫。
接下來的討論中我們將會用到 Python 3.5 和 Requests 2.10.0。
這篇文章改編自上周我在本地的 Python 聚會上的演講。讀者可以在這裡找到演講的投影片。
Requests 和 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 庫是在省略data參數的情況下發送 Get 請求,這種方式要更加隱晦
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()函數作為主要的流程式控制制函數。
所有的 HTTP 動作都有一個對應的“輔助函數”,然後在輔助函數中調用request()函數,這使得我們的函數調用更加明確。
用例 2:擷取請求狀態代碼
import urllib.request
r = urllib.request.urlopen('http://python.org/')
r.getcode()
200
import requests
r = requests.get('http://python.org/')
r.status_code
200
不需要 getters 和 setters
通過讀取屬性的方式(而不是調用方法)擷取對象的特效能使代碼更加清晰。
如果讀者接觸過其他物件導向的語言(比如說 Java),你可能會通過設定 getters 和 setters 來修改對象的屬性。在 Python 中則不必如此,讀者只需使用 @property裝飾器就能完成這一目標。
程式碼片段
http/client.py:
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 庫內部的結構。
庫代碼
requests/models.py
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
404
讓使用者選擇處理錯誤的方式
有些程式員傾向於用異常的方式處理錯誤,而有些則傾向於用檢查的方式處理錯誤。
某些情境下檢查的方式更為優雅,而另一些情境下則恰恰相反。
合理的做法是讓使用者可以選擇處理錯誤的方式。
預設返回錯誤碼可以實現上面所述的合理做法,而預設採用異常處理錯誤則無法實現。
用例:
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
以上所述就是這篇文章的全部內容。在準備這次演講和這篇文章的過程中,筆者收穫頗豐,也希望讀者在閱讀的過程中同樣能有所收穫。讀者可以通過在文章下方評論或者在 Twitter 上留言的方式(@noamelf)向我提供建議,我非常樂於傾聽這些建議