きり丸の技術日記

技術検証したり、資格等をここに残していきます。

AnyIOのテストをasyncioだけで動かす(asyncioとtrioの2倍動かないようにする)

AnyIOを使用しているテストで、意図せずにasynciotrioの2つで動いてしまったテストがあったので、asyncioだけにした時のメモ。

環境

  • Python
    • 3.11
  • AnyIO
    • 3.7.1
  • Pytest
    • 7.4.3

実装

conftest.pyに次の処理を入れると、asyncioだけが動きます。

@pytest.fixture
def anyio_backend():
    return 'asyncio'

テストをAnyIOで動かすには

次のどちらかで動きます。

# 先頭で宣言する
pytestmark = pytest.mark.anyio

# または、メソッドごとに定義する
@pytest.mark.anyio

備考

最新のpytest-asyncioのライブラリを入れるだけで非同期処理のテストができるようになっています。

昔は上のようにpytestmarkの宣言だったり、デコレータで@pytest.mark.anyioまたは@pytest.mark.asyncioと定義する必要がありました。

ソースコード

conftest.pyから上の定義を削除すると、test_task.pyのテストが失敗するようになっています。

終わりに

FastAPIでは非同期処理をする際に内部処理でAnyIOを使用しているので、テスト時にAnyIOを使用するチュートリアルがあります。

チュートリアルにしたがってテストをすると意図せずにテストケースが2倍動いてしまっていました。処理自体は高速に動いているのでそこまで苦労しないのですが、テストケースが失敗する時に2倍失敗してしまうのでTDDが地味に面倒になっていました。

また地味に面倒なことに、dependenciestrioに依存したライブラリはなかったのですが、dev-dependenciestrioに依存したライブラリがあってローカルだけテスト数が2倍になってしまうということも発生していました。

地味ながら修正をしたことで日々の開発が便利になっています。

参考文献

Pythonのenumはint等のプリミティブ型を継承すると便利

基本的には推奨されない書き方のようですが、使ってみて便利だったのでメモします。

環境

  • Python
    • 3.12

実装

int等のプリミティブな型の継承は次のとおりです。

from enum import Enum

class IntInheritEnum(int, Enum):
    ID = 1


# 列挙型ではなく、intとして振舞うため1にアクセスできる
IntInheritEnum.ID

# 本来のアクセス方法
IntInheritEnum.ID.value

ただし、自作クラス等は継承できません。metaclassの競合が起きるため、クラスで定義したかったものがあれば、namedtuple等を使用するとアクセスしやすくなります。

# metaclassの競合が起きるから、継承できない
# E   TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
# class DummyUser(User, Enum):
#     ADMIN = User(name="admin", email="admin@example.com")

DummyValue = namedtuple('DummyValue', ['name', 'email'])
class DummyUser(Enum):
    ADMIN = DummyValue("admin", "admin@example.com")

# こうやって各項目にアクセスする
DummyUser.ADMIN.value.name
DummyUser.ADMIN.value.email

推奨されない理由

型が違うまま計算できてしまうため、誤った処理をしかねないのが推奨されない原因のようです(ChatGPTより)

# 継承していなければエラーとなる
# 継承しているのでエラーならない
result: int = IntInheritEnum.ID + 10

# 型が違うのに計算できてしまうのは確かに違和感がある
def add(num1: int, num2: IntInheritEnum):
    return num1 + num2

ユースケース

indexでアクセスしたい時にintを継承したenumを使用することで簡潔にアクセスできる。

JOINしたテーブルの取得結果の操作や、pandasでCSVを読み込んだ時の列をenumを使用することで列名として代用できます。

本来はもっと人間に読みやすい定義方法もあると思うのですが、わかりませんでした。namedXXXを駆使すれば行けそうだとは思っていますが…。

ソースコード

終わりに

毎回.valueでアクセスしていた処理が無くなったので、コード自体は読みやすくなりました。

少しずつ、リーダブルなコードが書けるようにしたいですね。

Pythonの__iter__を素振りする(First Class Collection[FCC]で使えるかも)

Pythonではイテレータを簡単に作れます。もしかしたら何かに使えるかもしれないので、素振りします。

環境

  • Python
    • 3.12

ゴール

PythonのFCC(First Class Collection)にて、For文を使用する際にFCCのインスタンスのまま使用できること。

from typing import List

class User():
  # ... 省略

class Users():
  value: List[User]

  # コンストラクタ省略


users = Users([])
# この記載ができることがゴール
[user for user in users]
# ※ もちろん、対応前でも値を取得すれば元の通りにアクセスできます。
[user for user in users.value]

実装

iterメソッドか、yield fromで内部の値にアクセスすることで、外側からは意識せずともFor文で使用できます。

from typing import List, Iterable

class Users():
  value: List[User]

  def __iter__(self) -> Iterable[User]:
    # イテレータプロトコルを実装する
    return iter(self.value)
    # yield from でListのイテレータを呼ぶこともできる
    yield from self.value

ソースコード

終わりに

頭に良い人ならもうちょっと面白い使い方ができそうなのですが、今の私ではFCCを使用する際に記載量を減らせる、というくらいにしか使え無さそうでした。

Pythonで型を意識するような書き方自体が微妙かもしれませんが、ライブラリが優秀なおかげで他言語と遜色ない処理が色々とできますし、しばらくの期間はPythonを使い倒せるようにしたいですね。