-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
Description
Bug report
Bug description:
When instantiating objects through a method with functools.cached_property decorator, sometimes the objects won't be cleaned by the garbage collector. More specifically, when the method is called by iterating a list of objects. It is easier to understand from the reproduction code. This does not happen, when the class object is being cached (some_other_method). Additionally, it does not appear, when the instance in the list/dictionary is being directly accessed (third run), which means one or the other must be an unintended side-effect (that the direct access gets garbage collected or the iterative access does not). In the final run, trying to deleted the lists does not remove the SomeOtherClass instances. The original SomeClass objects do not get removed either. (The final del is to test, if deleting the dictionaries triggers the garbage collector somehow)
Code to reproduce (objgraph has to be installed; can be done with pip install objgraph
):
from functools import cached_property
import objgraph
import gc
class SomeOtherClass:
pass
class SomeClass:
@cached_property
def some_method(self):
return SomeOtherClass()
@cached_property
def some_other_method(self):
return SomeOtherClass
print("First run:")
e1_list = {0: SomeClass()}
for e1 in e1_list.values():
e1.some_method
e1_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()
print("Second run:")
e2_list = {0: SomeClass()}
for e2 in e2_list.values():
e2.some_method
e2_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()
print("Third run (no loop):")
e3_list = {0: SomeClass()}
e3_list[0].some_method
e3_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()
print("Fourth run (some_other_method):")
e4_list = {0: SomeClass()}
for e4 in e4_list.values():
e4.some_other_method()
e4_list.clear()
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
print()
print("Fifth run (Trying to clean up using del):")
del e1_list
del e2_list
del e3_list
del e4_list
gc.collect()
print("SomeClass:", objgraph.by_type('SomeClass'))
print("SomeOtherClass:", objgraph.by_type('SomeOtherClass'))
Output:
First run:
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>]
Second run:
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]
Third run (no loop):
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]
Fourth run (some_other_method):
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>, <__main__.SomeClass object at 0x7153db4d09d0>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]
Fifth run (Trying to clean up using del):
SomeClass: [<__main__.SomeClass object at 0x7153db48d6a0>, <__main__.SomeClass object at 0x7153db2f4690>, <__main__.SomeClass object at 0x7153db4d09d0>]
SomeOtherClass: [<__main__.SomeOtherClass object at 0x7153db48f0e0>, <__main__.SomeOtherClass object at 0x7153db2f47d0>]
CPython versions tested on:
CPython main branch, 3.15, 3.14, 3.13, 3.10
Operating systems tested on:
Linux