tldr & 動機
- Python は初心者向けの言語として名高い
- 従って Python ができる人ではなくて、プログラム歴自体短く Python しかできない人が少なくない
- 誰しも何にしても初めてはあるのでそれ自体を咎めるつもりはない。(それを咎める人は新しい分野を最近学んでいない臆病者。)あくまでこれ人に出せるモンじゃねえぞという話
- 従って Python ができる人ではなくて、プログラム歴自体短く Python しかできない人が少なくない
- 従って設計面以前にコード整頓のセンスが絶望的なものが散見される
- 家の整理整頓術的に言うと、トイレに炊飯器置いてあるレベルのものがある。 逃げたい。
- n番煎じであるのは理解している。そのため書き手の実体験の側面を強調したい。
disclaimer
上記で触れていますが、
- 胸糞要素あり
- 散文であり体系的ではない
- n番煎じ
です。それが嫌な方は退出願います(別に大したこと書いてないです)
それでは実例
データ型定義しろ辞書型乱用するな【基本】
BAD!
def process_data(item:dict[str,str]) -> dict[str,str]:
"""
item dict[str, str] この関数が処理するインプットだよ♡
"""
connection_string = item["foofoo"]
# 1234 行後
nanka_shiran = item["property"]
ret:dict[str,str]
ret["bazbaz"] = nanka_shiran_hensu
# 2345 行後
return ret
process_data というネーミングセンスは一旦さておき(後述するので)、型付けが完全に自己目的化してしまっていて、何の助けになっていません。
これでは何のキーにどのような情報が設定されている dict[str,str]
を渡す必要があるのかソースコードから読み解かないといけないです。
「タイプヒント書いてる俺偉い」みたいにうぬぼれていそうでム◎つくまであります。
def main():
# process_data 関数を使いたいな!
input:dict[str,str] = {
"kokoni": "naniwo settei sureba iino"
}
process_data(input) #型ヒントもコメントも何の参考にもならずに困る
コードが増えていくに従って認知しておかなければいけない範疇が無限に広がっていくので、このように中の実装を厳密に追いかけないと碌に使えない関数は万◎に値します。
GOOD!
import dataclasses
@dataclasses.dataclass
class Input:
"""
foofoo:str 接続文字列 みたいな感じでここにコメント書いても良い
"""
foofoo:str
property:str
@dataclasses.dataclass
class Output:
bazbaz:str
def process_data(item:Input) -> Output:
"""
関数のコメントを引き続き書きたいなら書いても良い
"""
connection_string = item.foofoo
# 1234 行後
nanka_shiran = item.property
nanka_shiran_hensu = "shiran"
ret = Output(nanka_shiran_hensu)
# 2345 行後
return ret
上記のようにして適切にclass
を定義してやることにより、この関数はこれを入れれば動いてくれて、これが返ってくるんだというのが分かりやすくなった。
また、 IDEに易しい。
私はPython専門ではないので(諸々の事情で自分で選ぶ言語ではないので)詳細なお作法が間違っていたらすみませんが、ちゃんと書けばこうやって利用者に明示的にドキュメントを読ませずに使わせてあげられるんですよ。
また仮にコメントが不十分でコードを追いかける必要が出たとしても、IDEにはフィールドの参照箇所を総検索するという機能が一般的についているので、少なくとも辞書型の時と比べると遥かに楽になります。
※スクショ内で使用している VS Code が IDE に属するのかという点は本質的ではないので議論の対象としません。
dataclasses.dataclass って何
詳細は割愛しますが:
- データの集合(構造体)の要素が強い時(例:座標。
{x:123, y:222, z:111}
みたいな)は、これを使ってやればよいです。def __eq__
などが自動でくっついてくるため同じデータか比較することがあるとき、そのロジックとかを書かなくてよいです。(詳細は operator overloadingなどで検索、他の言語にも同じような機能あります。) - データが状態を内包していたり勝手に編集されたりしたくないなどのオブジェクトの要素が強いときは
@dataclasses.dataclass
なしで使えばよいです。
この辺りは慣れというか、自動車運転でいう所の「車線変更も出来ないのに、適切なルートプラニングの心配をしても話が分かるわけないだろう」という感じですね。
またルートプラニングと同じで絶対的な正解があるわけではなく、経験論的なところも多分にあります。
興味があれば例によって stackoverflow にそのような記事があるので読んでみると良いと思います。
データ型定義しろ辞書型乱用するな【応用】
BAD!!!
def nanka_shorisuru(item:dict[str,str]):
#1234行後
ret :dict[str,str]
return {
"status_code": 123,
"hokano_meta_data":"yaaa~~~y!!!!",
"hoshikatta_atai":ret
}
def nanka_shorisuru2(item:dict[str,str]):
#1234行後
ret :dict[str,str]
return {
"status_code": 122,
"hokano_meta_data":"haaa~~~y!!!!",
"hoshikatta_atai":ret
}
def main():
nanka_shorisuru({"hogehoge":123123})
正直理由がわからないのですが、関数からの戻り値に対していちいちステータスコード的なものを含めているというケースが存在しました。はっきり言っていろんな次元で意味不明ではあります。
- 同じエラーだとしても、呼び出し元のコンテキストによってハンドリングが変わることは大いにある。従ってエラーの内容を伝えるのは分かるがステータスコード(=呼び出し元の終わり方)まで指定するのはなんかおかしい。
- 人間の例:ダメ元依頼と、本命の依頼、二つでは拒絶されたときの対応が異なる。後者ならリトライしたいが、前者ならそんなに力まない、とかとか。
- Rustでいうところの
Result
型みたいに既に参考にする実装があるわけで、実際に試行した実装もあるのになぜこうなったのか
と色々残念ではありますが、やるにしても型ヒントの恩恵を受けられるような努力はするべきです。
実際に利用したい値のほかにメタデータを含んでいる実装は珍しくなく、例えばC#だとboxingされた版、やや関連して Nullable<T>
版のものとかもそうだと思うので、実データのフィールドの名前はその辺を参考にするのが良いと思います。
GOOD!
import dataclasses
@dataclasses.dataclass
class ReturnType[T]:
status_code:int
hokano_meta_data:str
value: T
@dataclasses.dataclass
class Input:
foofoo:str
property:str
@dataclasses.dataclass
class Output:
bazbaz:str
def nanka_shorisuru(item:Input) ->ReturnType[Output]:
ret = Output("shiran")
return ReturnType(123,"yaaa~~~y!!!!",ret)
# もう一つの関数も同じような感じ
Pythonの型ヒントでもジェネリクスを使うことができ、このように他のデータを包むデータ構造を作りたい場合に便利です。これをするとIDEに易しく利用者の認知負荷がぐっと下がります。
関数の型ヒントから .value
の値は Output
型に決まっているのだからということで、そのフィールドであるbazbaz
が一番上に表示されていますね。
Tの型を制限したい(例えば 1 == 1
みたいに == で評価できることを確約したい)場合はProtocol
という機構があるようですが、もうその辺まで来ると脳のワーキングメモリ的な問題からコンパイラが整合性をある程度面倒みてくれる静的型付け言語に乗り換えるのが良いのではという感じはします。
ただし実経験には基づかないです。何故ならこの記事を書く程度には現行のPythonのプロジェクトはコードが酷いからです。
適切な関数名をつける
BAD
def send_email(item:Email):
#なんかファイルの上書きとかしている
pass
見ての通りなのですが、メールを送るという関数名なのにその名前から直感的に察することができないことをしています。前にも言った通りコードが増えていった時に名前から処理が察せないと、思わぬところで副作用があってバグになったみたいなことが発生しますし、あと単純にわかりにくいのでコードの再利用しにくいです。
また、先の例で出した process_data
みたいなものは最悪です。「食べ物を食べた」と同じで、何のヒントにもならず、 与えるものはストレスだけです。幾多と出来るであろう関数の中から選ぶのですから、名前を読むだけでこれが欲しかったものだと分かることが望ましいです。
関心の分離(separation of concerns) ができていない
BAD!
from typing import Generator
class A:
pass
def read_file() -> Generator[A, None,None]:
# ここで A のフィールドの成型を行う
pass
def save_to_db() -> None:
# ここでも A のフィールドの成型を行う
pass
先ほどの名前と実情が一致していないというのにかなり関連します。
上記の例だと、読みだされたインプットを起点にして保存されるデータに至るまでで行われる加工処理の総数を洗い出すのに複数の関数を読まないといけません。しかも、この例だと特に変形処理を行っていることが関数名からは察することができないのでたちが悪いです。
takeaways
- 型ヒントを適切に使用し、self-documenting なコードを心がけましょう
- self-documenting というのは「実装読めば分かるからドキュメント要らないよね」の意味ではありません。そうすると認知範囲が無限に増えるのでいつか詰みます。そうではなく、関数名・シグネチャ・データ型などでそれらを表現しようという事です。
- コードの再利用性、および関数の名実を一致させましょう
備考
初手として良いか疑問は残りますが、貼っておきます。
.NETのクラスライブラリ設計 改訂新版 開発チーム直伝の設計原則、コーディング標準、パターン