设计 Pythonic API
当编写一个包(库)的时候,为它提供一个良好的 API,几乎与它的功能本身一样重要(好吧,至少你想要让别人使用),但怎么才算一个良好的 API 呢?在这篇文章中,我将尝试通过比较 Requests 和 Urllib(Python 标准库的一部分)在一些经典的 HTTP 场景的使用,从而提供关于这个问题的一些见解,并看看为什么 Requests 已经成为了 Python 用户中的事实上的标准。
- 在我们的探究过程中,我们会使用 Python 3.5 和 Requests 2.10.0 。
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()
200
import requests
r = requests.get('http://python.org/')
r.status_code
200
无需 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
404
让用户选择如何处理错误
- 有些程序员喜欢异常,而有些喜欢检查。
- 在某些情况下,检查更优雅,而有时正好相反。
- 让你的用户根据实际情况选择使用哪个比较好。
- 默认返回代码允许这样,而默认
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~),我希望你也读读它。我会很高兴在下面或者在 Twitter (@noamelf) 上看到你的评论(Ele 注:欢迎去原文评论哈)。
如果你像许多人,包括我自己一样,最终好奇为什么在 Requests 和 Urllib 之间有如此鲜明的差异。Nick Coghlan 在 下面的注释 和下面的一篇博文(标题自解释): 它解决了什么问题? (Ele 注:刚好翻译了这篇的中文版) 中分享了它关于这个问题的广阔的视角。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论