Pythonチョットデキルになるためのテクニック集
Pythonは比較的自由な文法で記述できる言語で、様々な分野で利用されています。
一方で、その自由さ故、どうやって書くのが良いか分かりにくい側面もあります。そこで、本記事では、Python使いに布教したいテクニックをまとめてみました。
TLDR;
Pydantic ClassをModelとしてImmutableに運用しつつ、Controller, Service, etc にTyping.Protocol使うとだいぶ体験が良い。
リポジトリ
参照しやすいように、GitHubで動くfizzbuzzのコードを用意しました。必要に応じて、ご利用ください。
環境構築
Pythonでは、メジャーな環境構築方法がいくつかあります。
- virtual env
- Docker (この後に登場するツールとの併用を含む)
- PyEnv + Poetry
- uv
著者はuvを推奨しています。
uvを用いるといくつか恩恵がありますが、Pythonのインタープリタとパッケージ、開発者ツールをひとまとめに管理できるので環境構築が非常に楽な点が個人的なおすすめポイントです。
開発者ツール
開発者ツールを用いることで、開発やレビューの負荷を抑えることができます。下記は著者がよく使用しているツールです。
特に静的解析ツールは、動的型付け言語であるPythonを「実行前」に評価できるので、最も重要なツールの1つと言っても過言ではないでしょう。Typingと合わせて使うことが多いです。
事例を公式より引用します。mypyを用いることで型情報などが間違っているプログラムを実行前に検知できます。
number = input("What is your favourite number?")
print("It is", number + 1) # error: Unsupported operand types for + ("str" and "int")
Coverageは聞いたことのない方もいるかもしれません。これはテストの網羅性をチェックしてくれるツールです。
task: [validate] uv run coverage report -m
Name Stmts Miss Cover Missing
----------------------------------------------------------------------------------
app/__init__.py 0 0 100%
app/service/runner/fizzbuzz_presenter/buzz.py 5 0 100%
app/service/runner/fizzbuzz_presenter/fizz.py 5 0 100%
app/service/runner/fizzbuzz_presenter/fizz_buzz.py 5 0 100%
app/service/runner/fizzbuzz_presenter/otherwise.py 5 0 100%
app/service/runner/runner_config.py 11 0 100%
app/service/runner/selector.py 17 0 100%
----------------------------------------------------------------------------------
TOTAL 48 0 100%
こんなに開発者ツールを使うと、実行が面倒と思った方もいるかもしれません。
そのような時に活躍するのが、Taskfile などのタスクランナーです。Task
では、YAMLという書式でワークフローを記載します。
version: 3
tasks:
validate:
cmds:
- uv run ruff check --fix app
- ...
実行してみます。
$task validate
task: [validate] uv run ruff check --fix app
...
task: [validate] uv run ruff check --select I --fix app
...
task: [validate] uv run black .
...
21 files left unchanged.
task: [validate] uv run mypy ./app --strict
...
task: [validate] uv run coverage run -m pytest .
...
task: [validate] uv run coverage report -m
...
一撃必殺!全てのコマンドをコマンド1つで実行することができました。
コードのフォーマットやアノテーションなどをレビューで指摘するのは不毛です。CIやgitのhookツールなどで事前に検知するのが望ましいでしょう。著者はGitHub Actionsで検知する方法をよく用いています。
commitする前にチェックするツールとしては下記が有名です。
補足
-
isortも便利なツールですが、
ruff check --select I --fix
で代用できるのでここでは挙げませんでした。 - GNU Makeも強力なツールで、大体のOSにインストールされているツールです。Makefile警察と関わりたくないので、ここでは挙げませんでした。Makefile警察の解説
- リポジトリにおけるTask Runnerへのリンク: Taskfile.yml
コーディング: Syntax
コーディング編では、FizzBuzzのコードを使いながら、紹介をします
Typing.Final
Finalを使うと比較的簡単に変数の上書きから守ることができます。
hello: Final[str] = "Hello World"
hello = "Over Written!" # Error with Mypy
ちなみに定数関数は関数は上書きできてしまうので注意です。
def hello() -> str:
return "Hello, World!"
if __name__ == "__main__":
print(hello()) # Hello, World!
h = hello() # Hello, World!
h = lambda: print("Overwritten!")
h() # Overwritten!
詳細は下記をご参照ください。
Typing.Protocol
Python 3.8
以降では、typingモジュールにProtocol
を使うことができます。Protocol
は、静的解析ツールがダックタイピングを認識するための仕組みを提供します。Protocol
を用いれば、クラスが特定のメソッドやプロパティを持つことを静的に宣言できます。
FizzBuzzのコードで考えてみましょう。
from app.service.runner.runner_config import RunnerConfig
from app.service.runner.selector import Selector
class Runner:
def __init__(self, config: RunnerConfig):
self.config = config
def run(self) -> None:
"""Run the fizzbuzz algorithm"""
for i in range(self.config.start, self.config.end + 1):
presenter = Selector.select(i) # 👈 ここで出力するClassを決める
presenter.present() # 👈 presenterのClassはpresent()を実装している
このコードを静的解析するなら、presenter
に入るClassがpresent()
を実装しているか確認したいと思うはずです。それでは検証していきます。
まず、Protocol
を決めます。今回はpresent
を実装して欲しいので次のProtocol
を作成しました。
from typing import Protocol
class FizzBuzzPresenter(Protocol):
def present(self) -> None:
pass
次に実装クラスを作成します。
class Fizz:
def __str__(self) -> str:
return "Fizz"
def present(self) -> None:
print(self.__str__())
最後にSelector.select()
でProtocol
を返すようにします。
class Selector:
@staticmethod
def select(count: int) -> FizzBuzzPresenter:
これでProtocol
を実装しているクラスのみを返すことを保証できました。試しにFizz
の実装を変更してエラーを出してみましょう。
class Fizz:
def __str__(self) -> str:
return "Fizz"
def print(self) -> None: # 👈 presentを実装しない!
print(self.__str__())
静的解析に書けてみると、エラーがでます!
task: [validate] uv run mypy ./app --strict
app/service/runner/selector.py:25: error: Incompatible return value type (got "Fizz", expected "FizzBuzzPresenter") [return-value]
app/service/runner/selector.py:25: note: "Fizz" is missing following "FizzBuzzPresenter" protocol member:
app/service/runner/selector.py:25: note: present
Found 1 error in 1 file (checked 15 source files)
task: Failed to run task "validate": exit status 1
補足:
PythonにはJavaのようなInterfaceがありません。ABCという抽象化用のパッケージもありますが、静的解析との相性が悪いという課題があります。
要約変数
Syntaxから外れるかもしれませんが、強力なテクニックなので紹介。例えばFizzBuzzにおける条件分岐を考えてみましょう。次のコードが来たときにどのぐらいレビューに時間を書けますか...?
class Selector:
@staticmethod
def select(count: int) -> FizzBuzzPresenter:
if count % 3 == 0 and count % 5 == 0:
return FizzBuzz()
elif count % 3 == 0:
return Fizz()
elif count % 5 == 0:
return Buzz()
else:
return Otherwise(count)
上記のようなコードの場合、if文における条件式全てに目を通さないとコードが正しく書かれているか検討できなさそうです。一方で次のようなコードではどうでしょうか?
class Selector:
@staticmethod
def select(count: int) -> FizzBuzzPresenter:
divisible_by_3 = count % 3 == 0
divisible_by_5 = count % 5 == 0
if divisible_by_3 and divisible_by_5:
return FizzBuzz()
elif divisible_by_3:
return Fizz()
elif divisible_by_5:
return Buzz()
else:
return Otherwise(count)
ほとんどの人は最初の divisible_by_3
と divisible_by_5
を読み、その後のコードは流し目に読むことができたのではないでしょうか?要約変数を適切に用いれば、条件式の視認性を向上させることが可能です。
Match
※ この章だけ、FizzBuzzに入れられませんでした ><
Pythonのmatch文は、Python 3.10から導入された構文です。match文を使うことで、変数の値や構造に基づいた分岐処理を簡潔に記述できます。
match value:
case pattern1:
# pattern1 に一致した場合の処理
case pattern2:
# pattern2 に一致した場合の処理
case _:
# どのパターンにも一致しない場合の処理
PythonのMatch文は柔軟です。次のようなorやwildcardが使えますし、
value = 3
match value:
case 1 | 2 | 3:
print("1, 2, または 3です")
case _:
print("その他の値")
caseにガードをつけることもできます。
def evaluator(op, a, b):
match (op, a, b):
case ('+', a, b):
return a + b
case ('-', a, b):
return a - b
case ('*', a, b):
return a * b
case ('/', a, b) if b != 0:
return a / b
case _:
return "Invalid operation"
print(evaluator('+', 2, 3)) # 5
print(evaluator('/', 4, 0)) # Invalid operation
詳細を知りたい方は次のチュートリアルを読んでみましょう。
コーディング: パッケージ
Pydantic
PydanticはValidationライブラリです。
import json
from pydantic import BaseModel, PositiveInt, ValidationInfo, ValidationError, field_validator
class RunnerConfig(BaseModel):
start: PositiveInt
end: PositiveInt
@field_validator("end")
@classmethod
def check_start_less_than_end(
cls, end: PositiveInt, info: ValidationInfo
) -> PositiveInt:
start = info.data.get("start")
if start is not None and start > end:
raise ValueError("start must not be greater than end")
return end
class FizzbuzzController:
def __init__(self, start: int, end: int) -> None:
self.start = start
self.end = end
def run(self) -> None:
try:
runner = Runner(RunnerConfig(start=self.start, end=self.end))
runner.run()
except ValidationError as e:
print("UserError: Input Values are invalid")
print(json.dumps(e.errors(), indent=4))
exit(1)
上記のControllerに、int
型のstart
とend
があります。これらは、int
は満たしますが、FizzBuzzとしては想定しないような値が入りそうです。例えば、0
, -1
などが入る可能性があります。
そこで、RunnerConfig
で、Validationを行なっています。実際に-1
を入力してみます。
$ task run -- --start 10 --end -1
UserError: Input Values are invalid
[
{
"type": "greater_than",
"loc": [
"end"
],
"msg": "Input should be greater than 0",
"input": -1,
"ctx": {
"gt": 0
},
"url": "https://errors.pydantic.dev/2.10/v/greater_than"
}
]
task: Failed to run task "run": exit status 1
エラーとその理由が書かれたURLが表示されました。他にもjsonへのダンプ用のmethodが用意されていたり、Fieldによる柔軟な型定義が書けたりする機能があったりします。
TypeGuard
どうしても実行時に型の確認をしたい時におすすめなのがTypeGuard。
check_type()
とTypeCheckError
でifを使わずに型の検証ができます。
from typeguard import check_type
# Raises TypeCheckError if there's a problem
check_type([1234], List[int])
参考書籍・情報収集
本
著者が参考にしている本を紹介します
SNS
情報収集は次の場所で行なっています。(もちろん、GitHubでも。)
まとめ
紹介してきたテクニックを使えば上級者の入り口には立てるはずです...!!
まずは小さなところから実践してみてください。これまで真っ白だったあなたのソースコードがキラキラした彩りあるソースコードに変わっていることに気づいたら、上級者の入口です。
参照: https://zenn.dev/yosemat/books/1e021f1745566f/viewer/50516a
みんなのおすすめテクニックあったらコメント欄で教えてください!!
Discussion