何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『Real World HTTP』with Python

requestsを使ったら負けかなと思っている

『Real World HTTP』を読み始めたが、自分でも実際に書いてみるのもよいと思うのでPythonで書いてみた。 PythonのHTTPクライアントというと標準モジュールurllib.requestではなくrequestsが主流であるが、requestsをそのまま使うのでは勉強にならないので今回は標準モジュールの範囲内で頑張る。 「電池付き」であるPythonで実行するという時点ですでに「勉強にならない」可能性もあるが今回はHTTPを学ぶので一旦忘れよう。

www.oreilly.co.jp

1.1.1 テストエコーサーバーの実行

あるディレクトリをルートにしたHTTPサーバーを実行する場合は単純にpython -m http.server 18888とすれば実行できる。 今回はエコーサーバーであるのでhttp.serverにあるクラスを使って組み立てる必要がある。 ドキュメントには詳しい方法が書いていないので少し難儀した。 以下の例のようにdo_*メソッドを実装する必要がある。

import datetime
import http
import http.server


class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if "Cookie" in self.headers:
            body = b"<html><body>Hello</body></html>"
        else:
            body = b"<html><body>Welcome</body></html>"

        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(body))
        self.send_header('Set-Cookie', "LAST_ACCESS={}".format(datetime.datetime.now().isoformat()))
        self.end_headers()
        self.wfile.write(body)

    def do_HEAD(self):
        body = b"<html><body>hello</body></html>"
        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(body))
        self.end_headers()

    def do_POST(self):
        self.data_string = self.rfile.read(int(self.headers['Content-Length']))
        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(self.data_string))
        self.end_headers()
        self.log_message(self.data_string.decode("utf8"))


def run(server_class=http.server.HTTPServer, handler_class=HTTPRequestHandler):
    server_address = ('', 18888)
    httpd = server_class(server_address, handler_class)
    httpd.serve_forever()


if __name__ == "__main__":
    run()

POSTメソッドの実装が若干面倒であるが、Content-Lengthが必要となることは『Real World HTTP』に書いてある。 また、Cookieの取り扱いも行いたいのでGETのヘッダーにCookieを入れるようにしてある。

3.4 GETメソッドの送信と、ボディ、ステータスコード、ヘッダーの受信

Pythonの標準モジュールでGETメソッドを実行する場合はurllib.requestモジュールのurlopenを使う。

import urllib.error
import urllib.request


def main():
    try:
        with urllib.request.urlopen("http://localhost:18888") as response:
            html = response.read()
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)
            print(html.decode("utf8"))
            

    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()
Status Code: 200
Reason: OK
Server: BaseHTTP/0.6 Python/3.5.3
Date: Sat, 22 Jul 2017 05:21:48 GMT
Content-type: text/html; charset=utf-8
Content-length: 31


<html><body>hello</body></html>

3.5 GETメソッド+クエリーの送信

Pythonの標準モジュールでクエリ文字列のURLエンコードを行う場合はurllib.parseurlencode函数を使う。

import urllib.error
import urllib.parse
import urllib.request


def main():
    try:
        data = urllib.parse.urlencode({"query": "hello world"})
        url = "http://localhost:18888" + "?" + data
        with urllib.request.urlopen(url) as response:
            html = response.read()
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)
            print(html.decode("utf8"))


    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()

このスクリプトを実行すると、エコーサーバー側のログに次のような結果がロギングされる。

127.0.0.1 - - [22/Jul/2017 15:01:36] "GET /?query=hello+world HTTP/1.1" 200 -

3.6 HEADメソッドでヘッダーを取得

本文では、

HEADメソッドの実装は簡単です。http.Getの代わりにhttp.Head()を利用するだけです。

とあるように、メソッドをすり替えるだけでよいが、Pythonの標準モジュールの場合はそう簡単にはことが進まない。 GETやPOSTの場合はurllib.request.urlopenに直接URL(文字列)を渡せばよいが、PUTやHEADの場合はurllib.request.Requestオブジェクトを渡す必要がある。 urllib.request.Requestオブジェクトを生成する際にHTTPメソッドを指定する。

import urllib.error
import urllib.parse
import urllib.request


def main():
    try:
        request = urllib.request.Request(url="http://localhost:18888", method="HEAD")
        with urllib.request.urlopen(request) as response:
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)


    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()

なお、本文にはhttp.Getとあるが、正しくはhttp.Get()と表記するべきだと思う。

3.7 x-www-form-urlencoded形式のPOSTメソッドの送信

POSTで送信する内容は事前にBytesに変換する必要がある。ドキュメントを参照。

import urllib.error
import urllib.parse
import urllib.request


def main():
    try:
        data = urllib.parse.urlencode({'test': "value"})
        data = data.encode("ascii")
        with urllib.request.urlopen("http://localhost:18888", data) as response:
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)


    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()

3.10 クッキーの送受信

標準モジュールでCookieを扱うには複雑な手順を踏む。 まず、Cookieを保存するhttp.cookiejar.CookieJarクラスのインスタンスjarを作成し、そのインスタンスを備えるurllib.request.OpenerDirectorクラスのインスタンスopenerを作成する。 urllib.request.urlopenの代わりにopeanerでURLにアクセスする。 複雑ではあるものの、Go言語の場合と似たような手順を踏んでいることがわかる。

import http.cookiejar
import urllib.error
import urllib.request


def main():
    jar = http.cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
    for _ in range(2):
        try:
            with opener.open("http://localhost:18888") as response:
                html = response.read()
                print("Status Code:", response.code)
                print("Reason:", response.reason)
                print(response.headers)
                print(html.decode("utf8"))
                print()

        except urllib.error.HTTPError as err:
            print(err)

        except urllib.error.URLError as err:
            print(err)


if __name__ == "__main__":
    main()

クライアント側の出力は以下の通り。

Status Code: 200
Reason: OK
Server: BaseHTTP/0.6 Python/3.5.3
Date: Sat, 22 Jul 2017 15:48:44 GMT
Content-type: text/html; charset=utf-8
Content-length: 33
Set-Cookie: LAST_ACCESS=2017-07-23T00:48:44.741324


<html><body>Welcome</body></html>

Status Code: 200
Reason: OK
Server: BaseHTTP/0.6 Python/3.5.3
Date: Sat, 22 Jul 2017 15:48:45 GMT
Content-type: text/html; charset=utf-8
Content-length: 31
Set-Cookie: LAST_ACCESS=2017-07-23T00:48:45.761960


<html><body>Hello</body></html>

3.13 自由なメソッドの送信 及び 3.14 ヘッダーの送信

urllib.request.Requestオブジェクトを生成する際にヘッダーやメソッドを指定することができる。

3.15 国際化ドメイン

Pythonの場合は文字列のエンコーディングidnaを指定すればよい。

def main():
    src = "日本語ドメイン.jp"
    idna_src = src.encode("idna")
    print("{} -> {}".format(src, idna_src))


if __name__ == "__main__":
    main()

出力結果は以下の通りになる。

日本語ドメイン.jp -> b'xn--eckwd4c7c5976acvb2w6i.jp'

「エンジニアは業務外に勉強するべきか論」とシグナリング

われわれはかしこいので

ソフトウエアエンジニアは業務時間外にも勉強すべきか、という議論は度々話題に上がる。 先日も株式会社アクシアの米村社長の書いたエントリが話題になった。

axia.co.jp

以前、大学院生の時に学部生のゲーム理論セミナーの聴講をしていた。 そこで情報不完全ゲームにおけるシグナリングというのを知った。 「エンジニアは業務外に勉強するべきか論」と関係があると思い、岡田『ゲーム理論 新版』から引用しつつ自分の考えを整理したい。

www.yuhikaku.co.jp

情報不完備な労働市場

ゲームの流れ

  1. 企業が1人の労働者を雇用したいと考えている。
  2. 労働者には能力が高いタイプと低いタイプの2通り存在し、それぞれの割合は1 : 2と推測されている。
  3. 能力が高いタイプを雇った場合、低いタイプを雇った場合の企業の利得はそれぞれ1, -1である。雇わない場合の利得は0である。

労働者のタイプが事前にわからない情報不完備な労働市場の場合、もし企業が労働者を雇うならば、企業側の期待利得は

{ \displaystyle \frac{1}{3} \times 1 + \frac{2}{3} \times (-1) = -\frac{1}{3} }

となり、企業側の最適な戦略は労働者を雇わないことになる。 能力が低い労働者だけでなく能力が高い労働者も雇ってもらえないことになる。

労働者のシグナリング

プレイヤーがある行動を選択することで自分のタイプを顕示しようとする行為をシグナリングと呼ぶ。 今回のケースでは労働者が自身の教育に投資する行為がシグナリングとなる。

ゲームの流れ

  1. 最初に労働者が教育に投資するか否かの選択肢を持つ。
  2. 企業は労働者の投資の選択を知ったうえで雇用するか否かを決定する。
  3. 労働者の利得は雇用されれば6、雇用されなければ0とする。
  4. 教育の投資コストは、能力の高い労働者は3、能力の低い労働者は8とする。

まず、能力の低い労働者の場合、教育に投資しない戦略は教育に投資する戦略を支配している。よって能力の低い労働者は教育に投資しない。 労働者が教育に投資するならば、企業はその労働者の能力が高いと推測できるから雇用する。

労働者が教育に投資しない場合、労働者の能力が高い条件付き確率は、 能力が高い労働者が教育に投資する確率を{ p }とすると、 { \displaystyle \frac{p}{p + 2} }となり、{ \displaystyle \frac{p}{p + 2} \lt \frac{1}{2} }より、労働者が教育に投資しない場合の企業の最適戦略は労働者を雇用しないことである。 この企業の最適戦略を前提にすると、能力者の高い労働者の最適戦略は教育に投資することである。

以上より、能力の高い労働者は教育に投資する、能力の低い労働者は教育に投資しない、企業は教育に投資する労働者のみを雇用する。

ソフトウエアエンジニアは業務時間外にも勉強すべきか?

あくまでも上記の前提だと「教育に投資すること」がシグナリングになる、という話であり、本当に業務時間外に勉強することがシグナリングになるかどうかはわからない。 それでも、僕は大学院生時代にこの話を聞いてシグナリングの重要性を感じた。 自分の経験を言えば、業務時間外で学んだことが有利になったこそすれ不利になったことはない。 僕としては、業務時間外の勉強を進めて自分が有利になるように立ち振る舞いたいと考えている。

pipは何の略語なのか

pipとはPythonのパッケージ管理ツールである。 Python 3.4以降ではデフォルトでついてくる。 私がPythonに触り始めた学部生のころはまだeasy_installが現役であったが、今ではpip install -r requirements.txtで必要なサードパーティモジュールを仮想環境に導入する時代である。 ずっとpip、ピップと言い続けてきたが、pipの名称そのものについて気にする場面はなかった。

今回、Python関係のとある本を読んでいたら

pip short for Pip Install Python

という紹介がされていた。 再帰的頭字語だったのか。 Wikipediaには「Pip Install Python」に加えて「Pip Install Package」の略でもあるらしい。pip (package manager) - Wikipedia

@aodagさんに教えていただいたことによると、古いバージョンのドキュメントには「pip installs packages」と確かに記載されている。

pypi.python.org

バージョン1.3以降はそのような記述は見受けられない。

pypi.python.org

リリースノートを眺めても再帰的頭字語による説明が削除されたのかがわからない。

Release Notes — pip 9.0.1 documentation

Githubのコミットログを調べてみた。確かに「pip installs packages」という記述が削除されているものの、何故削除されたのかまではわからない。

github.com

結論は、以前は「Pip Install Package」という再帰的頭字語であったが、現在は何かの略語ではない、ということになる。