はじめに
この実験では、Python のクロージャ (Closure) について詳しく学びます。クロージャは強力なプログラミング概念で、外側の関数の実行が完了した後でも、関数がその外側のスコープの変数を記憶し、アクセスできるようにします。
また、クロージャをデータ構造として理解し、コード生成器として探索し、クロージャを使って型チェック (type - checking) を実装する方法を学びます。この実験を通じて、Python のクロージャのより特殊で強力な側面を明らかにする手助けとなります。
データ構造としてのクロージャ
Python では、クロージャ (Closure) はデータをカプセル化する強力な方法を提供します。カプセル化とは、データを非公開に保ち、それへのアクセスを制御することを意味します。クロージャを使用すると、クラスやグローバル変数を使用せずに、非公開データを管理および変更する関数を作成できます。グローバル変数はコード内のどこからでもアクセスおよび変更できるため、予期しない動作を引き起こす可能性があります。一方、クラスはより複雑な構造を必要とします。クロージャは、データのカプセル化においてよりシンプルな代替手段を提供します。
この概念を実証するために、counter.py という名前のファイルを作成しましょう。
WebIDE を開き、
/home/labex/projectディレクトリにcounter.pyという名前の新しいファイルを作成します。ここに、クロージャベースのカウンタを定義するコードを記述します。ファイルに以下のコードを追加します。
def counter(value):
"""
Create a counter with increment and decrement functions.
Args:
value: Initial value of the counter
Returns:
Two functions: one to increment the counter, one to decrement it
"""
def incr():
nonlocal value
value += 1
return value
def decr():
nonlocal value
value -= 1
return value
return incr, decr
このコードでは、counter() という関数を定義しています。この関数は初期の value を引数として受け取ります。counter() 関数の内部では、2 つの内部関数 incr() と decr() を定義しています。これらの内部関数は同じ value 変数にアクセスできます。nonlocal キーワードは、Python に対して外側のスコープ(counter() 関数)の value 変数を変更したいことを伝えるために使用されます。nonlocal キーワードがない場合、Python は内部関数内に新しいローカル変数を作成し、外側のスコープの value を変更するのではなくなります。
- これを実際に動作させるためのテストファイルを作成しましょう。以下の内容で
test_counter.pyという名前の新しいファイルを作成します。
from counter import counter
## Create a counter starting at 0
up, down = counter(0)
## Increment the counter several times
print("Incrementing the counter:")
print(up()) ## Should print 1
print(up()) ## Should print 2
print(up()) ## Should print 3
## Decrement the counter
print("\nDecrementing the counter:")
print(down()) ## Should print 2
print(down()) ## Should print 1
このテストファイルでは、まず counter.py ファイルから counter() 関数をインポートします。次に、counter(0) を呼び出して 0 から始まるカウンタを作成し、返された関数を up と down にアンパックします。その後、up() 関数を何度か呼び出してカウンタを増やし、結果を出力します。その後、down() 関数を呼び出してカウンタを減らし、結果を出力します。
- ターミナルで以下のコマンドを実行してテストファイルを実行します。
python3 test_counter.py
以下の出力が表示されるはずです。
Incrementing the counter:
1
2
3
Decrementing the counter:
2
1
ここではクラス定義が関与していないことに注意してください。up() と down() 関数は、グローバル変数でもインスタンス属性でもない共有値を操作しています。この値はクロージャ内に格納されており、counter() が返す関数のみがアクセスできます。
これは、クロージャがデータ構造としてどのように使用できるかの例です。閉じ込められた変数 value は関数呼び出し間で維持され、それにアクセスする関数に対して非公開です。これは、コードの他の部分がこの value 変数に直接アクセスまたは変更できないことを意味し、一定のレベルのデータ保護を提供します。
コード生成器としてのクロージャ
このステップでは、クロージャ (Closure) を使ってコードを動的に生成する方法を学びます。具体的には、クロージャを使ってクラス属性の型チェック (type-checking) システムを構築します。
まず、クロージャが何であるかを理解しましょう。クロージャは、メモリ上に存在しなくても、外側のスコープの値を記憶する関数オブジェクトです。Python では、ネストされた関数が外側の関数の値を参照するときにクロージャが作成されます。
では、型チェックシステムの実装を始めましょう。
/home/labex/projectディレクトリにtypedproperty.pyという名前の新しいファイルを作成し、以下のコードを記述します。
## typedproperty.py
def typedproperty(name, expected_type):
"""
Create a property with type checking.
Args:
name: The name of the property
expected_type: The expected type of the property value
Returns:
A property object that performs type checking
"""
private_name = '_' + name
@property
def value(self):
return getattr(self, private_name)
@value.setter
def value(self, val):
if not isinstance(val, expected_type):
raise TypeError(f'Expected {expected_type}')
setattr(self, private_name, val)
return value
このコードでは、typedproperty 関数がクロージャです。この関数は name と expected_type という 2 つの引数を受け取ります。@property デコレータは、プロパティのゲッターメソッドを作成するために使用され、これにより非公開属性の値が取得されます。@value.setter デコレータは、設定される値が期待される型であるかをチェックするセッターメソッドを作成します。もしそうでなければ、TypeError が発生します。
- これらの型付きプロパティを使用するクラスを作成しましょう。以下のコードで
stock.pyという名前のファイルを作成します。
from typedproperty import typedproperty
class Stock:
"""A class representing a stock with type-checked attributes."""
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Stock クラスでは、typedproperty 関数を使用して name、shares、price の型チェック付き属性を作成しています。Stock クラスのインスタンスを作成すると、型チェックが自動的に適用されます。
- これを実際に動作させるためのテストファイルを作成しましょう。以下のコードで
test_stock.pyという名前のファイルを作成します。
from stock import Stock
## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")
## Try to set an attribute with the wrong type
try:
s.shares = "hundred" ## This should raise a TypeError
print("Type check failed")
except TypeError as e:
print(f"Type check succeeded: {e}")
このテストファイルでは、まず正しい型で Stock オブジェクトを作成します。次に、shares 属性を文字列に設定しようとしますが、期待される型は整数なので TypeError が発生するはずです。
- テストファイルを実行します。
python3 test_stock.py
以下のような出力が表示されるはずです。
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>
この出力は、型チェックが正しく動作していることを示しています。
- 次に、
typedproperty.pyを拡張して、一般的な型のための便利関数を追加しましょう。ファイルの末尾に以下のコードを追加します。
def String(name):
"""Create a string property with type checking."""
return typedproperty(name, str)
def Integer(name):
"""Create an integer property with type checking."""
return typedproperty(name, int)
def Float(name):
"""Create a float property with type checking."""
return typedproperty(name, float)
これらの関数は typedproperty 関数をラップしたもので、一般的な型のプロパティを簡単に作成できるようにしています。
- これらの便利関数を使用する
stock_enhanced.pyという名前の新しいファイルを作成します。
from typedproperty import String, Integer, Float
class Stock:
"""A class representing a stock with type-checked attributes."""
name = String('name')
shares = Integer('shares')
price = Float('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
この Stock クラスは、便利関数を使用して型チェック付き属性を作成しており、コードがより読みやすくなっています。
- 拡張版をテストするための
test_stock_enhanced.pyという名前のテストファイルを作成します。
from stock_enhanced import Stock
## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")
## Try to set an attribute with the wrong type
try:
s.price = "490.1" ## This should raise a TypeError
print("Type check failed")
except TypeError as e:
print(f"Type check succeeded: {e}")
このテストファイルは前のものと似ていますが、拡張版の Stock クラスをテストしています。
- テストを実行します。
python3 test_stock_enhanced.py
以下のような出力が表示されるはずです。
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>
このステップでは、クロージャを使ってコードを生成する方法を実証しました。typedproperty 関数は型チェックを行うプロパティオブジェクトを作成し、String、Integer、Float 関数は一般的な型の特殊なプロパティを作成します。
ディスクリプタを使ったプロパティ名の冗長性の排除
前のステップで型付きプロパティを作成する際、プロパティ名を明示的に指定する必要がありました。これは、クラス定義ですでにプロパティ名が指定されているため、冗長です。このステップでは、ディスクリプタ (Descriptor) を使ってこの冗長性を排除します。
Python のディスクリプタは、属性アクセスの動作を制御する特殊なオブジェクトです。ディスクリプタに __set_name__ メソッドを実装すると、クラス定義から自動的に属性名を取得できます。
新しいファイルを作成して始めましょう。
- 以下のコードで
improved_typedproperty.pyという名前の新しいファイルを作成します。
## improved_typedproperty.py
class TypedProperty:
"""
A descriptor that performs type checking.
This descriptor automatically captures the attribute name from the class definition.
"""
def __init__(self, expected_type):
self.expected_type = expected_type
self.name = None
def __set_name__(self, owner, name):
## This method is called when the descriptor is assigned to a class attribute
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f'Expected {self.expected_type}')
instance.__dict__[self.name] = value
## Convenience functions
def String():
"""Create a string property with type checking."""
return TypedProperty(str)
def Integer():
"""Create an integer property with type checking."""
return TypedProperty(int)
def Float():
"""Create a float property with type checking."""
return TypedProperty(float)
このコードは、属性に割り当てられる値の型をチェックする TypedProperty というディスクリプタクラスを定義しています。__set_name__ メソッドは、ディスクリプタがクラス属性に割り当てられると自動的に呼び出されます。これにより、手動で指定することなく属性名を取得できます。
次に、これらの改良された型付きプロパティを使用するクラスを作成します。
- 改良された型付きプロパティを使用する
stock_improved.pyという名前の新しいファイルを作成します。
from improved_typedproperty import String, Integer, Float
class Stock:
"""A class representing a stock with type-checked attributes."""
## No need to specify property names anymore
name = String()
shares = Integer()
price = Float()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
型付きプロパティを作成する際に、プロパティ名を指定する必要がないことに注意してください。ディスクリプタはクラス定義から自動的に属性名を取得します。
では、改良されたクラスをテストしましょう。
- 改良版をテストするための
test_stock_improved.pyという名前のテストファイルを作成します。
from stock_improved import Stock
## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")
## Try setting attributes with wrong types
try:
s.name = 123 ## Should raise TypeError
print("Name type check failed")
except TypeError as e:
print(f"Name type check succeeded: {e}")
try:
s.shares = "hundred" ## Should raise TypeError
print("Shares type check failed")
except TypeError as e:
print(f"Shares type check succeeded: {e}")
try:
s.price = "490.1" ## Should raise TypeError
print("Price type check failed")
except TypeError as e:
print(f"Price type check succeeded: {e}")
最後に、すべてが期待通りに動作するかどうかを確認するためにテストを実行します。
- テストを実行します。
python3 test_stock_improved.py
以下のような出力が表示されるはずです。
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>
このステップでは、ディスクリプタと __set_name__ メソッドを使って型チェックシステムを改善しました。これにより、冗長なプロパティ名の指定が不要になり、コードが短くなり、エラーが発生する可能性も低くなります。
__set_name__ メソッドはディスクリプタの非常に便利な機能です。これにより、ディスクリプタはクラス定義での使用方法に関する情報を自動的に収集できます。これは、理解しやすく使いやすい API を作成するために利用できます。
まとめ
この実験 (Lab) では、Python のクロージャ (Closure) に関する高度な側面を学びました。まず、クロージャをデータ構造として使用する方法を調べました。これにより、データをカプセル化し、クラスやグローバル変数に依存することなく、関数が呼び出し間で状態を維持できるようになります。次に、クロージャがコード生成器として機能する方法を見ました。属性検証に対してより関数型のアプローチをとるために、型チェック付きのプロパティオブジェクトを生成することができます。
また、ディスクリプタプロトコル (Descriptor Protocol) と __set_name__ メソッドを使用して、クラス定義から自動的に名前を取得するエレガントな型チェック属性を作成する方法を発見しました。これらのテクニックは、クロージャの強力さと柔軟性を示しており、複雑な動作を簡潔に実装することができます。クロージャとディスクリプタを理解することで、保守可能で堅牢な Python コードを作成するためのより多くのツールを手に入れることができます。