Python をマスターする: 明確で組織化された効率的なコードを書くための 7 つの戦略
Python ワークフローの最適化: 本番環境に対応したコードを作成するための実証済みのテクニック
自分の Python コードを経験豊富な開発者のコードと比較して、明らかな違いを感じたことはありますか?オンライン リソースから Python を学習しても、初心者レベルのコードと専門家レベルのコードの間にはギャップがあることがよくあります。それは、経験豊富な開発者がコミュニティによって確立されたベスト プラクティスを遵守しているためです。これらの実践はオンライン チュートリアルでは見落とされがちですが、大規模なアプリケーションでは重要です。この記事では、コードをより明確で組織化するために私が運用コードで使用している 7 つのヒントを共有します。
1. タイプヒントと注釈
Python は動的に型指定されるプログラミング言語であり、変数の型は実行時に推論されます。これにより柔軟性が得られますが、共同作業環境でのコードの読みやすさと理解力が大幅に低下します。
Python は、関数の引数の型と戻り値の型の注釈として機能する関数宣言での型ヒントのサポートを提供します。 Python は実行時にこれらの型を強制しませんが、他の人 (そしてあなた自身!) にとってコードを理解しやすくするため、それでも役に立ちます。
基本的な例から始めて、型ヒントを使用した簡単な関数宣言を次に示します。
def sum(a: int, b: int) -> int:
return a + b
ここでは、関数は一目瞭然ですが、関数のパラメーターと戻り値が int 型として示されていることがわかります。関数本体は、このように 1 行である場合もあれば、数百行である場合もあります。ただし、関数の宣言を見るだけで、前提条件と戻り値の型を理解できます。
これらの注釈は単にわかりやすくするためのものであることを理解しておくことが重要です。実行中に型は強制されませんので、整数ではなく文字列など、異なる型の値を渡しても、関数は実行されます。ただし、予期した型を指定しないと、実行時に予期しない動作やエラーが発生する可能性があるので注意してください。たとえば、この例では、関数 sum() は引数として 2 つの整数を期待しています。ただし、文字列と整数を追加しようとすると、Python はランタイム エラーをスローします。なぜ?それは、文字列と整数を加算する方法がわからないからです。それはリンゴとオレンジを加えようとするようなもので、まったく意味がありません。ただし、両方の引数が文字列の場合は、問題なく連結されます。
テストケースを含む明確化されたバージョンは次のとおりです。
print(sum(2,5)) # 7
# print(sum('hello', 2)) # TypeError: can only concatenate str (not "int") to str
# print(sum(3,'world')) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
print(sum('hello', 'world')) # helloworld
高度なタイプヒンティングのためのタイピングライブラリ
高度な注釈を追加するために、Python には入力標準ライブラリが含まれています。より興味深いアプローチでの使用法を見てみましょう。
from typing import Union, Tuple, List
import numpy as np
def sum(variable: Union[np.ndarray, List]) -> float:
total = 0
# function body to calculate the sum of values in iterable
return total
ここでは、同じ合計関数を変更して、numpy 配列または反復可能なリストを受け入れるようにしました。それらの合計を計算し、浮動小数点値として返します。タイピング ライブラリの Union アノテーションを利用して、変数パラメータが受け入れることができるタイプを指定します。
関数宣言をさらに変更して、リストのメンバーも float 型である必要があることを示します。
def sum(variable: Union[np.ndarray, List[float]]) -> float:
total = 0
# function body to calculate the sum of values in iterable
return total
これらは、Python の型ヒントを理解するのに役立つ初心者向けの例にすぎません。プロジェクトが成長し、コードベースがよりモジュール化されると、型アノテーションによって可読性と保守性が大幅に向上します。型指定ライブラリは、オプション、さまざまなイテラブル、ジェネリック、カスタム定義型のサポートなどの豊富な機能セットを提供し、開発者が複雑なデータ構造と関係を正確かつ明確に表現できるようにします。
2. 防御関数と入力検証の作成
タイプヒントは便利に見えますが、注釈が強制されていないため、依然としてエラーが発生しやすくなります。これらは開発者向けの単なる追加ドキュメントですが、異なる引数の型が使用された場合でも関数は実行されます。したがって、防御的な方法で関数とコードの前提条件を強制する必要があります。したがって、これらのタイプを手動でチェックし、条件に違反する場合は適切なエラーを生成します。
以下の関数は、入力パラメータを使用して利息がどのように計算されるかを示しています。
def calculate_interest(principal, rate, years):
return principal * rate * years
単純な操作ですが、この機能はあらゆる解決策に適用できるのでしょうか?いいえ、無効な値が入力として渡されるようなエッジケースではありません。関数が正しく実行されるためには、入力値が有効な範囲内にバインドされていることを確認する必要があります。基本的に、関数を正しく実装するには、いくつかの前提条件が満たされる必要があります。
これは次のように行います。
from typing import Union
def calculate_interest(
principal: Union[int, float],
rate: float,
years: int
) -> Union[int, float]:
if not isinstance(principal, (int, float)):
raise TypeError("Principal must be an integer or float")
if not isinstance(rate, float):
raise TypeError("Rate must be a float")
if not isinstance(years, int):
raise TypeError("Years must be an integer")
if principal <= 0:
raise ValueError("Principal must be positive")
if rate <= 0:
raise ValueError("Rate must be positive")
if years <= 0:
raise ValueError("Years must be positive")
interest = principal * rate * years
return interest
入力検証には条件ステートメントを使用することに注意してください。 Python には、この目的で使用されるアサーション ステートメントもあります。ただし、 入力検証のアサーションはベスト プラクティスではありません。これらは簡単に無効にでき、本番環境で予期しない動作を引き起こす可能性があります。明示的な Python 条件式の使用は、入力検証と事前条件、事後条件、およびコードの不変条件の適用に推奨されます。
3. ジェネレーターと Yield ステートメントを使用した遅延ロード
ドキュメントの大規模なデータセットが提供されるシナリオを考えてみましょう。ドキュメントを処理し、各ドキュメントに対して特定の操作を実行する必要があります。ただし、サイズが大きいため、すべてのドキュメントをメモリにロードして同時に前処理することはできません。
考えられる解決策は、必要な場合にのみドキュメントをメモリにロードし、一度に 1 つのドキュメントのみを処理することです (遅延ロードとも呼ばれます)。どのような文書が必要になるかはわかっていても、必要になるまでリソースをロードしません。コード内でアクティブに使用されていないドキュメントの大部分をメモリ内に保持する必要はありません。これはまさに、ジェネレーターと yield ステートメントが問題に対処する方法です。
ジェネレーターを使用すると、Python コード実行のメモリ効率を向上させる遅延読み込みが可能になります。値は必要に応じてオンザフライで生成されるため、メモリ使用量が削減され、実行速度が向上します。
import os
def load_documents(directory):
for document_path in os.listdir(directory):
with open(document_path) as _file:
yield _file
def preprocess_document(document):
filtered_document = None
# preprocessing code for the document stored in filtered_document
return filtered_document
directory = "docs/"
for doc in load_documents(directory):
preprocess_document(doc)
上記の関数では、load_documents 関数で yield キーワードが使用されています。このメソッドは、タイプ <クラス ジェネレーター> のオブジェクトを返します。このオブジェクトを反復処理すると、最後の yield ステートメントから実行が継続されます。したがって、単一のドキュメントがロードされて処理されるため、Python コードの効率が向上します。
4. コンテキストマネージャーを使用したメモリリークの防止
どの言語でも、リソースを効率的に使用することが最も重要です。上で説明したように、ジェネレーターを使用して、必要な場合にのみメモリに何かをロードします。ただし、プログラムでリソースが不要になったときにリソースを閉じることも同様に重要です。メモリ リークを防止し、適切なリソースの破棄を実行してメモリを節約する必要があります。
コンテキスト マネージャーは、 リソースのセットアップと破棄の一般的な使用例を簡素化します。例外や障害が発生した場合でも、リソースが不要になったら解放することが重要です。コンテキスト マネージャーは、コードを簡潔で読みやすい状態に保ちながら、自動クリーンアップを使用してメモリ リークのリスクを軽減します。
リソースには、データベース接続、ロック、スレッド、ネットワーク接続、メモリ アクセス、ファイル ハンドルなど、複数のバリエーションを持つことができます。最も単純なケースであるファイル ハンドルに焦点を当てましょう。ここでの課題は、開かれた各ファイルが必ず 1 回だけ閉じられるようにすることです。ファイルを閉じないとメモリ リークが発生する可能性があり、ファイル ハンドルを 2 回閉じようとすると実行時エラーが発生します。これに対処するには、 ファイル ハンドルをtry-excel-finally ブロック内にラップする必要があります。これにより、実行中にエラーが発生したかどうかに関係なく、ファイルが適切に閉じられるようになります。実装は次のようになります。
file_path = "example.txt"
file = None
try:
file = open(file_path, 'r')
contents = file.read()
print("File contents:", contents)
finally:
if file is not None:
file.close()
ただし、Python は、リソース管理を自動的に処理するコンテキスト マネージャーを使用した、より洗練されたソリューションを提供します。ファイル コンテキスト マネージャーを使用して上記のコードを簡素化する方法は次のとおりです。
file_path = "example.txt"
with open(file_path, 'r') as file:
contents = file.read()
print("File contents:", contents)
このバージョンでは、ファイルを明示的に閉じる必要はありません。コンテキスト マネージャーがこれを処理し、潜在的なメモリ リークを防ぎます。
Python にはファイル処理用の組み込みコンテキスト マネージャーが用意されていますが、カスタム クラスや関数用に独自のコンテキスト マネージャーを作成することもできます。クラスベースの実装では、__enter__ および __exit__ ダンダー メソッドを定義します。基本的な例を次に示します。
class CustomContextManger:
def __enter__(self):
# Code to create instance of resource
return self
def __exit__(self, exc_type, exc_value, traceback):
# Teardown code to close resource
return None
これで、「with」 ブロック内でこのカスタム コンテキスト マネージャーを使用できるようになります。
with CustomContextManger() as _cm:
print("Custom Context Manager Resource can be accessed here")
このアプローチにより、コンテキスト マネージャーのクリーンで簡潔な構文が維持され、必要に応じてリソースを処理できるようになります。
5. 装飾者との関心の分離
同じロジックを持つ複数の関数が明示的に実装されているのをよく見かけます。これは一般的なコードの匂いであり、過剰なコードの重複によりコードの保守が困難になり、拡張性がなくなります。デコレータは、同様の機能を 1 か所にカプセル化するために使用されます。同様の機能を他の複数の関数で使用する場合、デコレータ内に共通の機能を実装することでコードの重複を減らすことができます。これは、アスペクト指向プログラミング (AOP) と単一責任の原則に従います。
デコレータは、Django、Flask、FastAPI などの Python Web フレームワークで頻繁に使用されます。デコレータを Python のロギング用ミドルウェアとして使用することで、その有効性を説明します。運用環境では、リクエストを処理するのにどれくらいの時間がかかるかを知る必要があります。これは一般的な使用例であり、すべてのエンドポイント間で共有されます。そこで、リクエストの処理にかかった時間を記録する単純なデコレータベースのミドルウェアを実装してみましょう。
以下のダミー関数は、ユーザー リクエストに対応するために使用されます。
def service_request():
# Function body representing complex computation
return True
ここで、この関数の実行にかかる時間を記録する必要があります。 1 つの方法は、次のようにこの関数内にログを追加することです。
import time
def service_request():
start_time = time.time()
# Function body representing complex computation
print(f"Time Taken: {time.time() - start_time}s")
return True
このアプローチは機能しますが、コードの重複が発生します。さらにルートを追加する場合は、各関数でログ コードを繰り返す必要があります。この共有ログ機能を各実装に追加する必要があるため、コードの重複が増加します。デコレータを使用してこれを削除します。
ロギングミドルウェアは以下のように実装されます。
def request_logger(func):
def wrapper(*args, **kwargs):
start_time = time.time()
res = func()
print(f"Time Taken: {time.time() - start_time}s")
return res
return wrapper
この実装では、外部関数はデコレーターであり、関数を入力として受け入れます。内部関数はロギング機能を実装し、入力関数はラッパー内で呼び出されます。
ここで、元の service_request 関数を request_logger デコレータで単純に装飾します。
@request_logger
def service_request():
# Function body representing complex computation
return True
@ 記号を使用すると、service_request 関数が request_logger デコレータに渡されます。かかった時間をログに記録し、コードを変更せずに元の関数を呼び出します。この関心の分離により、次のような同様の方法で他のサービス メソッドにロギングを簡単に追加できるようになります。
@request_logger
def service_request():
# Function body representing complex computation
return True
@request_logger
def service_another_request():
# Function body
return True
6. Match Case ステートメント
Match ステートメントは Python3.10 で導入されたため、Python 構文に新しく追加されたものです。これにより、よりシンプルで読みやすいパターン マッチングが可能になり、典型的な if-elif-else ステートメントにおける過剰なボイラープレートや分岐が防止されます。
パターン マッチングの場合、match case ステートメントは条件ステートメントのようにブール値を返す必要がないため、より自然な記述方法です。 Python ドキュメントの次の例は、match case ステートメントが条件ステートメントよりも柔軟性をどのように提供するかを示しています。
def make_point_3d(pt):
match pt:
case (x, y):
return Point3d(x, y, 0)
case (x, y, z):
return Point3d(x, y, z)
case Point2d(x, y):
return Point3d(x, y, 0)
case Point3d(_, _, _):
return pt
case _:
raise TypeError("not a point we support")
ドキュメントによると、パターン マッチングを使用しない場合、この関数の実装には複数の isinstance() チェック、1 つまたは 2 つの len() 呼び出し、およびより複雑な制御フローが必要になります。内部的には、一致例と従来の Python バージョンは同様のコードに変換されます。ただし、パターン マッチングに慣れていると、より明確で自然な構文が得られる大文字と小文字の一致のアプローチが好まれる可能性があります。
全体として、match case ステートメントは、パターン マッチングの改良された代替手段を提供しており、新しいコードベースではさらに普及する可能性があります。
7. 外部設定ファイル
運用環境では、コードの大部分は API キー、パスワード、さまざまな設定などの外部構成パラメーターに依存します。これらの値をコードに直接ハードコーディングすることは、スケーラビリティとセキュリティの理由から、不適切な方法であると考えられます。代わりに、構成をコード自体から分離しておくことが重要です。通常、JSON や YAML などの構成ファイルを使用してこれらのパラメーターを保存し、コード内に直接埋め込まなくてもコードに簡単にアクセスできるようにして、これを実現します。
日常的な使用例は、複数の接続パラメータを持つデータベース接続です。これらのパラメータは別の YAML ファイルに保存できます。
# config.yaml
database:
host: localhost
port: 5432
username: myuser
password: mypassword
dbname: mydatabase
この構成を処理するには、DatabaseConfig というクラスを定義します。
class DatabaseConfig:
def __init__(self, host, port, username, password, dbname):
self.host = host
self.port = port
self.username = username
self.password = password
self.dbname = dbname
@classmethod
def from_dict(cls, config_dict):
return cls(**config_dict)
ここで、 from_dict クラス メソッドは、DatabaseConfig クラスのビルダー メソッドとして機能し、ディクショナリからデータベース構成インスタンスを作成できるようにします。
メイン コードでは、パラメーター ハイドレーションとビルダー メソッドを使用してデータベース構成を作成できます。外部 YAML ファイルを読み取ることで、データベース ディクショナリを抽出し、それを使用して構成クラスをインスタンス化します。
import yaml
def load_config(filename):
with open(filename, "r") as file:
return yaml.safe_load(file)
config = load_config("config.yaml")
db_config = DatabaseConfig.from_dict(config["database"])
このアプローチにより、データベース構成パラメーターをコードに直接ハードコーディングする必要がなくなります。また、コードを実行するたびに複数のパラメータを渡す必要がなくなるため、引数パーサーを使用する場合よりも改善されます。さらに、引数パーサーを通じて構成ファイルのパスにアクセスすることで、コードの柔軟性を維持し、ハードコーディングされたパスに依存しないことを保証できます。この方法により、構成パラメータの管理が容易になり、コードベースを変更することなくいつでも変更できます。
エンディングノート
この記事では、本番環境に対応したコードに関して業界で使用されているベスト プラクティスのいくつかについて説明しました。これらは、実際の状況で直面する可能性のある複数の問題を軽減する業界の一般的な慣行です。
それにもかかわらず、このようなベスト プラクティスにもかかわらず、ドキュメント、ドキュメント文字列、およびテスト駆動開発が最も重要なプラクティスであることは注目に値します。コードベースに取り組む人々は時間の経過とともに変化するため、関数が何を行うべきかを考え、将来に向けてすべての設計上の決定と実装を文書化することが重要です。あなたが誓う洞察や実践方法がある場合は、以下のコメントセクションでお気軽にお知らせください。