Dartの開発環境を準備する

社内ライブラリのDart版の動作検証をするために、Dartの開発環境を準備する必要があったので、自分用にまとめておく。

DartSDKのインストール

Dartのコードを実行、ビルドするためにはSDKをセットアップする。

Get the Dart SDK | Dart

今回はUbuntu環境だったので、Linux用の手順でaptを使ってパッケージをインストールした。手順通りで特に問題なし。

dartコマンド

dartの実行、ビルドなどは dart コマンドからできる。

サードパーティパッケージ

pub.devというサービスがパブリックなサードパーティパッケージのホスト先。

アプリの依存パッケージは pubspec.yaml ファイルに記載。

dart pub get コマンドで依存パッケージをインストールできる。

dartコマンドからファイルを指定して実行

main.dart ファイルを実行する場合、

dart run main.dart

dartコマンドでコンパイル

main.dart ファイルをコンパイルする場合、

dart compile <target> main.dart

<target> には exe(実行ファイル) や jsJavaScript)などを指定できる。

exeの場合、Dartのランタイムを含む実行ファイルが出力される。クロスコンパイルの機能はないが、各コンパイル環境のOSで実行可能なバイナリを生成できる。

導入から実行、ビルドまではスムーズにできたのでよかったです。

Pythonのdataclasses.dataclassを使う

Pythonのdataclasses.dataclassは普段からたまに使っていますが、良く使っている書き方を人に紹介するためにメモを残します。

dataclasses - データクラス - Python 3.12.2 ドキュメント

dictと相互変換するクラス

オブジェクトとdictで相互変換するクラスをdataclassで書くことがあります。

asdict 関数が便利です。リストで保持したいメンバー変数は、 field 関数を使って定義すれば、asdictでそのまま対象にできます。

また from_dict メソッドは厳密に実装するなら引数のチェックなどをしてもよいですが、可変長のキーワード引数としてそのままコンストラクタに渡すように書けば、実装はシンプルです。

コード

main.py:

from dacite import from_dict
from dataclasses import dataclass, asdict, field


@dataclass
class Person:
    name: str
    age: int

    @classmethod
    def from_dict(cls, data: dict):
        return cls(**data)


@dataclass
class Group:
    name: str
    members: list[Person] = field(default_factory=list)

    @classmethod
    def from_dict(cls, data: dict):
        members_data = data.get("members", [])
        instance = cls(**data)
        if members_data:
            instance.members = [
                Person.from_dict(member_data) for member_data in members_data
            ]
        return instance


def main():
    person = Person(name="Alice", age=30)
    group = Group(name="Team1", members=[person])

    # groupをdictに変換すると、membersの要素もdictに変換される
    data = asdict(group)
    print(data)

    # dictからGroupオブジェクトを作成すると、membersの要素もPersonオブジェクトに変換される
    group2 = Group.from_dict(data)
    print(group2)

    # daciteを使えば、from_dictメソッドがなくても、dataclassのネストしたオブジェクトを作成できる
    group3 = from_dict(data_class=Group, data=data)
    print(group3)


if __name__ == "__main__":
    main()

実行結果

daciteを事前にインストール済み。

$ python main.py 
{'name': 'Team1', 'members': [{'name': 'Alice', 'age': 30}]}
Group(name='Team1', members=[Person(name='Alice', age=30)])
Group(name='Team1', members=[Person(name='Alice', age=30)])

PersonとGroupのdataclassはどちらも親クラスの記述は無し。 from_dict クラスは明示的に実装しています。

fieldを読み取って自動的に from_dict の振る舞いをかえるような実装もできますが、継承関係を作らないのと、仕組みを複雑にしないためにあえてしていないです。

from_dict メソッドの実装を省略したい場合は、 dacite のようなサードパーティパッケージを使うのもよさそうです。

Djangoテンプレートで継承を利用して各ページ共通のヘッダーやフッターを設定する

Djangoフレームワークのテンプレートの継承について、基本的な内容です。

base.html というファイルを作って、共通部分をまとめる話。

検索用に記事をまとめておきます。

※この記事は 力強くアウトプットする日の 20240301 のアウトプットです。

テンプレートの継承

Djangoのドキュメントに例と説明があります。

テンプレートの継承 - Djangoドキュメント

Djangoのテンプレートには『継承』という仕組みがあります。

どのようなときに利用するか

HTMLでウェブサイトを作る場合、複数のページでヘッダー部とフッター部を共通にすることがよくあります。 また、サイドバー、レイアウトなど、複数ページに渡って共通の構造とするパターンがとても多いです。

こうした構造のときにテンプレート継承がうまくハマります。

base.html:

<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
{% block content %}{% endblock %}
<script>
// 全ページ共通で差し込むJavaScript
</script>
</body>
</html>

test.html:

{% extends "base.html" %}

{% block title %}テストページ{% endblock %}

{% block content %}
<p>
  bodyタグ内のコンテンツ
</p>
{% endblock %}

views.py:

from django.shortcuts import render

def test(request): 
    return render(request, "test.html")

レンダリング結果

<html>
<head>
<meta charset="utf-8">
<title>テストページ</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>

<p>
  bodyタグ内のコンテンツ
</p>

<script>
// 全ページ共通で差し込むJavaScript
</script>
</body>
</html>

テンプレート継承の利点

extendsタグで検証したテンプレートファイルでは、継承元のblockのうち、 変更したい部分 だけを記載することができます。 継承元のテンプレートに変更可能なblockがたくさんあっても場合、必要な部分だけ書けばいいので、記述量の削減につながります。

テンプレート継承の動作イメージ

比較対象として、 includeタグ があります。 includeタグは、指定したテンプレートファイルの内容を、includeタグの記載位置に取り込みます。

includeタグの動作イメージ

includeの場合、この図の例のようにヘッダーとフッターが別のファイルに分離されます。HTMLの場合、 <html><body> も共通部に書きたい場合が多いと思いますが、閉じタグの対応が別々のファイルに含まれるのは見通しが良くないです。

また、includeだけで組み立てる場合は、includeタグを記述しないと対象のテンプレートが取り込まれないので、変更が少ししかないページでも記述量が多くなりがちです。

よくつかうsshコマンドのオプション

sshコマンドのオプションは、特定の環境下で作業する際にはよく使うのだけど、しばらく使わないと忘れてしまって毎度しらべているので、自分用にまとめておく。

※この記事は 力強くアウトプットする日の 20240216 のアウトプットです。

ssh(1)のドキュメント

man.openbsd.org

-L ポートフォワーディング

書式

ssh -L [bind_address:]port:host:hostport destination

destination側から見た host:hostport を接続元の環境のportにフォワードできる。bind_addressを省略した場合はlocalhost

ssh -L 8888:localhost:8000 example.com

example.com というsshでログインできるホストの localhost:8000 = 127.0.0.1:8000 を接続元の 8888フォワード。 接続元の端末で、 localhost:8888 につなぐと、 example.com上の localhost:8000 につながる、と考えてよい。

踏み台とする場合

hostの部分はdestinationからネットワーク的に接続可能であれば、localhost以外も指定できるので、たとえばdestinationのサーバーからしか接続できない、内部ネットワーク上にあるホストに接続することもできる。

ssh -L 8888:192.168.100.123:3306 example.com

example.comというホストから見て、内部ネットワークにあたる192.168.100.123というIPv4アドレスのホストでデータベースサーバーが動作している場合(3306はMySQLのデフォルトポート)

このコマンドを実行した環境で、 localhost:8888 に接続すると、192.168.100.123:3306ポートにつながる。

もし、MySQLのコマンドから利用するのであれば、以下のようなコマンドになる。

mysql -h localhost -P 8888 -u db_user -p db_name

このように、example.comを踏み台として、その先にあるホストに接続が可能となる

-D ダイナミックフォワーディング(SOCKSプロキシー

書式

ssh -D [bind_address:]port destination

destinationを起点としたSOCKSプロキシーを、接続元の環境のportで起動する。bind_addressを省略するとlocalhost

SOCKSプロキシーはアプリケーションレベルでフォワーディングをしたいときに使う。主にウェブブラウザ。

ssh -D 8888 example.com

接続元の環境の localhost:8888 でSOCKSプロキシーが起動する。たとえば、example.com経由でしか接続できない内部ネットワーク上のウェブサイト(例: internal.example.com)をブラウザで見たい場合。

ウェブブラウザのプロキシー設定で、SOCKSプロキシーのアドレスに localhost:8888 を設定し、ブラウザのアドレスバーに internal.example.com と入力して接続すると、 example.com のホストから通信を行うことになるため、閲覧できるようになる。

Chromeの場合、プロキシー設定は Proxy SwitchyOmega などの拡張を使うと有効/無効の管理がしやすい。

-L オプションだと1つのホスト、ポートを対象としたフォワーディングだが、 -D の場合はSOCKSプロキシーに対応したアプリからは、destinationのホスト経由で任意のホストに接続できる。

-R リモートフォワーディング

書式

ssh -R [bind_address:]port:host:hostport destination

destinationのホストのportに接続すると、接続元の host:hostport につながる。-L の逆向きと考えればよい。

-X X11フォワーディング

書式

ssh -X destination

接続元環境のX11ディスプレイサーバーにdestination先のX11の接続をフォワーディングする。

destinationの環境でデスクトップアプリを起動し、画面だけを接続元環境に持ってきたい場合に使う。

たとえば、destination先のLinux環境にChromiumをインストールして起動し、接続元環境で画面の操作をしたい場合。VNCなどのリモートデスクトップ接続を利用しなくても、sshだけつながれば使える。

その他

多段SSH

destinationを経由して更に別のホストにsshで接続したい(=多段SSHしたい)場合、ProxyCommandやncコマンドを使う例がでてくるが、最近のOpenSSHの場合は、 -J オプションや ProxyJump.ssh/config)を使えば記述がシンプルになる。

DjangoMeetupTokyo #12を開催しました

2024/1/28にDjangoMeetupTokyo #12を開催しました。

DjangoMeetupTokyo #12 - connpass

去年の夏以来の開催でしたが、今回は前回の倍ぐらいの参加者が集まりました。

コロナ禍が落ち着いてきて、勉強会などのイベントで基本的に制限がなくなったので、コロナ禍以前には活発だったオフラインの会を復活させていきたいなぁと個人的に思っています。

中級者向けハンズオン

今回、私が資料を準備して、中級者向けハンズオンというのをやりました。

DjangoMeetupTokyo #12 中級者向けハンズオン — django-meetup-tokyo-12 ドキュメント

説明にある通り、「チュートリアルは理解できている、Djangoは使ったことあるけど、使いこなせてない~という人向けのハンズオン」というのを以前からやりたいなと考えていたものを、ようやく実施できました。

資料の内容を順番に説明し、手元で動かしてもらいながら、Djangoの機能について詳しく見ていきました。

好評のようでしたので、他の題材も考えて、またやれればよいなーと思います。

ようす

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の教材作成についてや、プログラミング教育についてなど。