特殊メソッドの再定義

Intermediate

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

はじめに

この実験では、特殊メソッドを再定義することでオブジェクトの動作をカスタマイズする方法を学びます。また、ユーザー定義オブジェクトの表示方法を変更し、オブジェクトを比較可能にする方法も学びます。

さらに、コンテキストマネージャを作成する方法を学びます。この実験で変更するファイルは stock.py です。

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

__repr__ を使ったオブジェクト表現の改善

Python では、オブジェクトを文字列として 2 つの異なる方法で表現することができます。これらの表現はそれぞれ異なる目的を持ち、様々なシナリオで役立ちます。

1 つ目は 文字列表現 です。これは str() 関数によって作成され、print() 関数を使用すると自動的に呼び出されます。文字列表現は人間が読みやすいように設計されており、オブジェクトを理解しやすい形式で提示します。

2 つ目は コード表現 です。これは repr() 関数によって生成されます。コード表現は、オブジェクトを再作成するために必要なコードを示します。コード内でオブジェクトを正確かつ明確に表現する方法を提供することに重点が置かれています。

Python の組み込み date クラスを使った具体的な例を見てみましょう。これにより、文字列表現とコード表現の違いを実際に確認することができます。

>>> from datetime import date
>>> d = date(2008, 7, 5)
>>> print(d)              ## Uses str()
2008-07-05
>>> d                     ## Uses repr()
datetime.date(2008, 7, 5)

この例では、print(d) を使用すると、Python は date オブジェクト d に対して str() 関数を呼び出し、YYYY-MM-DD 形式の人間が読みやすい日付が得られます。対話型シェルで単に d と入力すると、Python は repr() 関数を呼び出し、date オブジェクトを再作成するために必要なコードが表示されます。

repr() 文字列を明示的に取得する方法はいくつかあります。以下に例を示します。

>>> print('The date is', repr(d))
The date is datetime.date(2008, 7, 5)
>>> print(f'The date is {d!r}')
The date is datetime.date(2008, 7, 5)
>>> print('The date is %r' % d)
The date is datetime.date(2008, 7, 5)

では、この概念を Stock クラスに適用しましょう。__repr__ メソッドを実装することで、このクラスを改善します。この特殊メソッドは、Python がオブジェクトのコード表現を必要とするときに呼び出されます。

これを行うには、エディタで stock.py ファイルを開きます。次に、Stock クラスに __repr__ メソッドを追加します。__repr__ メソッドは、Stock オブジェクトを再作成するために必要なコードを示す文字列を返す必要があります。

def __repr__(self):
    return f"Stock('{self.name}', {self.shares}, {self.price})"

__repr__ メソッドを追加した後、完成した Stock クラスは次のようになります。

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

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

では、修正した Stock クラスをテストしましょう。ターミナルで次のコマンドを実行して、Python の対話型シェルを開きます。

python3

対話型シェルが開いたら、次のコマンドを試してみましょう。

>>> import stock
>>> goog = stock.Stock('GOOG', 100, 490.10)
>>> goog
Stock('GOOG', 100, 490.1)

また、__repr__ メソッドが株式ポートフォリオでどのように機能するかも確認できます。以下に例を示します。

>>> import reader
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> portfolio
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44), Stock('MSFT', 200, 51.23), Stock('GE', 95, 40.37), Stock('MSFT', 50, 65.1), Stock('IBM', 100, 70.44)]

ご覧の通り、__repr__ メソッドにより、対話型シェルやデバッガで Stock オブジェクトを表示する際に、はるかに有益な情報が表示されるようになりました。現在では、各オブジェクトを再作成するために必要なコードが表示されるため、デバッグやオブジェクトの状態の理解に非常に役立ちます。

テストが終了したら、次のコマンドを実行して Python インタープリターを終了できます。

>>> exit()

__eq__ を使ってオブジェクトを比較可能にする

Python では、== 演算子を使って 2 つのオブジェクトを比較すると、実際には __eq__ という特殊メソッドが呼び出されます。デフォルトでは、このメソッドはオブジェクトの同一性を比較します。つまり、オブジェクトが同じメモリアドレスに格納されているかどうかをチェックし、内容を比較するわけではありません。

例を見てみましょう。Stock クラスがあり、同じ値を持つ 2 つの Stock オブジェクトを作成したとします。そして、== 演算子を使ってこれらを比較しようとします。Python インタープリターでは次のように操作できます。

まず、ターミナルで次のコマンドを実行して Python インタープリターを起動します。

python3

次に、Python インタープリターで以下のコードを実行します。

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
False

ご覧の通り、2 つの Stock オブジェクト ab の属性(namesharesprice)の値は同じですが、Python はこれらを異なるオブジェクトとみなします。なぜなら、これらは異なるメモリ位置に格納されているからです。

この問題を解決するために、Stock クラスに __eq__ メソッドを実装することができます。このメソッドは、Stock クラスのオブジェクトに == 演算子が使用されるたびに呼び出されます。

では、再度 stock.py ファイルを開きましょう。Stock クラスの中に、以下の __eq__ メソッドを追加します。

def __eq__(self, other):
    return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                         (other.name, other.shares, other.price))

このメソッドが行うことを分解してみましょう。

  1. まず、isinstance 関数を使って、other オブジェクトが Stock クラスのインスタンスであるかどうかをチェックします。これは、Stock オブジェクトを他の Stock オブジェクトとのみ比較したいからです。
  2. otherStock オブジェクトである場合、self オブジェクトと other オブジェクトの属性(namesharesprice)を比較します。
  3. 両方のオブジェクトが Stock インスタンスであり、それらの属性が同一である場合にのみ True を返します。

__eq__ メソッドを追加した後、完成した Stock クラスは次のようになります。

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

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

    def __eq__(self, other):
        return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                             (other.name, other.shares, other.price))

では、改善した Stock クラスをテストしましょう。再度 Python インタープリターを起動します。

python3

次に、Python インタープリターで以下のコードを実行します。

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
True
>>> c = stock.Stock('GOOG', 200, 490.1)
>>> a == c
False

素晴らしい!これで、Stock オブジェクトはメモリアドレスではなく、内容に基づいて適切に比較できるようになりました。

__eq__ メソッドの isinstance チェックは非常に重要です。これにより、Stock オブジェクト同士のみを比較することが保証されます。このチェックがない場合、Stock オブジェクトを Stock オブジェクトではないものと比較するとエラーが発生する可能性があります。

テストが終了したら、次のコマンドを実行して Python インタープリターを終了できます。

>>> exit()

コンテキストマネージャの作成

コンテキストマネージャは Python の特殊な種類のオブジェクトです。Python では、オブジェクトにはその振る舞いを定義するさまざまなメソッドがあります。コンテキストマネージャは特に、__enter____exit__ という 2 つの重要なメソッドを定義します。これらのメソッドは with 文と連携して動作します。with 文は、コードブロックに特定のコンテキストを設定するために使用されます。特定のことが起こる小さな環境を作成するようなもので、コードブロックが終了すると、コンテキストマネージャが後片付けを行います。

このステップでは、非常に便利な機能を持つコンテキストマネージャを作成します。これは、標準出力 (sys.stdout) を一時的にリダイレクトします。標準出力は、Python プログラムの通常の出力が行われる場所で、通常はコンソールです。標準出力をリダイレクトすることで、出力をファイルに送ることができます。これは、コンソールに表示されるだけの出力を保存したい場合に便利です。

まず、コンテキストマネージャのコードを書くための新しいファイルを作成する必要があります。このファイルを redirect.py と名付けます。ターミナルで以下のコマンドを使用して作成できます。

touch /home/labex/project/redirect.py

ファイルが作成されたら、エディタで開きます。開いたら、以下の Python コードをファイルに追加します。

import sys

class redirect_stdout:
    def __init__(self, out_file):
        self.out_file = out_file

    def __enter__(self):
        self.stdout = sys.stdout
        sys.stdout = self.out_file
        return self.out_file

    def __exit__(self, ty, val, tb):
        sys.stdout = self.stdout

このコンテキストマネージャが行うことを分解してみましょう。

  1. __init__: これは初期化メソッドです。redirect_stdout クラスのインスタンスを作成するときに、ファイルオブジェクトを渡します。このメソッドは、そのファイルオブジェクトをインスタンス変数 self.out_file に格納します。つまり、出力をリダイレクトする先を覚えておきます。
  2. __enter__:
    • まず、現在の sys.stdout を保存します。後で元に戻す必要があるため、これは重要です。
    • 次に、現在の sys.stdout をファイルオブジェクトで置き換えます。この時点から、通常はコンソールに出力されるものはすべてファイルに出力されるようになります。
    • 最後に、ファイルオブジェクトを返します。これは、with ブロック内でファイルオブジェクトを使用したい場合に便利です。
  3. __exit__:
    • このメソッドは、元の sys.stdout を復元します。したがって、with ブロックが終了した後は、出力は通常通りコンソールに戻ります。
    • このメソッドは 3 つのパラメータを受け取ります。例外の型 (ty)、例外の値 (val)、トレースバック (tb) です。これらのパラメータはコンテキストマネージャプロトコルで必要とされます。with ブロック内で発生する可能性のある例外を処理するために使用されます。

では、コンテキストマネージャをテストしましょう。テーブルの出力をファイルにリダイレクトするために使用します。まず、Python インタープリターを起動します。

python3

次に、インタープリターで以下の Python コードを実行します。

>>> import stock, reader, tableformat
>>> from redirect import redirect_stdout
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> formatter = tableformat.create_formatter('text')
>>> with redirect_stdout(open('out.txt', 'w')) as file:
...     tableformat.print_table(portfolio, ['name','shares','price'], formatter)
...     file.close()
...
>>> ## Let's check the content of the output file
>>> print(open('out.txt').read())
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

素晴らしい!コンテキストマネージャは期待通りに動作しました。テーブルの出力を out.txt ファイルに正常にリダイレクトしました。

コンテキストマネージャは Python の非常に強力な機能です。適切にリソースを管理するのに役立ちます。コンテキストマネージャの一般的な使用例をいくつか紹介します。

  • ファイル操作:ファイルを開くときに、コンテキストマネージャはエラーが発生した場合でもファイルが適切に閉じられることを保証します。
  • データベース接続:使用が終了した後にデータベース接続が閉じられることを確認できます。
  • スレッド化されたプログラムのロック:コンテキストマネージャは、リソースのロックとロック解除を安全に処理できます。
  • 一時的な環境設定の変更:コードブロックの間でいくつかの設定を変更し、自動的に元に戻すことができます。

このパターンは非常に重要です。with ブロック内で例外が発生した場合でも、リソースが適切にクリーンアップされることを保証するからです。

テストが終了したら、Python インタープリターを終了できます。

>>> exit()

まとめ

この実験では、__repr__ メソッドを使ってオブジェクトの文字列表現をカスタマイズする方法、__eq__ メソッドを使ってオブジェクトを比較可能にする方法、そして __enter____exit__ メソッドを使ってコンテキストマネージャを作成する方法を学びました。これらの特殊な「ダンダーメソッド」は、Python のオブジェクト指向機能の基礎となっています。

クラスにこれらのメソッドを実装することで、オブジェクトを組み込み型のように振る舞わせ、Python の言語機能とスムーズに統合することができます。特殊メソッドにより、カスタム文字列表現、オブジェクト比較、コンテキスト管理などのさまざまな機能が可能になります。Python の学習を進めるにつれて、その強力なオブジェクトモデルを活用するためのさらに多くの特殊メソッドを発見するでしょう。