Python オブジェクトシステムの基本

PythonPythonBeginner
今すぐ練習

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

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

はじめに

Python のオブジェクトシステムは、主に辞書を含む実装に基づいています。このセクションではそれについて説明します。

辞書の再訪

辞書は、名前付きの値のコレクションであることを覚えておきましょう。

stock = {
    'name' : 'GOOG',
    'shares' : 100,
    'price' : 490.1
}

辞書は、単純なデータ構造に一般的に使用されます。ただし、インタプリタの重要な部分にも使用されており、Python で最も重要なデータ型かもしれません。

辞書とモジュール

モジュール内では、辞書がすべてのグローバル変数と関数を保持します。

## foo.py

x = 42
def bar():
  ...

def spam():
  ...

foo.__dict__ または globals() を調べると、辞書が見えます。

{
    'x' : 42,
    'bar' : <function bar>,
   'spam' : <function spam>
}

辞書とオブジェクト

ユーザ定義オブジェクトも、インスタンスデータとクラスの両方に辞書を使用します。実際、オブジェクトシステム全体は、主に辞書の上に置かれる追加の層に過ぎません。

辞書はインスタンスデータである __dict__ を保持します。

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG','shares' : 100, 'price': 490.1 }

この辞書(およびインスタンス)は、self に代入することで埋めます。

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

インスタンスデータである self.__dict__ は、次のようになります。

{
    'name': 'GOOG',
   'shares': 100,
    'price': 490.1
}

各インスタンスは独自のプライベート辞書を持ちます。

s = Stock('GOOG', 100, 490.1)     ## {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45)     ## {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

あるクラスのインスタンスを100個作成した場合、データを保持する辞書が100個あります。

クラスメンバー

別の辞書がメソッドを保持します。

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, nshares):
        self.shares -= nshares

この辞書は Stock.__dict__ にあります。

{
    'cost': <function>,
   'sell': <function>,
    '__init__': <function>
}

インスタンスとクラス

インスタンスとクラスは互いに関連付けられています。__class__ 属性はクラスを指し戻します。

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG','shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>

インスタンス辞書は各インスタンス固有のデータを保持しますが、クラス辞書はすべてのインスタンスによって共有されるデータをまとめて保持します。

属性アクセス

オブジェクトを操作する際、. 演算子を使ってデータとメソッドにアクセスします。

x = obj.name          ## 取得
obj.name = value      ## 設定
del obj.name          ## 削除

これらの操作は、内部にある辞書に直接関連付けられています。

インスタンスの変更

オブジェクトを変更する操作は、その下にある辞書を更新します。

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG','shares': 100, 'price': 490.1 }
>>> s.shares = 50       ## 設定
>>> s.date = '6/7/2007' ## 設定
>>> s.__dict__
{ 'name': 'GOOG','shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        ## 削除
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

属性の読み取り

インスタンスの属性を読み取るとしましょう。

x = obj.name

属性は2つの場所に存在する可能性があります。

  • ローカルなインスタンス辞書。
  • クラス辞書。

両方の辞書を確認する必要があります。まず、ローカルの __dict__ を確認します。見つからなかった場合は、__class__ を通じてクラスの __dict__ を確認します。

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

この検索スキームが、クラスのメンバーがすべてのインスタンスによって共有される仕組みです。

継承の仕組み

クラスは他のクラスから継承することができます。

class A(B, C):
  ...

基底クラスは各クラスのタプルに格納されています。

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

これは親クラスへのリンクを提供します。

継承を伴う属性の読み取り

論理的には、属性を見つけるプロセスは以下の通りです。まず、ローカルの __dict__ を確認します。見つからなかった場合は、クラスの __dict__ を確認します。クラス内で見つからなかった場合は、__bases__ を通じて基底クラスを確認します。ただし、次に説明するこのプロセスにはいくつかの微妙な点があります。

単一継承による属性の読み取り

継承階層では、属性は順番に継承木を辿って見つけられます。

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

単一継承の場合、最上位までのパスは1つだけです。最初の一致が見つかったときに探索を停止します。

メソッド解決順序 (Method Resolution Order: MRO)

Python は継承チェーンを事前に計算し、それをクラスの _MRO_ 属性に格納します。これを確認することができます。

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
 <class '__main__.B'>, <class '__main__.A'>,
 <type 'object'>)
>>>

このチェーンは メソッド解決順序 と呼ばれます。属性を見つけるために、Python はこの MRO を順に辿ります。最初の一致が採用されます。

多重継承における MRO

多重継承の場合、最上位までの単一のパスはありません。例を見てみましょう。

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

属性にアクセスしたときに何が起こりますか?

e = E()
e.attr

属性検索プロセスが行われますが、その順序は何でしょうか? それが問題です。

Python は 協調的多重継承 を使用しており、クラスの順序付けに関するいくつかの規則を遵守しています。

  • 子クラスは常に親クラスよりも先にチェックされます
  • 親クラス(複数の場合)は常に列挙された順序でチェックされます。

MRO は、それらの規則に従って階層内のすべてのクラスをソートすることによって計算されます。

>>> E.__mro__
(
  <class 'E'>,
  <class 'C'>,
  <class 'A'>,
  <class 'D'>,
  <class 'B'>,
  <class 'object'>)
>>>

根本的なアルゴリズムは「C3 線形化アルゴリズム」と呼ばれます。家が火事になり避難しなければならない場合に従うのと同じ順序付けの規則をクラス階層が遵守している限り、正確な詳細は重要ではありません。子供たちが先で、その後に親が続きます。

奇妙なコードの再利用(多重継承を含む)

2つのまったく関係のないオブジェクトを考えてみましょう。

class Dog:
    def noise(self):
        return 'Bark'

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        ## LoudBike(以下)とのコードの共通性
        return super().noise().upper()

そして

class Bike:
    def noise(self):
        return 'On Your Left'

    def pedal(self):
        return 'Pedaling!'

class LoudBike(Bike):
    def noise(self):
        ## LoudDog(上記)とのコードの共通性
        return super().noise().upper()

LoudDog.noise()LoudBike.noise() の実装にはコードの共通性があります。実際、コードはまったく同じです。自然なことながら、そのようなコードはソフトウェアエンジニアを引き付けるはずです。

「ミックスイン」パターン

「ミックスイン」パターンは、コードの断片を持つクラスです。

class Loud:
    def noise(self):
        return super().noise().upper()

このクラスは単独では使用できません。継承を通じて他のクラスと混合します。

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

不思議なことに、大音量の機能が今度は一度だけ実装され、2つのまったく関係のないクラスで再利用されました。このようなトリックは、Pythonにおける多重継承の主な用途の1つです。

なぜ super() を使うのか

メソッドをオーバーライドする際は常に super() を使用します。

class Loud:
    def noise(self):
        return super().noise().upper()

super() は、MRO 上の 次のクラス に委譲します。

厄介なことに、それが何であるかはわかりません。特に多重継承が使われている場合、それが何であるかはわかりません。

いくつかの注意点

多重継承は強力なツールです。力には責任が伴うことを忘れないでください。フレームワークやライブラリは時々、コンポーネントの組み合わせに関する高度な機能にそれを使用します。さて、それを見たことを忘れましょう。

セクション4では、株式保有を表す Stock クラスを定義しました。この演習では、そのクラスを使用します。インタプリタを再起動していくつかのインスタンスを作成しましょう:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

演習5.1:インスタンスの表現

対話型シェルで、作成した2つのインスタンスの内部辞書を調べましょう:

>>> goog.__dict__
... 出力を見てください...
>>> ibm.__dict__
... 出力を見てください...
>>>

演習5.2:インスタンスデータの変更

上記のインスタンスの1つに新しい属性を設定してみましょう:

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... 出力を見てください...
>>> ibm.__dict__
... 出力を見てください...
>>>

上記の出力では、goog インスタンスに date という属性があるのに対し、ibm インスタンスにはないことに気付くでしょう。重要なことは、Pythonは実際に属性に対して何の制限も設けていないということです。たとえば、インスタンスの属性は __init__() メソッドで設定されたものに限定されません。

属性を設定する代わりに、新しい値を直接 __dict__ オブジェクトに入れてみましょう:

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

ここで、インスタンスが辞書の上にある単なる層であるという事実に気付きます。注:辞書の直接操作はまれであることを強調する必要があります。常にコードを (.) 構文を使うように書く必要があります。

演習5.3:クラスの役割

クラス定義を構成する定義は、そのクラスのすべてのインスタンスによって共有されます。すべてのインスタンスがその関連するクラスに戻るリンクを持っていることに注意してください:

>>> goog.__class__
... 出力を見てください...
>>> ibm.__class__
... 出力を見てください...
>>>

インスタンスでメソッドを呼び出してみましょう:

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

名前 'cost' は goog.__dict__ または ibm.__dict__ のどちらにも定義されていないことに注意してください。代わりに、それはクラス辞書によって提供されています。これを試してみましょう:

>>> Stock.__dict__['cost']
... 出力を見てください...
>>>

辞書を介して直接 cost() メソッドを呼び出してみましょう:

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

クラス定義で定義された関数をどのように呼び出しているかと、self 引数がインスタンスをどのように取得するかに注意してください。

Stock クラスに新しい属性を追加してみましょう:

>>> Stock.foo = 42
>>>

この新しい属性がすべてのインスタンスにどのように表示されるかに注意してください:

>>> goog.foo
42
>>> ibm.foo
42
>>>

ただし、それはインスタンス辞書の一部ではないことに注意してください:

>>> goog.__dict__
... 出力を見て、'foo' 属性がないことに注意してください...
>>>

インスタンスで foo 属性にアクセスできる理由は、Pythonがインスタンス自体に何かを見つけられない場合、常にクラス辞書をチェックするからです。

注:この演習のこの部分は、クラス変数として知られるものを示しています。たとえば、次のようなクラスがあるとしましょう:

class Foo(object):
     a = 13                  ## クラス変数
     def __init__(self,b):
         self.b = b          ## インスタンス変数

このクラスでは、クラス自体の本体で割り当てられた変数 a は「クラス変数」です。作成されるすべてのインスタンスによって共有されます。たとえば:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a          ## クラス変数を調べる(両方のインスタンスで同じ)
13
>>> g.a
13
>>> f.b          ## インスタンス変数を調べる(異なる)
10
>>> g.b
20
>>> Foo.a = 42   ## クラス変数の値を変更する
>>> f.a
42
>>> g.a
42
>>>

演習5.4:束縛メソッド

Pythonの微妙な機能の1つは、メソッドを呼び出すことが実際には2つのステップと束縛メソッドと呼ばれるものを含んでいることです。たとえば:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

束縛メソッドは実際にはメソッドを呼び出すために必要なすべての要素を含んでいます。たとえば、メソッドを実装する関数の記録を保持しています:

>>> s.__func__
<function sell at 0x10049af50>
>>>

これは、Stock 辞書にあるものと同じ値です。

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

束縛メソッドはまた、self 引数であるインスタンスを記録します。

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

() を使用して関数を呼び出すと、すべての要素がまとまります。たとえば、s(25) を呼び出すと、実際にはこれが行われます:

>>> s.__func__(s.__self__, 25)    ## s(25) と同じ
>>> goog.shares
50
>>>

演習5.5:継承

Stock から継承する新しいクラスを作成します。

>>> class NewStock(Stock):
        def yow(self):
            print('Yow!')

>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

継承は、属性の検索プロセスを拡張することによって実装されます。__bases__ 属性は、直近の親クラスのタプルを持っています:

>>> NewStock.__bases__
(<class'stock.Stock'>,)
>>>

__mro__ 属性は、属性の検索対象となるすべての親クラスのタプルを持っています。

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class'stock.Stock'>, <class 'object'>)
>>>

上記のインスタンス ncost() メソッドが見つけられる方法は次の通りです:

>>> for cls in n.__class__.__mro__:
        if 'cost' in cls.__dict__:
            break

>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

まとめ

おめでとうございます! 辞書の復習の実験を完了しました。 スキルを向上させるために、LabExでさらに実験を行って練習できます。