Python/Django のTips 備忘録
概要
Python/Django 開発での個人的な Tips を書き足していく。
あくまで Python 初心者である筆者の備忘録です。内容の正確性が担保されたものではないことに注意。
クラスメソッドのモック
テスト時、特定のクラスのメソッドをモックしたい場合
例: Foo クラスの do メソッドの中で Slack 通知を行うような実装
import unittest.mock
from foo import Foo
from utils.slack import SlackUtils
class TestFoo
@unittest.mock.patch.object(SlackUtils, 'send')
def test_get(self, mock_slack_send):
Foo.do()
assert "メッセージ内容" in mock_slack_send.call_args.kwargs['message']
related_name
Model 同士にリレーションを張る際、 related_name
を指定することができる。
from django.db import models
class User(models.Model):
# 色々なプロパティ
class Profile(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_profile')
user = User.objects.first()
user.user_profile
1対1でのリレーションの存在確認
ObjectDoesNotExist
でエラーハンドリングする。
from django.core.exceptions import ObjectDoesNotExist
class Person(models.Model):
def get_profile(self):
try:
return self.profile
except ObjectDoesNotExist:
return None
class Profile(models.Model):
person = models.OneToOneField(Person, on_delete=models.CASCADE)
あるいは has_attr
を利用してもよい。
class Person(models.Model):
def get_profile(self):
return self.profile if hasattr(self, 'profile') else None
class Profile(models.Model):
person = models.OneToOneField(Person, on_delete=models.CASCADE)
related_name 経由でリレーション解決すると Any になる
related_name で指定した名称で解決しようとすると、Any になってしまう。(自分の書き方が悪いのかもしれない)
from typing import Optional
class User(models.Model):
# 色々なプロパティ
class Profile(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_profile')
user = User.objects.first()
profile = user.user_profile # Any
# 型指定しても Any は取れない
profile: Optional[UserProfile] = user.user_profile # UserProfile | None | Any
cast を使うといったん解決。
from typing import cast, Optional
profile = cast(Optional[UserProfile], user.user_profile) # UserProfile | None
Django Admin の save_model メソッド
Django Admin 上の保存・変更をフックにして、何かの処理をしたいときがある。関連するデータも更新する、など。
そのような場合に save_model
メソッドが利用できる。
from models import Person
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
# 色々なプロパティ
# 色々なメソッド
def save_model(self, request, obj, form, change):
before = User.objects.get(id=obj.id)
# before と after(obj) を利用して、ここで処理を実行できる。
# 保存を続行するには、親メソッドを実行
super().save_model(request, obj, form, change)
request
には HTTP リクエスト情報が含まれる。あまり使うことはないかも?
change
には bool が入り、新規追加なら False が入り、既存のレコードへの編集なら True が入る。
admin.py のファイル分割
Django Admin の記述を1つの admin.py ファイルに加えていくと、ファイルの行数がどんどん増えていく。これを分割する方法。
# 分割前
- my_project
- admin.py # 例えば、Author, Book などの Model を含むとする
# 分割後
- my_project
- admin # ディレクトリ(モジュール)にしてしまう
- __init__.py
- author_admin.py
- book_admin.py
分割したファイル内で admin.register を行う。
from django.contrib import admin
from api.models.author import Author
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
# list_display や had_delete_permission など
そして __init__.py に呼び出せばOK
import my_project.admin.author
import my_project.admin.book
マイグレーション時に何もしない
マイグレーションファイルで「何もしない」を表現したいとき、 noop
が利用できる。
from django.db import migrations
def upgrade(apps, schema_editor):
# アップグレード時の処理
class Migration(migrations.Migration):
dependencies = [
('app_name', '1234_previous_migration'),
]
operations = [
migrations.AddField(
model_name='person',
name='address',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='app_name.address'),
),
migrations.RunPython(
code=upgrade,
reverse_code=migrations.RunPython.noop, # ロールバック時は何もしない
)
]
ちなみに、 RunPython の reverse_code だけを省略することはできない。
migrations.RunPython(
code=upgrade
)
TYPE_CHECKING で型定義の循環参照を回避する
Django でモデルが複雑になってくると、循環参照に陥ることがある。
# Person
from django.db import models
from models.book import Book
class Person(models.Model):
def get_books() -> list[Book]
return ...
# Book
from django.db import models
from models.person import Person
class Book(models.Model):
person = models.ForeignKey(Person, ...)
ForeignKey の指定に文字列を使えば、循環参照の問題は回避できる。が、型情報が失われてしまい不便。
from django.db import models
class Book(models.Model):
person = models.ForeignKey('Person', ...) # 文字列に変更した
typing が提供する TYPE_CHECKING
を利用すれば、この問題は解決できる。
# Person
from typing import TYPE_CHECKING
from django.db import models
if TYPE_CHECKING:
from models.book import Book
class Person(models.Model):
def get_books() -> list['Book']
return ...
# Book
from typing import TYPE_CHECKING
from django.db import models
if TYPE_CHECKING:
from models.person import Person
class Book(models.Model):
person = models.ForeignKey['Person']('Person', ...)
クォーテーションで囲まないと行けないのがちょっと気持ち悪いけど…。
参考:
複合インデックス、複合ユニーク制約
Django で複合インデックス、複合ユニーク制約を指定する方法。 Meta クラス内で指定する。
from django.db import models
class Person(models.Model):
foo = models.CharField()
bar = models.CharField()
class Meta:
indexes = [
models.Index(fields=['foo', 'bar'], name='%(class)s_foo_bar_idx')
]
constraints = [
models.UniqueConstraint(fields=['foo', 'bar'], name='%(class)s_foo_bar_unique')
]
name
はDB全体でユニークであるべきなので、 %(class)s_
プレフィックスをつけている。
除外クエリ(__ne)
除外クエリは __ne
で実現可能。
result = Book.objects.filter(category__ne=BOOK_CATEGORY.NOVELS)
ただし、以下のような複合クエリでは使えない。
result = Book.objects.filter(
Q(category__ne=BOOK_CATEGORY.NOVELS) |
Q(published_at__gte=...
)
# django.core.exceptions.FieldError: Unsupported lookup 'ne' for CharField or join on the field not permitted.
exclude で除外すればOK。__ne とは真偽が反転することに注意。
result = Book.objects \
.filter(published_at__gte=...) \
.exclude(category=BOOK_CATEGORY.NOVELS)