はじめに
この実験では、Python のクラス変数とクラスメソッドについて学びます。それらの目的と使い方を理解し、クラスメソッドを効果的に定義して使用する方法を学びます。
さらに、クラスメソッドを使用して代替コンストラクタを実装し、クラス変数と継承の関係を調べ、柔軟なデータ読み取りユーティリティを作成します。この実験中に、stock.py
と reader.py
のファイルを変更します。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
この実験では、Python のクラス変数とクラスメソッドについて学びます。それらの目的と使い方を理解し、クラスメソッドを効果的に定義して使用する方法を学びます。
さらに、クラスメソッドを使用して代替コンストラクタを実装し、クラス変数と継承の関係を調べ、柔軟なデータ読み取りユーティリティを作成します。この実験中に、stock.py
と reader.py
のファイルを変更します。
この最初のステップでは、Python のクラス変数とクラスメソッドの概念について深く掘り下げます。これらは、より効率的で組織的なコードを書くのに役立つ重要な概念です。クラス変数とクラスメソッドを使い始める前に、まず Stock
クラスのインスタンスが現在どのように作成されているかを見てみましょう。これにより、基本的な理解が得られ、改善の余地がわかります。
クラス変数は Python の特殊なタイプの変数です。クラスのすべてのインスタンス間で共有されます。これをよりよく理解するために、インスタンス変数と比較してみましょう。インスタンス変数は、クラスの各インスタンスに固有のものです。たとえば、クラスの複数のインスタンスがある場合、各インスタンスはインスタンス変数に独自の値を持つことができます。一方、クラス変数はクラスレベルで定義されます。これは、そのクラスのすべてのインスタンスがクラス変数の同じ値にアクセスして共有できることを意味します。
クラスメソッドは、クラス自体に対して動作するメソッドであり、クラスの個々のインスタンスに対してではありません。クラスにバインドされているため、インスタンスを作成せずにクラスに直接呼び出すことができます。Python でクラスメソッドを定義するには、@classmethod
デコレータを使用します。そして、最初のパラメータとしてインスタンス (self
) を取る代わりに、クラスメソッドはクラス (cls
) を最初のパラメータとして取ります。これにより、クラスレベルのデータに対して操作を行い、クラス全体に関連するアクションを実行することができます。
まず、現在 Stock
クラスのインスタンスをどのように作成しているかを見てみましょう。エディタで stock.py
ファイルを開き、現在の実装を確認します。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
このクラスのインスタンスは、通常、次のいずれかの方法で作成されます。
値を指定した直接的な初期化:
s = Stock('GOOG', 100, 490.1)
ここでは、name
、shares
、price
属性の値を指定して、Stock
クラスのインスタンスを直接作成しています。事前に値がわかっている場合、これはインスタンスを作成する簡単な方法です。
CSV ファイルから読み取ったデータからの作成:
import csv
with open('portfolio.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip the header
row = next(rows) ## Get the first data row
s = Stock(row[0], int(row[1]), float(row[2]))
CSV ファイルからデータを読み取ると、値は最初は文字列形式になっています。そのため、CSV データから Stock
インスタンスを作成するときは、文字列値を適切な型に手動で変換する必要があります。たとえば、shares
値は整数に変換する必要があり、price
値は浮動小数点数に変換する必要があります。
これを試してみましょう。~/project
ディレクトリに test_stock.py
という名前の新しい Python ファイルを作成し、以下の内容を記述します。
## test_stock.py
from stock import Stock
import csv
## Method 1: Direct creation
s1 = Stock('GOOG', 100, 490.1)
print(f"Stock: {s1.name}, Shares: {s1.shares}, Price: {s1.price}")
print(f"Cost: {s1.cost()}")
## Method 2: Creation from CSV row
with open('portfolio.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip the header
row = next(rows) ## Get the first data row
s2 = Stock(row[0], int(row[1]), float(row[2]))
print(f"\nStock from CSV: {s2.name}, Shares: {s2.shares}, Price: {s2.price}")
print(f"Cost: {s2.cost()}")
このファイルを実行して結果を確認します。
cd ~/project
python test_stock.py
以下のような出力が表示されるはずです。
Stock: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Stock from CSV: AA, Shares: 100, Price: 32.2
Cost: 3220.0
この手動変換は機能しますが、いくつかの欠点があります。データの正確な形式を知る必要があり、CSV データからインスタンスを作成するたびに変換を行う必要があります。これはエラーが発生しやすく、時間がかかります。次のステップでは、クラスメソッドを使用してよりエレガントな解決策を作成します。
このステップでは、クラスメソッドを使用して代替コンストラクタを実装する方法を学びます。これにより、CSV 行データから Stock
オブジェクトをよりエレガントな方法で作成することができます。
Python では、代替コンストラクタは便利なパターンです。通常、オブジェクトは標準の __init__
メソッドを使用して作成します。しかし、代替コンストラクタはオブジェクトを作成する追加の方法を提供します。クラスメソッドは、クラス自体にアクセスできるため、代替コンストラクタの実装に非常に適しています。
Stock
クラスにクラス変数 types
とクラスメソッド from_row()
を追加します。これにより、CSV データから Stock
インスタンスを作成するプロセスが簡素化されます。
以下の強調表示されたコードを追加して stock.py
ファイルを変更しましょう。
## stock.py
class Stock:
types = (str, int, float) ## Type conversions to apply to CSV data
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
@classmethod
def from_row(cls, row):
"""
Create a Stock instance from a row of CSV data.
Args:
row: A list of strings [name, shares, price]
Returns:
A new Stock instance
"""
values = [func(val) for func, val in zip(cls.types, row)]
return cls(*values)
## The rest of the file remains unchanged
では、このコードで何が起こっているかを段階的に理解しましょう。
types
を定義しました。これは、型変換関数 (str, int, float)
を含むタプルです。これらの関数は、CSV 行のデータを適切な型に変換するために使用されます。from_row()
を追加しました。@classmethod
デコレータは、このメソッドをクラスメソッドとしてマークします。cls
で、これはクラス自体への参照です。通常のメソッドでは、クラスのインスタンスを参照するために self
を使用しますが、ここではクラスメソッドなので cls
を使用します。zip()
関数を使用して、types
の各型変換関数を row
リストの対応する値とペアにします。row
リストの対応する値に各変換関数を適用します。これにより、CSV 行の文字列データを適切な型に変換します。Stock
クラスの新しいインスタンスを作成し、それを返します。次に、新しいクラスメソッドをテストするために test_class_method.py
という新しいファイルを作成します。これにより、代替コンストラクタが期待どおりに動作することを確認できます。
## test_class_method.py
from stock import Stock
## Test the from_row() class method
row = ['AA', '100', '32.20']
s = Stock.from_row(row)
print(f"Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost()}")
## Try with a different row
row2 = ['GOOG', '50', '1120.50']
s2 = Stock.from_row(row2)
print(f"\nStock: {s2.name}")
print(f"Shares: {s2.shares}")
print(f"Price: {s2.price}")
print(f"Cost: {s2.cost()}")
結果を確認するには、ターミナルで以下のコマンドを実行します。
cd ~/project
python test_class_method.py
以下のような出力が表示されるはずです。
Stock: AA
Shares: 100
Price: 32.2
Cost: 3220.0
Stock: GOOG
Shares: 50
Price: 1120.5
Cost: 56025.0
クラスの外で手動で型変換を行う必要なく、文字列データから直接 Stock
インスタンスを作成できることに注意してください。これにより、コードがクリーンになり、データ変換の責任がクラス自体の中で処理されることが保証されます。
このステップでは、クラス変数が継承とどのように相互作用し、カスタマイズのメカニズムとしてどのように機能するかを探っていきます。Python では、継承によりサブクラスは基底クラスから属性とメソッドを継承することができます。クラス変数は、クラス自体に属する変数であり、クラスの特定のインスタンスに属するものではありません。これらがどのように連携するかを理解することは、柔軟で保守可能なコードを作成するために重要です。
サブクラスが基底クラスから継承すると、自動的に基底クラスのクラス変数にアクセスできるようになります。ただし、サブクラスはこれらのクラス変数を上書きすることができます。こうすることで、サブクラスは基底クラスに影響を与えることなく自身の振る舞いを変更することができます。これは非常に強力な機能であり、特定のニーズに合わせてサブクラスの振る舞いをカスタマイズすることができます。
Stock
クラスのサブクラスを作成しましょう。これを DStock
と呼び、これは Decimal Stock の略です。DStock
と通常の Stock
クラスの主な違いは、DStock
が価格の値に float
ではなく Decimal
型を使用することです。金融計算では精度が非常に重要であり、Decimal
型は float
と比較してより正確な 10 進数演算を提供します。
このサブクラスを作成するには、decimal_stock.py
という名前の新しいファイルを作成します。このファイルに入れる必要があるコードは次のとおりです。
## decimal_stock.py
from decimal import Decimal
from stock import Stock
class DStock(Stock):
"""
A specialized version of Stock that uses Decimal for prices
"""
types = (str, int, Decimal) ## Override the types class variable
## Test the subclass
if __name__ == "__main__":
## Create a DStock from row data
row = ['AA', '100', '32.20']
ds = DStock.from_row(row)
print(f"DStock: {ds.name}")
print(f"Shares: {ds.shares}")
print(f"Price: {ds.price} (type: {type(ds.price).__name__})")
print(f"Cost: {ds.cost()} (type: {type(ds.cost()).__name__})")
## For comparison, create a regular Stock from the same data
s = Stock.from_row(row)
print(f"\nRegular Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price} (type: {type(s.price).__name__})")
print(f"Cost: {s.cost()} (type: {type(s.cost()).__name__})")
上記のコードで decimal_stock.py
ファイルを作成したら、結果を確認するために実行する必要があります。ターミナルを開き、以下の手順に従います。
cd ~/project
python decimal_stock.py
以下のような出力が表示されるはずです。
DStock: AA
Shares: 100
Price: 32.20 (type: Decimal)
Cost: 3220.0 (type: Decimal)
Regular Stock: AA
Shares: 100
Price: 32.2 (type: float)
Cost: 3220.0 (type: float)
この例から、いくつかの重要な結論を導き出すことができます。
DStock
クラスは、cost()
メソッドなど、Stock
クラスのすべてのメソッドを再定義することなく継承します。これは継承の主な利点の 1 つであり、冗長なコードを書く手間を省いてくれます。types
クラス変数を単に上書きすることで、DStock
の新しいインスタンスを作成する際のデータの変換方法を変更しました。これは、クラス変数がサブクラスの振る舞いをカスタマイズするためにどのように使用できるかを示しています。Stock
は変更されず、依然として float
値で動作します。これは、サブクラスに対して行った変更が基底クラスに影響を与えないことを意味しており、良い設計原則です。from_row()
クラスメソッドは、Stock
クラスと DStock
クラスの両方で正しく動作します。これは、同じメソッドが異なるサブクラスで使用できることを示しており、継承の強力さを実証しています。この例は、クラス変数が構成メカニズムとしてどのように使用できるかを明確に示しています。サブクラスはこれらの変数を上書きして、メソッドを書き直すことなく自身の振る舞いをカスタマイズすることができます。
型変換を __init__
メソッドに配置する別のアプローチを考えてみましょう。
class Stock:
def __init__(self, name, shares, price):
self.name = str(name)
self.shares = int(shares)
self.price = float(price)
このアプローチでは、次のようにデータの行から Stock
オブジェクトを作成することができます。
row = ['AA', '100', '32.20']
s = Stock(*row)
このアプローチは一見すると簡単に見えるかもしれませんが、いくつかの欠点があります。
__init__
メソッドは、入力がすでに正しい型である場合でも常に変換を行うため、柔軟性が低下します。__init__
メソッドに組み込まれている場合、サブクラスが変換ロジックを変更するのは難しくなります。一方、クラスメソッドのアプローチはこれらの関心事を分離します。これにより、コードの各部分が単一の責任を持つため、コードがより保守可能で柔軟になります。
この最後のステップでは、汎用的な関数を作成します。この関数は CSV ファイルを読み込み、from_row()
クラスメソッドを実装した任意のクラスのオブジェクトを作成することができます。これは、クラスメソッドを統一的なインターフェースとして使用する強力さを示しています。統一的なインターフェースとは、異なるクラスを同じように使用できることを意味し、これによりコードがより柔軟で管理しやすくなります。
まず、stock.py
ファイルの read_portfolio()
関数を更新します。新しい from_row()
クラスメソッドを使用します。stock.py
ファイルを開き、read_portfolio()
関数を次のように変更します。
def read_portfolio(filename):
'''
Read a stock portfolio file into a list of Stock instances
'''
import csv
portfolio = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
for row in rows:
portfolio.append(Stock.from_row(row))
return portfolio
この新しいバージョンの関数はよりシンプルです。型変換の責任を本当にそれが属する Stock
クラスに委ねています。型変換とは、データをある型から別の型に変更することで、例えば文字列を整数に変換することです。こうすることで、コードがより整理され、理解しやすくなります。
次に、reader.py
ファイルにより汎用的な関数を作成します。この関数は CSV データを読み込み、from_row()
クラスメソッドを持つ任意のクラスのインスタンスを作成することができます。
reader.py
ファイルを開き、次の関数を追加します。
def read_csv_as_instances(filename, cls):
'''
Read a CSV file into a list of instances of the given class.
Args:
filename: Name of the CSV file
cls: Class to instantiate (must have from_row class method)
Returns:
List of class instances
'''
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
for row in rows:
records.append(cls.from_row(row))
return records
この関数は 2 つの入力を受け取ります。ファイル名とクラスです。そして、CSV ファイルのデータから作成されたそのクラスのインスタンスのリストを返します。これは非常に便利です。なぜなら、from_row()
メソッドを持つ限り、異なるクラスで使用することができるからです。
汎用的なリーダーがどのように動作するかを確認するために、テストファイルを作成しましょう。次の内容で test_csv_reader.py
という名前のファイルを作成します。
## test_csv_reader.py
from reader import read_csv_as_instances
from stock import Stock
from decimal_stock import DStock
## Read portfolio as Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print(f"Portfolio contains {len(portfolio)} stocks")
print(f"First stock: {portfolio[0].name}, {portfolio[0].shares} shares at ${portfolio[0].price}")
## Read portfolio as DStock instances (with Decimal prices)
decimal_portfolio = read_csv_as_instances('portfolio.csv', DStock)
print(f"\nDecimal portfolio contains {len(decimal_portfolio)} stocks")
print(f"First stock: {decimal_portfolio[0].name}, {decimal_portfolio[0].shares} shares at ${decimal_portfolio[0].price}")
## Define a new class for reading the bus data
class BusRide:
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
@classmethod
def from_row(cls, row):
return cls(row[0], row[1], row[2], int(row[3]))
## Read some bus data (just the first 5 records for brevity)
print("\nReading bus data...")
import csv
with open('ctabus.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
bus_rides = []
for i, row in enumerate(rows):
if i >= 5: ## Only read 5 records for the example
break
bus_rides.append(BusRide.from_row(row))
## Display the bus data
for ride in bus_rides:
print(f"Route: {ride.route}, Date: {ride.date}, Type: {ride.daytype}, Rides: {ride.rides}")
このファイルを実行して結果を確認します。ターミナルを開き、次のコマンドを使用します。
cd ~/project
python test_csv_reader.py
Stock
と DStock
の両方のインスタンスとして読み込まれたポートフォリオデータ、および BusRide
インスタンスとして読み込まれたバス路線データが表示されるはずです。これは、汎用的なリーダーが異なるクラスで動作することを証明しています。
このアプローチはいくつかの強力な概念を示しています。
from_row()
メソッドを持っている限り、汎用的なリーダーはそれを使用することができます。Stock
または DStock
を使用して、ポートフォリオデータを異なる方法で処理することができます。これは Python で一般的なパターンであり、コードをよりモジュール化、再利用可能、保守可能にします。
クラスメソッドは Python で代替コンストラクタとしてよく使用されます。通常、その名前に "from" という単語が含まれていることで区別できます。例えば:
## Some examples from Python's built-in types
dict.fromkeys(['a', 'b', 'c'], 0) ## Create a dict with default values
datetime.datetime.fromtimestamp(1627776000) ## Create datetime from timestamp
int.from_bytes(b'\x00\x01', byteorder='big') ## Create int from bytes
この規則に従うことで、コードがより読みやすくなり、Python の組み込みライブラリとの一貫性が保たれます。これにより、他の開発者がコードをより簡単に理解することができます。
この実験では、Python の 2 つの重要な機能、クラス変数とクラスメソッドについて学びました。クラス変数はすべてのクラスインスタンス間で共有され、設定に使用することができます。クラスメソッドは、@classmethod
デコレータでマークされ、クラス自体に対して操作を行います。クラスメソッドの一般的な用途である代替コンストラクタは、オブジェクトを作成する異なる方法を提供します。クラス変数を用いた継承により、サブクラスはそれらを上書きすることで振る舞いをカスタマイズすることができ、クラスメソッドを使用することで柔軟なコード設計を実現することができます。
これらの概念は、整理された柔軟な Python コードを作成するために強力です。型変換をクラス内に配置し、クラスメソッドを介して統一的なインターフェースを提供することで、より汎用的なユーティリティを記述することができます。学習をさらに深めるために、より多くのユースケースを探索し、クラス階層を作成し、クラスメソッドを使用して複雑なデータ処理パイプラインを構築することができます。