クラスとカプセル化

PythonPythonBeginner
今すぐ練習

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

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

はじめに

クラスを書くとき、内部の詳細をカプセル化しようとするのが一般的です。このセクションでは、これに関するPythonのプログラミング慣用句をいくつか紹介します。これには、プライベート変数やプロパティが含まれます。

パブリックとプライベート

クラスの主な役割の1つは、オブジェクトのデータと内部実装の詳細をカプセル化することです。ただし、クラスはまた、外部世界がオブジェクトを操作するために使用するはずの「パブリック」インターフェイスも定義します。実装の詳細とパブリックインターフェイスのこの違いは重要です。

問題

Pythonでは、クラスとオブジェクトに関するほとんどすべてが「オープン」になっています。

  • オブジェクトの内部を簡単に調べることができます。
  • 好きなように変更することができます。
  • アクセス制御(すなわち、プライベートなクラスメンバー)の強い概念はありません。

これは、内部実装の詳細を分離しようとしているときの問題です。

Pythonにおけるカプセル化

Pythonは、何かの意図された使い方を示すためにプログラミング規約に依存しています。これらの規約は命名に基づいています。言語が規則を強制するのではなく、プログラマーが規則を守ることに責任があるという一般的な考え方があります。

プライベート属性

先頭に _ がある属性名はすべて「プライベート」と見なされます。

class Person(object):
    def __init__(self, name):
        self._name = 0

前述の通り、これはただのプログラミングスタイルにすぎません。まだアクセスしたり変更したりすることができます。

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

一般的なルールとして、先頭に _ がある名前は、変数、関数、モジュール名のいずれであれ、内部実装と見なされます。そのような名前を直接使用している場合、多分何か間違っています。より高レベルの機能を探してください。

単純な属性

次のクラスを考えてみましょう。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

驚くべきことに、属性を全く任意の値に設定できます。

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

これを見ると、追加のチェックが必要だと思うかもしれません。

s.shares = '50'     ## TypeErrorが発生します、これは文字列です

どのようにすればよいでしょうか。

管理属性

一つのアプローチ:アクセサメソッドを導入する。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.set_shares(shares)
        self.price = price

    ## "取得" 操作を層化する関数
    def get_shares(self):
        return self._shares

    ## "設定" 操作を層化する関数
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        self._shares = value

残念ながら、これで既存のコードがすべて壊れてしまいます。s.shares = 50s.set_shares(50) になります。

プロパティ

前のパターンに代わるアプローチがあります。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

通常の属性アクセスは、今では @property@shares.setter の下のゲッターとセッターメソッドをトリガーします。

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         ## @property をトリガー
50
>>> s.shares = 75    ## @shares.setter をトリガー
>>>

このパターンでは、ソースコードに変更は必要ありません。新しいセッターは、クラス内での代入時、__init__() メソッド内を含めても呼び出されます。

class Stock:
    def __init__(self, name, shares, price):
     ...
        ## この代入は下のセッターを呼び出します
        self.shares = shares
     ...

  ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

プロパティとプライベート名の使用の間にはしばしば混乱があります。プロパティは内部的に _shares のようなプライベート名を使用しますが、クラスの残りの部分(プロパティではない)は shares のような名前を引き続き使用できます。

プロパティは計算されたデータ属性にも便利です。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price
  ...

これにより、余分な丸括弧を省略でき、実際にはメソッドであることを隠すことができます。

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## インスタンス変数
100
>>> s.cost   ## 計算された値
49010.0
>>>

一貫したアクセス

最後の例は、オブジェクトにより一貫したインターフェイスを与える方法を示しています。これを行わない場合、オブジェクトの使い方が分かりにくくなるかもしれません。

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## メソッド
49010.0
>>> b = s.shares ## データ属性
100
>>>

なぜ cost には () が必要なのに、shares には必要ないのでしょうか。プロパティを使うことでこれを解決できます。

デコレータ構文

@ 構文は「デコレーション」と呼ばれます。これは、直後に続く関数定義に適用される修飾子を指定します。

...
@property
def cost(self):
    return self.shares * self.price

セクション7で詳細を説明します。

__slots__ 属性

属性名のセットを制限することができます。

class Stock:
    __slots__ = ('name','_shares','price')
    def __init__(self, name, shares, price):
        self.name = name
     ...

他の属性に対してはエラーが発生します。

>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in?
AttributeError: 'Stock' object has no attribute 'prices'

これはエラーを防ぎ、オブジェクトの使用を制限しますが、実際にはパフォーマンス向上のために使用され、Pythonがメモリをより効率的に使用するようになります。

カプセル化に関する最後のコメント

プライベート属性、プロパティ、スロットなどに対して過度にこだわらないでください。それらは特定の目的を果たし、他のPythonコードを読む際に見ることがあります。ただし、ほとんどの日常のコーディングでは必要ありません。

演習5.6:シンプルなプロパティ

プロパティは、オブジェクトに「計算済み属性」を追加するための便利な方法です。stock.py では、Stock オブジェクトを作成しました。作成したオブジェクトでは、異なる種類のデータの抽出方法にわずかな不整合があることに注意してください。

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

特に、cost がメソッドであるため、cost に追加の () を付ける必要があることに注意してください。

cost() の追加の () をなくすことができます。Stock クラスを取得して変更して、コスト計算が次のように機能するようにします。

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

cost をプロパティとして定義したため、s.cost() を関数として呼び出してみて、機能しないことを確認してください。

>>> s.cost()
... 失敗...
>>>

この変更を行うと、以前の pcost.py プログラムが壊れる可能性があります。cost() メソッドの () を取り除く必要がある場合があります。

✨ 解答を確認して練習

演習5.7:プロパティとセッター

shares 属性を変更して、値をプライベート属性に格納し、常に整数値に設定されるようにするための一対のプロパティ関数を使用します。期待される動作の例は次のとおりです。

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: expected an integer
>>>
✨ 解答を確認して練習

演習5.8:スロットの追加

Stock クラスを変更して、__slots__ 属性を持たせます。その後、新しい属性を追加できないことを確認します。

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... 何が起こるか見る...
>>>

__slots__ を使用すると、Pythonはオブジェクトのより効率的な内部表現を使用します。上記の s の基礎となる辞書を調べようとすると何が起こりますか?

>>> s.__dict__
... 何が起こるか見る...
>>>

__slots__ は、データ構造として機能するクラスに対する最適化として最も一般的に使用されます。スロットを使用することで、そのようなプログラムははるかに少ないメモリを使用し、少し高速に実行されます。ただし、他のほとんどのクラスでは __slots__ を避けるべきです。

✨ 解答を確認して練習

まとめ

おめでとうございます!クラスとカプセル化の実験を完了しました。LabExでさらに多くの実験を行って、スキルを向上させましょう。