[译] 设计 Pythonic API
DarSMY
8年前
<p>原文: <a href="/misc/goto?guid=4959676924726884801" rel="nofollow,noindex">Designing Pythonic APIs</a></p> <p>当编写一个包(库)的时候,为它提供一个良好的API,几乎与它的功能本身一样重要(好吧,至少你想要让别人使用),但怎么才算一个良好的API呢?在这篇文章中,我将尝试通过比较Requests和Urllib(Python标准库的一部分)在一些经典的HTTP场景的使用,从而提供关于这个问题的一些见解,并看看为什么Requests已经成为了Python用户中的事实上的标准。</p> <ul> <li>在我们的探究过程中,我们会使用 <strong>Python 3.5</strong> 和 <strong>Requests 2.10.0</strong> 。</li> </ul> <p>** 此博文是我上周的一个本地Python聚会( <a href="/misc/goto?guid=4959676924814746339" rel="nofollow,noindex">PywebIL</a> )上的演讲的改编。你可以 <a href="/misc/goto?guid=4959676924894799282" rel="nofollow,noindex">在这里</a> 找到幻灯片。</p> <h2><em>requests</em> vs. <em>urllib</em></h2> <h3>用例1:发送一个GET请求</h3> <pre> <code class="language-python">import urllib.request urllib.request.urlopen('http://python.org/')</code></pre> <pre> <code class="language-python"><http.client.HTTPResponse at 0x7fdb08b1bba8></code></pre> <pre> <code class="language-python">import requests requests.get('http://python.org/')</code></pre> <pre> <code class="language-python"><Response [200]></code></pre> <p>显式(API端点)优于隐式</p> <ul> <li>注意到requests对于它要做的事更简洁(因此,更清晰)。</li> <li><em>urllib</em> 被看成隐式发送GET请求,因为它并不接受一个 data 参数</li> <li><em>requests</em> 函数明确表明它要做什么。</li> </ul> <p>有用的对象表示</p> <ul> <li>当检查它的时候, <em>requests</em> 返回了一个带有请求状态码的帮助字符串 (通过实现 __repr__() 方法来完成)。</li> <li><em>urllib</em> 仅仅返回默认的(不清晰的)对象表示</li> </ul> <p>代码片段</p> <p>( <a href="/misc/goto?guid=4959676924974160124" rel="nofollow,noindex">requests/api.py</a> ):</p> <pre> <code class="language-python">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)</code></pre> <ul> <li>所有的HTTP动作在发送之前都遵循相同的流程,因此,有一个 request() 主流程函数。</li> <li>为每个调用 request() 的动作实现一个“辅助函数”,启用我们正在寻找的明确性。</li> </ul> <h3>用例2:获取请求状态码</h3> <pre> <code class="language-python">import urllib.request r = urllib.request.urlopen('http://python.org/') r.getcode()</code></pre> <pre> <code class="language-python">import requests r = requests.get('http://python.org/') r.status_code</code></pre> <p>无需getter和setter</p> <ul> <li>将对象属性作为实际属性访问(而不是进行方法调用)让代码更清晰些。</li> <li>如果你是从其他OOP语言过来的 (嗯…… Java),那么你可能会使用getter和setter,从而允许未来对对象属性进行改变。在Python中不需要这样,仅需使用 <a href="/misc/goto?guid=4959676925051643196" rel="nofollow,noindex"> @property </a> 装饰器。</li> </ul> <p>代码片段</p> <p><a href="/misc/goto?guid=4959676925131418190" rel="nofollow,noindex">http/client.py</a> :</p> <pre> <code class="language-python">class HTTPResponse(io.BufferedIOBase): # ... def getcode(self): return self.status</code></pre> <ul> <li><em>urllib</em> (或实际上是 <em>http</em> )使用一个“getter”来返回类属性。</li> </ul> <h3>用例3:编码、发送和解码POST请求</h3> <pre> <code class="language-python">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)</code></pre> <pre> <code class="language-python">import requests url = 'http://www.httpbin.org/post' data = {'name' : 'Michael Foord'} response = requests.post(url, data=data) response.json()</code></pre> <p>轻松访问常用功能</p> <ul> <li><em>requests</em> 为数据编码和加载JSON响应提供了一个开箱即用体验,然而在 <em>urllib</em> 中,你必须自己实现这些部分。</li> <li>在设计你的API时考虑:我的包被用的频率多高?我可以添加什么插件,从而使得使用更容易?</li> </ul> <p>同时注意, <em>requests</em> 还提供了一种优雅的方式来发送JSON内容:</p> <pre> <code class="language-python">import requests url = 'http://www.httpbin.org/post' data = {'name' : 'Michael Foord'} response = requests.post(url, json=data) response.json()</code></pre> <h3>用例4:发送鉴权请求</h3> <p>下面为HTTP请求创建了持久性凭证,然后发送请求:</p> <pre> <code class="language-python">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)</code></pre> <pre> <code class="language-python">import requests session = requests.Session() session.auth = ('user', 'pswd') session.get('https://api.github.com/user')</code></pre> <p>但如果我们只是想进行一次HTTP调用呢?我们需要所有的代码吗?这里, <em>requests</em> 允许你这样:</p> <pre> <code class="language-python">import requests requests.get('https://api.github.com/user', auth=('user', 'pswd'))</code></pre> <p>为简单和高级使用提供可能性</p> <ul> <li>当发送单个请求,和为多个请求发送一个更详细的请求时, <em>requests</em> 允许简洁使用。</li> <li>当用户需要一个简单的用例时,不要让他经过一个漫长的过程。</li> </ul> <p>比起自己创建一个,更喜欢使用Python数据类型</p> <ul> <li><em>requests</em> 对Python数据结构的使用使得它非常容易使用。没有必要去了解内部的 <em>requests</em> 包。</li> </ul> <p>库代码</p> <p><a href="/misc/goto?guid=4959676925208328122" rel="nofollow,noindex">requests/models.py</a> :</p> <pre> <code class="language-python">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)</code></pre> <ul> <li><em>requests</em> 在内部将 (user,pass) 元组转换成一个鉴权类。</li> </ul> <h3>用例5:处理错误</h3> <pre> <code class="language-python">from urllib.request import urlopen response = urlopen('http://www.httpbin.org/geta') response.getcode()</code></pre> <pre> <code class="language-python">--------------------------------------------------------------------------- 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</code></pre> <pre> <code class="language-python">import requests r = requests.get('http://www.httpbin.org/geta') r.status_code</code></pre> <p>让用户选择如何处理错误</p> <ul> <li>有些程序员喜欢异常,而有些喜欢检查。</li> <li>在某些情况下,检查更优雅,而有时正好相反。</li> <li>让你的用户根据实际情况选择使用哪个比较好。</li> <li>默认返回代码允许这样,而默认 exceptions 并不会。</li> </ul> <p>使用示例:</p> <pre> <code class="language-python">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')</code></pre> <pre> <code class="language-python">Page not found</code></pre> <pre> <code class="language-python">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')</code></pre> <pre> <code class="language-python">Page not found</code></pre> <pre> <code class="language-python">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')</code></pre> <pre> <code class="language-python">Page not found</code></pre> <p>目前就是这样了。在准备这个演讲/文章的过程中,我学到了很多(Ele注,在翻译的时候我也学到了很多,O(∩_∩)O~),我希望你也读读它。我会很高兴在下面或者在推ter (@noamelf)上看到你的评论(Ele注:欢迎去原文评论哈)。</p> <p>更新 (2016年8月8日)</p> <p>如果你像许多人,包括我自己一样,最终好奇为什么在Requests和Urllib之间有如此鲜明的差异。Nick Coghlan在 <a href="/misc/goto?guid=4959676925294106129" rel="nofollow,noindex">下面的注释</a> 和下面的一篇博文(标题自解释): <a href="/misc/goto?guid=4959676925378872241" rel="nofollow,noindex">它解决了什么问题?</a> (Ele注:刚好翻译了这篇的中文版)中分享了它关于这个问题的广阔的视角。</p> <p> </p> <p>来自:https://github.com/ictar/pythondocument/blob/master/Others/设计Pythonic API.md</p> <p> </p>