はじめに
この実験では、Python の関数引数の渡し方について学びます。また、データクラスの再利用可能な構造を作成し、オブジェクト指向の設計原則を適用してコードを簡素化します。
この演習の目的は、stock.py
ファイルをより整理された形で書き直すことです。作業を開始する前に、既存の stock.py
の内容を参照用に orig_stock.py
という新しいファイルにコピーしてください。作成するファイルは structure.py
と stock.py
です。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
この実験では、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 では、さまざまな種類の引数を持つ関数を呼び出す方法がいくつかあります。これらの方法を詳しく調べてみましょう。
関数に引数を渡す最も簡単な方法は、位置によって渡すことです。関数を定義するときには、パラメータのリストを指定します。関数を呼び出すときには、定義された順序でこれらのパラメータに値を渡します。
以下は例です。
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
に割り当てられます。そして、関数はこれらの値を足し合わせて結果を返します。
位置引数に加えて、引数を名前で指定することもできます。これをキーワード引数と呼びます。キーワード引数を使用する場合、引数の順序を気にする必要はありません。
以下は例です。
## Call with a mix of positional and keyword arguments
result = calculate(1, z=3, y=2)
print(result) ## Output: 6
この例では、まず x
の位置引数として 1
を渡します。その後、キーワード引数を使用して y
と z
の値を指定します。キーワード引数の順序は関係ありません。ただし、正しい名前を指定する必要があります。
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
関数に渡します。
場合によっては、任意の数の引数を受け取ることができる関数を定義したいことがあります。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 では、オブジェクトを異なる方法で表現するための 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
この出力は、私たちのクラスが誤った属性設定エラーを防止していることを示しています。有効な属性とプライベート属性の設定を許可しますが、無効な属性を設定しようとするとエラーを発生させます。
属性名を制限することは、堅牢で保守可能なコードを書くために非常に重要です。理由は以下の通りです。
属性名を制限することで、コードをより信頼性が高く、使いやすくすることができます。
よく定義された Structure
基底クラスができたので、Stock
クラスを書き換えましょう。この基底クラスを使用することで、コードを簡素化し、より整理されたものにすることができます。Structure
クラスは、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
クラスが期待通りに動作することを確認するために、テストファイルを作成する必要があります。以下のコードを含む 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
クラスを書き換えました。これらのテクニックは、コードの再利用のための継承、データの整合性のためのカプセル化、および共通インターフェースを通じたポリモーフィズムなどの主要なオブジェクト指向設計原則を示しています。これらの原則を適用することで、繰り返しが少なく、エラーが少ない、より堅牢で保守可能なコードを開発することができます。