クラス変数とクラスメソッド

PythonPythonBeginner
オンラインで実践に進む

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

この実験では、Python のクラス変数とクラスメソッドについて学びます。それらの目的と使い方を理解し、クラスメソッドを効果的に定義して使用する方法を学びます。

さらに、クラスメソッドを使用して代替コンストラクタを実装し、クラス変数と継承の関係を調べ、柔軟なデータ読み取りユーティリティを作成します。この実験中に、stock.pyreader.py のファイルを変更します。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 100%です。学習者から 100% の好評価を得ています。

クラス変数とクラスメソッドの理解

この最初のステップでは、Python のクラス変数とクラスメソッドの概念について深く掘り下げます。これらは、より効率的で組織的なコードを書くのに役立つ重要な概念です。クラス変数とクラスメソッドを使い始める前に、まず Stock クラスのインスタンスが現在どのように作成されているかを見てみましょう。これにより、基本的な理解が得られ、改善の余地がわかります。

クラス変数とは何か?

クラス変数は Python の特殊なタイプの変数です。クラスのすべてのインスタンス間で共有されます。これをよりよく理解するために、インスタンス変数と比較してみましょう。インスタンス変数は、クラスの各インスタンスに固有のものです。たとえば、クラスの複数のインスタンスがある場合、各インスタンスはインスタンス変数に独自の値を持つことができます。一方、クラス変数はクラスレベルで定義されます。これは、そのクラスのすべてのインスタンスがクラス変数の同じ値にアクセスして共有できることを意味します。

クラスメソッドとは何か?

クラスメソッドは、クラス自体に対して動作するメソッドであり、クラスの個々のインスタンスに対してではありません。クラスにバインドされているため、インスタンスを作成せずにクラスに直接呼び出すことができます。Python でクラスメソッドを定義するには、@classmethod デコレータを使用します。そして、最初のパラメータとしてインスタンス (self) を取る代わりに、クラスメソッドはクラス (cls) を最初のパラメータとして取ります。これにより、クラスレベルのデータに対して操作を行い、クラス全体に関連するアクションを実行することができます。

現在の Stock インスタンスの作成方法

まず、現在 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

このクラスのインスタンスは、通常、次のいずれかの方法で作成されます。

  1. 値を指定した直接的な初期化:

    s = Stock('GOOG', 100, 490.1)

    ここでは、namesharesprice 属性の値を指定して、Stock クラスのインスタンスを直接作成しています。事前に値がわかっている場合、これはインスタンスを作成する簡単な方法です。

  2. 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__ メソッドを使用して作成します。しかし、代替コンストラクタはオブジェクトを作成する追加の方法を提供します。クラスメソッドは、クラス自体にアクセスできるため、代替コンストラクタの実装に非常に適しています。

from_row() クラスメソッドの実装

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

では、このコードで何が起こっているかを段階的に理解しましょう。

  1. クラス変数 types を定義しました。これは、型変換関数 (str, int, float) を含むタプルです。これらの関数は、CSV 行のデータを適切な型に変換するために使用されます。
  2. クラスメソッド from_row() を追加しました。@classmethod デコレータは、このメソッドをクラスメソッドとしてマークします。
  3. このメソッドの最初のパラメータは cls で、これはクラス自体への参照です。通常のメソッドでは、クラスのインスタンスを参照するために self を使用しますが、ここではクラスメソッドなので cls を使用します。
  4. zip() 関数を使用して、types の各型変換関数を row リストの対応する値とペアにします。
  5. リスト内包表記を使用して、row リストの対応する値に各変換関数を適用します。これにより、CSV 行の文字列データを適切な型に変換します。
  6. 最後に、変換された値を使用して 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 クラスの作成

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)

クラス変数と継承に関する要点

この例から、いくつかの重要な結論を導き出すことができます。

  1. DStock クラスは、cost() メソッドなど、Stock クラスのすべてのメソッドを再定義することなく継承します。これは継承の主な利点の 1 つであり、冗長なコードを書く手間を省いてくれます。
  2. types クラス変数を単に上書きすることで、DStock の新しいインスタンスを作成する際のデータの変換方法を変更しました。これは、クラス変数がサブクラスの振る舞いをカスタマイズするためにどのように使用できるかを示しています。
  3. 基底クラスである Stock は変更されず、依然として float 値で動作します。これは、サブクラスに対して行った変更が基底クラスに影響を与えないことを意味しており、良い設計原則です。
  4. 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)

このアプローチは一見すると簡単に見えるかもしれませんが、いくつかの欠点があります。

  1. オブジェクトの初期化とデータの変換という 2 つの異なる関心事を混在させています。これにより、コードが理解しにくく、保守も難しくなります。
  2. __init__ メソッドは、入力がすでに正しい型である場合でも常に変換を行うため、柔軟性が低下します。
  3. サブクラスが変換プロセスをカスタマイズする方法が制限されます。変換ロジックが __init__ メソッドに組み込まれている場合、サブクラスが変換ロジックを変更するのは難しくなります。
  4. コードが脆弱になります。変換のいずれかが失敗すると、オブジェクトを作成できず、プログラムにエラーが発生する可能性があります。

一方、クラスメソッドのアプローチはこれらの関心事を分離します。これにより、コードの各部分が単一の責任を持つため、コードがより保守可能で柔軟になります。

汎用的な CSV リーダーの作成

この最後のステップでは、汎用的な関数を作成します。この関数は CSV ファイルを読み込み、from_row() クラスメソッドを実装した任意のクラスのオブジェクトを作成することができます。これは、クラスメソッドを統一的なインターフェースとして使用する強力さを示しています。統一的なインターフェースとは、異なるクラスを同じように使用できることを意味し、これによりコードがより柔軟で管理しやすくなります。

read_portfolio() 関数の修正

まず、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 クラスに委ねています。型変換とは、データをある型から別の型に変更することで、例えば文字列を整数に変換することです。こうすることで、コードがより整理され、理解しやすくなります。

汎用的な CSV リーダーの作成

次に、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() メソッドを持つ限り、異なるクラスで使用することができるからです。

汎用的な CSV リーダーのテスト

汎用的なリーダーがどのように動作するかを確認するために、テストファイルを作成しましょう。次の内容で 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

StockDStock の両方のインスタンスとして読み込まれたポートフォリオデータ、および BusRide インスタンスとして読み込まれたバス路線データが表示されるはずです。これは、汎用的なリーダーが異なるクラスで動作することを証明しています。

このアプローチの主な利点

このアプローチはいくつかの強力な概念を示しています。

  1. 関心事の分離:データの読み込みとオブジェクトの作成が分離されています。これは、CSV ファイルを読み込むコードがオブジェクトを作成するコードと混在しないことを意味します。コードが理解しやすく、保守しやすくなります。
  2. ポリモーフィズム:同じコードが同じインターフェースに従う異なるクラスで動作することができます。この場合、クラスが from_row() メソッドを持っている限り、汎用的なリーダーはそれを使用することができます。
  3. 柔軟性:異なるクラスを使用することで、データの変換方法を簡単に変更することができます。例えば、Stock または DStock を使用して、ポートフォリオデータを異なる方法で処理することができます。
  4. 拡張性:リーダーのコードを変更することなく、リーダーと互換性のある新しいクラスを追加することができます。これにより、コードが将来に対応しやすくなります。

これは 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 コードを作成するために強力です。型変換をクラス内に配置し、クラスメソッドを介して統一的なインターフェースを提供することで、より汎用的なユーティリティを記述することができます。学習をさらに深めるために、より多くのユースケースを探索し、クラス階層を作成し、クラスメソッドを使用して複雑なデータ処理パイプラインを構築することができます。