Pythonのクラス設計、なぜか気持ち悪い…その違和感の正体を考えてみた
Pythonのクラス設計、なぜか気持ち悪い…その違和感の正体を考えてみた
最近、とあるプロジェクトでPythonのクラス設計を触っていて、「なんかこれ、妙に気持ち悪いな……」とふと思ってしまった。
特に、継承まわりとか、抽象基底クラス(abc.ABC
)を使ったときの設計の雰囲気が、どうにもスッキリしない。
別にコードが動かないわけじゃないし、文法ミスもない。だけど、設計が「筋悪」な感じがしてモヤモヤする。
こういう感覚、ありませんか?
SlackSenderがNotificationSenderを継承した瞬間に「あっ…」
たとえばこんなコードを書いていて、ふと違和感を覚えました
from abc import ABC, abstractmethod
class NotificationSender(ABC):
@abstractmethod
def send_email(self, recipient, content):
pass
class SlackSender(NotificationSender):
def send_slack(self, channel, content):
...
ぱっと見「まあよくある継承構造」に見えるんだけど、SlackSenderがemail送らないのにsend_emailを継承してるってどうなの?と感じた瞬間に、設計の軸が一気にブレた気がした。
抽象クラスから派生したはずなのに、実装先では全く違う関心ごとが入り込んでる。それって本当に継承する意味あったんだっけ?
Pythonって自由に書けるぶん、こういう設計の揺らぎが起きやすい。
継承構文、簡潔すぎて情報が足りない問題
class Foo(Bar):
この構文、Pythonでは当たり前のように使うけど、継承なのか、インタフェース的な実装なのかが見た目でわからない。
Javaだったら implements
だし、C#なら :
だけど継承かインタフェースかは型定義で区別できる。
Pythonにはそれがない。単なる「括弧の中に親クラスっぽいものがいるだけ」。設計意図がコードからにじみ出てこない。
コードレビューのときも「これインタフェース的な設計なんだろうけど……send_email()
が強制されてるしな」と毎回首をかしげることになる。
Duck Typingなのに型を強制するという矛盾
Pythonの美学って、いわゆる「アヒルが鳴くならそれはアヒル(If it walks like a duck…)」な柔らかさだと思ってた。
だからクラスを継承しなくても、send()
を持ってればそれっぽく使えていいじゃん、って。
ところが abc.ABC
を使うと、「このクラスを使うには絶対にこのメソッドを実装してね」という Javaっぽい世界観が突然現れる。
この矛盾、構文としては成立してるけど、思想としてはどうなんだろう……って、やっぱり引っかかる。
Java脳の自分が混乱したポイント
自分はもともとJavaからキャリアをスタートしたタイプなので、「interface = 契約」っていう意識が強い。
interface NotificationSender {
void sendEmail(String recipient, String message);
}
Javaでは、こういう設計をしておけば「SlackSenderがこのインタフェースを満たすかどうか」が明確にわかる。
そして、インタフェースを満たしていないなら、コンパイルエラーになる。
だけどPythonでは、それを 実行時になって初めて知る。しかもそのチェックをしない限りは普通に通ってしまう。
型ヒントは書けても、それが「動作に効いてるわけじゃない」っていう非対称性もまた、モヤモヤを加速させる。
Protocolは一筋の光かもしれない
ここまでPythonの継承構造にモヤモヤしつつ使っていたが、typing.Protocol
を知って少し救われた。
from typing import Protocol
class Notifier(Protocol):
def send(self, recipient: str, message: str) -> None:
...
これ、クラスを継承しなくても send()
を持ってれば型チェッカー的にはOKってやつ。
まさに「構造で判断するインタフェース」という考え方で、Duck Typingとの折り合いもついている。
abc.ABC
が「従わせるための抽象化」なら、Protocol
は「認識するための抽象化」という感じ。
この違い、設計者として意識して使い分けるとかなり気持ちがラクになる。
「書けてしまう」自由が設計の重みを増す
Pythonって、クラスにしても継承にしても、制限が少ないからとにかく何でも書けてしまう。
でもそのぶん、構造をどう設計するかの責任がすべて設計者にのしかかる。
__init__
に何を書けばいいか- 抽象化の粒度をどう切るか
- 継承すべきか委譲にすべきか
この判断が毎回設計者の胸先三寸に委ねられていて、しかも構文的なガードがほとんどない。
結果として「SlackSenderがNotificationSenderを継承してるけど、メール送れないし……」みたいな構造が現れてしまう。
レビューする立場になって気づいた設計の不自然さ
コードを書く側だった頃は、「まあ動けばいいか」と思ってた。
でもレビューするようになってから、「このクラスは何を表現しようとしてるのか?」「本当に継承すべき構造なのか?」という観点で見るようになって、モヤモヤがどんどん増してきた。
Pythonの自由な構文が見た目の正しさを隠してしまうこともある。
そういう意味で、「違和感を言語化する能力」がレビューアーには必要なんだと感じるようになった。
最後に:違和感は設計力の入口かもしれない
Pythonのclass構文や抽象クラス設計に対する違和感。
あれって、単に「慣れてないから気持ち悪い」だけじゃない。
むしろ、「設計ってなんだろう?」「この構造って何を守ろうとしてる?」と問い直す入口なのかもしれない。
コードは書ける。でも「正しく設計されたコード」って何か?を考えるようになったとき、こういう違和感がヒントになる気がしている。

まとめ
Pythonの継承と抽象化に対する違和感は、「書けてしまうけど、設計としては成立していない」構造を簡単に作れてしまうことから生じている。 そしてそれを誰も止めてくれない、言語が補助してくれない、という点に本質がある。
違和感を抱くというのは、裏返せば「こうあるべきでは?」という設計意識が育っている証拠でもある。 それを構文が支えてくれないとき、人は自然と不快感を覚える。
だからこそ、Pythonで設計する際には常に「抽象化の粒度」「契約の明示」「自由の使い方」に目を配り、筋の通った構造を自らの手で組み上げる意識が不可欠なのだと思います。