はじめに
この実験では、Python のスコープルールについて学び、スコープを扱う高度なテクニックを探索します。Python のスコープを理解することは、クリーンで保守可能なコードを書くために重要であり、予期しない動作を回避するのに役立ちます。
この実験の目的は、Python のスコープルールを詳細に理解すること、クラスの初期化における実用的なスコープテクニックを学ぶこと、柔軟なオブジェクト初期化システムを実装すること、およびフレーム検査テクニックを適用してコードを簡素化することです。structure.py と stock.py のファイルを使用します。
クラス初期化の問題の理解
プログラミングの世界では、クラスはカスタムデータ型を作成するための基本的な概念です。以前の演習では、Structure クラスを作成したことがあるかもしれません。このクラスは、データ構造を簡単に定義するための便利なツールとして機能します。データ構造とは、データを効率的にアクセスおよび使用できるように整理して格納する方法です。Structure クラスは基底クラスとして、事前定義されたフィールド名のリストに基づいて属性を初期化します。属性はオブジェクトに属する変数であり、フィールド名はこれらの属性に付ける名前です。
Structure クラスの現在の実装を詳しく見てみましょう。これを行うには、コードエディタで structure.py ファイルを開く必要があります。このファイルには Structure クラスのコードが含まれています。プロジェクトディレクトリに移動してファイルを開くコマンドは次のとおりです。
cd ~/project
code structure.py
Structure クラスは、単純なデータ構造を定義するための基本的なフレームワークを提供します。Stock クラスのようなサブクラスを作成するときに、そのサブクラスに必要な特定のフィールドを定義できます。サブクラスは、基底クラス(この場合は Structure クラス)のプロパティとメソッドを継承します。たとえば、Stock クラスでは、name、shares、price というフィールドを定義します。
class Stock(Structure):
_fields = ('name', 'shares', 'price')
では、stock.py ファイルを開いて、Stock クラスが全体のコードの中でどのように実装されているかを見てみましょう。このファイルには、Stock クラスを使用してそれとやり取りするコードが含まれている可能性があります。次のコマンドを使用してファイルを開きます。
code stock.py
Structure クラスとそのサブクラスを使用するこのアプローチは機能しますが、いくつかの制限があります。これらの問題を特定するために、Python インタープリターを実行して、Stock クラスの動作を調べます。次のコマンドは、Stock クラスをインポートし、そのヘルプ情報を表示します。
python3 -c "from stock import Stock; help(Stock)"
このコマンドを実行すると、ヘルプ出力に表示されるシグネチャはあまり役に立たないことに気づくでしょう。name、shares、price のような実際のパラメータ名を表示する代わりに、*args だけが表示されます。このようにパラメータ名が明確でないと、ユーザーが Stock クラスのインスタンスを正しく作成する方法を理解するのが難しくなります。
キーワード引数を使用して Stock インスタンスを作成してみましょう。キーワード引数を使用すると、パラメータの名前で値を指定できるため、コードが読みやすくなります。次のコマンドを実行します。
python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"
次のようなエラーメッセージが表示されるはずです。
TypeError: __init__() got an unexpected keyword argument 'name'
このエラーが発生するのは、現在の __init__ メソッド(Stock クラスのオブジェクトを初期化する責任がある)がキーワード引数を処理しないためです。このメソッドは位置引数のみを受け付けます。つまり、パラメータ名を使用せずに特定の順序で値を指定する必要があります。これは、この実験で修正したい制限事項です。
この実験では、Structure クラスをより柔軟で使いやすくするためのさまざまなアプローチを探索します。これにより、Stock クラスや Structure の他のサブクラスの使い勝手を向上させることができます。
locals() を使用して関数の引数にアクセスする
Python では、変数のスコープを理解することが重要です。変数のスコープは、コード内でその変数にアクセスできる場所を決定します。Python には locals() という組み込み関数があり、初心者にとってスコープを理解するのに非常に便利です。locals() 関数は、現在のスコープ内のすべてのローカル変数を含む辞書を返します。これは、関数の引数を調べたいときに非常に役立ちます。なぜなら、コードの特定の部分で利用可能な変数を明確に把握できるからです。
これがどのように機能するかを確認するために、Python インタープリターで簡単な実験を行いましょう。まず、プロジェクトディレクトリに移動して Python インタープリターを起動する必要があります。ターミナルで次のコマンドを実行することでこれを行うことができます。
cd ~/project
python3
Python の対話型シェルに入ったら、Stock クラスを定義します。Python のクラスは、オブジェクトを作成するためのブループリントのようなものです。このクラスでは、特殊な __init__ メソッドを使用します。__init__ メソッドは Python のコンストラクタです。つまり、クラスのオブジェクトが作成されると自動的に呼び出されます。この __init__ メソッドの中で、locals() 関数を使用してすべてのローカル変数を表示します。
class Stock:
def __init__(self, name, shares, price):
print(locals())
では、この Stock クラスのインスタンスを作成しましょう。インスタンスとは、クラスのブループリントから作成された実際のオブジェクトです。name、shares、price パラメータにいくつかの値を渡します。
s = Stock('GOOG', 100, 490.1)
このコードを実行すると、次のような出力が表示されるはずです。
{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}
この出力は、locals() が __init__ メソッド内のすべてのローカル変数を含む辞書を返すことを示しています。self 参照は、Python クラス内の特殊な変数で、クラス自体のインスタンスを指します。他の変数は、Stock オブジェクトを作成する際に渡したパラメータの値です。
この locals() の機能を使用して、オブジェクトの属性を自動的に初期化することができます。属性は、オブジェクトに関連付けられた変数です。ヘルパー関数を定義し、Stock クラスを修正しましょう。
def _init(locs):
self = locs.pop('self')
for name, val in locs.items():
setattr(self, name, val)
class Stock:
def __init__(self, name, shares, price):
_init(locals())
_init 関数は、locals() から取得したローカル変数の辞書を受け取ります。まず、pop メソッドを使用して辞書から self 参照を削除します。次に、辞書内の残りのキーと値のペアを反復処理し、setattr 関数を使用して各変数をオブジェクトの属性として設定します。
では、この実装を位置引数とキーワード引数の両方でテストしましょう。位置引数は、関数のシグネチャで定義された順序で渡され、キーワード引数はパラメータ名を指定して渡されます。
## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)
## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)
両方のアプローチが機能するはずです!_init 関数により、位置引数とキーワード引数の両方をシームレスに処理することができます。また、関数のシグネチャ内のパラメータ名を保持するため、help() の出力がより有用になります。Python の help() 関数は、関数、クラス、モジュールに関する情報を提供し、パラメータ名がそのままになっていると、この情報がより意味のあるものになります。
実験が終了したら、次のコマンドを実行して Python インタープリターを終了できます。
exit()
スタックフレームの調査を探索する
これまで使用してきた _init(locals()) のアプローチは機能的ですが、欠点があります。__init__ メソッドを定義するたびに、明示的に locals() を呼び出す必要があります。これは、特に複数のクラスを扱う場合に少し面倒になることがあります。幸いなことに、スタックフレームの調査を使用することで、コードをよりクリーンで効率的にすることができます。このテクニックを使用すると、明示的に locals() を呼び出すことなく、呼び出し元のローカル変数に自動的にアクセスすることができます。
Python インタープリターでこのテクニックを探索し始めましょう。まず、ターミナルを開き、プロジェクトディレクトリに移動します。次に、Python インタープリターを起動します。これは、次のコマンドを実行することで行うことができます。
cd ~/project
python3
Python インタープリターに入ったら、sys モジュールをインポートする必要があります。sys モジュールは、Python インタープリターによって使用または維持されるいくつかの変数にアクセスするための機能を提供します。これを使用して、スタックフレームの情報にアクセスします。
import sys
次に、_init() 関数の改良版を定義します。この新しいバージョンでは、呼び出し元のフレームに直接アクセスするため、明示的に locals() を渡す必要がなくなります。
def _init():
## Get the caller's frame (1 level up in the call stack)
frame = sys._getframe(1)
## Get the local variables from that frame
locs = frame.f_locals
## Extract self and set other variables as attributes
self = locs.pop('self')
for name, val in locs.items():
setattr(self, name, val)
このコードでは、sys._getframe(1) が呼び出し元の関数のフレームオブジェクトを取得します。引数の 1 は、呼び出しスタックの 1 レベル上を指します。フレームオブジェクトを取得したら、frame.f_locals を使用してそのローカル変数にアクセスできます。これにより、呼び出し元のスコープ内のすべてのローカル変数の辞書が得られます。その後、self 変数を抽出し、残りの変数を self オブジェクトの属性として設定します。
では、この新しい _init() 関数を Stock クラスの新しいバージョンでテストしましょう。
class Stock:
def __init__(self, name, shares, price):
_init() ## No need to pass locals() anymore!
## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)
## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)
ご覧のとおり、__init__ メソッドはもはや明示的に locals() を渡す必要がありません。これにより、呼び出し元の観点からコードがクリーンで読みやすくなります。
スタックフレームの調査の仕組み
sys._getframe(1) を呼び出すと、Python は呼び出し元の実行フレームを表すフレームオブジェクトを返します。引数の 1 は、「現在のフレームから 1 レベル上」(呼び出し元の関数)を意味します。
フレームオブジェクトには、実行コンテキストに関する重要な情報が含まれています。これには、現在実行中の関数、その関数内のローカル変数、および現在実行中の行番号が含まれます。
frame.f_locals にアクセスすることで、呼び出し元のスコープ内のすべてのローカル変数の辞書が得られます。これは、そのスコープから直接 locals() を呼び出した場合と同様の結果になります。
このテクニックは非常に強力ですが、注意して使用する必要があります。これは一般的に高度な Python の機能と見なされ、Python の通常のスコープ境界を越えるため、少し「魔法のよう」に見えることがあります。
スタックフレームの調査の実験が終了したら、次のコマンドを実行して Python インタープリターを終了できます。
exit()
構造体における高度な初期化の実装
私たちは、関数の引数にアクセスするための 2 つの強力なテクニックを学びました。ここでは、これらのテクニックを使って Structure クラスを更新します。まず、なぜこれを行うのかを理解しましょう。これらのテクニックにより、特にさまざまなタイプの引数を扱う際に、クラスがより柔軟で使いやすくなります。
コードエディタで structure.py ファイルを開きます。ターミナルで次のコマンドを実行することでこれを行えます。cd コマンドはディレクトリをプロジェクトフォルダに変更し、code コマンドはコードエディタで structure.py ファイルを開きます。
cd ~/project
code structure.py
ファイルの内容を次のコードに置き換えます。このコードは、いくつかのメソッドを持つ Structure クラスを定義しています。それぞれの部分を見て、何を行っているかを理解しましょう。
import sys
class Structure:
_fields = ()
@staticmethod
def _init():
## Get the caller's frame (the __init__ method that called this)
frame = sys._getframe(1)
## Get the local variables from that frame
locs = frame.f_locals
## Extract self and set other variables as attributes
self = locs.pop('self')
for name, val in locs.items():
setattr(self, name, val)
def __repr__(self):
values = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
return f'{type(self).__name__}({values})'
def __setattr__(self, name, value):
if name.startswith('_') or name in self._fields:
super().__setattr__(name, value)
else:
raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')
このコードで行ったことは次の通りです。
- 古い
__init__()メソッドを削除しました。サブクラスが独自の__init__メソッドを定義するため、古いメソッドはもう必要ありません。 - 新しい
_init()静的メソッドを追加しました。このメソッドはフレーム調査を使用して、すべてのパラメータを自動的に捕捉して属性として設定します。フレーム調査により、呼び出し元のメソッドのローカル変数にアクセスできます。 __repr__()メソッドを残しました。このメソッドは、オブジェクトの適切な文字列表現を提供し、デバッグや表示に役立ちます。__setattr__()メソッドを追加しました。このメソッドは属性の検証を行い、オブジェクトに設定できるのは有効な属性のみであることを保証します。
次に、Stock クラスを更新しましょう。次のコマンドを使って stock.py ファイルを開きます。
code stock.py
その内容を次のコードに置き換えます。
from structure import Structure
class Stock(Structure):
_fields = ('name', 'shares', 'price')
def __init__(self, name, shares, price):
self._init() ## This magically captures and sets all parameters!
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
ここでの重要な変更点は、__init__ メソッドが各属性を手動で設定する代わりに self._init() を呼び出すようになったことです。_init() メソッドはフレーム調査を使用して、すべてのパラメータを自動的に捕捉して属性として設定します。これにより、コードがより簡潔で保守しやすくなります。
ユニットテストを実行して、私たちの実装をテストしましょう。ユニットテストは、コードが期待どおりに動作することを確認するのに役立ちます。ターミナルで次のコマンドを実行します。
cd ~/project
python3 teststock.py
以前は失敗していたキーワード引数のテストを含め、すべてのテストが合格するはずです。これは、私たちの実装が正しく動作していることを意味します。
Stock クラスのヘルプドキュメントも確認しましょう。ヘルプドキュメントは、クラスとそのメソッドに関する情報を提供します。ターミナルで次のコマンドを実行します。
python3 -c "from stock import Stock; help(Stock)"
これで、__init__ メソッドの適切なシグネチャが表示され、すべてのパラメータ名が示されるはずです。これにより、他の開発者がクラスの使い方を理解しやすくなります。
最後に、キーワード引数が期待どおりに動作することを対話的にテストしましょう。ターミナルで次のコマンドを実行します。
python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"
指定された属性で Stock オブジェクトが適切に作成されているのが見えるはずです。これにより、クラスの初期化システムがキーワード引数をサポートしていることが確認されます。
この実装により、次のような、はるかに柔軟で使いやすいクラスの初期化システムを実現しました。
- ドキュメントに適切な関数シグネチャを保持し、開発者がクラスの使い方を理解しやすくします。
- 位置引数とキーワード引数の両方をサポートし、オブジェクトを作成する際により柔軟性を提供します。
- サブクラスで必要な定型コードを最小限に抑え、書くコードの量を減らします。
まとめ
この実験では、Python のスコープルールと、スコープを扱うためのいくつかの強力なテクニックについて学びました。まず、locals() 関数を使って関数内のすべてのローカル変数にアクセスする方法を調べました。次に、sys._getframe() を使ってスタックフレームを調査し、呼び出し元のローカル変数にアクセスする方法を学びました。
また、これらのテクニックを適用して、柔軟なクラスの初期化システムを作成しました。このシステムは、関数のパラメータを自動的に捕捉してオブジェクトの属性として設定し、ドキュメントに適切な関数シグネチャを維持し、位置引数とキーワード引数の両方をサポートします。これらのテクニックは、Python の柔軟性とイントロスペクション機能を示しています。フレーム調査は高度なテクニックであり、注意して使用する必要がありますが、適切に使用すると定型コードを効果的に削減することができます。スコープルールとこれらの高度なテクニックを理解することで、よりクリーンで保守しやすい Python コードを書くためのより多くのツールを身につけることができます。