質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.36%

特殊属性のdictの挙動の違いの確認方法について

受付中

回答 4

投稿

  • 評価
  • クリップ 1
  • VIEW 735

score 123

下記のコードで test1 = TestBorg1()でインスタンス化する際に{'state': 'second'}がどこで保持され、二回目のインスタンス化であるtest2 = TestBorg1()の際に{'state': 'second'}を出力できるのでしょうか?

一方で、self.__dict__ にself._shared_stateを入れないケースのTestBorg2の一回目のインスタンス化の際には、アウトプットを見る限り__dict__に保持されていません。
なぜでしょうか?

当質問で一番気になっていることは、__dict__に代入した値が一回目と二回目のインスタンス化の間でどうやって保持されているのか、が気になっています。また、その確認方法が気になっています。

コード

class TestBorg1:
    _shared_state = {}

    def __init__(self):
        print(f"self._shared_state1: {self._shared_state}")
        self.__dict__ = self._shared_state
        self.__dict__["state"] = "second"


class TestBorg2:
    _shared_state = {}

    def __init__(self):
        print(f"self._shared_state2: {self._shared_state}")
        self.__dict__["val"] = self._shared_state
        self.__dict__["val"] = "test"


if __name__ == "__main__":
    test1 = TestBorg1()
    test2 = TestBorg1()

    test3 = TestBorg2()
    test4 = TestBorg2()

アウトプット

$ python special_attr_dict.py 
self._shared_state1: {}
self._shared_state1: {'state': 'second'}
self._shared_state2: {}
self._shared_state2: {}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

+3

簡略図を示します。参考程度にどうぞ。

TestBorg1の場合
イメージ説明

TestBorg2の場合
イメージ説明

(クリックすると原寸大で開けます)
(辞書のキー・値は実態とは大幅に異なります。本当はキー側の値のハッシュ値から作られるハッシュテーブルな訳ですが、面倒なのでキー側に直接文字列リテラルを書いています)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/05/19 14:27 編集

    >属性管理用のdictオブジェクトに保存される属性は、いつ初期化されるかご存知でしょうか?
    >つまり、当質問のコードでは1回目のインスタンス化で属性管理用のdictオブジェクトに属性が管理され、2回目のインスタンス化の際には、保存された属性を参照しにいってる、という理解は合っていますでしょうか?
    「初期化される」「管理され、」「参照しにいってる」といった語彙がなにを意味しているのか私から理解できないので、真偽はわからないです。
    普通のdictと同様に代入された時点でキーと値の対がセットされるだけであって、なにか特別な処理が行われる訳ではないです。

    キャンセル

  • 2021/05/19 14:42 編集

    たとえば、
    class TestBorg: pass

    d = {}
    d["state"] = "second"

    borg1 = TestBorg()
    borg2 = TestBorg()
    borg1.__dict__ = d
    borg2.__dict__ = d

    とか書けば、borg1とborg2のインスタンスの間では状態が共有されて属性"state"(値は"second")を持つようになる訳ですし。

    キャンセル

  • 2021/05/19 14:47 編集

    なんなら、上のコードを実行したあとに
    TestBorg.printhoge = lambda self : print("hoge")
    borg1.printhoge()
    borg2.printhoge()
    とやると、
    hoge
    hoge
    が出力されます。
    属性アクセスそのものは動的な探索によって為される言語ですから、オブジェクトはどこを探索するかだけ知っていれば良い訳です。そして、インスタンスではその主な探索先は__dict__に代入されている辞書オブジェクトとインスタンスが属するクラスの属性です。

    キャンセル

+2

まず(__dict__は完全に忘れて)普通のクラス変数の取り扱いは分かるでしょうか。

class Hoge:
    shared_list = []
    def test_shared(self):
        print(Hoge.shared_list)
        Hoge.shared_list.append(1)
a = Hoge()
a.test_shared()  # => []がprintされて、Hoge.shared_listは[1]になる

b = Hoge()
b.test_shared()  # => [1]がprintされて、Hoge.shared_listは[1, 1]になる

print(Hoge.shared_list)  # Hoge.shared_listは[1, 1]なので[1, 1]がprintされる


は分かりますか。
メソッドの中や、クラスの外で、クラス名.属性名という構文で、クラスが(ここでいうとHogeが)持っている属性にアクセスする機構のことです。

次に、上のコードと下のコードが(結果として)同じ動きになることが理解できるでしょうか

class Fuga:
    shared_list = []
    def test_shared(self):
        print(self.shared_list)
        self.shared_list.append(1)
a = Fuga()
a.test_shared()  # => []がprintされて、Fuga.shared_listは[1]になる

b = Fuga()
b.test_shared()  # => [1]がprintされて、Fuga.shared_listは[1, 1]になる

print(Fuga.shared_list)  # Fuga.shared_listは[1, 1]なので[1, 1]がprintされる
print(b.shared_list)  # shared_listがインスタンス属性から見つからないので、Fuga.shared_listが探索され、[1, 1]なので[1, 1]がprintされる

クラスとインスタンス変数 の

名前とオブジェクトについて で議論したように、共有データはリストや辞書のような mutable オブジェクトが関与すると驚くべき効果を持ち得ます。

のところのコードの動きと同じことを説明しています。

いろいろな注意点 に

インスタンスとクラスの両方で同じ属性名が使用されている場合、属性検索はインスタンスが優先されます。

とありますが、
「インスタンスとクラスの両方で同じ属性を持っていればインスタンスが優先されます」という説明は、裏から言うと
インスタンス.属性という形でアクセスする時、インスタンスがその属性を持っていなければクラスの属性を見つけてくるということです。


__dict__を使った話が理解できるのは、まずこれらが分かってからかと思いますが大丈夫でしょうか。


(追記)

もう一段基本的な方からいきましょう。

Pythonでは、「自分で宣言したクラス」のインスタンスに対して、インスタンス.属性 = 値という代入文でそのインスタンスになにかの値に属性を束縛できます。

class Foo:
    pass

a = Foo()
a.x = 1
a.y = 2

b = Foo()
b.y = 3

print(a.x)  # => 1がプリントされる
print(a.y)  # => 2がプリントされる
print(b.x)  # => 3がプリントされる
print(b.y)  # => エラー

です。
a.xb.xの値が異なることから、属性はインスタンス毎に付けることが分かります。
これがインスタンス変数です。

(脱線しますが、intやlistのようなPython言語のコアな組み込み型ではこの機構はありません。勝手に属性を追加されたりすると困るので)

クラス定義の外では、クラスに対して、クラス.属性 = 値という代入文でそのクラスになにかの値に属性を束縛できます。

Foo.c = 1

print(Foo.c)  # => 1がプリントされる
print(Foo.d)  # => エラー

"クラス定義の外では"とわざわざ注釈を入れたのは、クラス定義の中では書けないからです。

class Bar:
    Bar.c = 1  # => エラー


なぜかというと、Barという名前はclass文の実行が終わった時にできる名前だからです。
実行が終わってないところでは使えません。

どうするかというと

class Bar:
    c = 1

print(Bar.c)  # => 1


とします。

https://docs.python.org/ja/3/tutorial/classes.html#class-objects

クラスオブジェクトが生成された際にクラスの名前空間にあった名前すべてが有効な属性名です。

クラス宣言の実行部分で名前cが作られましたから、クラスBarは属性名cを持ちます
ということです。

class Bar:
    c = 1


class Bar:
    pass
Bar.c = 1


は(実行される順番は違うけれども)結果が同じになります。


ここまで分かったら

いろいろな注意点 に

インスタンスとクラスの両方で同じ属性名が使用されている場合、属性検索はインスタンスが優先されます。

とありますが、
「インスタンスとクラスの両方で同じ属性を持っていればインスタンスが優先されます」という説明は、裏から言うと
インスタンス.属性という形でアクセスする時、インスタンスがその属性を持っていなければクラスの属性を見つけてくる

と書いたのが分かるはずです。

class Bazz:
    x = 1

a = Bazz()
a.x = 10

b = Bazz()

print(a.x)  # => aに属性xがあって10に束縛されているので10がプリントされる
print(b.x)  # => bに属性xがないので、次にクラスの属性xを探す。1に束縛されているので1がプリントされる

(追記)

もう一段基本的なことを確認しておきたいんですが、

a = {}
b = a

a['x'] = 1

print(b)

とか

a = [1, 3, 2, 4]
b = a

a.sort()

print(b)


の結果は、実行せずにすぐ想像できるんでしょうか?


(追記)

では、以上の挙動をすべて理解できていれば、下の結果も実行せずにわかるはずです。

class SharedMutableObject:
    class_variable = {}

a = SharedMutableObject()
a.instance_variable = a.class_variable  # ポイントはこの代入文の意味
a.instance_variable["state"] = "first"

print(SharedMutableObject.class_variable)  # ここ


b = SharedMutableObject()
b.instance_variable = b.class_variable
b.instance_variable["state"] = "second"

print(a.class_variable)  # ここ
print(SharedMutableObject.class_variable)  # ここ

特に、真ん中のprint(SharedMutableObject.class_variable)の結果を実行せずに分かるなら、

{'state': 'second'}がどこで保持され、二回目のインスタンス化であるtest2 = TestBorg1()の際に{'state': 'second'}を出力できるのでしょうか?

という疑問は解消されているはずです。(ここに書いたコードでは1回目は'first'を入れていますが)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/05/16 11:00 編集

    申し訳ありません。誤読してコメントしてしまいました。大変失礼しました。
    回答を追記したのでお読み下さい。
    (疑問形になっているということは、確信を持ててないという状態なはずなので)

    キャンセル

  • 2021/05/17 01:01

    基礎から丁寧にありがとうございます。自分の理解は、かねがね合っていることが確認できました。

    キャンセル

  • 2021/05/17 11:31 編集

    質問の意図は

    a = {}
    b = a
    a['x'] = 1
    print(b)

    でaへの変更がbにも共有されることでの、メモリの管理上、裏側でおこっているのがなにか? だったりしますか?

    キャンセル

0

Pythonの組み込み関数idと同一性の比較isを使って考えてください。
これでご質問の疑問は解消するでしょうか。

ご存じない場合は先に以下をお読み下さい。
組み込み関数id
同一性の比較is

以下のコード実行してみると、何が同じで何が同じでないかが分かります。

class TestBorg1:
    _shared_state = {}
    def __init__(self):
        print(f"self._shared_state1: {self._shared_state}")
        self.__dict__ = self._shared_state
        self.__dict__["state"] = "second"

class TestBorg2:
    _shared_state = {}
    def __init__(self):
        print(f"self._shared_state2: {self._shared_state}")
        self.__dict__["val"] = self._shared_state
        self.__dict__["val"] = "test"

test1 = TestBorg1()
test2 = TestBorg1()
test3 = TestBorg2()
test4 = TestBorg2()

print(test1.__dict__ is test2.__dict__)
print(test3.__dict__ is test4.__dict__)

print(id(test1.__dict__))
print(id(test2.__dict__))
print(id(test3.__dict__))
print(id(test4.__dict__))

最後の部分の実行結果です。

>>> print(test1.__dict__ is test2.__dict__)
True
>>> print(test3.__dict__ is test4.__dict__)
False
つまり、test1.__dict__ と test2.__dict__は同じオブジェクトですが、
test3.__dict__ と test4.__dict__は同じオブジェクトではありません。
それは演算子isの定義であるidを表示した以下の結果から当然です。
>>> print(id(test1.__dict__))
2726063276352
>>> print(id(test2.__dict__))
2726063276352
>>> print(id(test3.__dict__))
2726064619392
>>> print(id(test4.__dict__))
2726064638656

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

この質問は以下で、順を追って構築していったメカニズムのどこかで、「えっ、なんでこれはこう動くの?」と感じる点があって、そこを尋ねたかったものでしょうか?

それともここに書いてあることに一切不思議な点はない上で何かを尋ねたかったものでしょうか?


(1) グローバル変数に共有オブジェクトを確保して、グローバル変数のオブジェクト間で共有する

g_shared_dict = {}

a = g_shared_dict
a['state'] = 'first'

b = g_shared_dict

# aであらかじめ更新されていた状態がbから参照できる
print(b['state'])  # => first

# bから更新するとaに及ぶ
b['state'] = 'second'
print(a['state'])  # => second

(2-a) クラス変数に共有オブジェクトを確保して、グローバル変数のオブジェクト間で共有する

※ (1) のグローバル変数g_shared_dictが、グローバル変数からクラス変数になっただけ

class Foo:
    _shared_dict = {}

a = Foo._shared_dict
a['state'] = 'first'

b = Foo._shared_dict

print(b['state'])  # => first

# bから更新するとaに及ぶ
b['state'] = 'second'
print(a['state'])  # => second

(2-b) グローバル変数に共有オブジェクトを確保して、クラスインスタンス間でクラス変数を通じて共有する

※ (1) のグローバル変数a, b が、辞書への参照ではなく Fooの別々のインスタンスになった
※ g_shared_dictへの参照がa, bではなく、aの属性とbの属性になった

g_shared_dict = {}

class Foo:
    pass

a = Foo()
a.local_dict_var = g_shared_dict
a.local_dict_var['state'] = 'first'

b = Foo()
b.local_dict_var = g_shared_dict

# aであらかじめ更新されていた状態がbから参照できる
print(b.local_dict_var['state'])  # => first

# bから更新するとaに及ぶ
b.local_dict_var['state'] = 'second'
print(a.local_dict_var['state'])  # => second

(3) クラス変数に共有オブジェクトを確保して、クラスインスタンス間でクラス変数を通じて共有する

※ (2-a) (2-b) の混交

class Foo:
    _shared_dict = {}

a = Foo()
a.local_dict_var = a._shared_dict  # ★
a.local_dict_var['state'] = 'first'

b = Foo()
b.local_dict_var = b._shared_dict  # ★

# aであらかじめ更新されていた状態がbから参照できる
print(b.local_dict_var['state'])  # => first

# bから更新するとaに及ぶ
b.local_dict_var['state'] = 'second'
print(a.local_dict_var['state'])  # => second

(4) 共有オブジェクトへの参照を初期化子に移す

※ (3)の★のコードが__init___に移動しただけ

class Foo:
    _shared_dict = {}

    def __init__(self):
        self.local_dict_var = self._shared_dict

a = Foo()
a.local_dict_var['state'] = 'first'  # ☆

b = Foo()

# aであらかじめ更新されていた状態がbから参照できる
print(b.local_dict_var['state'])  # => first

# bから更新するとaに及ぶ
b.local_dict_var['state'] = 'second'
print(a.local_dict_var['state'])  # => second

(5) 共有オブジェクトへの代入を初期化子に移す

※ (4)の☆のコードが__init___に移動しただけ

class Foo:
    _shared_dict = {}

    def __init__(self):
        self.local_dict_var = self._shared_dict
        self.local_dict_var['state'] = 'first'

a = Foo()

b = Foo()

# bから更新するとaに及ぶ
b.local_dict_var['state'] = 'second'
print(a.local_dict_var['state'])  # => second

(6) インスタンス変数名を__dict__

※ (5)のlocal_dict_varの名前を変えただけ

class Foo:
    _shared_dict = {}

    def __init__(self):
        self.__dict__ = self._shared_dict
        self.__dict__['state'] = 'first'

a = Foo()

b = Foo()

# bから更新するとaに及ぶ
b.__dict__['state'] = 'second'
print(a.__dict__['state'])  # => second

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.36%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る