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

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

新規登録して質問してみよう
ただいま回答率
85.48%
オブジェクト指向

オブジェクト指向プログラミング(Object-oriented programming;OOP)は「オブジェクト」を使用するプログラミングの概念です。オブジェクト指向プログラムは、カプセル化(情報隠蔽)とポリモーフィズム(多態性)で構成されています。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

903閲覧

pythonでオブジェクトごとに作ったインスタンスの数を数えたい

lukkio

総合スコア4

オブジェクト指向

オブジェクト指向プログラミング(Object-oriented programming;OOP)は「オブジェクト」を使用するプログラミングの概念です。オブジェクト指向プログラムは、カプセル化(情報隠蔽)とポリモーフィズム(多態性)で構成されています。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

1グッド

0クリップ

投稿2022/12/04 05:21

実現したいこと

pythonでオブジェクトごとに、同じクラスから作ったインスタンスの数を数えたいです。
できれば呼び出されたクラス側でカウントをしていきたいです。
よろしくお願いいたします。

該当のソースコード

python

1class Base(): 2 def __init__(self): 3 self.obj = Product() 4 self.obj = Product() 5 6class Product(): 7 num = 0 8 def __init__(self): 9 Product.num += 1 10 print(Product.num) 11 12if __name__ == '__main__': 13 test1 = Base() 14 test2 = Base() 15 16# これをインスタンスごとに独立にnumを増やしたい 17# Productを改変して、結果が1234 ではなく1212となるようにしたい

試したこと

クラス変数ではなく、インスタンス変数にすると常に1になってしまう。
inspect()では呼び出し元のインスタンス名の取得の仕方がわからなかった。

補足情報(FW/ツールのバージョンなど)

python3

melian👍を押しています

気になる質問をクリップする

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

python

1import inspect 2 3class Base(): 4 def __init__(self): 5 self.obj = Product() 6 self.obj = Product() 7 8class Product(): 9 def __init__(self): 10 base = inspect.stack()[1].frame.f_locals['self'] 11 base.num = getattr(base, 'num', 0) 12 base.num += 1 13 print(base.num) 14 15if __name__ == '__main__': 16 test1 = Base() 17 test2 = Base() 18 19# 1 20# 2 21# 1 22# 2

投稿2022/12/04 06:35

melian

総合スコア19771

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

lukkio

2022/12/04 23:59

ご回答ありがとうございます。 このコードでは実行処理が順にstackに入っており、[1]でクラスを呼び出した処理(フレーム)を呼び出し、 ([0]はProduct自身の処理?) frame.f_localsで呼び出した処理の所属するローカルの名前空間の自身のIDを獲得しbaseにIDを代入。 getattrでbaseにnumというインスタンス変数を作り、呼び出しごとに数を足していく。 baseは呼び出し側のオブジェクトの参照(Baseのインスタンス:test1, test2)なので test1,test2それぞれ独自のnumを増やせる ということでしょうか? つまり基本的にはBaseにインスタンス変数を保持させるが、質問のBaseのコードを改変しないという縛りのなかで実行できるという認識で合っていますでしょうか? わからないのは名前空間で >>print(inspect.stack()[1].frame.f_locals['self']) <spyder_kernels.console.shell.SpyderShell object at 0x000001D518E33E50> なぜshell自身がもっているのかよく解らず(たしかに実行環境が名前空間の範囲を知らなければならないというのは理にかなっている気がしますが)、 名前空間というものをもっと深く知る必要があると感じました。 (エンジニアではない初心者が、そこまで知る必要はないのかもしれませんが・・・) とても勉強になりました。
melian

2022/12/05 01:00

> つまり基本的にはBaseにインスタンス変数を保持させるが、質問のBaseのコードを改変しないという縛りのなかで実行できるという認識で合っていますでしょうか? はい、その通りです。 > わからないのは名前空間で Spyder をお使いなのですね。できればコマンドラインで実行してみて下さい。
lukkio

2022/12/05 16:46

>コマンドラインで実行してみて下さい。 これはIDEで、変数を確認せよという意味であっていますでしょうか? spyderのvariable Exprorerを調べて少しわかってきました。フレームを持っているのも、名前空間を持っているのもshellなわけですね。shellがフレームのスタックを持ち、それぞれのフレームの名前空間を把握することで変数の範囲を制御してるということですね。 この考えが合っているのかは分かりませんが、インタープリター型の言語がどう動いているのか少しわかった気がしました。
guest

0

ベストアンサー

GCによるオブジェクトの破棄は考えなくても大丈夫ですか?

呼び出されたクラス側でカウント

呼び出し元のインスタンスに依存するのは、
クラスの設計的に、Baseに依存するならカウントは Base 側で行わないと
Productクラスの再利用性が犠牲になります。

実質 Base クラス内でしかインスタンスを生成できなくなり、
functools.partial lambda での利用ができなくなったり、
単体テストを作成するのにひと手間必要になり、利便性が損なわれます。

python

1class Base: 2 def __init__(self): 3 self.count = 0 4 self.obj = Product(self) 5 self.obj = Product(self) 6 7class Product: 8 # NOTE: 明示的に baseオブジェクトを渡す 9 def __init__(self, base): 10 assert isinstance(base, Base) 11 base.count += 1 12 self.base = base 13 14 # ※ デストラクタを考慮するかどうかは、要件次第 15 def __del__(self): 16 self.base.count -= 1 17 18 19# 明示的に渡すことで、Base 外でも base を与えれば Product のインスタンスを作れるようになります。 20# => 単体テストや lambda での利用に配慮

Base 側でカウントする場合は、
Product のデストラクタをどう検知するかという問題がでてきますが、
weakref.WeakSet を使うことで、解消できます。

python

1 2from weakref import WeakSet 3 4class Base: 5 def __init__(self): 6 self._counter = WeakSet() # 生存しているオブジェクトのみを数える場合 7 self._total = 0 # 累計の生成数をカウントしたい場合 8 9 self.obj = self.createProduct() 10 self.obj = self.createProduct() # この時点で前に生成した self.obj は破棄される 11 12 def createProduct(self, *args, **kw): 13 obj = Product(*args, **kw) 14 self._counter.add(obj) 15 self._total += 1 16 return obj 17 18 @property 19 def total(self): 20 return self._total 21 22 @property 23 def count(self): 24 return len(self._counter) 25 26class Product: 27 def __init__(self, num=0): 28 self.num = num 29 30 def __repr__(self): 31 return f"<{self.__class__.__name__} {self.num=}>" 32 33 34if __name__ == "__main__": 35 36 base = Base() 37 print(base) # 1 38 obj = base.createProduct() 39 print(base.count) # 2 40 del obj 41 print(base.count) # 1 42 43 from functools import partial 44 create10 = partial(base.createProduct, 10) 45 obj = create10() 46 print(obj) 47 print(base.count) # 2 48 49 create12 = lambda: base.createProduct(12) 50 obj2 = create12() 51 print(base.count) # 3 52

投稿2022/12/04 09:11

編集2022/12/05 03:17
teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

lukkio

2022/12/05 00:00

ご回答ありがとうございます。 たしかにオブジェクト指向に反する設計であり、Product側にカウントを保持させるのは危険ですね。 設計から見直すべきというのはその通りかと思います。 WeskSetを使う方法は WeakSet型の_counterというオブジェクトを追加していき、その数を数えることによってインスタンスの数を把握するということですね。 WeakSetをつかっているのでオブジェクトを破棄したときに、_counter内のオブジェクトも破棄され、 GCのタイミングなどが関係がないということでしょうか。 一つ疑問は、クラスの再利用性が悪くなるのは分かるのですが、functools.partialでの利用ができなくなるというところが調べても解りませんでした。 Product側にカウントさせると、2つめのコード例でいうと”せっかくcreate10という新たなメソッドを作っているのに、Product側にすでにカウントが入ってしまっているので、 まったく新しいメソッドにならなくなってしまう”、ということでしょうか? なにぶん、オブジェクト指向を学んで半年もたっていないので、なかなか理解が追いつかず申し訳ありません。 回答的にはお二方の回答が同じくらい勉強にあったのですが、注釈があった分勉強しやすかったという点でベストアンサーにさせていただきます。 お二人ともありがとうございました。
lukkio

2022/12/05 00:50

今更ですが、勉強しているうちになぜProduct側にカウントさせたかったを忘れていました。 実際のコードではBaseはファクトリーメソッドパターンによってProductを作っており、 Base側にカウントさせてしまうと、ファクトリーによって異なったProductをつくってもカウントを増してしまうことが問題でした。 そのためにファクトリーで作ったProduct側にカウントさせたかったので、この質問になりました。 inspectを使う方法WeskSetを使う方法を試しましたがうまくいきませんでした。 回答を締め切ってしまったので、テストコードを作ったら新たに質問したいと思います。
teamikl

2022/12/05 04:48 編集

> Product側にカウントを保持させるのは危険ですね。 設計次第ではProduct側に移譲でも問題ないですが。 問題点として挙げたいのは主に、「コールスタックを使った」呼び出し元のインスタンス参照であり、 これにより様々な制限が生じます。→回答の一つ目のコードは、明示的に引数で渡すことである程度制限を緩和 > GCのタイミングなどが関係がないということでしょうか。 追記: WeakSet の挙動に関するに理解は、おおむねその通りです。 タイミングは関係ありますが、おそらく想定されているタイミング GCの参照カウントのタイミングと、 実際にオブジェクトが収集・破棄されるタイミングで異なります。 > functools.partialでの利用ができなくなるというところが調べても解りませんでした。 ここはpartial だとうまくいくので、例が適切でなかったです。回答を訂正します。 例えば、lambda だと呼び出し元のフレームが変わってしまうので # Base.__init__ 内で # func = lambda: Product() # ERROR # func = lambda self=self: Product() # 回避策はありますが、呼び出し元が深くなると self をそこまで伝搬しないといけない手間が発生 func = functools.partial(Product)# OK func() > テストコードを作ったら ここが一番の問題で、テストコードを作るなら実際に困るはず 呼び出し元依存だとBase クラス内でしか Product の単体テストができなくなってしまいます。
lukkio

2022/12/05 16:47

>「コールスタックを使った」呼び出し元のインスタンス参照 なるほどこれはつまり、オブジェクトの相互参照をしてしまうので、結合度がより上がってしまうということですね。そこまで考えていませんでした。 >例えば、lambda だと呼び出し元のフレームが変わってしまうので つまりlambda式自体がフレームを一つ増やしてしまうので、扱いが厄介になるということですね。 >呼び出し元依存だとBase クラス内でしか Product の単体テストができなくなってしまいます。 これに関しては、もう少しクラス設計の見直しと、質問の仕方を練ってから新たに質問をしたいと思っています。 ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問