问题
I'm trying to write a class in Python that behaves as a generator object, particularly in that when it's garbage collected .close() is called on it. That's important because it means that when the generator is interrupted I can make sure it'll clean up after itself, for example closing files or releasing locks.
Here's some explanatory code:
If you interupt a generator, then when it's garbage collected, Python calls .close() on the generator object, which throws a GeneratorExit error into the generator, which can be caught to allow cleanup, like follows:
from threading import Lock
lock = Lock()
def CustomGenerator(n, lock):
lock.acquire()
print("Generator Started: I grabbed a lock")
try:
for i in range(n):
yield i
except GeneratorExit:
lock.release()
print("Generator exited early: I let go of the lock")
raise
print("Generator finished successfully: I let go of the lock")
for i in CustomGenerator(100, lock):
print("Received ", i)
time.sleep(0.02)
if i==3:
break
if not lock.acquire(blocking=False):
print("Oops: Finished, but lock wasn't released")
else:
print("Finished: Lock was free")
lock.release()
Generator Started: I grabbed a lock
Received 0
Received 1
Received 2
Received 3
Generator exited early: I let go of the lock
Finished: Lock was free
However, if you try to implement your own generator object by inheriting from collections.abc.Generator, Python doesn't seem to notice that it should call close when the object is collected:
from collections.abc import Generator
class CustomGeneratorClass(Generator):
def __init__(self, n, lock):
super().__init__()
self.lock = lock
self.lock.acquire()
print("Generator Class Initialised: I grabbed a lock")
self.n = n
self.c = 0
def send(self, arg):
value = self.c
if value >= self.n:
raise StopIteration
self.c += 1
return value
def throw(self, type, value=None, traceback=None):
print("Exception Thrown in Generator: I let go of the lock")
self.lock.release()
raise StopIteration
for i in CustomGeneratorClass(100, lock):
print("Received ", i)
time.sleep(0.02)
if i==3:
break
if not lock.acquire(blocking=False):
print("Oops: Finished, but lock wasn't released")
else:
print("Finished: Lock was free")
lock.release()
Generator Class Initialised: I grabbed a lock
Received 0
Received 1
Received 2
Received 3
Oops: Finished, but lock wasn't released
I thought that inheriting Generator would be sufficient to convince python that my CustomGeneratorClass was a generator and should have .close() called on it when garbage collected.
I assume this has something to do with the fact that while 'generator object' are some kind of special Generator:
from types import GeneratorType
c_gen = CustomGenerator(100)
c_gen_class = CustomGeneratorClass(100)
print("CustomGenerator is a Generator:", isinstance(c_gen, Generator))
print("CustomGenerator is a GeneratorType:",isinstance(c_gen, GeneratorType))
print("CustomGeneratorClass is a Generator:",isinstance(c_gen_class, Generator))
print("CustomGeneratorClass is a GeneratorType:",isinstance(c_gen_class, GeneratorType))
CustomGenerator is a Generator: True
CustomGenerator is a GeneratorType: True
CustomGeneratorClass is a Generator: True
CustomGeneratorClass is a GeneratorType: False
Can I make a user defined class object that is GeneratorType?
Is there something I don't understand about how python decides what to call .close() on?
How can I ensure that .close() is called on my custom generator?
This question is not a duplicate of How to write a generator class. For actually making a generator class, the accepted answer for that question does recommends exactly the structure I'm trying here, which is a generator class but is not correctly garbage collected, as shown in the code above.
回答1:
PEP342, states:
[generator].__del__()is a wrapper for[generator].close(). This will be called when the generator object is garbage-collected ...
The Generator class in collections.abc does not implement __del__, and neither do its superclasses or metaclass.
Adding this implementation of __del__ to the class in the question results in the lock being freed:
class CustomGeneratorClass(Generator):
...
def __del__(self):
self.close()
Output:
Generator Class Initialised: I grabbed a lock
Recieved 0
Recieved 1
Recieved 2
Recieved 3
Exception Thrown in Generator: I let go of the lock
Finished: Lock was free
Caveat:
I'm not experienced with the intricacies of object finalisation in Python, so this suggestion should be examined critically, and tested to destruction. In particular, the warnings about __del__ in the language reference should be considered.
A higher-level solution would be to run the generator in a context manager
with contextlib.closing(CustomGeneratorClass(100, lock)):
# do stuff
but this is cumbersome, and relies on users of the code remembering to do it.
来源:https://stackoverflow.com/questions/58775283/how-to-create-a-custom-generator-class-that-is-correctly-garbage-collected