cover_effective_python

【 Effective Python, 2nd Edition 】Python においてクラス属性に厳密な private が無いのは何故? できる限り利用すべきではない理由とは? それでも private の使用が有効な状況とは? 投稿一覧へ戻る

Tags: Python , Effective , attribute , private , public

Published 2020年7月13日20:17 by T.Tsuyoshi

Python におけるクラス属性へのアクセスは基本 public ですが、属性名の前に __ ( _ を2つ) 付けることで private にすることができます。


class MyObject:
def __init__(self):
self.public_field = 5
self.__private_field = 10

def get_private_field(self):
return self.__private_field

@classmethod
def get_private_field_of_instance(cls, instance):
return instance.__private_field * 10



Public の属性へはドット演算子 ( dot [.] operator ) を使って誰でもアクセスすることができますよね。


foo = MyObject()
print(foo.public_field)
# 5



Private の属性へは同じクラス内で定義されているメソッドからは直接アクセスできます。


print(foo.get_private_field())
# 10



クラスメソッドもクラス内で定義されていますから、もちろん private 属性にアクセスできます。


print(MyObject.get_private_field_of_instance(foo))
# 100



が、クラス外から直接アクセスしようとするとエラーになっちゃいますね。


print(foo.__private_field)
# Traceback ...
# AttributeError: 'MyObject' object has no attribute '__private_field'



そしてご期待通り、サブクラスから親クラスの private フィールドにアクセスすることはできません。


class MyParentClass:
def __init__(self):
self.__private_field = 77


class MyChildClass(MyParentClass):
def get_private_field(self):
return self.__private_field


boo = MyChildClass()
print(boo.get_private_field())
# Traceback ...
# AttributeError: 'MyChildClass' object has no attribute '_MyChildClass__private_field'



Python においてここまでの例のように private 属性が機能するのは、ただ単純に Python コンパイラが 属性名を変更 しているからに過ぎません。


直近のサブクラスの例で言えば、MyChildClass.get_private_field() による private 変数 __private_field へのアクセスは、Python コンパイラによって _MyChildClass__private_field というフィールド名へのアクセス、に変更されているんです。


実際の __private_field は親クラスである MyParentClass の __init__() 内で定義されていますから、この private 変数の正式な名前は _MyParentClass__private_field です。


結局サブクラスから親クラスの private 属性へのアクセスが失敗しているのは、このように属性名が変更された結果、「一致する変数名は存在しません」と判断されたからなんです。


ということは、この名前変更のスキームを逆手に取れば、サブクラスのようなクラス外部から簡単に private 属性にアクセスできる、ということにほかなりません。
そして、実際その通りなんです。


print(boo._MyParentClass__private_field)
# 77



しかも、オブジェクトの属性を保存する辞書を見てみると、private 属性も変更後の名前でしっかり確認することができちゃうんです。


print(boo.__dict__)
# {'_MyParentClass__private_field': 77}



private 属性だからといって「絶対に秘密だよ」という気は Python には毛頭無い、ということです、開けっぴろげなんです。


こういった言語仕様になっているのは、 Python という言語を語る上でよく引用されるモットーの1つが関連しています。


"We are all consenting adults here." (Python を使っている人達は全員『分をわきまえた』大人だよ)


つまり、「やりたいと思ったことができない言語は要らない」し「望んだ通りに機能を拡張するのはその本人の意思次第」だけれども「その結果としてのリスクを負うのもその本人の責任」だ、ということです。
「尻拭いを自分でできるのなら、本人のやりたい通りにやらせよう」、ということです。


Python では、クラスを思い通りに拡張できる自由とそれに伴う利点はそこから生じる短所に勝る、と考えられているんですね。


しかも、__getattr__、__getattribute__、__setattr__ といった属性値を操作する関数までが提供されいつでもオブジェクト内部を「弄る」ことが可能な環境において、果たして属性へのアクセスを制限するための仕様がどれだけの意味を持つのでしょうか?


「大人」である Python の使い手は、予期せぬ内部属性へのアクセスの影響を最小限にするために、PEP 8 で定義されている Python のコーディング規約 (PEP 8 -- Style Guide for Python Coding) に可能な限り従いつつ、自由を謳歌しましょう。


続いて、private 属性がクラスの拡張に障害になってしまう例を見てみます。


class MyStringClass:
def __init__(self, value):
self.__value = value

def get_value(self):
return str(self.__value)


woo = MyStringClass(5)
print(f"{woo.get_value()!r}")
# '5'



将来的に誰かが(自分も含めて、です)、常に string として値が返ってきてしまう点を変更するためにサブクラスを作成するとします。


しかし内部で private として入力値を保持するようにしてしまっているため、この値に直接アクセスすることはできません、あっ、できますね!


class MyIntSubclass(MyStringClass):
def get_value(self):
return int(self._MyStringClass__value)


hoo = MyIntSubclass('5')
print(f"{hoo.get_value()!r}")
# 5



もしこのように private 属性にアクセスをしている大元の親クラスの階層設計が変更されてしまったら...


# 新たにベースクラスを作成しました。このクラスの private フィールドで入力値を保存します。
class MyBaseClass:
def __init__(self, value):
self.__value = value

def get_value(self):
return self.__value


# 新たに作成したベースクラスから派生させました。もうこのクラスでは private フィールド ( __value ) を持っていません。
class MyStringClass(MyBaseClass):
def get_value(self):
return str(super().get_value())


# 親クラス ( MyStringClass ) が、新しく作成されたクラス ( MyBaseClass ) のサブクラスとして設計し直された、ということは知りません。
class MyIntSubclass(MyStringClass):
def get_value(self):
return int(self._MyStringClass__value)


koo = MyIntSubclass(5)
print(f"{koo.get_value()!r}")
# Traceback...
# AttributeError: 'MyIntSubclass' object has no attribute '_MyStringClass__value'



もちろんエラーになります。
それは、このクラス階層の中で _MyStringClass__value はもはや存在していないからです。
_MyBaseClass__value になってしまっています。


上の例のようにクラス設計上の自由度を奪わないためにも private 属性の使用は控え、最低でも protected 属性を利用し、将来的に親クラスとして利用するかもしれない自分も含めたプログラマーのためにしっかりとしたドキュメントを残しましょう。


class MyBaseClass:
def __init__(self, value):
"""
ユーザーからの入力値を protected 属性として保存します。
各サブクラスではこの属性値を直接変更すべきではなく、immutable として扱ってください。
"""
self._value = value

def get_value(self):
raise NotImplementedError


class MyStringClass(MyBaseClass):
def get_value(self):
return str(self._value)


class MyIntSubclass(MyStringClass):
def get_value(self):
return int(self._value)


boo = MyStringClass(5)
print(f"{boo.get_value()!r}")
# '5'

hoo = MyIntSubclass(5)
print(f"{hoo.get_value()!r}")
# 5



ただし private 属性の採用を真剣に検討すべき場面がただ1つだけあります。


それは、クラス階層内において「名前の衝突 ( naming conflicts )」が懸念される場合です。


class MyBottom:
def __init__(self):
self._value = 5

def get(self):
return self._value


class MyTop(MyBottom):
def __init__(self):
super().__init__()
self._value = 'hello'


woo = MyTop()
print(f"{woo.get()!r} と {woo._value!r} の値は違うはずです。")
# 'hello' と 'hello' の値は違うはずです。



こういった問題は、API として外部に公開しているプログラムを構成するクラスに常について回るものです。
特に、"value" であるとか "key" といった使用される頻度の高い属性名に関しては特に懸念されます。


当たり前ですけど、API を提供する側では、ユーザーが作成するサブクラスについて「口出し」をすることはできませんからね。


そして、この問題に関しては、属性名に自動的に '_classname' を付加してクラス独自の属性名に変更してアクセスしてくれる private 属性は非常に有効なんです。


class MyBottom:
def __init__(self):
self.__value = 5

def get(self):
return self.__value


class MyTop(MyBottom):
def __init__(self):
super().__init__()
self._value = 'hello'


woo = MyTop()
print(f"{woo.get()!r} と {woo._value!r} の値は違うはずです。")
# 5 と 'hello' の値は違うはずです。



まとめ:

1: Python における private 属性は、完全なプライベートアクセスを強制するものではありません。

2: クラス設計時は、サブクラスに最大限の自由度を与えられるようにするところから始めて、必要に応じて制限を加えていくようにしましょう。

3: private 属性によってアクセスを制限するのではなく、protected 属性 + ドキュメントによってサブクラス設計時の自由度と注意喚起を両立させましょう。

4: サブクラス作成時の「名前の衝突」が懸念される場合に限り private 属性の使用を検討しましょう。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)