Pythonコード品質と設計
いつ使用するか
- 動作するが、理解、拡張、保守が困難なPythonコードを作成またはレビューする場合。
- JavaやC++からの直接的な翻訳を、言語に適した構造に置き換える場合。
- 抽象化を選択する、結合度を評価する、新しい要件後にリファクタリングする場合。
- パターンの意図を、その名前をアーキテクチャ上の義務にすることなく認識する場合。
- この範囲はコードに関する推論に関係します。デプロイ環境やフレームワークを規定するものではありません。
哲学
想像できる最も一般的な設計ではなく、観測された問題を解決する最小の構造を追求してください。 Pythonが既にその構造を提供している場合、パターンはその意図を保持します。その構造を削除することは、洗練されていないことではなく、改善です。 優れた抽象化は、クライアントが必要とする情報を消去することなく、変更のコストを低減します。 Bruce Eckel著『Thinking in Python』の思想の実践的統合。コーパスa8feb860。 プロジェクトが実際にサポートするPythonのバージョンを尊重してください。コーパスの最近のターゲットは、暗黙のバージョンアップグレードを許可しません。
ルール
読みやすい構文と予測可能な名前
- インデントを実際のブロック構造に一致させ、4つのスペースを使用し、タブとスペースを混在させないでください。
- ブロックヘッダーはコロンで終了し、セミコロンや、コードを人為的に圧縮する複数の文は避けてください。
- 長い文の場合は、追跡が困難な分割ではなく、括弧、角括弧、または中括弧内での継続を使用してください。
- ブロックとスコープを混同しないでください。
ifやfor内の代入は、ブロック後も可視のままである可能性があります。 - 名前が使用される前に、すべてのパスで代入が実行されることを確認してください。インデントだけではそのバインドは保証されません。
- 変数、関数、メソッド、ファイルには
snake_caseを、クラスにはPascalCaseを、定数には大文字を使用してください。 - 関数のように名前が付けられたクラスの例外は、実際にそのように使用されるオブジェクトのために予約してください。一般的なスタイルにしないでください。
- 大文字の名前から実行時の保護を推測しないでください。それは読者への意図を示すものです。
- プロジェクトの一貫した規約を維持し、任意のローカルルールを作成する代わりに、既存のリンターを使用してください。
参照、値、およびミューテーション
- 代入は、名前をオブジェクトにバインドするものとして読んでください。オブジェクトをコピーするものではありません。
- 再代入とミューテーションを明示的に区別してください。名前の再代入はその以前のエイリアスを変更しませんが、オブジェクトのミューテーションはそれらすべてに影響を与える可能性があります。
- 値の等価性には
==を使用し、同一性のため、特にis Noneやその他のシングルトンにはisを予約してください。 - 文字列や小さな整数の同一性を値テストとして使用しないでください。共有の最適化はその契約を提供しません。
- リストのスライスまたは浅いコピーは、外側の構造を再作成しますが、ネストされた要素は再作成しません。どのオブジェクトが共有されたままかを確認してください。
- 含まれているオブジェクトも複製する必要がある場合にのみ、深いコピーを使用してください。代入はこの分離を提供できません。
x += yを機械的にx = x + yに置き換えないでください。リストの場合、最初の形式はその場で変更し、2番目は新しいものを生成します。- 不変の数値の場合、拡張代入は共有された数値を変更するのではなく、名前を結果に再バインドすることを理解してください。
- 進化する必要がない場合は不変データを優先しますが、コンテナの不変性とその内容の不変性を混同しないでください。
数値と真偽値
- 整数オペランドに対して浮動小数点数を生成する除算
/と、負の無限大に向かって丸める除算//を区別してください。 - 移植時には負のケースをテストしてください。整数の商はゼロへの切り捨てではなく、剰余
%は除数の符号に従います。 - Pythonの整数に固定幅があると想定しないでください。他の言語の有界整数のようにオーバーフローしません。
- 正確な整数演算と浮動小数点演算を分離し、一方の保証を他方にインポートしないでください。
round()とフォーマットにおける偶数への丸めを考慮し、常にゼロから離れる方向に丸められると想定しないでください。- リテラルで
_セパレータを使用して、値を変更せずに大きな数値を読みやすくします。 - インクリメントまたはデクリメントには
+= 1と-= 1を使用してください。Pythonには++や--演算子はありません。 - ブール値は整数でもあります。条件を合計することで真のケースをカウントできますが、
Trueはintとは異なる数値型を構成しません。 - 単にコンテナが空かどうかを問う場合は、長さテストよりも
if items:を優先してください。 - オブジェクトの真理値は
__bool__()または、それがなければ__len__()で定義できることを覚えておいてください。 andとorは短絡評価を行い、必ずしもブール値ではなくオペランドを返します。名前だけに基づいて結果に注釈を付けないでください。- すべての偽の値を置き換える必要がある場合にのみ
x or defaultを使用し、ゼロや空のテキストは明示的なNoneテストで保持してください。 - マスクの場合、ビット演算子と論理演算子を区別してください。
~xは固定幅ワードを反転しません。 - 組み込みの数値に対して
@が積を実行することを期待しないでください。行列乗算は型に依存し、デコレータ構文とは異なります。
文字列と補間
- 文字列は不変です。変換の結果を再利用する必要がある場合は保持してください。
- エスケープを制限する引用符を選択し、改行がテキストの一部である場合は三重引用符を使用してください。
- リテラルのバックスラッシュには生文字列を使用しますが、そのリテラルを単独のバックスラッシュで終了できないことに注意してください。
- 特定の必要性なしに文字操作ループではなく、組み込みの検索、分割、置換、結合操作を使用してください。
- 正確なプレフィックスを削除するために
strip、lstrip、rstripを使用しないでください。これらの引数は文字を表し、不可分な部分文字列ではありません。 - ターゲットバージョンがサポートする場合、正確な先頭または末尾の部分文字列を削除するには
removeprefix()とremovesuffix()を使用してください。 +は暗黙的に数値を文字列に変換しません。明示的な変換またはf-stringを使用してください。- 通常のテキストを生成するにはf-stringを優先してください。その式は評価され、結果は即座にレンダリングされます。
- 精度、幅、配置にはフォーマット指定を使用し、
!rは診断に役立つ表現を表示し、=は式にラベルを付けます。 - 古い
%やstr.format()の使用を、存在するという理由だけで書き換えないでください。それらは依然として有効であり、移行は対象のコードを改善する必要があります。 - ターゲットバージョンがサポートし、コンシューマがアセンブリ前にリテラルと値を区別する必要がある場合にのみ、t-stringを検討してください。
- t-stringは完成した文字列ではなく構造化オブジェクトを生成します。コンシューマはそのドメインに従って、実際に補間を引用符で囲む、エスケープする、または拒否する必要があります。
- 補間構文のみに基づいてセキュリティを約束したり、例示的な文字拒否を一般的な保護として流用したりしないでください。
- テンプレートコンシューマは、テキストと値の厳密な交互配置を想定してはなりません。反復は空のリテラルを省略する可能性があります。
抽象化、依存関係、凝集性
- 新しいクラスを追加する前に、関数、辞書、モジュール、または言語プロトコルで十分かどうかを確認してください。
- 単にメソッドをホストするだけのクラスは、その編成だけでは正当化されません。
- 実際の要件に基づいて変更の軸に名前を付けます。同じ場所での繰り返しの変更は、予想される柔軟性よりも有用な証拠です。
- 変更されるものと安定したままのものを分離し、何も変化しないときに変動メカニズムを構築しないでください。
- イディオム、特定のソリューション、一般的なパターンを、登らなければならないはしごとしてではなく、可能な段階として扱います。
- 各抽象化について、それが消去する詳細を特定し、正当なクライアントがそれらを回復する必要がないことを確認します。
- また、インターフェースの背後に隠された作業も調査します。コピー、ソート、間接アクセスは、実際のデータサイズで支配的になる可能性があります。
- ある部分が知っていることによって結合度を評価します。親の詳細、具象クラス、インターフェース、呼び出しシグネチャ、または単純な値。
- 型付き依存関係を依存関係の不在と混同しないでください。
Protocolは宣言された継承を回避しますが、そのメンバーは依然として契約です。 - クライアントが1つの操作のみを必要とする場合は、関数シグネチャを優先してください。使用法なしでマルチメソッドインターフェースを作成しないでください。
- パターンはしばしば依存関係を削除するのではなく移動します。依然として具象クラスに名前を付けなければならない部分を見つけてください。
- 変更するのにコストがかからないアセンブリポイントに意図的に配置されている場合は、具象依存関係を受け入れてください。
- 間接的に取得されたオブジェクトの内部詳細に到達しないでください。明示的なインターフェースを通じて必要なサービスを要求してください。
- 同じ主題にサービスを提供する要素を1つのコンポーネントにグループ化し、独立して進化できるアイデアを分離してください。
- 実装の詳細を継承せずにコラボレータを変えるにはコンポジションを優先し、真の置換可能性がある場合は継承を保持してください。
- サブタイプが、そのメソッドの名前だけでなく、基本型のクライアントの期待を満たしているかを確認してください。
- 同じ理由で同じ意図を表現する重複を排除します。偶然の類似性は十分ではありません。
- 一般的なケースを単純に、まれなケースを可能にし、インターフェースを一貫したものにします。動作の例外は記憶のコストを課します。
- 削除しても必要な動作も有用な分離も排除されない場合は、レイヤーを削除します。2つの十分なソリューションは、その単純さによって選択してください。
- 可能な場合は計算を純粋にします。同じ入力は同じ結果を生成し、外部世界への観察可能な変更はありません。
拡張の2つの軸に沿ったリファクタリング
- 型の追加と操作の追加を区別します。設計はしばしば一方を他方を犠牲にして優先します。
- オープンファミリーのすべてのサブクラスを列挙する
matchは、リストの維持を必要とし、新しい型を黙って無視する可能性があります。 - 操作が正確なクラスでのみグループ化する場合、
type(obj)をインデックスとする辞書はこの列挙を回避します。 - この置き換えを純粋に構文的なものとして扱わないでください。サブクラスは、親クラスパターンとは異なり、独自のキーを取得します。
- 最初の出現時に作成されるグループの場合、
defaultdict(list)は新しいリストを提供します。辞書の注釈はこの動作を作成しません。 - 均一な操作は、関連する属性を読み取る通常の関数として残します。アルゴリズムが型によって変化しない場合、ディスパッチは何も追加しません。
- 各型が自然にその応答を持ち、操作が少ないままである場合、メソッドを小さく制御された階層に保持します。
- 外部階層を変更したり、新しい操作をすべてのクラスに分散したりせずに、型によって変化する操作を追加するには
functools.singledispatchを検討してください。 singledispatchでは、実装は最初の引数で選択され、MROに従って継承される場合があります。正確なクラステーブルではありません。- デフォルトの応答は、未登録の型に対して真に有効でなければなりません。そうでない場合は、省略を可視化するために
NotImplementedErrorを発生させてください。 - 任意のモジュールからメソッドを追加しないでください。読者はクラスの定義の外でその動作を検索することを強いられます。
- リファクタリングは、削除された行数だけでなく、変更された場所と保持されたデータによって測定します。
強制されたカタログなしでパターン語彙を使用する
- 最初に問題を説明します。作成、インターフェース適応、アルゴリズム選択、通知、ツリーまたは状態復元。名前は後から来ます。
- 関数の受け渡しや保存で十分な場合、StrategyとCommandをそれに削減します。チェーンは順に試行される関数のリストにすることができます。
- 共有モジュール、クラスのテーブル、または反復プロトコルが、Singleton、Factory、またはIteratorの意図をすでに満たしている可能性があることを認識します。
- インターフェースを変換するAdapter、サブシステムを簡素化するFaçade、別のオブジェクトへのアクセスを制御するProxyを区別します。
- 状態に応じて動作を変更するStateと、入力に応じて遷移を選択する状態機械を区別します。
- 外部からコラボレータを注入することと、レジストリでそのコラボレータを検索することを区別します。どちらも同じ方法で依存関係を公開せずに制御を移動します。
- アーキテクチャ、永続化、メッセージング、クラウドの名前は、説明された意図のレベルに保ちます。カタログに存在することは、実装のレシピを確立しません。
- Circuit Breaker、Saga、Repository、Event Sourcingに言及しただけで、デプロイ、リカバリ、または一貫性の推奨を推測しないでください。
手順
- ターゲットのPython、既に使用されている規約とチェックを特定し、保持する動作を記録します。
- 具体的な要件と観測された変更の軸を述べ、今日影響を受けるコンポーネントをリストします。
- 関連する共有参照、真理値、数値操作、文字列変換を確認します。
- 最小限のソリューションを記述し、提案された抽象化を比較します。消去された詳細、隠された作業、移動された依存関係。
- 型の追加と操作の追加を別々にテストし、各バリアントで必要な変更を記録します。
- 動作テストと入出力データのカウントを保持しながら、段階的にリファクタリングします。
- 不要になったレイヤーを削除し、サポートされているバージョンでプロジェクトのテストとチェックを実行します。
落とし穴
- プログラムは、新しい型を見失った後でも、もっともらしい合計を生成する可能性があります。出力の表示だけでなく、データの保存を確認してください。
- フォールバックブランチまたはデフォルトの実装は、忘れられた拡張を隠す可能性があります。中立的な応答が正しくない場合は、明示的な失敗を優先してください。
- いくつかの実際のケースを持つ前に一般化すると、有用性の証拠なしにオプションが作成されます。投機的なフレームワークは拒否してください。
- 短い抽象化は、高いコストを隠したり、不可欠な詳細を消去したりする可能性があります。行数を数えるだけではその品質を判断するのに十分ではありません。
- コーパスからの最近の構文とAPIは、古いプロジェクトでは必ずしも利用できるとは限りません。それらを採用するために最小Pythonバージョンを引き上げないでください。
検証
- 新しいクラスまたはインターフェースはそれぞれ、名前の付いたニーズを保護します。名前のためだけに存在するパターンはありません。
- 条件付きパスは、使用前に変数をバインドします。同一性、ミューテーション、等価性は混同されていません。
- ゼロ、空の値、
None、負の数、ネストされたデータは、それらの区別が重要な場合に実行されています。 - 新しいサブクラスは黙って消えず、正確なクラスまたは継承ベースのディスパッチはテストされています。