Pythonstandard
2022Spring
目次
developmenttools
package manager
format
lint
test
typing
async/await
その他
developmenttools
結論的には下記の組み合わせがおすすめ
packagemanager
poetry
format
black,isort,docformatter
lint
pyright,mypy,flake8
test
doctest,pytest
近代的な Python 開発ツール
packagemanager
ビルドルール(依存関係)の明示
作者・ライセンスなどのメタデータの明示
ライブラリのパッケージリポジトリへの公開
例
Python:poetry
Rust:cargo
JavaScript:npm
(C:meson)
依存関係を含めてパッケージの実行(ビルド)環境の構築を自動化するツール
poetry–packagemanager
pyproject.toml(PEP518) に基づく packagemanager
この先スタンダードになると思われる
black や pyright などのツールの設定もこの中に書ける
pipenvのようなタスクランナー機能 (npm run のようなもの) はない
poethepoet などの追加ツールで対応
参考:pipenvとの比較
$ poetry new my-project

$ cd my-project

$ poetry add fastapi

$ poetry add --dev poethepoet

$ poetry run python my_project/__init__.py

$ poetry run poe lint
format
近年は自動フォーマットが主流
十分に高性能、レビューでの指摘が面倒
例
Python:black,isort,docformatter
Rust:rustfmt
TypeScript 等:prettier
C,C++:clang-format
ソースコード整形ツール
black,isort,docformatter–format
black
厳しい PEP8フォーマッター
設定項目がほぼない(プロジェクト間でブレない)
isort
モジュールの import 順のソートを行う
システム標準 → 外部(依存) → ローカル
ブロックごとにアルファベット順
docformatter
docstring の整形を行う
これだけで見た目は非常に一貫してきれいに。
pyproject.toml設定例 –format
black は 88桁改行なので、isort にそれを教える必要がある
[tool.poe.env]

LINT_TARGETS = "my_project tests"

[tool.poe.tasks]

fmt-black = "black $LINT_TARGETS"

fmt-docformatter = "docformatter -ri $LINT_TARGETS"

fmt-isort = "isort $LINT_TARGETS"

fmt = ["fmt-isort", "fmt-black", "fmt-docformatter"]

[tool.isort]

profile = "black"
$ poetry run poe fmt

$ poetry run poe fmt-black
lint
時代は型安全かつ静的型付けへ
C言語(型安全ではない)→ SEGVで死ぬ
多くのスクリプト言語(型安全の動的型付け)→ 型エラーで死ぬ
Rust,TypeScript など(型安全の静的型付け)
動的型付けで正しいプログラムを書くのは難しい(とても大変)
例
Python:pyright,mypy,flake8
Rust,TypeScript,C,… (静的型付け言語):各種コンパイラ
Rust:clippy
参考:https://ja.wikipedia.org/wiki/型システム
ソースコード検査ツール
pyright–lint
Microsoft 製型チェックプログラム (TypeScript で実装)
(おそらく) TypeScript の実装で培われた型チェック機構を Python に応用
未使用変数の警告など、型チェック以外の検査機能も強力
mypy,flake8は併用せずにこれ一本でもほぼ十分
使い方
strict モードを推奨
外部モジュールが影響する項目だけ制限を弱める
欠点
未インポートエラーなどの一部の実行時エラーが検出できない
pyright設定例 –lint
設定例
[tool.pyright]

venvPath = "."

venv = ".venv"

typeCheckingMode = "strict"

#reportImportCycles = "warning"

reportMissingTypeStubs = "none"

reportUnknownArgumentType = "none"

reportUnknownLambdaType = "none"

reportUnknownMemberType = "none"

reportUnknownParameterType = "none"

reportUnknownVariableType = "none"

reportUnnecessaryIsInstance = "none"

reportUntypedFunctionDecorator = "none"
mypy–lint
Python 製の型検査プログラム
pyright に比べると貧弱
未インポートエラーなどは検出できる
設定例
[tool.mypy]

python_version = "3.10"

strict = true

implicit_reexport = true

ignore_missing_imports = true

disallow_untyped_decorators = false
flake8–lint
昔からある Python の lint プログラム
pyright があれば不要かもしれないが念のため使うくらい
pyproject.tomlの採用を頑なに拒んでいるくらい既に香ばしい
設定例 (setup.cfg)
[flake8]

max-line-length = 88

extend-ignore = E203
pyproject.toml設定例 –lint
pyright,mypy,flake8,format ツールの順がおすすめ
[tool.poe.env]

LINT_TARGETS = "my_project tests"

[tool.poe.tasks]

lint-black = "black --check --diff $LINT_TARGETS"

lint-docformatter = "docformatter -rc $LINT_TARGETS"

lint-flake8 = "flake8 $LINT_TARGETS"

lint-isort = "isort --check --diff $LINT_TARGETS"

lint-mypy = "mypy $LINT_TARGETS"

lint-pyright = "pyright $LINT_TARGETS"

lint = ["lint-pyright", "lint-mypy", "lint-flake8",

"lint-docformatter", "lint-black", "lint-isort"]

$ poetry run poe lint
test
テストの重要性は昔から変わらず、目立ったトレンドはない
単体テスト・結合テストをうまく書き分けられるように
mock して外部非依存的にテストできるように
例
Python:pytest,doctest
Rust:標準モジュール,mockall
TypeScript:Jest
自動テスト
doctest–test
docstring の中に(ドキュメントとして)テストを書ける
関数・メソッドの単体テストが書きやすい
def div(x: int, y: int) -> int:

"""

>>> div(4, 2)

2

>>> div(5, 2)

2

>>> div(2, 0)

Traceback (most recent call last):

...

ZeroDivisionError: integer division or modulo by zero

"""

return x // y
pytest–test
統合的な Python のテストフレームワーク
fixture,mock,parametarize,…
doctest の自動実行も可能
プラグインでカバレッジの測定も可能 (pytest-cov)
設定例
[tool.poe.tasks]

test = "pytest"

[tool.pytest.ini_options]

addopts = "--doctest-modules --cov=fastapi_sample"
$ poetry run poe test
typing
pyright や mypyで型検査をするために、プログラマが型を明示する
ローカル変数など型を明示しなくても型推論が可能な箇所もある
型は静的型検査用につけるだけで、実行時に検査はされない
言語仕様ではないため Rust や TypeScript と比べると機能は貧弱
それでもないよりはかなりマシ
Python の型アノテーション機能
型アノテーションの基礎(1)–typing
a: str = "Hello, World!" # OK

b: int = "Hello, World!" # NG

c = "Hello, World!" # c は str と推論される

c = 3 # NG: c は str なのでエラー

d: str | int = "Hello, World!" # OK: Python 3.10 未満では Union[str, int]

d = 3 # OK

d = [3] # NG: list[int] を代入しようとしている

e: Any = "Hello, World!" # OK

e = 3 # OK

e = [3] # OK
型アノテーションの基礎(2)–typing
t1: tuple[float, float] = (0.0, 0.0) # OK

t2: tuple[str, int] = ("foo", "bar") # NG: "bar" は int ではない

l1: list[int] = [1, 2, 3] # OK

l2: list[int] = [1, 2, "string"] # NG: "string" は int ではない

l3 = [1, 2, True] # l3 は list[int | bool] と推論される

l3.append(3) # OK

l3.append([5]) # NG: list[int] は追加できない

l4: list[Any] = [1, 2, True]

l4.append([5]) # OK

d = {"a": 1, "b": 2} # d は dict[str, int]

d[(1, 2)] = 1 # NG: key の (1, 2) は str ではない

d["c"] = "string" # NG: value の "string" は int ではない
型アノテーションの基礎(3)–typing
def square(x: float) -> float:

return x * x

square("3") # NG: "3" は float ではない

square(3.0) # OK

square(3) # OK!: 3 は float ではないが…

a: str = square(3) # NG: square(3) の結果は str ではない

def pow(x: float, y: float) -> float:

return "string" # NG: 戻り値が float ではない

f: Callable[[float, float], float] = pow
型アノテーションの基礎(4)–typing
a: Optional[int] = 1 # OK: Optional[int] は int | None と同じ意味

a = None # OK

a = "string" # NG: int でも None でもない

def _sq_opt(x: Optional[int]) -> int:

return x * x # NG: x は None かもしれない

def sq_opt(x: Optional[int]) -> int:
if x is None:

return 0

return x * x # OK: ここでは x が None ではない保証がある

def union_test(x: str | int) -> None:

y: int = x # NG: x は str かもしれない

if isinstance(x, int):

y: int = x # OK

else: # ここが isinstance(x, str) だと pyright が警告 (reportUnnecessaryIsInstance)

y: str = x # OK
型アノテーションの基礎(5)–typing
class A:

pass

class B(A):

pass

class C(A):

pass

a: type[A] = A # OK

b: type[A] = B # OK (サブクラスなら通る)

c: type[B] = C # NG
dataclass–typing
Python のクラス定義の新標準
@dataclass

class NewClass:

version: ClassVar[str] = "1.0"

name: str

address: str

latitude: float

longitude: float
class OldClass:

version = "1.0"

def __init__(self, name: str, addrss: str, latitude: float, longitude: float) -> None:
self.name: str = name

self.address: str = name # 書き間違い!

self.latitude: float = latitude

self.longitude: float = longitude
dataclassの例 –typing
from __future__ import annotations # User から User の再帰的参照のため

@dataclass

class User:

version: ClassVar[str] = "1.0"

id: int

name: str

following: list[User] = field(default_factory=list)

a = User(1, "User A")

# User(id=1, name='User A', following=[])

b = User(2, "User B", [a])

# User(id=2, name='User B', following=[User(id=1, name='User A', following=[])])

a.following.append(b)

# User(id=1, name='User A', friends=[User(id=2, name='User B', friends=[...])])
dataclass_json–typing
dataclassの JSON シリアライザ・デシリアライザ
Rust の serde/serde_json のように使える
# 循環参照があるとシリアライズがエラーになるので例はあまり良くない

@dataclass

class User(DataClassJsonMixin):

id: int

name: str

following: list[User] = field(default_factory=list)

a = User(1, "User A")

b = User(2, "User B", [a])

# User(id=2, name='User B', following=[User(id=1, name='User A', following=[])])

b.to_dict()

# {'id': 2, 'name': 'User B', 'following': [{'id': 1, 'name': 'User A', 'following': []}]}

b.to_json()

# '{"id": 2, "name": "User B", "following": [{"id": 1, "name": "User A", "following": []}]}'

assert b == User.from_dict(b.to_dict()) # OK

assert b == User.from_json(b.to_json()) # OK
dataclass_json応用(1)–typing
# シリアライズ・デシリアライズの失敗を警告からエラーに変更する

warnings.filterwarnings("error", module=dataclasses_json.__name__)

# フィールド名を camelCase に変更する

class DataClassCamelJsonMixin(DataClassJsonMixin, ABC):

dataclass_json_config = config(letter_case=LetterCase.CAMEL)[ # type: ignore

"dataclasses_json"

]

class CamelClass(DataClassCamelJsonMixin):

user_name: str

x = CamelClass("name")

# CamelClass(user_name='name')

x.to_dict()

# {'userName': 'name'}

assert x == CamelClass.from_dict(x.to_dict()) # OK
dataclass_json応用(2)–typing
class MyEnum(Enum):

V1 = 1

V2 = 2

class EnumTest(DataClassCamelJsonMixin):

my_enum: MyEnum

x = EnumTest(MyEnum.V1)

# EnumTest(my_enum=<MyEnum.V1: 1>)

x.to_dict()

# {'myEnum': <MyEnum.V1: 1>}

x.to_dict(encode_json=True)

# {'myEnum': 1}

assert x == EnumTest.from_dict(x.to_dict(encode_json=True)) # OK
dataclass_json応用(3)–typing
@mapper_registry.mapped

@dataclass

class Project(DataClassJsonMixin):

...

@mapper_registry.mapped

@dataclass

class User(DataClassJsonMixin):

__tablename__ = "users"

__sa_dataclass_metadata_key__ = "sa"

id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})

name: str = field(metadata={"sa": Column(String)})

projects: list[Project] = field(

default_factory=list,

metadata={"sa": relationship(...)},

)
型検査の毒薬:Anyと dict–typing
すべての型を Anyにすれば型検査は必ず通る (ただし何の意味もない)
当然の帰結として Anyは原則禁止
例外は外部モジュールからの戻り値など、本当に型がわからない場合
dict は構造について何も示さない
dict もそれが本当に必要な場合を除いて原則禁止
必要なのは keyが予め想定できないものを格納する場合
dic: dict[str, Any] = f() # 何か API からの JSON データが入る
name = dic["name"]
# 本当に name というフィールドはあるのか?

project_id = dic["projects"][0]["id"]

# 正しいのかもしれないが、静的に構造の正しさのチェックができない。
Anyの回避 (型変数)–typing
もらった引数をそのまま返す id関数を定義するとしたらどうするか
引数の型を決めたくないので Anyを使いたいが…
もしも x が str なら戻り値も str
一般に x が T 型なら戻り値は T 型
型変数 T を使って書き換える
def id(x: Any) -> Any: # ???

return x
T = TypeVar("T")

def id(x: T) -> T:

return x
Anyの回避 (複数の型変数)–typing
A = TypeVar("A")

B = TypeVar("B")

def option_get(x: Optional[A], default: A) -> A:

if x is None:

return default

return x

option_get(None, 3) # -> 3

option_get(6, 3) # -> 6

option_get(6, "string") # ERROR: "string" は int ではない

def option_map(x: Optional[A], f: Callable[[A], B]) -> Optional[B]:

if x is None:

return None

return f(x)

option_map(None, str) # -> None

option_map(3, str) # -> "3"

v: Optional[int] = option_map(3, str) # ERROR: int にはならない
Anyの回避 (ジェネリクス)–typing
CARとCDR -Wikipediaより:
T = TypeVar("T")

@dataclass

class Cons(Generic[T]):

car: T

cdr: Optional[Cons[T]] = None

def to_list(self) -> list[T]: # 効率は良くないが短く書く場合

if self.cdr is None:

return [self.car]

return [self.car] + self.cdr.to_list()

l: Cons[int] = Cons(1)

# Cons(car=1, cdr=None)

l.cdr = Cons(2)

# Cons(car=1, cdr=Cons(car=2, cdr=None))

l.to_list()

# [1, 2]

l.cdr.cdr = Cons("string") # NG
dictの回避 –typing
基本的には dataclass_json でシリアライズ・デシリアライズする
TypedDict で cast して迂回することも可能
dic = {

"id": 1,

"name": "my name",

"project_ids": [5, 6, 10]

}

class UserDict(TypedDict):

id: int

name: str

project_ids: list[int]

user = cast(UserDict, dic)

id: str = user["id"] # 型エラー!: id フィールドは str ではない

user["projects"] = [1, 2, 3] # 未定義フィールドエラー!

user["project_ids"]["myid"] # 型エラー!: "myid" は int ではない
型検査の限界 (Json型の定義)–typing
Json = dict | list | str | int | float | bool | None

# 正確にはこうなるが、Python 3.10 現在では再帰的型定義が書けない

Json = dict[str, Json] | list[Json] | str | int | float | bool | None
型検査の限界 (型検査の曖昧性)–typing
🤔
m: list[float] = [1.0, 2, True] # OK

[type(x) for x in m]

# [<class 'float'>, <class 'int'>, <class 'bool'>]

[x * x for x in m]

# [1.0, 4, 1]

[type(x * x) for x in m]

# [<class 'float'>, <class 'int'>, <class 'int'>]
型検査のまとめ –typing
ちゃんとやるとそれなりに面倒くさい
型を正しく書くのはテストを書くのと同じ
なくても実装が正しければ問題なく動く
メンテナンス性・リファクタリング自由度の向上
Python の型検査は現時点では完全ではない
表現できないデータ型がある
数値型の検査が曖昧
検査漏れの可能性がある
型定義されていないライブラリが多い
それでも高品質なプログラムのためには必須です。
async/await
非同期処理を同期処理のように見せるための構文
Rust,JavaScript などの言語でも使用可能
基本コンセプト
処理の継続 (接続)
暗黙の CPU放棄
非同期実行構文
処理の継続(1)–async/await
Aを実行後にその結果を使って B を実行する
A() が非同期(ノンブロッキング)で実行される場合
3つ接続する場合
a = A(0)

B(a)
def A_cb(x, callback):

# A の処理

callback(result) # A の処理の結果 (result) で callback を呼ぶ

A_cb(0, B) # A の処理の最後に B を呼んでもらう
A_cb(B_cb(C)) # ???
処理の継続(2)–async/await
クロージャを返す関数を定義すればいくつでも接続可能
ただしちょっと書くのはしんどい
def A_cb_cl(callback)

def internal(x):

# A の処理

if callback is not None:

callback(result) # A の処理の結果 (result) で callback を呼ぶ

return internal # という関数(クロージャ)を返す

C_cb = C_cb_cl(None)

B_cb = B_cb_cl(C_cb)

A_cb = A_cb_cl(B_cb)

A_cb(0)
処理の継続(3)–async/await
await 構文を使うと同じようなことを見た目は同期的に書ける
asyncdef したものはクロージャ (正確にはコルーチン) になる
await するとコルーチンを実行した結果を受け取れる
async def A(x):

# A の処理

return result # A の結果を返す

async def B(x):

# B の処理

return result # B の結果を返す

async def C(x):

# C の処理

return result # C の結果を返す

a = await A(0)

b = await B(a)

c = await C(b)
処理の継続(4)–async/await
(余談ではあるが…) 処理の継続は関数型言語ではモナドを使うと簡単に書ける
ただし引数・戻り値の型が一定である必要があるので、表現力が弱い
近代プログラミングは 2010年前後に流行した Haskellの影響が大きい
-- 「「0 を a に渡して実行した結果」を b に渡して実行した結果」を c に渡して実行する

a 0 >>= b >>= c

-- もしくは

do

ra <- a 0

rb <- b ra

return (c rb)
暗黙の CPU放棄 –async/await
await がある箇所では CPU放棄が発生する可能性がある
放棄された CPUを使って別のタスクが動作可能 (並行処理)
async def async_sleep5() -> None:

print(f"async: start: {time.perf_counter()}")

await asyncio.sleep(5) # CPU を放棄する

print(f"async: end: {time.perf_counter()}")

async def do_async() -> None:

await asyncio.gather(

async_sleep5(),

async_sleep5(),

)

asyncio.run(do_async()) # 並列 (並行) に実行

# async: start: 81477.041671433

# async: start: 81477.041722131

# async: end: 81482.045057883

# async: end: 81482.045123142
async def sync_sleep5() -> None:

print(f"sync: start: {time.perf_counter()}")

time.sleep(5) # CPU を放棄しない

print(f"sync: end: {time.perf_counter()}")

async def do_sync() -> None:

await asyncio.gather(

sync_sleep5(),

sync_sleep5()

)

asyncio.run(do_sync()) # 直列に実行

# sync: start: 81482.045933061

# sync: end: 81487.051035888

# sync: start: 81487.051202991

# sync: end: 81492.056521034
Executorと待ち合わせ処理 –async/await
一体誰が待ち合わせ処理(コルーチンの実行)をしているの? → Executor
デフォルトではメインスレッドで並行処理 (1:N モデル)
並列・並行処理を行う Executor も使用可能 (M:N モデル)
ThreadPoolExecutor
ProcessPoolExecutor
どうやって待ち合わせしてもらう(CPUを放棄する)の?
fdや socket などは await 可能な API があるのでそれを使う
ブロックしてしまう場合に勝手に CPUを放棄してくれる
自分で待ち合わせ処理を書くこともできるが、普通そんなことは不要なはず
awaitの伝染性 –async/await
await は コルーチン (async関数) 内からしか発行できない
await を使うためには、コールスタックのトップレベルから asyncである必要がある
この性質を伝染性を呼ぶことがある
select()との比較 –async/await
select() は fdの待ち合わせ(+ タイムアウト)
async/await では「一連の処理」をまとめて待ち合わせ
多数のキーワードを Googleで検索して最上位でヒットしたページを並列処理で保存した
い…
select() でシングルポイントで socket を切り替えながら待つのは大変
async/await なら「検索して最上位でヒットしたページを保存」という一連の処理を同
時にスケジュールできる
async/await をサポートする言語で select() を使うことはもうありません。
その他
今回扱わなかった話題
複数バージョンの Python を扱いたい
pyenv:実行時の Python のバージョンを切り替える
tox:複数バージョンの Python でテストを実行する
エディタでリアルタイムに型検査したい
LSPで pyright を使ってください
サンプル
Docker 化されたビルド環境込みの小さなサンプル
https://github.com/anyakichi/fastapi-sample

Python standard 2022 Spring