シンプルさのためのPythonアプリケーションのリファクタリング

シンプルさのためのPythonアプリケーションのリファクタリング

よりシンプルなPythonコードが必要ですか? 常に、最善の意図、クリーンなコードベース、素晴らしい構造でプロジェクトを開始します。 しかし、時間が経つにつれて、アプリに変更が加えられ、事態は少し面倒になります。

簡潔でシンプルなPythonコードを記述して維持できれば、長期的に多くの時間を節約できます。 コードが適切にレイアウトされ、従うのが簡単な場合、テスト、バグの発見、および変更にかかる時間を短縮できます。

このチュートリアルでは、次のことを学びます。

  • Pythonコードとアプリケーションの複雑さを測定する方法

  • コードを壊さずに変更する方法

  • 余分な複雑さを引き起こすPythonコードの一般的な問題とその修正方法

このチュートリアルでは、大都市の地下鉄システムのナビゲートは複雑になる可能性があるため、地下鉄道ネットワークのテーマを使用して複雑さを説明します。 うまく設計されたものもあれば、過度に複雑に見えるものもあります。

Free Bonus:5 Thoughts On Python Masteryは、Python開発者向けの無料コースで、Pythonスキルを次のレベルに引き上げるために必要なロードマップと考え方を示しています。

Pythonのコードの複雑さ

アプリケーションとそのコードベースの複雑さは、実行するタスクに関連しています。 NASAのジェット推進研究所(文字通りrocket science)のコードを書いている場合、それは複雑になります。

質問はそれほど多くありません。「私のコードは複雑ですか?」 「私のコードは必要以上に複雑ですか?」

東京の鉄道網は、世界で最も広範で複雑なものの1つです。 これは、東京が3,000万人を超える大都市であることに一部起因しますが、3つのネットワークが互いに重複しているためでもあります。

都営および東京メトロの高速輸送ネットワークに加えて、東京中央部を通過するJR東日本の電車があります。 最も経験豊富な旅行者でさえ、東京の中心部をナビゲートするのは非常に複雑です。

以下は、東京鉄道網の地図です。

コードがこのマップのように見え始めている場合、これがチュートリアルです。

最初に、ミッションの相対的な進捗を測定してコードを簡素化するためのスケールを与えることができる複雑さの4つのメトリックを検証します。

Code Complexity Metrics Table of Contents

指標を調べた後、それらの指標の計算を自動化するwilyというツールについて学習します。

複雑さを測定するためのメトリック

コンピュータソフトウェアの複雑さの分析には、多くの時間と研究が費やされています。 過度に複雑で保守不可能なアプリケーションには、非常に大きなコストがかかる可能性があります。

ソフトウェアの複雑さは品質と相関しています。 読みやすく理解しやすいコードは、今後開発者によって更新される可能性が高くなります。

プログラミング言語のいくつかの指標を以下に示します。 Pythonだけでなく、多くの言語に適用されます。

コードの行

LOC、またはコード行は、複雑さの最も粗雑な尺度です。 コード行とアプリケーションの複雑さの間に直接的な相関関係があるかどうかは議論の余地がありますが、間接的な相関関係は明らかです。 結局、5行のプログラムは、500万行のプログラムよりも単純である可能性が高いです。

Pythonメトリックスを見るとき、空白行とコメントを含む行を無視しようとします。

コード行は、LinuxおよびMac OSでwcコマンドを使用して計算できます。ここで、file.pyは測定するファイルの名前です。

$ wc -l file.py

すべての.pyファイルを再帰的に検索して、結合された行をフォルダーに追加する場合は、wcfindコマンドと結合できます。

$ find . -name \*.py | xargs wc -l

Windowsの場合、PowerShellはMeasure-Objectで単語カウントコマンドを提供し、Get-ChildItemで再帰的なファイル検索を提供します。

$ Get-ChildItem -Path *.py -Recurse | Measure-Object –Line

応答では、行の総数が表示されます。

アプリケーションのコード量を定量化するためにコード行が使用されるのはなぜですか? 前提は、コードの行はおおよそ文に等しいということです。 行は、空白を含む文字よりも優れた尺度です。

Pythonでは、各行に1つのステートメントを配置することをお勧めします。 この例は、9行のコードです。

 1 x = 5
 2 value = input("Enter a number: ")
 3 y = int(value)
 4 if x < y:
 5     print(f"{x} is less than {y}")
 6 elif x == y:
 7     print(f"{x} is equal to {y}")
 8 else:
 9     print(f"{x} is more than {y}")

複雑さの尺度としてコード行のみを使用した場合、間違った動作を助長する可能性があります。

Pythonコードは読みやすく、理解しやすいものでなければなりません。 最後の例を使用すると、コードの行数を3行に減らすことができます。

 1 x = 5; y = int(input("Enter a number:"))
 2 equality = "is equal to" if x == y else "is less than" if x < y else "is more than"
 3 print(f"{x} {equality} {y}")

しかし、結果は読みにくく、PEP 8には最大行長と改行に関するガイドラインがあります。 PEP 8の詳細については、How to Write Beautiful Python Code With PEP 8を確認できます。

このコードブロックは、2つのPython言語機能を使用してコードを短くします。

  • ;を使用するCompound statements:

  • 連鎖した条件文または3項文: name = value if condition else value if condition2 else value2

コードの行数を減らしましたが、Pythonの基本的な法則の1つに違反しています。

「読みやすさのカウント」

— Pythonの禅、ティムピーターズ

コードのメンテナーは人間であり、この短いコードは読みにくいため、この短縮されたコードは潜在的に保守が困難です。 複雑さについて、より高度で有用ないくつかのメトリックを検討します。

巡回複雑度

循環的複雑度は、アプリケーションを介した独立コードパスの数の尺度です。 パスは、アプリケーションの最後に到達するためにインタープリターがたどることができる一連のステートメントです。

循環的な複雑さとコードパスを考える1つの方法は、コードが鉄道網のようなものだと想像することです。

旅のために、目的地に着くために列車を変える必要があるかもしれません。 ポルトガルのリスボン首都圏鉄道システムは、シンプルで簡単にナビゲートできます。 旅行の循環的な複雑さは、旅行する必要がある路線の数と同じです。

AlvaladeからAnjosに移動する必要がある場合は、linha verde(緑色の線)で5ストップ移動します。

この旅行の循環的複雑度は1です。これは、列車が1本しかないためです。 簡単な旅です。 この類推では、そのトレインはコードブランチに相当します。

Aeroporto(空港)から移動してfood in the district of Belémをサンプリングする必要がある場合は、より複雑な移動です。 AlamedaCais do Sodréで列車を変更する必要があります。

3つの列車に乗るため、この旅行の循環的複雑度は3です。 タクシーに乗る方がいいかもしれません!

リスボンをナビゲートするのではなく、コードを書く方法として見ると、列車の路線の変更は、ifステートメントのように実行中のブランチになります。 この例を見てみましょう。

x = 1

このコードを実行できる方法は1つしかないため、循環的複雑度は1です。

決定を追加するか、ifステートメントとしてコードに分岐すると、複雑さが増します。

x = 1
if x < 2:
    x += 1

このコードを実行できる方法は1つしかありませんが、xは定数であるため、循環的複雑度は2です。 すべての循環的複雑度アナライザーは、ifステートメントをブランチとして扱います。

これは、非常に複雑なコードの例でもあります。 xの値は固定されているため、ifステートメントは役に立ちません。 この例を次のように単純にリファクタリングできます。

x = 2

これはおもちゃの例でしたので、もう少し現実を探ってみましょう。

main()の循環的複雑度は5です。 コードの各ブランチをコメントして、どこにあるかを確認します。

# cyclomatic_example.py
import sys

def main():
    if len(sys.argv) > 1:  # 1
        filepath = sys.argv[1]
    else:
        print("Provide a file path")
        exit(1)
    if filepath:  # 2
        with open(filepath) as fp:  # 3
            for line in fp.readlines():  # 4
                if line != "\n":  # 5
                    print(line, end="")

if __name__ == "__main__":  # Ignored.
    main()

確かに、コードをはるかに単純な代替手段にリファクタリングできる方法があります。 後で説明します。

循環的複雑度尺度は1976年にdeveloped by Thomas J. McCabe, Srであった。 McCabe metricまたはMcCabe numberと呼ばれることがあります。

次の例では、radonライブラリfrom PyPiを使用してメトリックを計算します。 今すぐインストールできます:

$ pip install radon

radonを使用して循環的複雑度を計算するには、例をcyclomatic_example.pyというファイルに保存し、コマンドラインからradonを使用します。

radonコマンドは2つの主要な引数を取ります:

  1. 分析のタイプ(循環的複雑度のcc

  2. 分析するファイルまたはフォルダーへのパス

cyclomatic_example.pyファイルに対してcc分析を使用してradonコマンドを実行します。 -sを追加すると、出力に循環的複雑度が生じます。

$ radon cc cyclomatic_example.py -s
cyclomatic_example.py
    F 4:0 main - B (6)

出力は少し不可解です。 各部分の意味は次のとおりです。

  • Fは関数を意味し、Mはメソッドを意味し、Cはクラスを意味します。

  • mainは関数の名前です。

  • 4は、関数が開始する行です。

  • Bは、AからFまでの評価です。 Aは最高のグレードであり、最も複雑ではありません。

  • 括弧内の数字6は、コードの循環的複雑度です。

Halstead Metrics

Halsteadの複雑さのメトリックは、プログラムのコードベースのサイズに関連しています。 それらはモーリス・Hによって開発されました。 1977年のハルステッド。 Halstead方程式には4つのメジャーがあります。

  • Operandsは、変数の値と名前です。

  • Operatorsは、ifelseforwhileなどのすべての組み込みキーワードです。

  • Length (N)は、演算子の数にプログラムのオペランドの数を加えたものです。

  • Vocabulary (h)は、unique演算子の数に、プログラム内のuniqueオペランドの数を加えたものです。

これらのメジャーには、さらに3つのメトリックがあります。

  • Volume (V)は、lengthvocabularyの積を表します。

  • Difficulty (D)は、一意のオペランドの半分とオペランドの再利用の積を表します。

  • Effort (E)は、volumedifficultyの積である全体的なメトリックです。

これらはすべて非常に抽象的なので、相対的な用語で説明しましょう。

  • 多数の演算子と一意のオペランドを使用する場合、アプリケーションの労力は最大になります。

  • 少数の演算子と少ない変数を使用する場合、アプリケーションの労力は低くなります。

cyclomatic_complexity.pyの例では、演算子とオペランドの両方が最初の行にあります。

import sys  # import (operator), sys (operand)

importは演算子であり、sysはモジュールの名前であるため、オペランドです。

もう少し複雑な例では、いくつかの演算子とオペランドがあります。

if len(sys.argv) > 1:
    ...

この例には5つの演算子があります。

  1. if

  2. (

  3. )

  4. >

  5. :

さらに、2つのオペランドがあります。

  1. sys.argv

  2. 1

radonは演算子のサブセットのみをカウントすることに注意してください。 たとえば、括弧は計算から除外されます。

radonでHalsteadメジャーを計算するには、次のコマンドを実行できます。

$ radon hal cyclomatic_example.py
cyclomatic_example.py:
    h1: 3
    h2: 6
    N1: 3
    N2: 6
    vocabulary: 9
    length: 9
    calculated_length: 20.264662506490406
    volume: 28.529325012980813
    difficulty: 1.5
    effort: 42.793987519471216
    time: 2.377443751081734
    bugs: 0.009509775004326938

radonが時間とバグのメトリックを提供するのはなぜですか?

Halsteadは、労力(E)を18で割ることにより、コーディングにかかる​​時間を秒単位で見積もることができると理論付けました。

ハルステッドはまた、予想されるバグの数は、ボリューム(V)を3000で割ると推定できると述べました。 これは、Pythonが発明される前の1977年に書かれたことに留意してください! パニックに陥らず、まだバグを探し始めてください。

保全性指数

保守性指標は、McCabe Cyclomatic ComplexityとHalstead Volumeの測定値をおよそ0〜100のスケールでもたらします。

興味がある場合、元の方程式は次のとおりです。

MI Equation

この式で、VはHalsteadボリュームメトリック、Cは循環的複雑度、Lはコードの行数です。

最初にこの方程式を見たときと同じように困惑している場合、それは次のことを意味します。変数、操作、決定パス、コード行の数を含むスケールを計算します。

多くのツールや言語で使用されているため、より標準的な指標の1つです。 ただし、方程式には多数の修正が加えられているため、正確な数値を事実とみなすべきではありません。 radonwily、およびVisual Studioの上限は、0〜100です。

保守性インデックススケールでは、注意が必要なのは、コードが大幅に低くなったとき(0に向かって)です。 スケールは、25未満のものはすべてhard to maintainと見なし、75を超えるものはすべてeasy to maintainと見なします。 保守性指数はMIとも呼ばれます。

保守性指標は、アプリケーションの現在の保守性を取得し、リファクタリング中に進捗を確認するための尺度として使用できます。

radonから保守性インデックスを計算するには、次のコマンドを実行します。

$ radon mi cyclomatic_example.py -s
cyclomatic_example.py - A (87.42)

この結果では、Aは、radonがスケール上の数値87.42に適用したグレードです。 このスケールでは、Aが最も保守しやすく、Fが最も少なくなります。

wilyを使用して、プロジェクトの複雑さをキャプチャおよび追跡する

wily is an open-source software projectは、Halstead、Cyclomatic、LOCなど、これまでに取り上げたものを含む、コードの複雑さの指標を収集します。 wilyはGitと統合され、Gitのブランチとリビジョン全体でメトリックの収集を自動化できます。

wilyの目的は、時間の経過に伴うコードの複雑さの傾向と変化を確認できるようにすることです。 車を微調整したり、フィットネスを改善したりする場合は、ベースラインの測定と経時的な改善の追跡から始めます。

wilyのインストール

wilyon PyPiで利用可能であり、pipを使用してインストールできます。

$ pip install wily

wilyをインストールすると、コマンドラインでいくつかのコマンドを使用できるようになります。

  • wily build:は、Git履歴を反復処理し、各ファイルのメトリックを分析します

  • wily report:は、特定のファイルまたはフォルダーのメトリックの履歴傾向を確認します

  • wily graph:は、HTMLファイル内の一連のメトリックをグラフ化します

キャッシュを構築する

wilyを使用する前に、プロジェクトを分析する必要があります。 これは、wily buildコマンドを使用して実行されます。

チュートリアルのこのセクションでは、HTTP APIとの通信に使用される非常に人気のあるrequestsパッケージを分析します。 このプロジェクトはオープンソースであり、GitHubで利用できるため、ソースコードのコピーに簡単にアクセスしてダウンロードできます。

$ git clone https://github.com/requests/requests
$ cd requests
$ ls
AUTHORS.rst        CONTRIBUTING.md    LICENSE            Makefile
Pipfile.lock       _appveyor          docs               pytest.ini
setup.cfg          tests              CODE_OF_CONDUCT.md HISTORY.md
MANIFEST.in        Pipfile            README.md          appveyor.yml
ext                requests           setup.py           tox.ini

Note: Windowsユーザーは、従来のMS-DOSコマンドラインの代わりに、次の例のPowerShellコマンドプロンプトを使用する必要があります。 PowerShell CLIを開始するには、Win[.kbd .key-r]#R## and type `+powershell` then [.keys] [.kbd .key-enter]Enter#を押します。

ここには、テスト、ドキュメント、および構成用のいくつかのフォルダーがあります。 requestsというフォルダーにあるrequestsPythonパッケージのソースコードのみに関心があります。

複製されたソースコードからwily buildコマンドを呼び出し、最初の引数としてソースコードフォルダーの名前を指定します。

$ wily build requests

コンピューターのCPU能力に応じて、分析に数分かかります。

Screenshot capture of Wily build command

プロジェクトに関するデータを収集する

requestsソースコードを分析したら、任意のファイルまたはフォルダーにクエリを実行して、主要なメトリックを確認できます。 チュートリアルの前半で、以下について説明しました。

  • コードの行

  • 保全性指数

  • 巡回複雑度

これらは、wilyの3つのデフォルトメトリックです。 特定のファイル(requests/api.pyなど)のこれらのメトリックを表示するには、次のコマンドを実行します。

$ wily report requests/api.py

wilyは、各Gitコミットのデフォルトメトリックに関する表形式のレポートを日付の逆順で出力します。 最新のコミットが一番上に表示され、最も古いコミットが一番下に表示されます。

リビジョン 著者 Date MI コードの行 巡回複雑度

f37daf2

ネイト・プレウィット

2019-01-13

100(0.0)

158(0)

9(0)

6dd410f

Ofek Lev

2019-01-13

100(0.0)

158(0)

9(0)

5c1f72e

ネイト・プレウィット

2018-12-14

100(0.0)

158(0)

9(0)

c4d7680

マシュー・モイ

2018-12-14

100(0.0)

158(0)

9(0)

c452e3b

ネイト・プレウィット

2018-12-11

100(0.0)

158(0)

9(0)

5a1e738

ネイト・プレウィット

2018-12-10

100(0.0)

158(0)

9(0)

これは、requests/api.pyファイルに次のものがあることを示しています。

  • 158行のコード

  • 100の完全な保守性インデックス

  • 9の循環的複雑度

他のメトリックを表示するには、まずそれらの名前を知る必要があります。 これを確認するには、次のコマンドを実行します。

$ wily list-metrics

演算子、コードを分析するモジュール、およびそれらが提供するメトリックのリストが表示されます。

レポートコマンドで代替メトリックを照会するには、ファイル名の後にそれらの名前を追加します。 必要な数のメトリックを追加できます。 保守性ランクとソースコード行の例を次に示します。

$ wily report requests/api.py maintainability.rank raw.sloc

テーブルには、代替メトリックを含む2つの異なる列が表示されます。

グラフ化メトリック

メトリックの名前とコマンドラインでのクエリ方法がわかったので、それらをグラフで視覚化することもできます。 wilyは、reportコマンドと同様のインターフェイスを備えたHTMLおよびインタラクティブチャートをサポートします。

$ wily graph requests/sessions.py maintainability.mi

デフォルトのブラウザが開き、次のようなインタラクティブなチャートが表示されます。

Screenshot capture of Wily graph command

特定のデータポイントにカーソルを合わせると、Gitのコミットメッセージとデータが表示されます。

HTMLファイルをフォルダまたはリポジトリに保存する場合は、ファイルへのパスとともに-oフラグを追加できます。

$ wily graph requests/sessions.py maintainability.mi -o my_report.html

これで、他のユーザーと共有できるmy_report.htmlというファイルが作成されます。 このコマンドは、チームのダッシュボードに最適です。

pre-commitフックとしてのwily

wilyは、プロジェクトに変更をコミットする前に、複雑さの改善または低下を警告できるように構成できます。

wilyにはwily diffコマンドがあり、最後にインデックスが付けられたデータをファイルの現在の作業コピーと比較します。

wily diffコマンドを実行するには、変更したファイルの名前を入力します。 たとえば、requests/api.pyに変更を加えた場合、ファイルパスを指定してwily diffを実行すると、メトリックへの影響がわかります。

$ wily diff requests/api.py

応答では、変更されたすべてのメトリックと、循環的な複雑さのために変更された関数またはクラスが表示されます。

Screenshot of the wily diff command

diffコマンドは、pre-commitと呼ばれるツールと組み合わせることができます。 pre-commitは、git commitコマンドを実行するたびにスクリプトを呼び出すフックをGit構成に挿入します。

pre-commitをインストールするには、PyPIからインストールできます。

$ pip install pre-commit

プロジェクトのルートディレクトリの.pre-commit-config.yamlに以下を追加します。

repos:
-   repo: local
    hooks:
    -   id: wily
        name: wily
        entry: wily diff
        verbose: true
        language: python
        additional_dependencies: [wily]

これを設定したら、pre-commit installコマンドを実行して次のことを完了します。

$ pre-commit install

git commitコマンドを実行すると、ステージングされた変更に追加したファイルのリストとともにwily diffが呼び出されます。

wilyは、コードの複雑さをベースライン化し、リファクタリングを開始したときに行った改善を測定するための便利なユーティリティです。

Pythonでのリファクタリング

リファクタリングとは、アプリケーション(コードまたはアーキテクチャ)を変更して、外部で同じように動作するが、内部的に改善する手法です。 これらの改善は、安定性、パフォーマンス、または複雑さの軽減です。

世界最古の地下鉄の1つであるロンドン地下鉄は、1863年にメトロポリタン線が開通したときに始まりました。 蒸気機関車で運ばれるガス灯のある木製の馬車がありました。 鉄道の開通時には、目的に合っていました。 1900年に電気鉄道の発明がもたらされました。

1908年までに、ロンドン地下鉄は8本の鉄道に拡大しました。 第二次世界大戦中、ロンドンの地下鉄の駅は列車に閉鎖され、防空sheとして使用されました。 現代のロンドン地下鉄は、270以上の駅で1日数百万人の乗客を運んでいます。

初めて完全なコードを記述することはほとんど不可能であり、要件は頻繁に変更されます。 2020年に鉄道の元の設計者に1日1,000万人の乗客に適したネットワークを設計するように依頼した場合、今日存在するネットワークは設計されません。

代わりに、鉄道は都市の変化に合わせて運行、設計、レイアウトを最適化するために一連の継続的な変更を受けています。 リファクタリングされました。

このセクションでは、テストとツールを活用して安全にリファクタリングする方法を探ります。 また、Visual Studio CodeおよびPyCharmでリファクタリング機能を使用する方法についても説明します。

Refactoring Section Table of Contents

リファクタリングによるリスクの回避:ツールの活用とテストの実施

リファクタリングのポイントが外部に影響を与えずにアプリケーションの内部を改善することである場合、外部が変更されていないことをどのように確認しますか?

主要なリファクタリングプロジェクトに突入する前に、アプリケーションの堅牢なテストスイートがあることを確認する必要があります。 理想的には、テストスイートはほとんど自動化されている必要があります。これにより、変更を加えたときにユーザーへの影響を確認し、迅速に対処できます。

Pythonでのテストについて詳しく知りたい場合は、Getting Started With Testing in Pythonから始めるのが最適です。

アプリケーションに対して行うテストの完全な数はありません。 ただし、テストスイートの堅牢性と徹底性が高いほど、コードをより積極的にリファクタリングできます。

リファクタリングを行うときに実行する最も一般的な2つのタスクは次のとおりです。

  • モジュール、関数、クラス、およびメソッドの名前変更

  • 関数、クラス、およびメソッドの使用法を見つけて、それらが呼び出される場所を確認する

これはsearch and replaceを使用して手動で簡単に行うことができますが、時間とリスクの両方がかかります。 代わりに、これらのタスクを実行するための優れたツールがいくつかあります。

リファクタリングにropeを使用する

ropeは、Pythonコードをリファクタリングするための無料のPythonユーティリティです。 Pythonコードベースのコンポーネントをリファクタリングおよび名前変更するためのAPIのextensiveセットが付属しています。

ropeは、次の2つの方法で使用できます。

  1. Visual Studio CodeEmacs、またはVimのエディタープラグインを使用する

  2. アプリケーションをリファクタリングするスクリプトを作成して直接

ロープをライブラリとして使用するには、最初にpipを実行してropeをインストールします。

$ pip install rope

REPLでropeを操作すると、プロジェクトを探索して変更をリアルタイムで確認できるので便利です。 開始するには、Projectタイプをインポートし、プロジェクトへのパスを使用してインスタンス化します。

>>>

>>> from rope.base.project import Project

>>> proj = Project('requests')

proj変数は、get_filesget_fileなどの一連のコマンドを実行して、特定のファイルを取得できるようになりました。 ファイルapi.pyを取得し、それをapiという変数に割り当てます。

>>>

>>> [f.name for f in proj.get_files()]
['structures.py', 'status_codes.py', ...,'api.py', 'cookies.py']

>>> api = proj.get_file('api.py')

このファイルの名前を変更したい場合は、ファイルシステム上で単純に名前を変更できます。 ただし、プロジェクト内の古い名前をインポートした他のPythonファイルは破損します。 api.pyの名前をnew_api.pyに変更しましょう。

>>>

>>> from rope.refactor.rename import Rename

>>> change = Rename(proj, api).get_changes('new_api')

>>> proj.do(change)

git statusを実行すると、ropeがリポジトリにいくつかの変更を加えたことがわかります。

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   requests/__init__.py
    deleted:    requests/api.py

Untracked files:
  (use "git add ..." to include in what will be committed)

   requests/.ropeproject/
   requests/new_api.py

no changes added to commit (use "git add" and/or "git commit -a")

ropeによって行われた3つの変更は次のとおりです。

  1. requests/api.pyを削除し、requests/new_api.pyを作成しました

  2. apiではなくnew_apiからインポートするようにrequests/__init__.pyを変更しました

  3. .ropeprojectという名前のプロジェクトフォルダを作成しました

変更をリセットするには、git resetを実行します。

ropeで実行できるhundreds of other refactoringsがあります。

リファクタリングのためのVisual Studioコードの使用

Visual Studio Codeは、独自のUIを介してropeで使用可能なリファクタリングコマンドの小さなサブセットを開きます。

あなたはできる:

  1. ステートメントから変数を抽出する

  2. コードブロックからメソッドを抽出する

  3. インポートを論理的な順序に並べ替える

コマンドパレットからExtract methodsコマンドを使用する例を次に示します。

Screenshot of Visual Studio Code refactoring

リファクタリングにPyCharmを使用する

PythonエディターとしてPyCharmを使用している、または使用を検討している場合、PyCharmの強力なリファクタリング機能に注意する価値があります。

Ctrl[.kbd .key-t]#T## command on Windows and macOS. The shortcut to access refactoring in Linux is [.keys]#[.kbd .key-control]##Ctrl##Shift[.kbd .key-alt]##Alt##[.kbd .key-t]#T#を使用して、すべてのリファクタリングショートカットにアクセスできます。

呼び出し元と関数とクラスの使用法を見つける

メソッドまたはクラスを削除したり、動作を変更したりする前に、それに依存するコードを知る必要があります。 PyCharmは、プロジェクト内のメソッド、関数、またはクラスのすべての使用法を検索できます。

この機能にアクセスするには、右クリックしてメソッド、クラス、または変数を選択し、Find Usagesを選択します。

Finding usages in PyCharm

検索条件を使用するすべてのコードは、下部のパネルに表示されます。 任意のアイテムをダブルクリックして、問題の行に直接移動できます。

PyCharmリファクタリングツールの使用

他のリファクタリングコマンドには、次の機能が含まれます。

  • 既存のコードからメソッド、変数、定数を抽出する

  • 抽象メソッドを指定する機能など、既存のクラスシグネチャから抽象クラスを抽出する

  • 変数からメソッド、ファイル、クラス、またはモジュールまで、実質的にすべての名前を変更します

これは、ropeモジュールを使用して以前に名前を変更したのと同じapi.pyモジュールの名前をnew_api.pyに変更する例です。

How to rename methods in pycharm

renameコマンドはUIにコンテキスト化されているため、リファクタリングが迅速かつ簡単になります。 新しいモジュール名で__init__.pyのインポートを自動的に更新しました。

もう1つの便利なリファクタリングは、Change Signatureコマンドです。 これを使用して、関数またはメソッドに引数を追加、削除、または名前変更できます。 使用法を検索し、それらを更新します。

Changing method signatures in PyCharm

デフォルト値を設定し、リファクタリングが新しい引数を処理する方法を決定することもできます。

概要

リファクタリングは、開発者にとって重要なスキルです。 この章で学んだように、あなたは一人ではありません。 ツールとIDEには、強力なリファクタリング機能がすでに付属しており、すばやく変更を加えることができます。

複雑さの反パターン

複雑さの測定方法、測定方法、およびコードのリファクタリング方法がわかったので、次はコードを必要以上に複雑にする5つの一般的なアンチパターンを学習します。

Anti-Patterns Table of Contents

これらのパターンを習得し、それらをリファクタリングする方法を知っていれば、すぐに保守性の高いPythonアプリケーションを軌道に乗せることができます。

1. オブジェクトになる関数

Pythonは、関数を使用するprocedural programmingと、inheritable classesをサポートします。 どちらも非常に強力であり、さまざまな問題に適用する必要があります。

画像を操作するためのモジュールの例をご覧ください。 簡潔にするため、関数のロジックは削除されました。

# imagelib.py

def load_image(path):
    with open(path, "rb") as file:
        fb = file.load()
    image = img_lib.parse(fb)
    return image

def crop_image(image, width, height):
    ...
    return image

def get_image_thumbnail(image, resolution=100):
    ...
    return image

この設計にはいくつかの問題があります。

  1. crop_image()get_image_thumbnail()が元のimage変数を変更するのか、それとも新しいイメージを作成するのかは明確ではありません。 画像をロードし、トリミングされた画像とサムネイル画像の両方を作成する場合、最初にインスタンスをコピーする必要がありますか? 関数のソースコードを読むことはできますが、これを行うすべての開発者に頼ることはできません。

  2. 画像関数を呼び出すたびに、画像変数を引数として渡す必要があります。

呼び出し元のコードは次のようになります。

from imagelib import load_image, crop_image, get_image_thumbnail

image = load_image('~/face.jpg')
image = crop_image(image, 400, 500)
thumb = get_image_thumbnail(image)

次に、クラスにリファクタリングできる関数を使用したコードの症状をいくつか示します。

  • 関数間で同様の引数

  • ハルステッドの数が多いh2unique operands

  • 可変関数と不変関数の混合

  • 複数のPythonファイルにまたがる関数

これらの3つの関数のリファクタリングバージョンを以下に示します。

  • .__init__()load_image()を置き換えます。

  • crop()はクラスメソッドになります。

  • get_image_thumbnail()はプロパティになります。

サムネイルの解像度はクラスプロパティになっているため、グローバルに、またはその特定のインスタンスで変更できます。

# imagelib.py

class Image(object):
    thumbnail_resolution = 100
    def __init__(self, path):
        ...

    def crop(self, width, height):
        ...

    @property
    def thumbnail(self):
        ...
        return thumb

このコードにさらに多くの画像関連の関数がある場合、クラスへのリファクタリングは劇的な変更を加える可能性があります。 次の考慮事項は、消費するコードの複雑さです。

これは、リファクタリングされた例の外観です。

from imagelib import Image

image = Image('~/face.jpg')
image.crop(400, 500)
thumb = image.thumbnail

結果のコードで、元の問題を解決しました。

  • thumbnailはプロパティであるためサムネイルを返し、インスタンスを変更しないことは明らかです。

  • コードでは、トリミング操作のために新しい変数を作成する必要がなくなりました。

2. 関数であるべきオブジェクト

時には、その逆の場合もあります。 1つまたは2つの単純な関数により適したオブジェクト指向コードがあります。

クラスの誤った使用のいくつかの物語の兆候は次のとおりです。

  • 1つのメソッドを持つクラス(.__init__()以外)

  • 静的メソッドのみを含むクラス

次の認証クラスの例をご覧ください。

# authenticate.py

class Authenticator(object):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        ...
        return result

usernamepasswordを引数として取るauthenticate()という名前の単純な関数がある方が理にかなっています。

# authenticate.py

def authenticate(username, password):
    ...
    return result

座って、これらの基準に一致するクラスを手動で探す必要はありません。pylintには、クラスに少なくとも2つのパブリックメソッドが必要であるというルールがあります。 PyLintおよびその他のコード品質ツールの詳細については、Python Code Qualityを確認してください。

pylintをインストールするには、コンソールで次のコマンドを実行します。

$ pip install pylint

pylintは、いくつかのオプションの引数を取り、次に1つ以上のファイルとフォルダーへのパスを取ります。 pylintをデフォルト設定で実行すると、pylintには膨大な数のルールがあるため、多くの出力が得られます。 代わりに、特定のルールを実行できます。 too-few-public-methodsルールIDはR0903です。 これはdocumentation websiteで調べることができます:

$ pylint --disable=all --enable=R0903 requests
************* Module requests.auth
requests/auth.py:72:0: R0903: Too few public methods (1/2) (too-few-public-methods)
requests/auth.py:100:0: R0903: Too few public methods (1/2) (too-few-public-methods)
************* Module requests.models
requests/models.py:60:0: R0903: Too few public methods (1/2) (too-few-public-methods)

-----------------------------------
Your code has been rated at 9.99/10

この出力は、auth.pyにパブリックメソッドが1つしかない2つのクラスが含まれていることを示しています。 これらのクラスは、72行目と100行目にあります。 60行目にmodels.pyのクラスがあり、パブリックメソッドは1つだけです。

3. 「三角」コードからフラットコードへの変換

ソースコードをズームアウトして頭を右に90度傾けると、空白はオランダのように平らに見えますか、ヒマラヤのように山のように見えますか? 山岳コードは、コードに多くのネストが含まれていることを示しています。

Zen of Pythonの原則の1つは次のとおりです。

「フラットはネストよりも優れています」

— Pythonの禅、ティムピーターズ

ネストされたコードよりフラットなコードの方が優れているのはなぜですか? ネストされたコードは、何が起こっているかを読み、理解するのを難しくするからです。 読者は、ブランチを通過する際の条件を理解し、記憶する必要があります。

これらは、高度にネストされたコードの症状です。

  • コードブランチの数のために高度な循環的複雑さ

  • コードの行数に比べて循環的複雑度が高いため、保守性指数が低い

単語errorに一致する文字列の引数dataを調べるこの例を見てください。 最初に、data引数がリストであるかどうかをチェックします。 次に、それぞれを反復処理し、アイテムが文字列かどうかを確認します。 文字列で、値が"error"の場合、Trueを返します。 それ以外の場合は、Falseを返します。

def contains_errors(data):
    if isinstance(data, list):
        for item in data:
            if isinstance(item, str):
                if item == "error":
                    return True
    return False

この関数は小さいため、保守性指数が低くなりますが、循環的複雑度が高くなります。

代わりに、「早期に戻る」ことでこの関数をリファクタリングして、ネストのレベルを削除し、dataの値がリストされていない場合はFalseを返すことができます。 次に、リストオブジェクトで.count()を使用して、"error"のインスタンスをカウントします。 戻り値は、.count()がゼロより大きいという評価です。

def contains_errors(data):
    if not isinstance(data, list):
        return False
    return data.count("error") > 0

ネストを減らす別の方法は、リストの内包表記を活用することです。 新しいリストを作成するこの一般的なパターンは、リスト内の各アイテムを調べて基準に一致するかどうかを確認し、すべての一致を新しいリストに追加します。

results = []
for item in iterable:
    if item == match:
        results.append(item)

このコードは、より高速で効率的なリスト内包表記に置き換えることができます。

最後の例をリスト内包表記とifステートメントにリファクタリングします。

results = [item for item in iterable if item == match]

この新しい例はより小さく、複雑さが少なく、パフォーマンスが向上しています。

データが単一のディメンションリストでない場合は、データ構造からイテレータを作成するための関数を含む標準ライブラリのitertoolsパッケージを利用できます。 これを使用して、イテラブルを連鎖させたり、構造をマッピングしたり、既存のイテラブルを繰り返したり繰り返したりすることができます。

Itertoolsには、filterfalse()などのデータをフィルタリングするための関数も含まれています。 Itertoolsの詳細については、Itertools in Python 3, By Exampleを確認してください。

4. クエリツールを使用した複雑な辞書の処理

Pythonの最も強力で広く使用されているコアタイプの1つは辞書です。 速く、効率的で、スケーラブルで、非常に柔軟です。

辞書を初めて使用する場合、または辞書をさらに活用できると思われる場合は、Dictionaries in Pythonを読んで詳細を確認してください。

1つの大きな副作用があります。辞書が高度にネストされている場合、それらを照会するコードもネストされます。

先ほど見た東京メトロの路線のサンプルであるこのデータの例をご覧ください。

data = {
 "network": {
  "lines": [
    {
     "name.en": "Ginza",
     "name.jp": "銀座線",
     "color": "orange",
     "number": 3,
     "sign": "G"
    },
    {
     "name.en": "Marunouchi",
     "name.jp": "丸ノ内線",
     "color": "red",
     "number": 4,
     "sign": "M"
    }
  ]
 }
}

特定の番号に一致する行を取得したい場合、これは小さな関数で実現できます。

def find_line_by_number(data, number):
    matches = [line for line in data if line['number'] == number]
    if len(matches) > 0:
        return matches[0]
    else:
        raise ValueError(f"Line {number} does not exist.")

関数自体は小さいですが、データが非常にネストされているため、関数の呼び出しは不必要に複雑です。

>>>

>>> find_line_by_number(data["network"]["lines"], 3)

Pythonで辞書を照会するためのサードパーティツールがあります。 最も人気のあるものには、JMESPathglomasq、およびflupyがあります。

JMESPathは私たちの鉄道ネットワークを支援します。 JMESPathはJSON用に設計されたクエリ言語であり、Python辞書で動作するPython用のプラグインを使用できます。 JMESPathをインストールするには、次の手順を実行します。

$ pip install jmespath

次に、Python REPLを開いて、JMESPath APIを調べ、dataディクショナリにコピーします。 開始するには、jmespathをインポートし、クエリ文字列を最初の引数として、データを2番目の引数としてsearch()を呼び出します。 クエリ文字列"network.lines"は、data['network']['lines']を返すことを意味します。

>>>

>>> import jmespath

>>> jmespath.search("network.lines", data)
[{'name.en': 'Ginza', 'name.jp': '銀座線',
  'color': 'orange', 'number': 3, 'sign': 'G'},
 {'name.en': 'Marunouchi', 'name.jp': '丸ノ内線',
  'color': 'red', 'number': 4, 'sign': 'M'}]

リストを操作するときは、角括弧を使用して内部でクエリを提供できます。 「すべて」のクエリは単に*です。 その後、一致する各アイテム内に属性の名前を追加して返すことができます。 すべての行の行番号を取得したい場合、これを行うことができます:

>>>

>>> jmespath.search("network.lines[*].number", data)
[3, 4]

==<などのより複雑なクエリを提供できます。 構文はPython開発者にとって少し珍しいので、参照用にdocumentationを手元に置いておきます。

番号3の行を検索する場合は、次の1つのクエリで実行できます。

>>>

>>> jmespath.search("network.lines[?number==`3`]", data)
[{'name.en': 'Ginza', 'name.jp': '銀座線', 'color': 'orange', 'number': 3, 'sign': 'G'}]

その線の色を取得したい場合は、クエリの最後に属性を追加できます。

>>>

>>> jmespath.search("network.lines[?number==`3`].color", data)
['orange']

JMESPathを使用して、複雑な辞書を照会および検索するコードを削減および簡素化できます。

5. attrsdataclassesを使用してコードを削減する

リファクタリングのもう1つの目標は、同じ動作を実現しながらコードベース内のコードの量を単純に減らすことです。 これまでに示した手法は、コードをより小さくシンプルなモジュールにリファクタリングするのに大いに役立ちます。

他のいくつかの手法では、標準ライブラリといくつかのサードパーティライブラリの知識が必要です。

ボイラープレートとは

定型コードは、ほとんどまたはまったく変更せずに多くの場所で使用する必要があるコードです。

電車のネットワークを例にとると、PythonクラスとPython 3タイプヒントを使用してそれを型に変換する場合、次のようになります。

from typing import List

class Line(object):
    def __init__(self, name_en: str, name_jp: str, color: str, number: int, sign: str):
        self.name_en = name_en
        self.name_jp = name_jp
        self.color = color
        self.number = number
        self.sign = sign

    def __repr__(self):
        return f""

    def __str__(self):
        return f"The {self.name_en} line"

class Network(object):
    def __init__(self, lines: List[Line]):
        self._lines = lines

    @property
    def lines(self) -> List[Line]:
        return self._lines

ここで、.__eq__()などの他のマジックメソッドを追加することもできます。 このコードは定型です。 ここにはビジネスロジックやその他の機能はありません。データをある場所から別の場所にコピーするだけです。

dataclassesのケース

Python 3.7の標準ライブラリに導入され、PyPI上のPython 3.6のバックポートパッケージにより、dataclassesモジュールは、データを保存するだけのこれらのタイプのクラスの多くの定型を削除するのに役立ちます。

上記のLineクラスをデータクラスに変換するには、すべてのフィールドをクラス属性に変換し、タイプアノテーションがあることを確認します。

from dataclasses import dataclass

@dataclass
class Line(object):
    name_en: str
    name_jp: str
    color: str
    number: int
    sign: str

次に、以前と同じ引数、同じフィールドを使用して、Lineタイプのインスタンスを作成できます。さらに、.__str__().__repr__()、および.__eq__()が実装されます。

>>>

>>> line = Line('Marunouchi', "丸ノ内線", "red", 4, "M")

>>> line.color
red

>>> line2 = Line('Marunouchi', "丸ノ内線", "red", 4, "M")

>>> line == line2
True

データクラスは、標準ライブラリで既に利用可能な単一のインポートでコードを削減するための優れた方法です。 完全なウォークスルーについては、The Ultimate Guide to Data Classes in Python 3.7をチェックアウトできます。

いくつかのattrsのユースケース

attrsは、データクラスよりもはるかに長いサードパーティパッケージです。 attrsにはさらに多くの機能があり、Python2.7および3.4​​以降で使用できます。

Python 3.5以下を使用している場合、attrsdataclassesの優れた代替手段です。 また、さらに多くの機能を提供します。

attrsの同等のデータクラスの例は似ています。 型注釈を使用する代わりに、クラス属性にはattrib()の値が割り当てられます。 これは、デフォルト値や入力を検証するためのコールバックなど、追加の引数を取ることができます。

from attr import attrs, attrib

@attrs
class Line(object):
    name_en = attrib()
    name_jp = attrib()
    color = attrib()
    number = attrib()
    sign = attrib()

attrsは、ボイラープレートコードを削除し、データクラスの検証を入力するための便利なパッケージです。

結論

複雑なコードを特定して対処する方法を学習したので、アプリケーションを簡単に変更および管理できるようにするために実行できる手順を思い出してください。

  • wilyなどのツールを使用して、プロジェクトのベースラインを作成することから始めます。

  • いくつかのメトリックを見て、最も保守性の低いインデックスを持つモジュールから始めます。

  • テストで提供された安全性とPyCharmやropeなどのツールの知識を使用してそのモジュールをリファクタリングします。

これらの手順とこの記事のベストプラクティスに従えば、新しい機能の追加やパフォーマンスの改善など、アプリケーションに他のエキサイティングなことを行うことができます。