Make組ブログ

Python、Webアプリや製品・サービス開発についてhirokikyが書きます。

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が好きです。

他のオススメ記事

blog.hirokiky.org

Python3.8のPEP572 (Assignment Expression) とリスト内包表記でフィボナッチ数列を作って遊んだ記録

あつおさんがPEP572でフィボナッチ数列を作って遊んでいたので、僕も遊んでみた。

Python 3.8.0a1時点の話です。

我ながら [(p2 := (p1 + (p1:=p2))) for _ in ...] はよく出来てるなと思った。 あと、PEP572のnamed assignmentは使ってみると意外と制限があるので、思ったよりも良いなと思った。

ただこの動きは直感的にはビックリした。

>>> a = 1
>>> b = 2
>>> a + (a := b)
3
>>> a
2
>>> b
2

あつおさんのブログも見てね

atsuoishimoto.hatenablog.com

aiohttp.ClientSessionのリクエストをaioresponsesでモックする。特定のホストへのアクセスは許可する

aiohttp.ClientSession() でのアクセスをテスト時にモックしたいときに、 aioresponses というライブラリーが使えます。

github.com

Pythonのrequestsライブラリーを簡単にモックできる responses というライブラリーがあるのですが、そのresponses-likeに使えれて便利です。

from aioresponses import aioresponses


def test():
    with aioresponses() as mocked:
        moked.get('http://foobar/api', payload={...})

       call_function_under_test()

特定のホストへのアクセスは許可する

この aioresponses 、良いのですがデフォルトで全てのリクエストをモックするようです (0.5.2時点) 。 問題としては例えば、 aiohttp でサーバーのテストを書いているとき、そのテストサーバーへのアクセスなどもモックして動作しなくなることです。

そこで、以下のようなpytestのフィクスチャーを書いておくと便利です。 ここでは pytest-aiohttp をインストールしている前提です。

@pytest.fixture
def responses(cli):
    base = cli.make_url('/')
    with aioresponses(passthrough=[str(base), ...]) as mocked:
        yield mocked

この例では、テストサーバーのホスト(上記の base 変数の値)へのアクセスを許可しています。 バックエンドのミドルウェアを通してテストする場合などは、 'unix://' なども許可しておくと良いです。

基本的に全てのアクセスは許可しておいて mock.get(...) のように追加したURLだけモックするようにしたい気はします。

ともあれ、aiohttp.ClientSessionを使うと aioresponses を使うと便利です。

pytest-asyncioとpytest-aiohttpを一緒に使うとRuntimeError got Future attached to a different loopがでる

pytest-asynciopytest-aiohttp を同時に使うと、テストの中で使うイベントループが別のものになってしまってエラーが出ます。

RuntimeError: Task <Task pending coro=<...> cb=[...]> got Future <Future pending> attached to a different loop

github.com

github.com

aiohttpを使ってるWebアプリのテストを書くときは pytest-aiohttp に寄せたほうが良さそうです。 pytest-aiohttp を使えば loop のフィクスチャーで、テストで使うイベントループが取れるのでそれに寄せるようにします。

github.com

asyncioとpytestでテスト用fixtureを作るときにgot Future <Future pending> attached to a different loopが出る話

Pythonでasyncioを使うアプリケーションのテストを書くとき、event loopに気を遣ってあげないとエラーになるという話です (今回はaiohttpを使ってWebアプリを作っているときに起こりました)。

こんなエラーがでました。

RuntimeError: Task <Task pending coro=<...> cb=[...]> got Future <Future pending> attached to a different loop

テスト側で動かすイベントループと、アプリ側が動かそうとする、イベントループに違いがあると発生してしまいます。 例えばアプリ側で何らかのバックエンドにアクセスするクライアントを使っていて、テスト側でもデータ(フィクスチャー)を作ったりクリーンアップしたいとします。 そのときに双方の使うクライアントが内部で使っているイベントループが違うものだと上記のエラーが発生します。

テスト用の pytest-asyncio などのライブラリーが、テスト用のイベントループを用意する場合は、アプリ側でクライアントを起動時にモジュールグローバルに保存しないようにしておきます。 そのクライアントがインスタンス化されるときに、内部で asyncio.get_event_loop を呼ぶことがあるからです。 もしくはテスト時にクライアントを差し替えるようにします。

この例だと pytest-asyncioevent_loop フィクスチャーを取っておくことでテスト用のイベントループを有効にしておきます。

import pytest

import somebackend


@pytest.fixture
async def somebackend(event_loop):
    client = somebackend.Client()
    try:
        yield client
    finally:
        do_cleanup_here()

テストでは以下のように使えます。

@pytest.mark.asyncio
async def test_foo(somebackend):
    await somebackend.create("some thing")

    actual = await call_function_under_test()

    assert actual == "expected"

今回は aiodocker を使っていて起こりました。

この情報が何かの参考になれば嬉しいです。 また、asyncioやイベントループ周りについて詳しければ教えてください。

近そうな話

github.com

Pythonのaiohttpでストリーム(aiohttp.web_ws.WebSocketResponse)をPipeする

最近Pythonの非同期処理、asyncioを使ったプログラムを書いています。 今までは非同期だとNode.jsを使っていたんですが、aiohttpや周辺ライブラリーが揃ってきたようなので使っています。

Node.jsの場合、Streamは stream.pipe(other_stream) のようにPipeできるのですが、それをaiohttpの WebSocketResponse でやる方法を書きます。

Server Reference — aiohttp 3.4.4 documentation

asyncioの Streams でも要領は同じなので参考になると思います。

(注意: Pythonのasyncioやaiohttpはまだ新しいものなので、情報が古くなってないか気をつけてください)

Pipeを1回やる(一方通行の)例

1つのストリームから読み込んで、もう片方のストリームに書き込むのは簡単です。

async for msg in ws:
    await to_ws.send_str(msg.data)

WebSocketでアクセスを受け付けて、バックエンドの別のWebSocketにつなぎ込むaiohttpのView関数を考えると、以下のようになります。

from aiohttp import web

routes = web.RouteTableDef()


@routes.post('...')
async def websocket_gateway(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)

    session = aiohttp.ClientSession()
    to_ws = await session.ws_connect('...')

    async for msg in ws:
        await to_ws.send_str(msg.data)

    await ws.close()
    await to_ws.close()

この場合、サーバー側のWebSocket ws がクローズすると処理が終了します。

他にも書き込み先のWebSocket to_ws が先にクローズした場合の処理や、 msg.data がバイト列の場合に to_ws.send_bytes にする処理なども必要そうです。

相互にPipeする例

上記の例ではWebSocketは一方通行にしか仲介されていません。ここで、 to_ws からの入力も、 ws に返すようにしましょう。 その場合、 2つの非同期処理を同時に実行する必要があります 。 1つの while ループの中で2つのストリームの読み書きをしても、片方が書き込まないと、もう片方の入力が受け取れないようになるので難しいです。 以下のように別々のループをして、それを asyncio.gather するとうまくいきます。

import asyncio

from aiohttp import web
from aiohttp.http import WSMsgType

WS_CLOSE_TYPES = (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED)

routes = web.RouteTableDef()


async def pipe(f, t):
    while not f.closed and not f.closed:
        msg = await f.receive()
        if msg.type in WS_CLOSE_TYPES:
            break

        await t.send_str(msg.data)
    return


@routes.post('...')
async def websocket_gateway(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)

    session = aiohttp.ClientSession()
    to_ws = await session.ws_connect('...')

    await asyncio.gather(
        pipe(ws, cs),
        pipe(cs, ws)
    )

    await ws.close()
    await to_ws.close()

asyncio.gather を使うことで、2つの処理を同時に実行して、両方が終了するのを待ってくれます。 優秀なやつですが、今回は片方のWebSocketが終わったら両方終了してほしいので、 pipe 関数内に工夫をしています。

pipe 関数の中で async for msg in ws せずに while ループを使っているのは、両方のWebSocketがクローズしていない限り処理を続けるようにするためです。 そうしないと、片方のWebSocketがクローズしてるのに、反対のWebSocketから書き込まれようとしてエラーになるからです。

こんな感じで、 asyncioaiohttp はかなり刺激的で面白いのでぜひ試してみてください。 誰得情報かもしれませんが、 aiohttpasyncio についての情報を出していけたらなと思います。