日々のあれこれφ(..)

もっぱら壁打ち

【Python】 Requestsモジュールのコードを読む

プログラミングの設計を知りたく、ここ最近はOSSPythonのコードを読むなどしています。

その中でRequestsモジュールを選んだのは、おそらく一番広く使われていて量も少なく読みやすそうだったからです。

本記事ではRequestsモジュールの中で、HTTPステータスコードがどのように定義されているかを忘備録がてら解説します。

github.com

ステータスコードの参照

>>> import requests
>>> requests.codes['ok']
200
>>> requests.codes['OK']
200
>>> requests.codes['\o/']
200
>>> requests.codes.get('internal_server_error')
500
>>> requests.codes['hoge']
>>> 

codesという変数で名前からステータスコードが引けるようになっています。

[]とgetでアクセスできることから、一見ただの辞書のようですが、辞書と異なる点に次のような挙動があります。

  • 一部のもの(\o/など)を除き、大文字と小文字でどちらでも引ける
  • 存在していないキーを入れてもKeyErrorにはならず、Noneが返る

これらの挙動は次のようなコードで定義されています。

ステータスコードの定義

ブログでは最小限の箇所のみを取り上げています。実際のコードにコメントが丁寧に書いてあるので本家を見ることをお勧めします。

requets/requests/status_codes.py

from .structures import LookupDict

_codes = {  # 1
    200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\o/', '✓'),
    201: ('created',),
}

codes = LookupDict(name='status_codes'). # 3

def _init():
    for code, titles in _codes.items(): 
        for title in titles:
            setattr(codes, title, code)  # 2
            if not title.startswith(('\\', '/')):
                setattr(codes, title.upper(), code)

    def doc(code):
        names = ', '.join('``%s``' % n for n in _codes[code])
        return '* %d: %s' % (code, names)

    global __doc__
    __doc__ = (__doc__ + '\n' +
               '\n'.join(doc(code) for code in sorted(_codes))
               if __doc__ is not None else None)

_init()
  1. まず_codes変数の中で、ステータスコードと対応する名前が100番代から500番代まで全て定義されています。

  2. それを_init()の中で、codesという実際に使う変数に、setattr()で属性値として大文字と小文字両方で定義しています。

  3. codesはLookupDictクラスインスタンスになります。

setattr(codes, title, code)codes.title = code と同義です。

LookupDictの定義は以下のファイルにあります。

requets/requests/structures.py

class LookupDict(dict):
    """Dictionary lookup object."""

    def __init__(self, name=None):
        self.name = name
        super(LookupDict, self).__init__() # 1

    def __repr__(self):  # 2
        return '<lookup \'%s\'>' % (self.name)

    def __getitem__(self, key):  # 3
        # We allow fall-through here, so values default to None

        return self.__dict__.get(key, None)

    def get(self, key, default=None): # 4
        return self.__dict__.get(key, default)

LookupDictクラスは辞書を継承したオブジェクトになります。

  1. 継承している親クラスの処理も行われるように定義されています。Python3ではsuper()の引数は省略できるのですが、Requestsモジュールは広く使われているからか、全体的にPython2系と互換性のある書き方がされている箇所が多くあり、ここもそうなっているようです。

  2. __repr__()repr(codes)をしたときの返り値を定義しています。ちなみにreprは「representation」の略称らしいですね。

  3. __getitem__()codes['ok'] の形で呼び出されたときの返り値を定義しています。

  4. get()codes.get('ok') での返り値を定義しています。この定義がないと、getで呼び出してもNoneが返ります。扱いは辞書だけど実際にはcodesの属性にステータスコードが定義されているだけで、codes自体はただの空の配列のためです。

おわりに

コードの読み方に関しては手探りです。とりあえずディレクトリの構造を把握しつつ、全体を軽く見て、基本的なところ(今回はgetリクエストをしたときの処理の部分)を読みつつ、気になった部分をメモしてさらに読んでいくみたいな流れで今は行っています。

ステータスコードの部分は短く分かりやすかったので取っ掛かりに丁度よかったです😃