Djangoのキャッシュフレームワークを使った場合のキーと値の取り扱い

Djangoフレームワークには、データをキャッシュする仕組みを抽象化、共通化したキャッシュフレームワークが含まれています。

Django's cache framework | Django ドキュメント | Django

どのようなキーと値が保存されるのか

キャッシュフレームワークAPIで、どのようなキーと値がミドルウェアなどのバックエンドに保存されるのか確認してみます。

キャッシュに値を入れる

今回はDjango 5.0とredisバックエンドで試してみます。ローカル環境ではRedisが起動している想定。

myproject/settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://",
    }
}

この設定でDjangomanage.py shell からキャッシュを保存します。

>>> from django.core.cache import cache
>>> cache.set("my-cache-key", "キャッシュの値")
>>> cache.get("my-cache-key")  # キャッシュから取り出せるか確認
'キャッシュの値'

キーと値を見てみる

保存できたら、 redis-cli で確認してみます。

$ redis-cli  # シェルからredis-cliを起動
127.0.0.1:6379> KEYS *
1) ":1:my-cache-key"
127.0.0.1:6379> GET :1:my-cache-key
"\x80\x05\x95\x19\x00\x00\x00\x00\x00\x00\x00\x8c\x15\xe3\x82\xad\xe3\x83\xa3\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa5\xe3\x81\xae\xe5\x80\xa4\x94."

まず、 KEYS <pattern> コマンドでpatternに * を指定し、すべてのキー一覧を取得しています。

:1:my-cache-key という値がキーになっています。

settings.pyでキー生成の関数を設定していないので、デフォルトのDjangoのキー生成関数が使われています。 実装はこの辺です。 https://github.com/django/django/blob/617bcf611f3daa796e4054ba041089ece30a32fc/django/core/cache/backends/base.py#L40

return "%s:%s:%s" % (key_prefix, version, key)
  • key_prefix は、settings.CACHESの各キャッシュの設定で KEY_PREFIX キーにて変更できます。デフォルトは空文字列です。
  • version settings.CACHESの各キャッシュの設定で VERSION キーで指定できます。

https://github.com/django/django/blob/617bcf611f3daa796e4054ba041089ece30a32fc/django/core/cache/backends/base.py#L82

GET コマンドで取得した値はバイナリ値になっています。これはDjango側のRedisバックエンドの中で、保存する値をpickleモジュールでシリアライズしているからです。

https://github.com/django/django/blob/617bcf611f3daa796e4054ba041089ece30a32fc/django/core/cache/backends/redis.py#L21

どのように値を保持するかは、キャッシュバックエンドごとで異なるので、シリアライズ・デシリアライズ処理を行うかどうかは、バックエンドクラスの実装次第です。

Djangoのキャッシュフレームワークを通してRedisに保存した値を、他のアプリなどから読み込んで使いたい場合、Pickleフォーマットだと扱いづらいかもしれません。その場合は自分でdumps, loadsメソッドを持ったクラスを実装するか、 json モジュールなどを指定することもできます(この記事では手順は説明しません)

キー生成の関数を変更してみる

キーを生成する関数を変更するには、settings.pyのCACHESで KEY_FUNCTION を設定します。

myproject/utils.py

def my_key_func(key, key_prefix, version):
    return "spam:{}".format(key)

myproject/settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://",
        "KEY_FUNCTION": "myproject.utils.my_key_func",  # 文字列指定だと実行時にインポートして利用される
    }
}

この設定の状態でキャッシュを manage.py shell から追加してみます。

>>> from django.core.cache import cache
>>> cache.set("my-cache-key-2", "キャッシュの値")
>>> cache.get("my-cache-key-2")
'キャッシュの値'

redis-cliにてキーを見てみましょう。

$ redis-cli
127.0.0.1:6379> keys *
1) "spam:my-cache-key-2"

キー文字列が変わったことを確認できました。

ありそうな質問

キャッシュキーの一覧を取得したいですが、うまくいきません。どうすればよいですか?

Djangoのキャッシュフレームワークでは、キー一覧を返す仕組みを持っていません。この記事の例ではRedisの KEYS コマンドを使用しています。

キャッシュを保存しておくバックエンドのミドルウェアがキーの一覧を返す仕組みを持たない場合もあります。キャッシュキーを検索したい要件がある場合は、キャッシュバックエンドの選定に気をつけるとよいでしょう。

cacheのすべてのkeyの取得の方法。

キャッシュを使っているのに本番環境が遅いです。ローカル環境では問題ないのになぜですか?

ネットワーク経由で外部キャッシュサーバーを使用している場合は、通信のレイテンシがあります。リクエスト内で何度もキャッシュを読み書きすると、遅くなる場合があります。複数のキーを指定してまとめて取得、まとめて更新する方法を使うと改善する可能性があります。

Djangoのcacheフレームワークで複数の値をまとめて取得、更新する - 偏った言語信者の垂れ流し

terapyon channel podcastにゲストで呼ばれてきました

terapyon がやっている terapyon channel podcast にゲストでお呼ばれして、しゃべってきました。

#88 tokibitoさんをゲストに JMOOCの無料Python講義のリニューアルとプログラミング教育 | terapyon channel podcast

話題は先日のJMOOCの教材作成についてや、プログラミング教育についてなど。

Pythonの入門者向け動画教材 2023年改訂版

JMOOCで2020年に公開していたPythonの入門者向けの動画教材ですが、昨年の11月に内容を更新したものを公開していました。

まだ記事にしていなかったので、ここでも書いておきます。

Python入門2023改訂版 - Python3.11対応版

Pythonによるプログラミング入門の動画教材です。

GoogleアカウントでPlatJaMというシステムにログインすると、無料で受講できます。

Windowsパソコンの操作ができ、プログラミングを初めて勉強する人を想定した内容となっています。

2020年公開版との違い

  • スライド資料をアップデートしました。内容はほとんど同じですが、Python3.11を前提とした説明に変更しています。
  • 動画をすべて再撮影しました。動画コンテンツとしては完全に別物で新しくなりました。
  • 実際にコマンドを実行したり、Pythonの対話インターフェースで動かすデモ、VisualStudioCodeでコードを入力するデモを追加しました。
    • 2020年のバージョンではスライドと音声による説明だけでした。

2020年公開版についての記事

Pyodideを試す

Pyodideは、CPythonをWebAssembly(WASM)/Emscriptenにポーティングしたソフトウェア。

PythonがWASMとして動作するので、ブラウザ上でPythonを動かせる。

pyodide.org

ドキュメントには実際に動作するREPLのリンクがある。

https://pyodide.org/en/stable/console.html

Pyodideをウェブサイト上で動かす

PyodideはWASMなので、JavaScriptから呼び出して利用可能。

また、CDNでホストされたバージョンもあるため、少し組み込んで使うくらいであれば、少量のコードでできる。

https://pyodide.org/en/stable/usage/quickstart.html

サンプルコード(ドキュメントより抜粋):

<!doctype html>
<html>
  <head>
      <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
  </head>
  <body>
    Pyodide test page <br>
    Open your browser console to see Pyodide output
    <script type="text/javascript">
      async function main(){
        let pyodide = await loadPyodide();
        console.log(pyodide.runPython(`
            import sys
            sys.version
        `));
        pyodide.runPython("print(1 + 2)");
      }
      main();
    </script>
  </body>
</html>

CDNからスクリプトをロードし、 loadPyodide() でWASMをロード、初期化。その後は runPython()Pythonコードを実行できる。

サードパーティ製パッケージを動かす

Pyodideでは、micropipというAPIを使って、外部のPythonパッケージを動かすことができる。

Pyodideが標準モジュールをある程度サポートしていることもあり、Pure Pythonで書かれたパッケージであれば、動かすハードルは低め。

標準モジュールの互換性についてもドキュメントに記載がある。

pyodide.org

JS側、Python側どちらからでもmicropipを使える。柔軟性は高いように見える。

<script type="text/javascript">
  async function main(){
    const pyodide = await loadPyodide();
    await pyodide.loadPackage("micropip");
    const micropip = pyodide.pyimport("micropip");
    await micropip.install("regex")
  }
</script>

DjangoのIt works画面をうごかしてみる

では、Djangoを無理矢理うごかしてみる。

<!doctype html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      async function main(){
        // setup
        const pyodide = await loadPyodide();
        await pyodide.FS.writeFile("/home/pyodide/urls.py", "urlpatterns=[]");
        await pyodide.loadPackage("micropip");
        const micropip = pyodide.pyimport("micropip");
        await micropip.install("Django")
        // run django app
        const output = pyodide.runPython(`
            import io
            import sys
            import django
            from wsgiref.handlers import BaseCGIHandler
            from django.conf import settings
            from django.core.handlers.wsgi import WSGIHandler

            settings.configure(
                ROOT_URLCONF="urls",
                SECRET_KEY="dummy",
                DEBUG=True,
            )
            django.setup()
            app = WSGIHandler()
            output = io.BytesIO()
            env = {
                "REQUEST_METHOD": "GET",
                "SERVER_NAME": "pyodide",
                "SERVER_PORT": "8000",
            }
            BaseCGIHandler(
                sys.stdin.buffer,
                output,
                sys.stderr,
                env,
                multithread=False
            ).run(app)
            response = output.getvalue().decode("utf-8")
            "".join(response.splitlines()[2:])
        `);
        document.open();
        document.write(output);
        document.close();
      }
      main();
    </script>
  </body>
</html>

Pyodideを初期化してからDjangoをインストール、その後WSGIハンドラを実行している。

実行すると、ブラウザ上でDjangoを実行して、It worksの画面が表示される。

Djangoのシグナル機能(django.dispatch.Signal)

Djangoには、フレームワーク内での各種アクションの発生を他の機能へ通知する、シグナル(Signals)という機能があります。

Signals | Django ドキュメント | Django

シグナルの機能を使うと、Djangoアプリケーション間のモジュールの依存関係を緩くできたり、拡張性のあるアプリを作れたりします。

試した環境は Python 3.11, Django 5.0

シグナルの使い方

シグナルの使い方は以下の通りです。

  1. シグナルを定義する ... my_signal = Signal()
  2. (受信側)シグナルにレシーバーを接続する ... Signal.connect()
  3. (送信側)シグナルを送信する ... Signal.send()
  4. (受信側)レシーバーの処理が実行される

自分で定義したシグナルを使う場合は、1のシグナル定義から。既定のシグナルを利用する場合は、2のレシーバーの接続からになります。

シグナルを使わない例

base、app1、app2 のように、3つのDjangoアプリケーションがある例です。

baseアプリは最初に作成され、後から追加されたapp1とapp2は、baseアプリから処理が呼び出される、といったモジュール構成を想定します。

base/spam.py:

from app1.egg import receiver as app1_receiver
from app2.egg import receiver as app2_receiver

def main():
    print("base/spam")
    app1_receiver("by base")
    app2_receiver("by base")

app1/egg.py:

def receiver(param):
    print(f"app1 {param}")

app2/egg.py:

def receiver(param):
    print(f"app2 {param}")

この場合、Django shellから base/spam.py:main() を実行すると、以下のようになります。

>>> from base.spam import main
>>> main()
base/spam
app1 by base
app2 by base

このとき、base、app1、app2の依存関係について考えてみます。

  • baseアプリからは、app1とapp2の関数をimportしているので、Pythonモジュールとして直接依存している
  • app1とapp2の間には依存関係はない

また、「app1とapp2は後から追加される」という想定だと、app1やapp2を実装するときに、呼び出し元のbaseのアプリも編集が必要となります。

base/spam.py:main() の処理は、密結合のapp1とapp2のreceiverの処理を含むため、このような構成だと main()ユニットテストは複雑になっていきます。

シグナルを使うと、このような密結合のモジュールの依存を変更し、緩い結合にできます。

モジュール、関数の依存と呼び出しの関係は、以下の図のようになります。

シグナルを使わない構成

シグナルを利用する形に書き換えた例

base/signals.py:

from django.dispatch import Signal

spam_main = Signal()

base/spam.py:

from .signals import spam_main

def main():
    print("base/spam")
    spam_main.send(main, "by base")

app1/egg.py:

def receiver(sender, **kwargs):
    print(f"app1 {kwargs['param']}")

app1/apps.py:

from django.apps import AppConfig
from base.signals import spam_main
from .egg import receiver


class App1Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'app1'

    def ready(self):
        spam_main.connect(receiver)

app2/egg.py:

def receiver(sender, **kwargs):
    print(f"app2 {kwargs['param']}")

app2/apps.py:

from django.apps import AppConfig
from base.signals import spam_main
from .egg import receiver


class App2Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'app2'

    def ready(self):
        spam_main.connect(receiver)

実行結果は、シグナルを使わない場合と同じです。

ポイントは以下の通りです。

  • app1とapp2を後から追加するときに、baseを変更しなくてよい
  • base側からapp1とapp2へのimportをしなくてよい
  • 記述するコード量は増える
  • シグナルへの接続は、AppConfig.readyでやっている

モジュール、関数の依存と呼び出しの関係は、以下の図のようになります。

シグナルを利用した構成

図ではAppConfig部分を省略していますが、シグナルを使わない例よりも複雑になっています。

base/main.py、app1/egg.py、app2/egg.pyでそれぞれのモジュールが独立している構成になるため、ユニットテストを書いたりするときに楽になります。

まとめ

  • Djangoのシグナル機能は機能間でアクションを通知できる
  • シグナルを使うとアプリ間の依存を緩くできる

検証に使ったコードはGitHub上にあります。

https://github.com/tokibito/sample_nullpobug/tree/main/django/django-signal

畑 2023

この記事は pyspa Advent Calendar 2023 の3日目の記事です。昨日は tokoroten の「2023年活動報告」でした。

今年も畑の成果報告です。

市内の民間の農園で4m×5mの小さい畑を借りています。以前は2016年~2018年に3年間借りていて、その後、去年から借りているところです。

以前の畑についての記事

2023年の成果

2022年の9月から使用している畑なので、去年のアドベントカレンダーの記事では、あまり多くの収穫がまだなかったのですが、今年は1年通して野菜を作れたので、たくさん成果がありました。

年始は1月にキャベツを収穫しました。

ブロッコリーは12月~4月ころまで収穫できました。長い。

きぬさや、スナップエンドウ、そら豆、ナス

食用ほおずき

食用ほおずきは、ミニトマトに似た食味で結構甘いです。7月~9月ごろにたくさん収穫できました。

ナス、ししとう、食用ほおずき、ミニトマト、とうがらし、ピーマン、錦糸瓜

糸瓜は前年に実を買って食べたあと、残しておいた種から育てたのですが、うまくいきました。

すいか、赤紫蘇、きゅうり

ピーマン

とうがらし

さつまいも

うまくいった・良かった点

  • 接ぎ木タイプの苗
    • ナスやトマトは接ぎ木した苗がホームセンターに売っていて、それを使った。過去にタネから育てたときよりも生育状態は比較的良かった。
  • 雑草除去
    • 春、夏ごろは、雑草除去を過去の畑よりも比較的うまくできていた。
  • マルチング
    • マルチで畝を覆うのは、ほとんどの野菜で有効だった。土が乾きづらくなるので水やりの頻度を下げれたし、土に潜って根を食べる害虫への対策にもなる。
  • 敷き藁
    • 夏場の乾燥対策に使用したが効果があった。畝の間に敷くと土が乾きにくくなる&雑草対策になる。マルチと違って空気を通すため、完全に乾かないわけではないので、根腐りを防ぎつつ乾燥対策ができる資材。
    • さつまいものツルを伸ばす時には、土への接地を避けれてツルが根を張る対策になるのも良かった。
  • 収穫量増加
    • 乾燥対策、枝の伸ばし方により、実の収穫量は過去最高だった。過去4年の畑とは、かなり違いがあった。地植えの利点を活かす栽培が重要。
  • おすそ分け
    • 収穫量増加で消費しきれない分を家族、友人、近所の人などにおすそ分けできた。消費しきれない場合は今後もやっていきたいと思う。
  • 収穫体験
    • 友人の子たちに、野菜の収穫体験をしてもらうことができた。機会が作れれば、またやりたい。
  • ししとう・ピーマン
    • 5月に植えて、6月から12月まで収穫できた。長くてよかった。
  • ナス
    • 5月に植えて、6月から11月まで収穫できた。長くてよかった。消費しきれないので株は少し減らしてよいかも。

失敗した・悪かった点

  • 蝶・蛾対策
    • 蝶や蛾などが葉物野菜に卵を産み付けて、孵化した幼虫(イモムシ)が葉や実を食い荒らす。卵を見つけたら除去する対処が必要。農薬の使用はなるべく避けたいが、ひどい場合はオルトランなどの農薬使用もやむなし。
  • ウリハムシ対策
    • イカ、キュウリ、錦糸瓜などのウリ科の葉はウリハムシが大量に発生して食い荒らされた。木酢液や、捕獲用の道具などで対処を考えたい。
  • カメムシ対策
  • 蟻対策
    • 食用ほおずきは蟻が実を食い荒らす。アリの巣を見つけたら苦土石灰で対処を考えたい。
  • ネズミ対策
    • イカ、さつまいもはネズミ被害があった。金網、木酢液などで対処を考えたい。
  • イカ
    • ネットで上に伸ばしてみたところ、実の重さでつるが切れてしまった。
    • 植え付けが遅くて実が大きくならなかった。
  • 行者にんにく
    • ホームセンターに苗が売っていたので植えてみたが、生育環境が合ってなさそうだった。気温が高すぎて枯れてしまう。
  • にんにく
    • 球根が大きくならなかった。肥料不足か土が合ってないかのどちらか。
  • 玉ねぎ
    • 球根が大きくならなかった。肥料不足か土が合ってないかのどちらか。
  • ブロッコリー
    • 長期間収穫できていたが、2月にムクドリに葉を全部食べられてしまった。ネットで鳥よけするなどの対策を考えたい。
  • キャベツ
    • ムクドリの被害あり。蝶や蛾の幼虫の食害あり。
  • ビーリー
    • 雑草対策がうまくできてなくて、イモムシに食い荒らされたりして失敗。
  • さつまいものツルの伸ばし方
    • さつまいもはある程度ツルを伸ばさないと根が大きくならないので伸ばしたのだが、取り回しに失敗して他の野菜の株の生育を妨げることになった部分があった。範囲を抑制するために囲いのネットを増やすなどの工夫をしたい。
  • 基本的にF1種のタネを使うべき
    • 糸瓜とスイカは、前年に実を買って食べたあとにタネを残しておいて、そこから栽培したのですが、発芽時期や病害虫対策などの点で弱い可能性があるので、できるだけ早めに市販のF1種のタネを用意しておいて栽培したほうがよい。

おわりに

2023年は収穫量が多かったのがとてもよかったです。良い点、悪い点どちらもありましたが、来年に活かそうと思います。

1年通して栽培して土が疲弊しているので、来年の春に向けて肥料を足したりして、調整するのが必要そうだなと考えてるところです。

来年も1年通して畑を利用できるので、引き続きやっていきます。

明日は、 id:shiumachi の記事です。

Co-KoNPILe東青梅合宿

2023/11/23~11/25の2泊3日でCo-KoNPILeの合宿をやっている

cokonpile.connpass.com

東青梅駅近くの古民家の宿を貸し切り。

自分のアウトプットは、合宿飯を作ること。

そこそこ予定していたものを作れました。