[譯] 設計 Pythonic API

DarSMY 9年前發布 | 12K 次閱讀 Python開發 Pythonic API

原文: Designing Pythonic APIs

當編寫一個包(庫)的時候,為它提供一個良好的API,幾乎與它的功能本身一樣重要(好吧,至少你想要讓別人使用),但怎么才算一個良好的API呢?在這篇文章中,我將嘗試通過比較Requests和Urllib(Python標準庫的一部分)在一些經典的HTTP場景的使用,從而提供關于這個問題的一些見解,并看看為什么Requests已經成為了Python用戶中的事實上的標準。

  • 在我們的探究過程中,我們會使用 Python 3.5Requests 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 裝飾器。

代碼片段

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

讓用戶選擇如何處理錯誤

  • 有些程序員喜歡異常,而有些喜歡檢查。
  • 在某些情況下,檢查更優雅,而有時正好相反。
  • 讓你的用戶根據實際情況選擇使用哪個比較好。
  • 默認返回代碼允許這樣,而默認 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

 

 本文由用戶 DarSMY 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!