【Python】datetime.timedeltaは負の値になると特殊な挙動をする
datetime.timedeltaとは
datetime.timedeltaは、期間を表すクラス。
datetime --- 基本的な日付と時間の型 — Python 3.13.7 ドキュメント
例えば、ある2つの日時(datetime)を引き算すると、結果がtimedeltaクラスになる。
import datetime # 2025/01/01 dt1 = datetime.datetime(2025, 1, 1) # 2025/01/02 dt2 = datetime.datetime(2025, 1, 2) td = dt2 - dt1 print(type(td)) print(td)
<class 'datetime.timedelta'> 1 day, 0:00:00
この例では、2025/01/02から2025/01/01を引いたので、差の「1日」がtimedeltaとして取得されている。
負の値もある
逆に、2025/01/01から2025/01/02を引いた場合は「-1日」となる。
import datetime # 2025/01/01 dt1 = datetime.datetime(2025, 1, 1) # 2025/01/02 dt2 = datetime.datetime(2025, 1, 2) print(dt2 - dt1) print(dt1 - dt2)
1 day, 0:00:00 -1 day, 0:00:00
負の値は、ちょっと不思議な挙動をする
上の例では気にならないが、1日未満の部分(時/分/秒)を持つtimedeltaには注意が必要。
import datetime # 2025/01/01 12:00 dt1 = datetime.datetime(2025, 1, 1, 12, 0) # 2025/01/02 00:00 dt2 = datetime.datetime(2025, 1, 2) print(dt2 - dt1) print(dt1 - dt2)
12:00:00 -1 day, 12:00:00
-12時間の期間を表すtimedeltaが、-1 day, 12:00:00 となっている。(-12:00:00 とはならない)
これはtimedeltaクラスの仕様によるもの。
公式ドキュメントより引用。
days, seconds, microseconds だけが内部的に保持されます。 引数は以下のようにして変換されます:
・ 1 ミリ秒は 1000 マイクロ秒に変換されます。
・ 1 分は 60 秒に変換されます。
・ 1 時間は 3600 秒に変換されます。
1 週間は 7 日に変換されます。さらに、値が一意に表されるように days, seconds, microseconds が以下のように正規化されます.
・ 0 <= microseconds < 1000000
・ 0 <= seconds < 3600*24 (一日中の秒数)
・ -999999999 <= days <= 999999999
ようは、日の部分(days)は負の値を取りうるけど、それ以外の部分(seconds, microseconds)は正の値しか取らないように正規化されるということ。
先ほどの例の「-12時間」は「-1日と+12時間」と正規化されて、-1 day, 12:00:00 になった。
この仕様を知らずにtimedeltaの日の部分(days)だけで処理しようとすると、バグの原因になるかもしれない。
【Python】mock_openで複数のファイルの読み書きをMock化する
前にmock_openを使って1つのファイルの読み書きをMock化する方法を書いた。
複数のファイルの読み書きをMock化する場合は少しコツがあったので、メモしておく。
複数ファイルの読み込みをMock化する場合
import unittest from unittest.mock import MagicMock, patch, mock_open def read_2files(file_path1: str, file_path2: str) -> tuple[str, str]: # ファイルを2つ読み込んで返すだけ with open(file_path1, 'r', encoding='UTF-8') as f: data1 = f.read() with open(file_path2, 'r', encoding='UTF-8') as f: data2 = f.read() return data1, data2 class TestRead2Files(unittest.TestCase): def test_read_2files(self): # ファイルのモックを準備する file_dict = { 'file1.txt': mock_open(read_data='AAA')(), 'file2.txt': mock_open(read_data='BBB')(), } mock = MagicMock() # args[0]にファイルパスが入っているので、それに合わせてファイルのモックを切り替える。 # openを呼び出す際に名前付き引数を使っているなら、lambdaの引数はargsだけではなくkwargsも用意する必要があるので注意。 # 今回の場合、encodingを名前付き引数で指定している。 mock.side_effect = lambda *args, **kwargs: file_dict[args[0]] with patch('builtins.open', mock): # 処理を呼び出す result = read_2files('file1.txt', 'file2.txt') # 結果を確認 self.assertEqual(result, ('AAA', 'BBB'))
以下の部分で、ファイルパスごとにread_dataを設定したファイルのモックを用意している。
file_dict = {
'file1.txt': mock_open(read_data='AAA')(),
'file2.txt': mock_open(read_data='BBB')(),
}
mock_open(read_data='AAA')()のところは、カッコが続いて面食らうけど、以下のように段階を踏んで考えるとわかりやすい。
mock_open(read_data='AAA')- open関数のモック
mock_open(read_data='AAA')()- open関数を呼び出した結果
- つまり、
with open(file_path1, 'r', encoding='UTF-8') as f:でいうfのこと
複数ファイルの書き込みをMock化する場合
import unittest from unittest.mock import MagicMock, patch, mock_open def write_2files(file_path1: str, file_path2: str): # ファイル2つに書き込むだけ with open(file_path1, 'w', encoding='UTF-8') as f: f.write("AAA") with open(file_path2, 'w', encoding='UTF-8') as f: f.write("BBB") class TestWrite2Files(unittest.TestCase): def test_write_multi(self): file_mock1 = mock_open()() file_mock2 = mock_open()() file_dict = { 'file1.txt': file_mock1, 'file2.txt': file_mock2, } mock = MagicMock() mock.side_effect = lambda *args, **kwargs: file_dict[args[0]] with patch('builtins.open', mock): write_2files('file1.txt', 'file2.txt') file_mock1.write.assert_called_once_with("AAA") file_mock2.write.assert_called_once_with("BBB")
読み込みとあまり変わらない。
書き込み(write)が呼ばれたことをassertしたいので、ファイルのモックは変数に持たせている。(file_mock1、file_mock2)
複数ファイルの読み書き両方をMock化する場合
import unittest from unittest.mock import MagicMock, patch, mock_open def read_write_multi(input_file_path1: str, input_file_path2: str, output_file_path1: str, output_file_path2: str): # ファイル2つを読み込んで、それぞれの内容に追記して別ファイル2つに書き出す with open(input_file_path1, 'r', encoding='UTF-8') as f: data1 = f.read() with open(input_file_path2, 'r', encoding='UTF-8') as f: data2 = f.read() with open(output_file_path1, 'w', encoding='UTF-8') as f: f.write(data1 + " APPEND 1") with open(output_file_path2, 'w', encoding='UTF-8') as f: f.write(data2 + " APPEND 2") class TestReadWriteMulti(unittest.TestCase): def test_read_write_multi(self): output_file_mock1 = mock_open()() output_file_mock2 = mock_open()() file_dict = { 'input1.txt': mock_open(read_data='AAA')(), 'input2.txt': mock_open(read_data='BBB')(), 'output1.txt': output_file_mock1, 'output2.txt': output_file_mock2, } mock = MagicMock() mock.side_effect = lambda *args, **kwargs: file_dict[args[0]] with patch('builtins.open', mock): read_write_multi('input1.txt', 'input2.txt', 'output1.txt', 'output2.txt') output_file_mock1.write.assert_called_once_with("AAA APPEND 1") output_file_mock2.write.assert_called_once_with("BBB APPEND 2")
読み込みの例と書き込みの例を組み合わせただけ。
【Python】MagicMockの使い方メモ2 - ユニットテストでの使い方とpatch
ユニットテストでは、開発環境から呼び出せない処理はモックに差し替えてテストしたい。
たとえば、外部のAPIの呼び出しやファイルの読み書きなどをモックにすることが多い。
モックにするやり方が色々あるので、簡単にまとめる。
インスタンスのメソッドをMagicMockで置き換える
以下のhoge_funcメソッドをモックにしてみる。
class Hoge: def hoge_func(self): return 'called func'
hoge_funcメソッドにMagicMockを設定してみる。
メソッドがモックに置き換わり、戻り値が'called mock'になっている。
def test(self): hoge = Hoge() hoge.func = MagicMock(return_value='called mock') self.assertEqual(hoge.func(), 'called mock')
クラスメソッドをMagicMockで置き換える
クラスメソッドもモック化できる。
たとえば、以下のようにクラスメソッドを持つクラスがあったとする。
class Hoge2: @classmethod def class_func(cls): # 1から10のランダムな数値を返す return random.randint(1, 10)
クラスメソッドをMagicMockで置き換えれば、動作を変えることができる。
def test(self): print(Hoge2.class_func()) # 3 Hoge2.class_func = MagicMock(return_value=99) print(Hoge2.class_func()) # 99
対象メソッドのパスを指定してモック化する(patch)
ユニットテストで、モック化したいインスタンスのメソッドにアクセスできることは少ないと思う。
大抵は、テストしたいモジュールから呼び出される別のモジュールをモック化したいはず。
たとえば、以下のようなコードを考えてみる。
class Hoge: def hoge_func(self): # 1から10のランダムな数値を返す return random.randint(1, 10) class Fuga: def __init__(self): self.hoge = Hoge() def fuga_func(self): val = self.hoge.hoge_func() # 2倍にして返す return val * 2
Fugaクラスはインスタンス変数にHogeのオブジェクトを持っている。
Fugaクラスのfuga_funcのテストをしたい場合、Hogeクラスのhoge_funcの返す値がランダムなのでテストし辛い。
このような場合、patchでhoge_funcをモック化すると楽にテストできる。
def test(self): with patch('module.Hoge.hoge_func') as hoge_func_mock: # hoge_funcの戻り値を10に固定する hoge_func_mock.return_value = 10 fuga = Fuga() self.assertEqual(fuga.fuga_func(), 20)
patchは以下のようにデコレータとして書くこともできる。
@patch('module.Hoge.hoge_func', return_value=5) def test(self, hoge_func_mock): fuga = Fuga() self.assertEqual(fuga.fuga_func(), 10)
デコレータで指定したモックはユニットテストの引数として記載する必要がある。(hoge_func_mockの部分)
複数のデコレータを設定した場合は、 デコレータでで書いた逆順 に引数を書く必要があるので注意する。
【Python】MagicMockの使い方メモ1 - MagicMockとは
普段なんとなく使っていたMagicMockについて調べ直したのでまとめる。
MagicMockとは
いきなりうまく説明できないけど、定義していないメソッドでも呼べてしまう、ぐにゃぐにゃの粘土みたいなイメージ、、
m = MagicMock() func1_result = m.func1() print(func1_result) # <MagicMock name='mock.func1()' id='4313588304'> func2_result = func1_result.func2() print(func2_result) # <MagicMock name='mock.func1().func2()' id='4313657552'>
上の例では、生成したインスタンスmに対して、定義していないメソッドfunc1を呼び出している。
結果、func1は新しいMagicMockインスタンスを返している。
さらにそのインスタンスに対してメソッドfunc2を呼び出しても、同様に新しいMagicMockインスタンスを返している。
return_valueでメソッドが返す値を指定する
そのままでは単にMagicMockのインスタンスを返すだけで役に立たない。
MagicMockは、メソッドが返す値をreturn_valueに設定できる。
m = MagicMock() m.func1.return_value = "hoge" print(m.func1()) # hoge
side_effectでメソッドが呼ばれるたびに返す値を変える
メソッドを複数回呼び出したときに、返す値を変えたい場合はside_effectを使う。
m = MagicMock() m.func1.side_effect = ["hoge1", "hoge2"] print(m.func1()) # hoge1 print(m.func1()) # hoge2
メソッドを呼ばれたときに例外を投げたい場合もside_effectに設定する。
m = MagicMock() m.func1.side_effect = Exception("テスト用の例外") try: m.func1() except Exception as e: print(e) # テスト用の例外
値を返したり、例外を投げたりを組み合わせることもできる。
以下のコードは、3回目のfunc1で例外を投げている。
m = MagicMock() m.func1.side_effect = ["hoge1", "hoge2", Exception("テスト用の例外")] m.func1() m.func1() try: m.func1() except Exception as e: print(e)
ユニットテストでの使い方は、後で書く、、
【Python】mock_openでファイルの読み書きをMock化する
ファイルの読み書きをするプログラムをテストするとき、実際にファイルを用意するのは面倒。
ファイルの読み書きはMock化したいが、そんなときはPythonが公式で用意してくれているmock_open関数が便利。
たとえば、以下のようなプログラムをテストしたいとする。
(テキストファイルを読み込んで、1行追加した内容で別のファイルに保存しているだけ)
def func(input_file_path: str, output_file_path: str): with open(input_file_path, 'r', encoding='UTF-8') as f: data = f.read() with open(output_file_path, 'w', encoding='UTF-8') as f: f.write(data + '\nAPPEND!')
ファイルの読み書きをMock化してテストする場合、以下のようになる。
import unittest from unittest.mock import mock_open, patch, call import read_write # テスト対象のスクリプト class TestTextAppender(unittest.TestCase): def test_append(self): m = mock_open(read_data='INITIAL TEXT') with patch("builtins.open", m): # テスト対象のメソッドを実行 read_write.func("input.txt", "output.txt") # ファイル読み込みのためのopenが呼ばれたことの確認 self.assertEqual(m.call_args_list[0], call('input.txt', 'r', encoding='UTF-8')) # ファイル書き込みのためのopenが呼ばれたことの確認 self.assertEqual(m.call_args_list[1], call('output.txt', 'w', encoding='UTF-8')) # mock化されたopenを呼び、ファイルオブジェクト(これもmock化されている)を取得する file = m() # ファイル書き込みのwriteが呼ばれたことの確認 file.write.assert_called_once_with("INITIAL TEXT\nAPPEND!")
入力ファイルはmock_openメソッドの引数read_dataに設定すればOK。
出力ファイルはファイルオブジェクトのwriteから取得する。
mock_open についてのメモ
公式のリファレンスを見ると、mock_openは以下のように説明されている。
open() の利用を置き換えるための mock を作るヘルパー関数。 open() を直接呼んだりコンテキストマネージャーとして利用する場合に使うことができます。
コンテキストマネージャーとは、withブロックで使うことでファイルやリソースの開放を自動でやってくれるやつのこと。
JavaでいうClosableやAutoClosableみたいなもの。
ファイルの入出力は、コンテキストマネージャーとしての処理とreadやwriteの処理があって、mock化するのが大変だからmock_openが作られたのかな、、?
【PyCharm】選択中の変数のハイライト色を変える
PyCharmを使うときに設定しておきたい点。
選択中の変数をハイライトする色を変える。
設定変更前

dataを選択しているのだけど、色が地味過ぎて分かりづらい、、
設定変更後

これなら見やすい。

Editor > Color Scheme > General にある、Code > Identifier under caret と Identifire under caret (write) の色を変える。
変数の使用箇所を確認しながらコードを読みたいので、目立つ色だと嬉しい。
【Django】Factory Boyで、カテゴリを外部テーブルに持つようなレコードを簡単に作成する
以下のような、「本」と「本の種別(マスターデータ)」を表すModelがあるとする。
class BookCategory(models.Model): name = models.TextField() class Book(models.Model): name = models.TextField() book_category = models.ForeignKey(BookCategory, on_delete=models.DO_NOTHING)
実データの例は以下のような感じ。

ER図にすると以下のようになる。

BookのレコードをFactory Boyで作りたいとする。
単純に作ると、以下のようになる。
class BookFactory(DjangoModelFactory): class Meta: model = models.Book name = "Test Name" book_category = BookCategory.objects.get(name='comic')
この例では、book_categoryのデフォルト値を'comic'のレコードにしている。
ただ、ユニットテストではカテゴリを色々と変えてレコードを作りたい。
# カテゴリがデフォルトの'comic'ならこれでいいけど book1 = BookFactory.create() # 別のカテゴリを設定したいときは、マスターデータを取得してくる必要がある。ちょっと面倒。 category = BookCategory.objects.get(name='science') book2 = BookFactory.create(book_category=category)
こういうときは、excludeとlazy_attributeを組み合わせて、factory側でマスターデータを取得すると便利。
class BookFactory(DjangoModelFactory): class Meta: model = models.Book exclude = ["category_name"] name = "Test Name" # Bookのmodelに存在しない、カテゴリを取得するキーとして使うだけのフィールド category_name = "comic" book_category = factory.LazyAttribute(lambda o: BookCategory.objects.get(name=o.category_name))
category_nameというフィールドを追加し、BookCategoryを検索するキーとして使用している。
book_categoryはLazyAttributeとして遅延評価させて取得している。
category_nameはBookには存在しないフィールドなので、excludeにに指定する必要がある。
(excludeに指定することで、factory内で使うだけのフィールドだということを明示している)
book2 = BookFactory.create(category_name='history')
Bookの生成は以上のようになるため、スッキリ書けるようになった。