How to create a custom generator class that is correctly garbage collected

隐身守侯 提交于 2020-01-15 02:37:13

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!