terapyon がやっている terapyon channel podcast にゲストでお呼ばれして、しゃべってきました。
#88 tokibitoさんをゲストに JMOOCの無料Python講義のリニューアルとプログラミング教育 | terapyon channel podcast
話題は先日のJMOOCの教材作成についてや、プログラミング教育についてなど。
terapyon がやっている terapyon channel podcast にゲストでお呼ばれして、しゃべってきました。
#88 tokibitoさんをゲストに JMOOCの無料Python講義のリニューアルとプログラミング教育 | terapyon channel podcast
話題は先日のJMOOCの教材作成についてや、プログラミング教育についてなど。
JMOOCで2020年に公開していたPythonの入門者向けの動画教材ですが、昨年の11月に内容を更新したものを公開していました。
まだ記事にしていなかったので、ここでも書いておきます。
Python入門2023改訂版 - Python3.11対応版
Pythonによるプログラミング入門の動画教材です。
GoogleアカウントでPlatJaMというシステムにログインすると、無料で受講できます。
Windowsパソコンの操作ができ、プログラミングを初めて勉強する人を想定した内容となっています。
Pyodideは、CPythonをWebAssembly(WASM)/Emscriptenにポーティングしたソフトウェア。
PythonがWASMとして動作するので、ブラウザ上でPythonを動かせる。
ドキュメントには実際に動作するREPLのリンクがある。
https://pyodide.org/en/stable/console.html
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で書かれたパッケージであれば、動かすハードルは低め。
標準モジュールの互換性についてもドキュメントに記載がある。
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を無理矢理うごかしてみる。
<!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には、フレームワーク内での各種アクションの発生を他の機能へ通知する、シグナル(Signals)という機能があります。
Signals | Django ドキュメント | Django
シグナルの機能を使うと、Djangoアプリケーション間のモジュールの依存関係を緩くできたり、拡張性のあるアプリを作れたりします。
試した環境は Python 3.11, Django 5.0
シグナルの使い方は以下の通りです。
my_signal = Signal()
Signal.connect()
Signal.send()
自分で定義したシグナルを使う場合は、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の依存関係について考えてみます。
また、「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)
実行結果は、シグナルを使わない場合と同じです。
ポイントは以下の通りです。
モジュール、関数の依存と呼び出しの関係は、以下の図のようになります。
図ではAppConfig部分を省略していますが、シグナルを使わない例よりも複雑になっています。
base/main.py、app1/egg.py、app2/egg.pyでそれぞれのモジュールが独立している構成になるため、ユニットテストを書いたりするときに楽になります。
検証に使ったコードはGitHub上にあります。
https://github.com/tokibito/sample_nullpobug/tree/main/django/django-signal
この記事は pyspa Advent Calendar 2023 の3日目の記事です。昨日は tokoroten の「2023年活動報告」でした。
今年も畑の成果報告です。
市内の民間の農園で4m×5mの小さい畑を借りています。以前は2016年~2018年に3年間借りていて、その後、去年から借りているところです。
2022年の9月から使用している畑なので、去年のアドベントカレンダーの記事では、あまり多くの収穫がまだなかったのですが、今年は1年通して野菜を作れたので、たくさん成果がありました。
年始は1月にキャベツを収穫しました。
ブロッコリーは12月~4月ころまで収穫できました。長い。
食用ほおずきは、ミニトマトに似た食味で結構甘いです。7月~9月ごろにたくさん収穫できました。
錦糸瓜は前年に実を買って食べたあと、残しておいた種から育てたのですが、うまくいきました。
2023年は収穫量が多かったのがとてもよかったです。良い点、悪い点どちらもありましたが、来年に活かそうと思います。
1年通して栽培して土が疲弊しているので、来年の春に向けて肥料を足したりして、調整するのが必要そうだなと考えてるところです。
来年も1年通して畑を利用できるので、引き続きやっていきます。
明日は、 id:shiumachi の記事です。
2023/11/23~11/25の2泊3日でCo-KoNPILeの合宿をやっている
東青梅駅近くの古民家の宿を貸し切り。
自分のアウトプットは、合宿飯を作ること。
そこそこ予定していたものを作れました。
Djangoフレームワークには、ウェブ用のFormを作る機能があります。
フォームを使う | Django ドキュメント | Django
フォームを定義して実際にブラウザ上で表示するためには、ビューやテンプレートを用意する必要があるのですが、少しフォームを試したいだけであれば、毎回すべてを用意するのは少し手間です。
フォームクラスの動作を確認するだけであれば、Djangoのrequestオブジェクトには異存しないので、Django shell(manage.py shell)からでも手軽に試せます。
適当にmyappというDjangoアプリケーションを作成し、プロジェクトのsettings.LANGUAGEを 'ja'
に変更してからフォームを作ってみます。
myapp/forms.py:
from django import forms class ContactForm(forms.Form): name = forms.CharField(label="お名前", max_length=10) email = forms.EmailField(label="メールアドレス") content = forms.CharField(widget=forms.Textarea)
manage.py shell
を起動して、定義しておいた ContactForm
クラスをインポートします。
>>> from myapp.forms import ContactForm
これでContactFormクラスを試す準備ができました。
Djangoのフォームは引数を省略した場合、空のフォーム(新規入力相当)を生成します。
>>> form = ContactForm() >>> form <ContactForm bound=False, valid=Unknown, fields=(name;email;content)> >>> form.as_p() # pタグでフィールドをレンダリングしたHTMLを生成 '<p>\n <label for="id_name">お名前:</label>\n <input type="text" name="name" maxlength="10" required id="id_name">\n \n \n </p>\n\n \n <p>\n <label for="id_email">メールアドレス:</label>\n <input type="email" name="email" maxlength="320" required id="id_email">\n \n \n </p>\n\n \n <p>\n <label for="id_content">Content:</label>\n <textarea name="content" cols="40" rows="10" required id="id_content">\n</textarea>\n \n \n \n \n </p>'
form.as_p()
のようにして、formの各メソッドを試すことができます。
この例だと、pタグでフォームのHTMLを出力するメソッドを試しています。定義したフィールドに対応するHTMLの文字列が生成されていることを確認できます。
Djangoのフォームでよく試したいのは、入力バリデーションだと思います。試していきましょう。
Djangoのフォームを使う際に、ビューでは request.POST
のようなオブジェクトを引数で渡していることが多いですが、これは辞書ライクなオブジェクト(QueryDict)です。
実際、Djangoのフォームは入力値としてPythonの辞書を渡せば動かすことができる、シンプルな作りになっています。
formには空の辞書を渡した場合、bound=Trueの状態のフォームインスタンスが作られます。Unbound Form, Bound Formについては、ドキュメントを参照してください。
フォーム API | Django ドキュメント | Django
Bound Formの場合、errorsを参照すると、バリデーションが実行されるので、必須入力のチェックが実行されていることを確認できます。
>>> form = ContactForm({}) >>> form <ContactForm bound=True, valid=Unknown, fields=(name;email;content)> >>> form.errors {'name': ['このフィールドは必須です。'], 'email': ['このフィールドは必須です。'], 'content': ['このフィールドは必須です。']}
nameフィールドの初期値だけを与えてテストしてみましょう。
>>> form = ContactForm({'name': 'おなまえおなまえおなまえ'}) >>> form.is_valid() False >>> form.errors {'name': ['この値は 10 文字以下でなければなりません( 12 文字になっています)。'], 'email': ['このフィールドは必須です。'], 'content': ['このフィールドは必須です。']}
このように、Django shellで、フォームのバリデーションを試すことができます。
フォームの is_valid()
がTrueを返す状態であれば、 cleaned_data
プロパティでクリーニングしたフィールド毎のデータも取得できます。
フォームフィールドやウィジェットでデータ変換が発生するフォームの場合は、これもDjango shell上で試せるとデバッグがはかどると思います。
>>> form = ContactForm({'name': 'モンティ・パイソン', 'email': 'foo@example.com', 'content': 'お問い合わせ内容'}) >>> form.is_valid() True >>> form.cleaned_data {'name': 'モンティ・パイソン', 'email': 'foo@example.com', 'content': 'お問い合わせ内容'}
フォームを頻繁に書き換えて試す場合、毎回shellを起動しなおして、インポートからやりなおすのは手間です。
IPythonのshellであれば、autoreloadという拡張を使って少し楽をできるかもしれません。
autoreload — IPython 3.2.1 documentation
IPythonをインストールした状態で、 manage.py shell
を起動すると、IPythonのシェルになります。
%load_ext autoreload
で自動リロードの拡張をロードし、 %autoreload 2
で自動リロードのモードを変更します。
モード2は、「Reload all modules」となっていて、コード実行前に毎回すべてのモジュールがリロードされます。
In [1]: %load_ext autoreload In [2]: %autoreload 2 In [3]: from myapp.forms import ContactForm In [4]: form = ContactForm({}) In [5]: form Out[5]: <ContactForm bound=True, valid=Unknown, fields=(name;email;content)> # ここで、ソースコード内のcontentの行をコメントアウト In [6]: form = ContactForm({}) In [7]: form Out[7]: <ContactForm bound=True, valid=Unknown, fields=(name;email)> # 自動でリロードされたクラスが使われている
プロジェクトが大きいと、毎回全てのモジュールをリロードするのは遅い可能性があるので、その場合はドキュメントにあるように %aimport
を使うなどして、部分的にリロードするだけでもいいかもしれません。
Django shellでフォームをインポートして試せることを紹介しました。
最終的にはユニットテストのコードを書いて、保守しやすい状態にするのがよいですが、試行錯誤する段階では、このようにshell上でデバッグする方法も手軽なので、知っておくとよいかなと思います。