Pythonでの属性へのアクセス
著者: Leonardo Giordani - 12/01/2015
はじめに
Pythonは、オブジェクト指向のパラダイムを最大限に推し進めようとする言語です。このことは、そのオブジェクトモデルが他の言語に比べて非常に強力であることを意味していますが、同時にPythonコードの振る舞いが新しいプログラマーにとって驚くべきものになるかもしれません。
この記事では、Pythonがオブジェクトの属性にアクセスするために提供しているメソッドをレビューし、この美しい言語でプログラミングを始めようとしている人たちに、この問題の包括的な概要を提供しようと思います。
属性とは?
名前の付け方は言語によって異なるかもしれませんので、名前を付けてみましょう。Pythonでは、オブジェクトの中に含まれるすべてのものを 属性(Attribute) と呼びます。Pythonでは、単純なデータと関数の間に実際の区別はなく、どちらもオブジェクトであるため、属性について述べることは、メソッドについても完全に有効です。
今回の記事では、次のようなクラスを例に挙げます。このクラスは本を表していて、タイトルと著者を属性として持っています。また、本の文字列表現を返す get_entry() メソッドを提供しています。
code: python
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def get_entry(self):
return "{0} by {1}".format(self.title, self.author)
このクラスの全てのインスタンスは、オブジェクトの祖先が提供する標準的な属性に加えて、title、 author、 get_entryの3つの属性を含みます。
Python 2ユーザー:Python 2では、class Book(object): を書いて、Book が新しいスタイルのクラスであることを指定しなければならないことを覚えておいてください。
基本的な属性アクセス
Pythonでは、広く受け入れられているドット構文を使用して、オブジェクトの属性を呼び出すことができます。
code: python
>> b = Book(title="Pawn of Prophecy", author="David Eddings")
>> b.title
'Pawn of Prophecy'
すでに述べたように、この仕組みはメソッドにも有効です。
code: python
>> b.get_entry
<bound method Book.get_entry of <__main__.Book object at 0xb703952c>>
ここでは、メソッドにアクセスするときに何が起こるかを示すために、呼び出し用の括弧(())を意図的に省略しています。関数とバインドメソッドの違いについての詳しい説明は次の記事をご覧ください。
探している属性がオブジェクトに含まれていない場合、PythonはAttributeError例外を発生させます。
code: python
>> b.publisher
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Book' object has no attribute 'publisher'
>>
また、Pythonのオブジェクトは __class__ や __doc__ のような幅広い自動生成の属性を提供していることを覚えておいてください。これらの属性は、標準的な属性と全く同じように読むことができます。なぜなら、Pythonでは、ダブルアンダースコア(__)の接頭辞と接尾辞は、プログラマー間の紳士的な合意に過ぎないからです。
code: python
>> b.__class__
<class '__main__.Book'>
属性の値を変更しようとしたとき(属性を書いたとき)、構文は変わりません。
code: python
>> b = Book(title="Pawn of Prophecy", author="David Eddings")
>> b.author = "David Carroll Eddings"
>> b.author
'David Carroll Eddings'
プロパティ
時には、他の属性から得られる値や、一般的にはその時点で計算される値を持つ属性を持ちたいことがあります。このような状況に対処する標準的な方法は、get_entry() で行ったように、ゲッター(Getter) と呼ばれる読み込みメソッドを作成することです。
Pythonではメソッドを 「マスク」することができ、データ属性(この場合は プロパティ(Property) と呼びます)でエイリアス化する(別名を割り当てること)ことができます。
code: python
class Book(object):
def __init__(self, title, author):
self.title = title
self.author = author
def get_entry(self):
return "{0} by {1}".format(self.title, self.author)
entry = property(get_entry)
上記の構文は、entry属性を定義するもので、読み込まれると自動的に self.get_entry() が呼び出されます。
code: python
>> b = Book(title="Pawn of Prophecy", author="David Eddings")
>> b.entry
'Pawn of Prophecy by David Eddings'
プロパティでは、セッター(Setter) と呼ばれる書き込みメソッドを指定することができ、プロパティ自体の値を変更しようとすると、自動的に呼び出されます。
code: python
class Book(object):
def __init__(self, title, author):
self.title = title
self.author = author
def _get_entry(self):
return "{0} by {1}".format(self.title, self.author)
def _set_entry(self, value):
if " by " not in value:
raise ValueError("Entries shall be formatted as '<title> by <author>'")
self.title, self.author = value.split(" by ")
entry = property(_get_entry, _set_entry)
_set_entry()メソッドは、2番目のパラメータとして値を受け取ることに注意してください。ゲッターやセッターがプロパティで隠蔽されている場合、それらが直接使用されることを意図していないことを示すために、その名前をアンダースコアで始めるのは良いことかもしれません。ただし、これには言語上の特別な意味はなく、プログラマー間の慣習に過ぎないことを覚えておいてください。
code: python
>> b = Book(title="Pawn of Prophecy", author="David Eddings")
>> b.entry
'Pawn of Prophecy by David Eddings'
>> b.entry = "Queen of Sorcery by David Carroll Eddings"
>> b.title
'Queen of Sorcery'
>> b.author
'David Carroll Eddings'
>> b.entry = "Magician's Gambit, David Carroll Eddings"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in _set_entry
ValueError: Entries shall be formatted as '<title> by <author>'
属性は通常、__init__() メソッド(コンストラクタ機構の一部)を通じてインスタンスに定義されますが、プロパティはクラス自体に含まれます。
code: python
>> Book.title
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'Book' has no attribute 'title'
>> Book.entry
<property object at 0xb676a374>
ただし、そのクラスのすべてのインスタンスは、属性とメソッドの間の自動結合のようなものである同じプロパティ・オブジェクトを共有しており、属性の値そのものではないことを覚えておいてください。
属性アクセスのソフトコード化
最初のセクションで行ったような基本的な属性アクセスを書いた場合、あなたは属性名をハードコーディングしています。探している属性はソースコード自体の一部であり、コーディング時に知ることができます。
変数に文字列として含まれている属性名にアクセスしたい場合はどうなるでしょうか?これは通常、ユーザが見たい属性を対話的に指定できるデバッガや検査ツールを書くときに起こります。
この 「間接的 」なアクセスを行うために、Pythonはオブジェクトと属性名を受け取る getattr() 組み込み関数を提供しています。
code: python
>> getattr(b, 'title')
'Pawn of Prophecy'
>> getattr(b, 'get_entry')
<bound method Book.get_entry of <__main__.Book object at 0xb703952c>>
このタイプの属性アクセスは、多くの属性にアクセスする場合にも便利で、単純なforループやリスト内包を書くことができます。
code: python
... getattr(b, attr)
...
'Pawn of Prophecy'
'David Eddings'
このタイプのアクセスはPythonでは完全に有効ですが、直接的なドットアクセスが使える限りは使用すべきではありません。トリッキーなコードを書くことはとても賢いことのように見えますが、デバッグに関しては「シンプルであるほど良い」のです。
失敗しないために
ドット構文や getattr() で存在しない属性にアクセスしようとしたとき、Python は AttributeError 例外を発生させる前に最後のチャンスを与えます。属性の名前を渡して 特殊メソッド __getattr__()を呼び出します (ダブルアンダースコアに注意してください): 先の例では、b.publisher にアクセスするとき、Python は b.__getattr__('publisher') を呼び出します。
この場合、Bookクラスやその祖先が __getattr__( )メソッドを定義していないので、属性アクセスは失敗し、Pythonは例外を発生させます。
実際に動作することを示すために、このメソッドを定義してみましょう。
code: ppython
class Book(object):
def __init__(self, title, author):
self.title = title
self.author = author
def get_entry(self):
return "{0} by {1}".format(self.title, self.author)
def __getattr__(self, attr):
return None
警告: これは __getattr__() がどのように動作するかを示すための例に過ぎません。ここで紹介されているコードは、Pythonプログラミングの良いお手本とはみなされません。
code: python
>> b = Book(title="Pawn of Prophecy", author="David Eddings")
>> b.title
'Pawn of Prophecy'
>> b.publisher
>> b.somename
見ての通り、publisher 属性と somename 属性は、実際にはオブジェクト内に存在しなくても、 正しくアクセスされています。
__getattr__() と getattr() の上手な使い方を見つけるためには、もう少し複雑なことに踏み込まなければなりません。このトピックについては、こちらの記事で詳しく説明しています。
getattr() と __getattr__() には、書き込みアクセスを管理するための対応策、すなわち setattr() と __setattr__() があります。
奥深くにある秘密
すべてのPythonオブジェクト(object自身から始まる)は、__getattribute__() という非常に特別なメソッドを含んでおり、これは決して明示的に呼び出したりオーバーライドしたりしてはいけません。このメソッドは、オブジェクト内部の属性解決を実装し、継承階層を介した属性検索を行い、プロパティを解決し、__getattr__() を呼び出し、必要に応じてAttributeError 例外を発生させます。
このメソッドは非常に複雑な性質を持っており、Pythonオブジェクトを動作させる上で最も重要な役割を担っているため、絶対にオーバーライドしてはいけません。もし __getattribute__() を明示的に扱う必要がある状況に遭遇したら、何か間違ったことをしたのだと確信するかもしれません。
まとめ
お分かりのように、あなたのPythonコードの95%は標準的なドット属性アクセスに基づいています。しかし、属性を管理する他の方法(プロパティ)を知り、舞台裏で何が起こっているか(getattr、 __getattr__()、 __getattribute__())を知っておくことは、Pythonの全ての力を使いこなすために必要不可欠であり、時には他の方法では非常に困難な非常にエレガントな解決策を導くことができます。