何かを書き留める何か

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

PythonによるエコーHTTPサーバーの実装

前回requestsモジュールなしでHTTPクライアント側を実装した。

xaro.hatenablog.jp

サーバー側は適当に実装したが、今回はもう少しまじめにエコーHTTPサーバーを実装してみる。 目指すは開発時に手元でさっと起動できるHTTPサーバースクリプトである。 なお、『The Python 3 Standard Library by Example』を参照した。*1

www.informit.com

$ python -m http.serverの中身

Python標準モジュールであるhttp.serverを使えば起動したディレクトリ以下にあるファイルをそのままHTTPで扱うことができる。 デフォルトのポートは8000であるが引数を渡せば別のポートでも起動ができる。

$ python -m http.server 8888

これを実行すると何が起きているのか? 実際にソースコードを参照すると次のような処理が行われている。

def test(HandlerClass=BaseHTTPRequestHandler,
         ServerClass=HTTPServer, protocol="HTTP/1.0", port=8000, bind=""):
    """Test the HTTP request handler class.
    This runs an HTTP server on port 8000 (or the port argument).
    """
    server_address = (bind, port)

    HandlerClass.protocol_version = protocol
    with ServerClass(server_address, HandlerClass) as httpd:
        sa = httpd.socket.getsockname()
        serve_message = "Serving HTTP on {host} port {port} (http://{host}:{port}/) ..."
        print(serve_message.format(host=sa[0], port=sa[1]))
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\nKeyboard interrupt received, exiting.")
            sys.exit(0)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--cgi', action='store_true',
                       help='Run as CGI Server')
    parser.add_argument('--bind', '-b', default='', metavar='ADDRESS',
                        help='Specify alternate bind address '
                             '[default: all interfaces]')
    parser.add_argument('port', action='store',
                        default=8000, type=int,
                        nargs='?',
                        help='Specify alternate port [default: 8000]')
    args = parser.parse_args()
    if args.cgi:
        handler_class = CGIHTTPRequestHandler
    else:
        handler_class = SimpleHTTPRequestHandler
    test(HandlerClass=handler_class, port=args.port, bind=args.bind)

細かい点はいくつもあるが、次の2つを実装すれば自分の好きなHTTPサーバーが実装できそうである。

  • サーバー(HTTPServerの代わりになるもの)
  • ハンドラー (BaseHTTPRequestHandlerの代わりになるもの)

HTTPサーバー

HTTPサーバーに関してはできることは少なく、http.serverにあるhttp.server.HTTPServerをそのまま使うことになる。 http.server.HTTPServersocketserver.TCPServerの単純なサブクラスである。

class HTTPServer(socketserver.TCPServer):

    allow_reuse_address = 1    # Seems to make sense in testing environment

    def server_bind(self):
        """Override server_bind to store the server name."""
        socketserver.TCPServer.server_bind(self)
        host, port = self.server_address[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

今回は開発時にさっと使えるHTTPサーバーを目指すので並列処理や並行処理を考慮するのはオーバースペックであるが、socketserver.ThreadingMixInsocketserver.ForkingMixInクラスを使えば複数の処理を非同期に処理するHTTPサーバーを実装できる。実装も簡単でsocketserver.ThreadingMixInsocketserver.ForkingMixInに多重継承すればよい。

class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
    """HTTPリクエストをスレッドに分割して処理するHTTPサーバー"""
    pass

class ForkedHTTPServer(socketserver.ForkingMixIn, http.server.HTTPServer):
    """HTTPリクエストをプロセスに分割して処理するHTTPサーバー"""
    pass

なお、socketserver.ForkingMixInos.fork()が利用可能な環境のみ利用できる。つまりWindowsでは動作しない。

`

HTTPリクエストハンドラー

ハンドラーはhttp.server.BaseHTTPRequestHandlerを継承してdo_METHODという名前のメソッドを実装するのが基本である。 HTTP 1.0の範囲ではGET, POST, HEADができれば十分である。 今回はエコーサーバーを目指すので、受け取ったパラメータをそのまま表示させればよい。 パラメータに関してはドキュメントにある属性を参照すればよい。

曲者はPOSTされたデータをどうするかである。 『The Python 3 Standard Library by Example』ではcgi.FieldStorageを駆使して処理する方法が紹介されていた。 流れとしてはBaseHTTPRequestHandler.rfilecgi.FieldStorageに渡してPOSTされた中身を処理する、となる。

スクリプトの全体図

import argparse
import cgi
import http.server
import io
import socketserver
import sys
import urllib.parse


class EchoHandler(http.server.BaseHTTPRequestHandler):
    """リクエストを処理するハンドラ"""

    def gen_message(self):
        """HTTPメソッドに共通したメッセージを生成する"""
        parsed_path = urllib.parse.urlparse(self.path)
        message_parts = [
            "CLIENT_VALUES:",
            "client_address={} ({})".format(self.client_address, self.address_string()),
            "command={}".format(self.command),
            "path={}".format(self.path),
            "real_path={}".format(parsed_path.path),
            "query={}".format(parsed_path.query),
            "request_version".format(self.request_version),
            "",
            "SERVER VALUES:",
            "server_version={}".format(self.server_version),
            "sys_version={}".format(self.sys_version),
            "protocol_version={}".format(self.protocol_version),
            "",
            "HEADERS RECEIVED:"
        ]
        for name, value in sorted(self.headers.items()):
            message_parts.append("{}={}".format(name, value.rstrip()))
        else:
            message_parts.append("")
        message = "\n".join(message_parts)
        return message

    def do_GET(self):
        message = self.gen_message()
        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-length', len(message))
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(message.encode("utf8"))

    def do_HEAD(self):
        message = self.gen_message()
        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-length', len(message))
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()

    def do_POST(self):
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                "REQUEST_METHOD": "POST",
                "CONTENT_TYPE": self.headers["Content-Type"],
            },
        )
        message = self.gen_message()
        self.send_response(http.HTTPStatus.OK)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()

        self.wfile.write(message.encode("utf8"))
        self.wfile.write("\n".encode("utf8"))

        out = io.TextIOWrapper(
            self.wfile,
            encoding="utf8",
            line_buffering=False,
            write_through=True,
        )

        out.write("FORM DATA:\n")

        for field in form.keys():
            field_item = form[field]
            if field_item.filename:
                file_data = field_item.file.read()
                file_len = len(file_data)
                del file_data
                out.write("Upload {} as {!r} ({} bytes)\n".format(field, field_item.filename, file_len))
            else:
                out.write("{}={}\n".format(field, form[field].value))
        else:
            out.detach()

class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
    """HTTPリクエストをスレッドに分割して処理するHTTPサーバー"""
    pass

class ForkedHTTPServer(socketserver.ForkingMixIn, http.server.HTTPServer):
    """HTTPリクエストをプロセスに分割して処理するHTTPサーバー"""
    pass

def runserver(server, handler, protocol="HTTP/1.0", port=8000, bind=""):
    server_address = (bind, port)
    handler.protocol_version = protocol
    httpd = server(server_address, handler)
    sa = httpd.socket.getsockname()
    serve_message = "Serving HTTP on {host} port {port} (http://{host}:{port}/) ..."
    print(serve_message.format(host=sa[0], port=sa[1]))
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nKeyboard interrupt received, exiting.")
        sys.exit(0)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--thread', action='store_true', help='Run Threading HTTP Server')
    parser.add_argument('--process', action='store_true', help='Run Processing HTTP Server')
    parser.add_argument('--bind', '-b', default='', metavar='ADDRESS',
                        help='Specify alternate bind address '
                             '[default: all interfaces]')
    parser.add_argument('port', action='store',
                        default=8000, type=int,
                        nargs='?',
                        help='Specify alternate port [default: 8000]')
    args = parser.parse_args()

    if args.thread:
        server = ThreadedHTTPServer
    elif args.process:
        server = ForkedHTTPServer
    else:
        server = http.server.HTTPServer

    handler = EchoHandler
    runserver(server=server, handler=handler, port=args.port, bind=args.bind)

使用例

開発用にさっと使えるHTTPサーバーであるので、http.serverと同じように使える。 同じように書いたのだから当たり前である。

実行例は次の通りになる。

GET

$ curl -v -X GET http://127.0.0.1:8000/foo?q=bar
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /foo?q=bar HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.52.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.5.3
< Date: Fri, 28 Jul 2017 14:07:55 GMT
< Content-length: 302
< Content-Type: text/plain; charset=utf-8
<
CLIENT_VALUES:
client_address=('127.0.0.1', 60502) (127.0.0.1)
command=GET
path=/foo?q=bar
real_path=/foo
query=q=bar
request_version

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.3
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept=*/*
Host=127.0.0.1:8000
User-Agent=curl/7.52.1
* Curl_http_done: called premature == 0
* Closing connection 0

HEAD

$ curl -v -I http://127.0.0.1:8000/foo?q=bar
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> HEAD /foo?q=bar HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.52.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.5.3
Server: BaseHTTP/0.6 Python/3.5.3
< Date: Fri, 28 Jul 2017 14:09:41 GMT
Date: Fri, 28 Jul 2017 14:09:41 GMT
< Content-length: 303
Content-length: 303
< Content-Type: text/plain; charset=utf-8
Content-Type: text/plain; charset=utf-8

<
* Curl_http_done: called premature == 0
* Closing connection 0

POST

$ curl -v -X POST http://127.0.0.1:8000/ -F name=dhellmann -F foo=bar -F datafile=@profiles.ini
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 527
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=------------------------0508b2c32fa178f2
>
* Done waiting for 100-continue
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.5.3
< Date: Fri, 28 Jul 2017 14:10:42 GMT
< Content-Type: text/plain; charset=utf-8
<
CLIENT_VALUES:
client_address=('127.0.0.1', 60509) (127.0.0.1)
command=POST
path=/
real_path=/
query=
request_version

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.3
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept=*/*
Content-Length=527
Content-Type=multipart/form-data; boundary=------------------------0508b2c32fa178f2
Expect=100-continue
Host=127.0.0.1:8000
User-Agent=curl/7.52.1

FORM DATA:
name=dhellmann
foo=bar
Upload datafile as 'profiles.ini' (122 bytes)
* Curl_http_done: called premature == 0
* Closing connection 0

この先は

方向性としては、WSGIを調べる、socketserverを調べるとなるが、なかなか大変そうである。 これ以上複雑なHTTPサーバーが必要ならばNginxをインストールしたほうが楽そうではある。

*1:紀伊國屋で購入したが、ちょっとタイミングが悪く高値をつかんでしまった。

『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} }より、労働者が教育に投資しない場合の企業の最適戦略は労働者を雇用しないことである。 この企業の最適戦略を前提にすると、能力者の高い労働者の最適戦略は教育に投資することである。

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

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

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