はじめに
この実験では、Python の関数引数の渡し方について学びます。また、データクラスの再利用可能な構造を作成し、オブジェクト指向の設計原則を適用してコードを簡素化します。
この演習の目的は、stock.py ファイルをより整理された形で書き直すことです。作業を開始する前に、既存の stock.py の内容を参照用に orig_stock.py という新しいファイルにコピーしてください。作成するファイルは structure.py と stock.py です。
関数引数の渡し方の理解
Python では、関数は一連の文をまとめて特定のタスクを実行するための基本的な概念です。関数を呼び出す際には、多くの場合、関数に何らかのデータを渡す必要があります。これを引数と呼びます。Python では、これらの引数を関数に渡す方法がいくつかあります。この柔軟性は非常に便利で、よりクリーンで保守しやすいコードを書くのに役立ちます。これらの技術をプロジェクトに適用する前に、引数の渡し方について詳しく見てみましょう。
作業のバックアップを作成する
stock.py ファイルに変更を加える前に、バックアップを作成するのが良い習慣です。こうすることで、実験中に何か問題が発生した場合でも、常に元のバージョンに戻すことができます。バックアップを作成するには、ターミナルを開き、次のコマンドを実行します。
cp stock.py orig_stock.py
このコマンドは、ターミナルの cp(コピー)コマンドを使用しています。stock.py ファイルをコピーし、orig_stock.py という名前のファイルを作成します。これにより、元の作業内容が安全に保存されます。
関数引数の渡し方を調べる
Python では、さまざまな種類の引数を持つ関数を呼び出す方法がいくつかあります。これらの方法を詳しく調べてみましょう。
1. 位置引数
関数に引数を渡す最も簡単な方法は、位置によって渡すことです。関数を定義するときには、パラメータのリストを指定します。関数を呼び出すときには、定義された順序でこれらのパラメータに値を渡します。
以下は例です。
def calculate(x, y, z):
return x + y + z
## Call with positional arguments
result = calculate(1, 2, 3)
print(result) ## Output: 6
この例では、calculate 関数は 3 つのパラメータ x、y、z を受け取ります。calculate(1, 2, 3) という呼び出しでは、値 1 が x に割り当てられ、2 が y に割り当てられ、3 が z に割り当てられます。そして、関数はこれらの値を足し合わせて結果を返します。
2. キーワード引数
位置引数に加えて、引数を名前で指定することもできます。これをキーワード引数と呼びます。キーワード引数を使用する場合、引数の順序を気にする必要はありません。
以下は例です。
## Call with a mix of positional and keyword arguments
result = calculate(1, z=3, y=2)
print(result) ## Output: 6
この例では、まず x の位置引数として 1 を渡します。その後、キーワード引数を使用して y と z の値を指定します。キーワード引数の順序は関係ありません。ただし、正しい名前を指定する必要があります。
3. シーケンスと辞書のアンパック
Python では、* と ** 構文を使用して、シーケンスと辞書を引数として渡す便利な方法が提供されています。これをアンパックと呼びます。
以下は、タプルを位置引数にアンパックする例です。
## Unpacking a tuple into positional arguments
args = (1, 2, 3)
result = calculate(*args)
print(result) ## Output: 6
この例では、1、2、3 の値を含むタプル args があります。関数呼び出しで args の前に * 演算子を使用すると、Python はタプルをアンパックし、その要素を位置引数として calculate 関数に渡します。
以下は、辞書をキーワード引数にアンパックする例です。
## Unpacking a dictionary into keyword arguments
kwargs = {'y': 2, 'z': 3}
result = calculate(1, **kwargs)
print(result) ## Output: 6
この例では、'y': 2 と 'z': 3 のキーバリューペアを含む辞書 kwargs があります。関数呼び出しで kwargs の前に ** 演算子を使用すると、Python は辞書をアンパックし、そのキーバリューペアをキーワード引数として calculate 関数に渡します。
4. 可変引数の受け取り
場合によっては、任意の数の引数を受け取ることができる関数を定義したいことがあります。Python では、関数定義で * と ** 構文を使用することでこれを実現できます。
以下は、任意の数の位置引数を受け取る関数の例です。
## Accept any number of positional arguments
def sum_all(*args):
return sum(args)
print(sum_all(1, 2)) ## Output: 3
print(sum_all(1, 2, 3, 4, 5)) ## Output: 15
この例では、sum_all 関数は *args パラメータを使用して、任意の数の位置引数を受け取ります。* 演算子はすべての位置引数を args という名前のタプルにまとめます。そして、関数は組み込みの sum 関数を使用して、タプル内のすべての要素を合計します。
以下は、任意の数のキーワード引数を受け取る関数の例です。
## Accept any number of keyword arguments
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Python", year=1991)
## Output:
## name: Python
## year: 1991
この例では、print_info 関数は **kwargs パラメータを使用して、任意の数のキーワード引数を受け取ります。** 演算子はすべてのキーワード引数を kwargs という名前の辞書にまとめます。そして、関数は辞書内のキーバリューペアを反復処理して表示します。
これらの技術は、次のステップでより柔軟で再利用可能なコード構造を作成するのに役立ちます。これらの概念に慣れるために、Python インタープリタを開いて、上記の例を試してみましょう。
python3
Python インタープリタに入ったら、上記の例を入力してみてください。これにより、これらの引数の渡し方の技術を実際に体験することができます。
構造体の基底クラスを作成する
これで関数引数の渡し方を理解したので、データ構造用の再利用可能な基底クラスを作成します。このステップは非常に重要です。データを保持する単純なクラスを作成する際に、同じコードを何度も書くのを避けることができるからです。基底クラスを使用することで、コードを簡素化し、効率的にすることができます。
繰り返しコードの問題
前の演習では、以下のように Stock クラスを定義しました。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
__init__ メソッドをよく見ると、かなり繰り返しが多いことがわかります。各属性を手動で 1 つずつ割り当てる必要があります。特に多くの属性を持つクラスが多数ある場合、これは非常に面倒で時間がかかる作業になります。
柔軟な基底クラスを作成する
属性の割り当てを自動的に処理できる Structure 基底クラスを作成しましょう。まず、WebIDE を開き、structure.py という名前の新しいファイルを作成します。次に、以下のコードをこのファイルに追加します。
## structure.py
class Structure:
"""
A base class for creating simple data structures.
Automatically populates object attributes from _fields and constructor arguments.
"""
_fields = ()
def __init__(self, *args):
## Check that the number of arguments matches the number of fields
if len(args) != len(self._fields):
raise TypeError(f"Expected {len(self._fields)} arguments")
## Set the attributes
for name, value in zip(self._fields, args):
setattr(self, name, value)
この基底クラスにはいくつかの重要な特徴があります。
_fieldsクラス変数を定義しています。デフォルトでは、この変数は空です。この変数は、クラスが持つ属性の名前を保持します。- コンストラクタに渡された引数の数が
_fieldsで定義されたフィールドの数と一致するかどうかをチェックします。一致しない場合は、TypeErrorを発生させます。これにより、エラーを早期に検出することができます。 - フィールド名と引数として提供された値を使用して、オブジェクトの属性を設定します。
setattr関数を使用して属性を動的に設定します。
構造体の基底クラスをテストする
では、Structure 基底クラスを継承するいくつかのサンプルクラスを作成しましょう。structure.py ファイルに以下のコードを追加します。
## Example classes using Structure
class Stock(Structure):
_fields = ('name', 'shares', 'price')
class Point(Structure):
_fields = ('x', 'y')
class Date(Structure):
_fields = ('year', 'month', 'day')
実装が正しく動作するかどうかをテストするために、test_structure.py という名前のテストファイルを作成します。このファイルに以下のコードを追加します。
## test_structure.py
from structure import Stock, Point, Date
## Test Stock class
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}, shares: {s.shares}, price: {s.price}")
## Test Point class
p = Point(3, 4)
print(f"Point coordinates: ({p.x}, {p.y})")
## Test Date class
d = Date(2023, 11, 9)
print(f"Date: {d.year}-{d.month}-{d.day}")
## Test error handling
try:
s2 = Stock('AAPL', 50) ## Missing price argument
print("This should not print")
except TypeError as e:
print(f"Error correctly caught: {e}")
テストを実行するには、ターミナルを開き、以下のコマンドを実行します。
python3 test_structure.py
以下の出力が表示されるはずです。
Stock name: GOOG, shares: 100, price: 490.1
Point coordinates: (3, 4)
Date: 2023-11-9
Error correctly caught: Expected 3 arguments
ご覧の通り、基底クラスは期待通りに動作しています。同じ定型コードを繰り返し書くことなく、新しいデータ構造を定義するのがはるかに簡単になりました。
オブジェクトの表現を改善する
私たちの Structure クラスは、オブジェクトの作成とアクセスに便利です。しかし、現在はオブジェクトを文字列として表現する良い方法がありません。オブジェクトを印刷したり、Python インタープリタで表示したりするときには、明確で有益な表示が望まれます。これにより、オブジェクトが何であり、その値が何であるかを理解することができます。
Python のオブジェクト表現を理解する
Python では、オブジェクトを異なる方法で表現するための 2 つの特殊メソッドがあります。これらのメソッドは、オブジェクトの表示方法を制御できるため重要です。
__str__- このメソッドはstr()関数とprint()関数によって使用されます。オブジェクトの人間が読みやすい表現を提供します。たとえば、Stockオブジェクトがある場合、__str__メソッドは "Stock: GOOG, 100 shares at $490.1" のようなものを返すかもしれません。__repr__- このメソッドは Python インタープリタとrepr()関数によって使用されます。オブジェクトのより技術的で曖昧さのない表現を提供します。__repr__の目的は、オブジェクトを再作成するために使用できる文字列を提供することです。たとえば、Stockオブジェクトの場合、"Stock('GOOG', 100, 490.1)" を返すかもしれません。
Structure クラスに __repr__ メソッドを追加しましょう。これにより、オブジェクトの状態を明確に見ることができるため、コードのデバッグが容易になります。
良い表現を実装する
では、structure.py ファイルを更新しましょう。Structure クラスに __repr__ メソッドを追加します。このメソッドは、オブジェクトを再作成するために使用できる形式の文字列を作成します。
def __repr__(self):
"""
Return a representation of the object that can be used to recreate it.
Example: Stock('GOOG', 100, 490.1)
"""
## Get the class name
cls_name = type(self).__name__
## Get all the field values
values = [getattr(self, name) for name in self._fields]
## Format the fields and values
args_str = ', '.join(repr(value) for value in values)
## Return the formatted string
return f"{cls_name}({args_str})"
このメソッドは以下のように動作します。
type(self).__name__を使用してクラス名を取得します。これは、扱っているオブジェクトの種類を知るために重要です。- インスタンスからすべてのフィールド値を取得します。これにより、オブジェクトが保持しているデータがわかります。
- クラス名と値を含む文字列表現を作成します。この文字列は、オブジェクトを再作成するために使用できます。
改善された表現をテストする
改善された実装をテストしましょう。test_repr.py という名前の新しいファイルを作成します。このファイルでは、クラスのいくつかのインスタンスを作成し、それらの表現を印刷します。
## test_repr.py
from structure import Stock, Point, Date
## Create instances
s = Stock('GOOG', 100, 490.1)
p = Point(3, 4)
d = Date(2023, 11, 9)
## Print the representations
print(repr(s))
print(repr(p))
print(repr(d))
## Direct printing also uses __repr__ in the interpreter
print(s)
print(p)
print(d)
テストを実行するには、ターミナルを開き、以下のコマンドを入力します。
python3 test_repr.py
以下の出力が表示されるはずです。
Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)
Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)
この出力は以前よりもはるかに有益です。Stock('GOOG', 100, 490.1) を見ると、オブジェクトが何を表しているかがすぐにわかります。この文字列をコピーして、コード内でオブジェクトを再作成するために使用することさえできます。
良い表現の利点
良い __repr__ の実装は、デバッグに非常に役立ちます。インタープリタでオブジェクトを見たり、プログラム実行中にオブジェクトをログに記録したりするときに、明確な表現によって問題をすばやく特定することができます。オブジェクトの正確な状態を見ることができ、何がうまくいっていないのかを理解することができます。
属性名の制限
現在、私たちの Structure クラスは、そのインスタンスに任意の属性を設定できるようになっています。初心者にとっては、これは最初は便利に見えるかもしれませんが、実際には多くの問題を引き起こす可能性があります。クラスを使用する際には、特定の属性が存在し、特定の方法で使用されることを期待します。ユーザーが属性名を誤って入力したり、元の設計に含まれていない属性を設定しようとしたりすると、見つけにくいエラーが発生する可能性があります。
属性制限の必要性
属性名を制限する必要がある理由を理解するために、簡単なシナリオを見てみましょう。以下のコードを考えてみます。
s = Stock('GOOG', 100, 490.1)
s.shares = 50 ## Correct attribute name
s.share = 60 ## Typo in attribute name - creates a new attribute instead of updating
2 行目では、タイプミスがあります。shares の代わりに share と書いています。Python では、エラーを発生させる代わりに、share という新しい属性を作成します。これは、shares 属性を更新していると思っているのに、実際には新しい属性を作成しているため、微妙なバグを引き起こす可能性があります。これにより、コードが予期せぬ動作をし、デバッグが非常に困難になる可能性があります。
属性制限の実装
この問題を解決するために、__setattr__ メソッドをオーバーライドすることができます。このメソッドは、オブジェクトに属性を設定しようとするたびに呼び出されます。これをオーバーライドすることで、どの属性を設定できるか、できないかを制御することができます。
structure.py の Structure クラスを以下のコードで更新します。
def __setattr__(self, name, value):
"""
Restrict attribute setting to only those defined in _fields
or attributes starting with underscore (private attributes).
"""
if name.startswith('_'):
## Allow setting private attributes (starting with '_')
super().__setattr__(name, value)
elif name in self._fields:
## Allow setting attributes defined in _fields
super().__setattr__(name, value)
else:
## Raise an error for other attributes
raise AttributeError(f'No attribute {name}')
このメソッドは以下のように動作します。
- 属性名がアンダースコア (
_) で始まる場合、それはプライベート属性と見なされます。プライベート属性は、クラスの内部目的でよく使用されます。これらの属性はクラスの内部実装の一部であるため、設定を許可します。 - 属性名が
_fieldsリストに含まれている場合、それはクラス設計で定義された属性の 1 つであることを意味します。これらの属性はクラスの期待される動作の一部であるため、設定を許可します。 - 属性名がこれらの条件のいずれにも該当しない場合、
AttributeErrorを発生させます。これにより、ユーザーに対して、クラスに存在しない属性を設定しようとしていることを伝えます。
属性制限のテスト
属性制限を実装したので、期待通りに動作することを確認するためにテストしましょう。以下のコードを含む test_attributes.py という名前のファイルを作成します。
## test_attributes.py
from structure import Stock
s = Stock('GOOG', 100, 490.1)
## This should work - valid attribute
print("Setting shares to 50")
s.shares = 50
print(f"Shares is now: {s.shares}")
## This should work - private attribute
print("\nSetting _internal_data")
s._internal_data = "Some data"
print(f"_internal_data is: {s._internal_data}")
## This should fail - invalid attribute
print("\nTrying to set an invalid attribute:")
try:
s.share = 60 ## Typo in attribute name
print("This should not print")
except AttributeError as e:
print(f"Error correctly caught: {e}")
テストを実行するには、ターミナルを開き、以下のコマンドを入力します。
python3 test_attributes.py
以下の出力が表示されるはずです。
Setting shares to 50
Shares is now: 50
Setting _internal_data
_internal_data is: Some data
Trying to set an invalid attribute:
Error correctly caught: No attribute share
この出力は、私たちのクラスが誤った属性設定エラーを防止していることを示しています。有効な属性とプライベート属性の設定を許可しますが、無効な属性を設定しようとするとエラーを発生させます。
属性制限の価値
属性名を制限することは、堅牢で保守可能なコードを書くために非常に重要です。理由は以下の通りです。
- 属性名のタイプミスを検出するのに役立ちます。属性名を入力する際にミスを犯した場合、コードは新しい属性を作成する代わりにエラーを発生させます。これにより、開発プロセスの早い段階でエラーを見つけて修正することが容易になります。
- クラス設計に存在しない属性を設定しようとする試みを防止します。これにより、クラスが意図された通りに使用され、コードが予測可能に動作することが保証されます。
- 新しい属性の誤った作成を回避します。新しい属性を作成すると、予期せぬ動作が発生し、コードの理解と保守が困難になる可能性があります。
属性名を制限することで、コードをより信頼性が高く、使いやすくすることができます。
Stock クラスの書き換え
よく定義された Structure 基底クラスができたので、Stock クラスを書き換えましょう。この基底クラスを使用することで、コードを簡素化し、より整理されたものにすることができます。Structure クラスは、Stock クラスで再利用できる一連の共通機能を提供します。これは、コードの保守性と読みやすさにとって大きな利点となります。
新しい Stock クラスの作成
まず、stock.py という名前の新しいファイルを作成しましょう。このファイルには、書き換えた Stock クラスが含まれます。stock.py ファイルに入れるコードは以下の通りです。
## stock.py
from structure import Structure
class Stock(Structure):
_fields = ('name', 'shares', 'price')
@property
def cost(self):
"""
Calculate the cost as shares * price
"""
return self.shares * self.price
def sell(self, nshares):
"""
Sell a number of shares
"""
self.shares -= nshares
この新しい Stock クラスが何をするかを分解してみましょう。
Structureクラスを継承しています。これは、StockクラスがStructureクラスが提供するすべての機能を使用できることを意味します。利点の 1 つは、Structureクラスが属性の割り当てを自動的に処理するため、自分で__init__メソッドを書く必要がないことです。_fieldsを定義しています。これは、Stockクラスの属性を指定するタプルです。これらの属性はname、shares、priceです。costプロパティが定義されており、株式の総コストを計算します。sharesの数にpriceを掛けた値を返します。sellメソッドは、株式の数を減らすために使用されます。このメソッドを売却する株式の数で呼び出すと、現在の株式数からその数が減算されます。
新しい Stock クラスのテスト
新しい Stock クラスが期待通りに動作することを確認するために、テストファイルを作成する必要があります。以下のコードを含む test_stock.py という名前のファイルを作成しましょう。
## test_stock.py
from stock import Stock
## Create a stock
s = Stock('GOOG', 100, 490.1)
## Check the attributes
print(f"Stock: {s}")
print(f"Name: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost}")
## Sell some shares
print("\nSelling 20 shares...")
s.sell(20)
print(f"Shares after selling: {s.shares}")
print(f"Cost after selling: {s.cost}")
## Try to set an invalid attribute
print("\nTrying to set an invalid attribute:")
try:
s.prices = 500 ## Invalid attribute (should be 'price')
print("This should not print")
except AttributeError as e:
print(f"Error correctly caught: {e}")
このテストファイルでは、まず stock.py ファイルから Stock クラスをインポートします。次に、名前が 'GOOG'、株式数が 100、価格が 490.1 の Stock クラスのインスタンスを作成します。株式の属性を出力して、正しく設定されているかを確認します。その後、20 株を売却し、新しい株式数と新しいコストを出力します。最後に、無効な属性 prices(正しくは price であるべき)を設定しようとします。Stock クラスが正しく動作していれば、AttributeError が発生するはずです。
テストを実行するには、ターミナルを開き、以下のコマンドを入力します。
python3 test_stock.py
期待される出力は以下の通りです。
Stock: Stock('GOOG', 100, 490.1)
Name: GOOG
Shares: 100
Price: 490.1
Cost: 49010.0
Selling 20 shares...
Shares after selling: 80
Cost after selling: 39208.0
Trying to set an invalid attribute:
Error correctly caught: No attribute prices
ユニットテストの実行
前の演習でユニットテストを作成している場合は、新しい実装に対してそれらを実行することができます。ターミナルで以下のコマンドを入力します。
python3 teststock.py
一部のテストが失敗する場合があります。これは、まだ実装していない特定の動作やメソッドを期待しているためかもしれません。心配しないでください!将来の演習でこの基礎を元にさらに構築していきます。
進捗の振り返り
ここまでで達成したことを振り返ってみましょう。
再利用可能な
Structure基底クラスを作成しました。このクラスは以下のことを行います。- 属性の割り当てを自動的に処理するため、多くの繰り返しコードを書く必要がなくなります。
- 良い文字列表現を提供するため、オブジェクトの印刷とデバッグが容易になります。
- 属性名を制限してエラーを防止するため、コードがより堅牢になります。
Stockクラスを書き換えました。このクラスは以下のことを行います。Structureクラスを継承して共通機能を再利用します。- フィールドとドメイン固有のメソッドのみを定義するため、クラスが焦点を絞り、クリーンになります。
- 明確でシンプルな設計なので、理解と保守が容易です。
このアプローチは、コードにいくつかの利点をもたらします。
- 繰り返しが少ないため、保守性が向上します。共通機能に何かを変更する必要がある場合、
Structureクラスでのみ変更すればよいからです。 Structureクラスによるより良いエラーチェックのため、堅牢性が向上します。- 各クラスの責任が明確なため、読みやすさが向上します。
将来の演習では、この基礎を元にさらに洗練された株式ポートフォリオ管理システムを作成していきます。
まとめ
この実験では、Python の関数引数の渡し方について学び、より整理された保守可能なコードベースを構築するためにそれらを適用しました。Python の柔軟な引数渡しメカニズムを探索し、データオブジェクト用の再利用可能な Structure 基底クラスを作成し、デバッグを容易にするためにオブジェクトの表現を改善しました。
また、一般的なエラーを防止するために属性検証を追加し、新しい構造を使用して Stock クラスを書き換えました。これらのテクニックは、コードの再利用のための継承、データの整合性のためのカプセル化、および共通インターフェースを通じたポリモーフィズムなどの主要なオブジェクト指向設計原則を示しています。これらの原則を適用することで、繰り返しが少なく、エラーが少ない、より堅牢で保守可能なコードを開発することができます。