Make組ブログ

Python、Webサービスや製品開発、ライブラリー開発についてhirokikyが書きます

匠メソッドでステークホルダーをこぼさない発想法 〜 使う人と買う人が違うときがある?

匠メソッドのステークホルダーモデルは、価値分析モデルや要求分析ツリーと比べると簡単に見られがちに思います。 ですが、 ステークホルダーをこぼすと、価値も要求もこぼれていきます 。 今日はステークホルダーを発想していく方法を紹介します。

罠なのは、ステークホルダーを考えるとき、その製品を直接使う人だけを想像しがちなことです。 例えばPythonのオンライン学習サイトである PyQ であれば、「Pythonを学習したい人」や「チームでPythonを教えたい人」などは簡単に想像できます。

  • PyQ
    • Pythonを学習したい人
    • チームでPythonを教えたい人

ここで終わってしまうと危険です。 発想を広げていきましょう。

ステップ1: 人のまとまりから発想する

使う人というよりも「人のまとまり」で想像してみましょう。 例えば「組織」という軸で考えて、上から掘り下げて考えていきましょう。

  • 個人
    • Pythonをこれから学びたい人
    • Pythonを知っているけど使いこなせてない人
    • ...
  • 組織
    • 会社
      • 研修会社
      • Pythonをこれから使いたい会社
      • Pythonを使っている会社
    • 学校
      • ゼミで教えたい人
      • 授業で教えたい人

「まとまり」が分かればそこに属する他の人も考えられます 。 発想を広げるために「どんな組織やまとまりがあるだろう」と考えてみましょう。 まとまりが分かれば、その中にいる人を考えていきましょう(細かくは次のステップで)。

他にも組織であれば「地域社会」や「コミュニティ」など考えられます。 製品に直接関係ないものであれば無理に書き出す必要はありません。

ステップ2: 組織にいる人は使う人だけでない

会社や組織の面白いところは、「使う人」と「買う人」が別れていることがよくあるところです。 以下のステークホルダーモデルには、「決算する人」という視点が書けています。

  • Pythonを使っている会社
    • Pythonを教える先輩
    • 新しく入社した人

「教える先輩」というのはどんな人でしょうか?入社2,3年目で、次に入社する人に技術を教える先輩が想像できます。

  • 「先輩」はなぜ「教えなきゃいけない」のでしょうか?
  • 誰が製品を導入するのでしょうか?

組織というものは複雑で色んな人が連携していますので、大切なステークホルダーをこぼしがちです。 そういった 人の日々や関係を想像してステークホルダーを埋めていきましょう

  • Pythonを使っている会社
    • 人事部長
    • チームリーダー(教育担当)
    • Pythonを教える先輩
    • 新しく入社した人

「教える先輩」は複数人いそうですが、他にも「教育担当」の人が考えられます。 その人は「教える」だけでなく「新しい人を働けるように成長させよう」という責務を持っていると考えられます。 極端に別人でなくても 「そういった役割を持っている」人がいればステークホルダーとして分離しておきましょう

ステークホルダーモデルにおいて「決済権を持っている人」や「統括して面倒を見ている人」の視点は抜けがち です。 特に僕は製品や使う人への指向がかなり強いので、他にいる人のことを忘れてしまいます。

ですがそのステークホルダーを見逃さないことで、例えば「教育担当者の人は結果をレポートで集計してほしい」のような要求に気づけます。 (もちろんどこにフォーカスするのかは後の価値分析モデルや要求分析ツリーで判断すれば良いです)。

そもそもなぜ気付けるのか

匠メソッドのような方法論や発想法も大事ですが、そもそも視点に気付けるのは「自分がよく理解しているから」こそです。 「決済する人は別の人だろう」と知っていること、つまり根本的に製品に関わる人の生活や悩み、文化を理解していることが大切だと僕は思います。 ステークホルダーをこぼさないようにしよう、こう発想していこうと紹介しましたが、根本的にお客様の悩みや自分が普段感じている問題、誰が悩んでいるのかの顔を思い浮かべられるようになっておきましょう。

そのためにも大切なのは、人に接することと、課題を聞くことだと思います。 価値は課題から生まれます。課題は人が持っています。 プログラミングや製品開発と言うとすごく固いイメージもありますが、実は誰よりも人を理解している必要があるのかもしれません。

PyQはこういったプロセスを通して「どんな人が製品に関わるのか」、「その人たちの悩みや要求は何なのか」を考えて製品を作っています。 PyQはまったく完璧な製品だとは思いませんが、日々、製品に関わる人に目を向けて作るようにしています。

pyq.jp

Marketo(マルケト)のメールで変数(トークン)からリンクを作るときはスキーム(https...)をなしにすべきらしい

Marketo で動的なメールを書くときの話です。 以下の条件での話です

  • Marketoでメールを書く
  • メール内のリンクを、リードのフィールド値などトークンを使いたい
  • メール内のリンクのクリックをトラッキングしたい

このとき、変数の値を https://example.com/ とするとトラッキングが効きません(URLは正常に機能します)。 トラッキングを有効にするには、 変数の値は example.com/ としておいて、メールを書いてリンクを設定する際に https://{{ lead.URLのフィールド }} とする必要があります。

Marketoがメールのリンクをトラッキング用のリンクに置き換える仕様上、こういうワークアラウンドが必要みたいです。

nation.marketo.com

aiohttpのWebSocketクライアントの実装をソースコードリーディングしていく

こんにちは、最近 aiohttp をすごく使っています。 Web-DBな処理はDjangoで実装して、非同期処理が必要なサーバーやクライアントをaiohttpで書くという住み分けをしています (今までNode.jsを使っていたところをaiohttpで実装しています)。

平たく言うとaiohttpはかなり最高なので、今日はそのWebSocketクライアントの実装をソースコードリーディングしていきましょう。

(なぜ読むかと言うと、Dockerのexec start APIがWebSocketのようでそうでない変則的な仕様になっていて、それに対応するためにWebSocketクライアントの実装を読んでいました。 exec start wsのようなAPIを用意してくれると良いのですが)

ws_connectから読んでいこう

aiohttpClientSession にある .ws_connect() メソッドから実装を紐解いていきます。 まずはこのメソッドのドキュメントを読んでおきましょう。 https://aiohttp.readthedocs.io/en/stable/client_quickstart.html#websockets

今回は細かい仕様には焦点を当てずに、大まかな実装がどのようになっているかを見ていきましょう。 ClientSession.ws_connect の実装はここにあります (_ws_connect というメソッドに処理があるのでこれを見ていきます)。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L662

まずは必要になるヘッダーの処理が書かれています。 必須になる Connection: UpgradeUpgrade: Websocket などが設定されます。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L686-L711 ヘッダーの値はここにあります。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/hdrs.py#L89-L90

次に、対象のURLにリクエストを送信する処理があります。 ここで Connection: Upgrade のリクエストを送って、WebSocket通信を開始します。

        # send request
        resp = await self.request(method, url,
                                  headers=real_headers,
                                  read_until_eof=False,
                                  auth=auth,
                                  proxy=proxy,
                                  proxy_auth=proxy_auth,
                                  ssl=ssl,
                                  proxy_headers=proxy_headers)

Upgradeのリクエストは read_until_eof=False オプションを指定して、レスポンスを読み切るまで待たないようにしています。

レスポンスが返ってきた後は、ステータスコード101 かや、ヘッダーがWebSocketであるかどうかを検証します。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L714-L721h

その後はWebSocket通信のキーの計算などの処理が入りますが、ここは割愛します。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L714-L721h

ここで、今後WebSocketで通信できるように先程のレスポンスからコネクションを取り出します。

            conn = resp.connection
            assert conn is not None
            proto = conn.protocol
            assert proto is not None
            transport = conn.transport
            assert transport is not None

https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L714-L721h

細かいことを抜きにしてしまえば、この transport.write() で書きこめばUpgrade後の通信を通してサーバーに書き込みができます。 ですが、書き込み時にヘッダーをつけたり、圧縮したり、読み込んだ通信結果を解釈して WSMessage クラスでラップしたり、ping/pongをやりとりしたりする処理が必要になります。

そのために readerwriter を設定します。

            reader = FlowControlDataQueue(
                proto, limit=2 ** 16, loop=self._loop)  # type: FlowControlDataQueue[WSMessage]  # noqa
            proto.set_parser(WebSocketReader(reader, max_msg_size), reader)
            writer = WebSocketWriter(
                proto, transport, use_mask=True,
                compress=compress, notakeover=notakeover)

https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L797-L802

FlowControlDataQueue は読み込んだデータを貯めておくキューで、実際に読み込んだ通信を解釈しているのは WebSocketReader というクラスです。 また、書き込みを行う WebSocketWriter も作成されています。

readerとwriterの詳細は後述します。 変則的にWebSocketのプロトコルを少し変えたい場合などはこの reader/writer を差し替えれば良いわけですが、そのフックポイントはありません

あとはこの reader と writer をまとめた ClientWebSocketResponse というインスタンスで返せば ws_connect の処理は終了になります (self._ws_response_class で返していますが、これは ClientSession のコンストラクタで受け取る ws_response_class 引数です。デフォルトで ClientWebSocketResponse です)。

            return self._ws_response_class(reader,
                                           writer,
                                           protocol,
                                           resp,
                                           timeout,
                                           autoclose,
                                           autoping,
                                           self._loop,
                                           receive_timeout=receive_timeout,
                                           heartbeat=heartbeat,
                                           compress=compress,
                                           client_notakeover=notakeover)

https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L807-L818

ClientWebSocketResponse にまとめることで、ライブラリーのユーザーからはreader/writerを分けて考えずに使えるようになっています。また、コネクションをクローズする処理などまとめられています。

aiohttpのClientWebSocketResponseの実装はこちらです。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client_ws.py#L28

平たく言うと receive メソッドでは reader.read() を、 send_str メソッドでは writer.send() を呼び出すためクラスです。他にも例外を処理したり、 ping/pong を勝手に処理してくれたり使いやすくするためのクラスでもあります。

Reader/Writer

WebSocketReaderの実装はここにあります。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/http_websocket.py#L241

このreaderは feed_datafeed_eof を実装しています。 ws_connectproto.set_parser することで、このメソッドに通信されてきた文字を渡すことができます。 役割としてはWebSocketの通信 (メッセージなのか、ping/pongなのか) を解釈して、 WSMessage クラスにラップした上でキューに書き込みます。 このキューというのは ClientWebSocketResponse_reader に設定されている FlowControlDataQueue です。

WebSocketWriterの実装はここにあります。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/http_websocket.py#L544

writerは sendcloseping, pong を実装しています。 ClientWebSocketReader_writer に設定されているので、 self._writer.send のように呼ばれます。

WebSocketWriter は書き込まれた内容を self.transport.write を使ってサーバーに送信します。 writerはメッセージの圧縮や、ヘッダーを足す処理をしてtransportに書き込みます。

まとめ

ざっと処理の流れを確認するとこのようになっています。 細かいことは割愛しましたが、ますUpgradeのリクエストを送って、そのあとはその通信をそのまま利用してWebSocketのやり取りをするというのがよく分かりました。

なぜ食べ物を残してはいけないのに積読をするのだろう?

「食べ物を粗末にしてはいけない」と言われても、それを疑う人はいないと思います。 幼少期から全日本人が教えられていることだと思います。強制力の違いはあれど、誰しも教えられたことだと思います。 ですが、粗末にしてはいけないのは食べ物だけでしょうか?何か、この常識には偏りを感じています。

食べ物を粗末にしてはいけないのは、もちろん僕も同意です。命をいただく気持ちが大事だと思います。 ですが、なぜ食べ物についてだけなんでしょうか。俗に言う「積読」なんかも、すごく「粗末」にしていると言えるのではないでしょうか。 例えばその本を印刷するための木材や、運搬するためのガソリン、人件費、販売にかかるコストや書籍サイトの運営費などを粗末にしてると言えます。

「食べ物は命をいただいているが、他はそうじゃないからだ」と言い切れるでしょうか? 本や衣類も、木材や綿、動物の革という直接的な物質や命をいただいています。何らかのサービスについても他人の人生をいただいています。その他人の人生には、その人の食べているお肉や野菜も含まれていると僕は思います。

  • 「食べ物は残せないが、他は残せる」: 機会や知識というものも期限はあると思います
  • 「食べ物は無ければ死んでしまうものだが、他はそうじゃない」: 衣服や本がなくて生きていけるでしょうか。同様に大切だと思います
  • 「食べ物は他の人に渡せないからだ」: これは一理あると思います。ですが人からの好意やサービスは渡せませんし、死蔵してそのまま捨てられるものもあるでしょう

ちなみに僕は気にせず積読をしています。でも誰もそれで僕を怒らないのは、それが僕の本だからです。なぜ食べ物についてはとやかく言われたり、言ったりするのでしょう。

考えを広げれば、人の好意を無下にしたり、話をちゃんと聞かなかったり、機会を無駄にしたり、大切な時間を真剣に取り組まなかったり、無駄にしているものはたくさんあります。 それも同じことと考えられます。他人の人生や、その人の食べた命を間接的にいただいているものだと思います。雑に扱われがちですが、サービスを受け取るのも命を受け取ることだと思います。

「食べ物を粗末にしてはいけない」「残してはいけない」、それは確かにそうだと思います。 でも、それと同じようにサービスや親切心、それ以外のものについても大切にしないといけないんじゃないでしょうか。そして十分な対価を払うべきではないでしょうか。 同時に、そこまで強迫的に「食べものを残してはいけない」と思わなくて良いんじゃないでしょうか。 それは単なる呪いだと思います。もちろん大切にいただくべきものですが、それ以外にも粗末にしてるものやサービスを見直しても良いんじゃないでしょうか。

雑談: AIは牛乳を注ぐ女の夢を見るか?

雑談です。

AIは牛乳を注ぐ女の夢を見るのか? 牛乳を注ぐ女というのはフェルメールのあの絵のことです。

ja.wikipedia.org

今の世の中はAI、ディープラーニングの話題がホットです。 ですが今言われるAIはとどのつまり機械学習で、僕が学生のときは「弱いAI」と呼ばれてたように思います。 「AI」なんて言おうものなら鼻で笑われていたので、あえて「集合知」のような言い方をしていたような覚えもあります。

機械学習は十二分にすごいものですが、結局のところは人間にはなれていないなと思います。 人間は、何かを判断するときにも日々の風景や、過去の習慣や常識、昨日食べたステーキの味も参考にして判断しています。 ステーキの味と株価の予測が関係するかは定かではありませんが、その店の中で見た若者の多さとか、店の値段設定から外食産業への投資判断を行ったりはありえます。 要するに、機械学習には感性というか、全く関係がないことを結びつけて考える能力がないわけです (AIさんがランチに出かける日が来るまでは)。

ですが逆に機械学習が今後ももっと発達すると考えると、人類は「強いAI」を求めなくなると思います。 「弱いAI」的な判断や考え方が中心になって、人間的な判断や会話が二の次と考えられるようになると思います。

そういう時代が来るだろうからこそ僕は人間的な感性や、数値に出ないデータが大事だと思います。 なぜなら、人間というのは結局のとこ人間を相手にするものだからです。 どれだけ機械学習や最適化をしても、それは限られた軸の中での世界の話です。 人間の感性を相手に世界が回っている以上、究極は人間の感性にしかできないことがあると思います (少なくとも人間の感性や時代背景、共感性をすべて学習したAIさんが登場するまでは)。

人間の感性的な部分、RX-8に乗って最高のドライブフィーリングを味わう感覚や、アンドロイドは電気羊の夢を見るか?のワクワクや、PH5の光やタリアセンライトにうっとりする感覚や、牛乳を注ぐ女をみて感じるものや、ジムノペディを聴いたりWelcome To The Jungleを聴いて感嘆することが大事なんだと思います。その喜びを知ってるからこそ全く関係ないように思える日々の仕事が素晴らしいものになり、人類にとっての価値が産まれると僕は思っています (機械学習があったとして当時ジムノペディは絶対に作れないと思います)。

ほんとに、フェルメール展に行くのを忘れてたのを後悔してます。 それが言いたかっただけです。

Djangoのresponse.set_cookieはmax_ageを指定すればexpiresが勝手に計算される

DjangoCookieを設定するときは response.set_cookie を使います。

response = TemplateResponse(...)
response.set_cookie("key", "value", max_age=3600)

今まで知らずに expires も自分で計算して設定していましたが、 max_age を指定して expires を指定しない場合は勝手に設定されるので不要みたいです。

DjangoのHttpResponseBaseのset_cookieにこういう実装がありました(Django 2.1で確認)。

        if max_age is not None:
            self.cookies[key]['max-age'] = max_age
            # IE requires expires, so set it if hasn't been already.
            if not expires:
                self.cookies[key]['expires'] = http_date(time.time() + max_age)

django/response.py at 2.1.7 · django/django · GitHub

なので、わざわざこういう関数を作る必要はないです。

from datetime import timedelta

from django.utils import timezone


def set_cookie(response, key, value, max_age):
    expires_at = timezone.now() + timedelta(seconds=max_age)
    expires = datetime.strftime(expires_at, "%a, %d-%b-%Y %H:%M:%S GMT")
    response.set_cookie(key, value, max_age=max_age, expires=expires)

ちゃんと実装を調べてよかった。

PythonとRubyの比較をよく聞かれるので明示するか略せるかが違うよという説明をまとめておいた

よくプログラミング初心者の方に「PythonRubyの違いを教えてください」と聞かれるので書いておきます。

免責

あくまで僕がプログラミング初心者さんに説明するときにこの説明を使うよという視点です。読んでる方が思う視点とか別の意見はぜひご自身のブログに書いてください(読みますので)。 違いはたくさんありますが、5分で簡潔に伝わる違いを伝えたいと思っています。 あと私はPythonをずっと10年近く使っていますが、Rubyはちょっとだけです。でもRubyもとても良い言語だと思っています。

(コードサンプルの色味が読みにくくてごめんなさい。気が向いたら直しときます)

本文

プログラミング初心者に簡単にPythonRubyの違いを説明する場合ベストは「Pythonはより明示します。Rubyはより略せます」だと思っています (あくまでプログラミング初心者に5分以内に伝えると考えたときの視点ですよ)。

細かな違いはあれど、個人的には言語としてPythonRubyではそれほど大きな違いはないと思います (少なくともHaskelとPythonや、ErlangPythonのような違いはないですよね)。

もちろん今回は言語の話です。コミュニティの色や存在しているフレームワーク、ライブラリーの違いは大きくあると思います。 機械学習やデータ処理をしたいなぁ、しかもWebでも同じ言語を使いたいなぁというのであればPythonをオススメします。 でもそういう場合は言語仕様関係なく選びますよね。

言語として「PythonRubyの違いが知りたい」と言うプログラミング初心者の人に分かりやすく違いを説明するのであれば、やはり「明示するか略すか」が良いと思います。

Pythonの明示姓

言うよりも見るということで、フィボナッチ数列の計算をキャッシュありで行うプログラムを2つ書きました。

Pythonの例:

fib = {}
fib[1] = 1
fib[2] = 1


def fibonacci(num):
    if num in fib:
        return fib[num]

    fib[num] = fibonacci(num-1) + fibonacci(num-2)
    return fib[num]


print(fibonacci(100))

Rubyの例:

@fib = {}
@fib[1] = 1
@fib[2] = 1

def fibonacci(num)
  @fib[num] ||= fibonacci(num-1) + fibonacci(num-2)
end

p fibonacci(100)

この2つの例はかなりPythonらしさとRubyらしさを表していると思います。 (もちろんRubyでもPythonのような書き方はできますが、Rubyで書くのであればこの書き方のほうが美しいと思います)

この違いを産んでいるのはこの辺の違いからでしょうか:

  • return の略
  • nil の扱い
  • 代入の違い
  • 例外の違い

Ruby||= でうまく書けると気持ちいい。 Pythonは愚直ですが、何やっているかはパット見で分かりやすいなぁと感じます。

クラスで紹介する他の例

他の例として、Pythonのほうがより明示的に書く必要があるという説明をします。 Rubyでは def メソッド名=(...)@属性名 と記号で書けるところを、Pythonでは明示的に self. などを書く必要があります。

説明用のプログラムでは、PythonRubyAccount クラスを作っています。 Accountfullname というゲッターとセッターを持っており、 fullname という属性に値をそのまま保存します。

Pythonの例:

class Account:
    @property
    def fullname(self):
        return self.fullname

    @fullname.setter
    def set_fullname(self, fullname):
        self.fullname = fullname


user = Account()
user.fullname = "Hiroki Kiyohara"
print(user.fullname)

Rubyの例:

class Account
  def fullname
    @fullname
  end

  def fullname=(fullname)
    @fullname = fullname
  end
end


user = Account.new()
user.fullname = "Hiroki Kiyohara"
p user.fullname

この例は極端に分かりやすい例なので、「見た目Rubyのほうが良いじゃん」と思われるかもしれません。 ですが、Pythonはより「明示的に」書く必要があり、記号や略記が少ないのが特徴です。

Pythonは美しいと僕も思いますが、結構、言語開発者とか深い仕様レベルでの美しさだなぁと思います。実にパースしやすそうです。 パッと見た感じはRubyのほうが小奇麗だと思います。Rubyの言語の深い部分は知らないです。

Slackにて話してると

shimizukawa [11:41 AM]
Rubyは言語仕様が美しくなるように言語設計している
Pythonは文法のパースがシンプルになるように言語設計している

わかり哲也。

僕はそういう意味で、Pythonが好きです。