Who is holding a reference to Joe? Pythonic method for destructors/cleanup?

Question:

I have the following Python code:

#!/usr/bin/python3

import time

class Greeter:
    def __init__(self, person):
        self.person = person
        print("Hello", person);

    def __del__(self):
        print("Goodbye", self.person);

    def inspect(self):
        print("I greet", self.person);

if __name__ == '__main__':
    to_greet = []
    try:
        to_greet.append(Greeter("world"));
        to_greet.append(Greeter("john"));
        to_greet.append(Greeter("joe"));

        while 1:
            for f in to_greet:
                f.inspect()
            time.sleep(2)

    except KeyboardInterrupt:
        while (len(to_greet) > 0):
            del to_greet[0]
        del to_greet
        print("I hope we said goodbye to everybody. This should be the last message.");

When I run it and Ctrl+C during the sleep, I get:

Hello world
Hello john
Hello joe
I greet world
I greet john
I greet joe
^CGoodbye world
Goodbye john
I hope we said goodbye to everybody. This should be the last message.
Goodbye joe

I don’t understand why Goodbye joe is only printed after This should be the last message. Who is holding a reference to it? Is it the print statement? Even if I put gc.collect() before the last print I don’t see us saying goodbye to Joe before the last print. How can I force it?

From what I have been Googling, it seems I should be using the with statement in such a situation, but I’m not exactly sure how to implement this when the number of people to greet is variable (e.g., read from a names.txt file) or just big.

It seems I won’t be able to get C++-like destructors in Python, so what is the most idiomatic and elegant way to implement destructors like this? I don’t necessarily need to delete/destroy the objects, but I do need to guarantee we absolutely say goodbye to everybody (either with exceptions, killed by signal, etc.). Ideally I’d like to do so with the most elegant possible code. But for now, being exception-resistant should be fine.

Asked By: XOpenDisplay

||

Answers:

It’s f. The loop variable isn’t scoped to the loop; the only syntax constructs that create a new scope in Python are classes, functions, comprehensions, and generator expressions.

f survives all the way until interpreter shutdown. While the current CPython implementation tries to clean up module contents at interpreter shutdown, I don’t think this is promised anywhere.

Answered By: user2357112

user2357112‘s answer hits the nail on the head, as far as your immediate question.

As for the way forward, you’re correct: the idiomatic Python way to handle things like this (cleanup that needs to always happen, even if an exception occurs) is a context manager. The "standard", low-level way to create one is to define a class with __enter__ and __exit__ methods, but Python’s contextlib provides a convenient shortcut. (For a more thorough rundown on both forms, RealPython has a good intro.)

(If you haven’t seen the @ before, that’s a decorator—the short version is that it modifies this function for us to let it perform as a context manager.)

from contextlib import contextmanager

@contextmanager
def greetings_context(people):
    # Given a list of strings, create a Greeter object for each one.
    # (This is called a list comprehension.)
    greeters = [Greeter(person) for person in people]
    try:
        # Yield the list of Greeter objects for processing in the 'with'
        # statement
        yield greeters
    finally:
        # This will always get executed when done with the 'with', even
        # if an exception occurs:
        for greeter in greeters:
            greeter.leave()
        print("This should be the last message.")

class Greeter:
    # Same as yours, except renamed __del__ to leave
    def __init__(self, person):
        self.person = person
        print("Hello", person)

    def leave(self):
        print("Goodbye", self.person)

    def inspect(self):
        print("I greet", self.person)

if __name__ == '__main__':
    people = ['world', 'john', 'joe']
    
    with greetings_context(people) as to_greet:
        for f in to_greet:
            f.inspect()
        raise Exception('Manually raised exception')

Even though we manually raised an exception within the with block, the context manager still executed its cleanup code:

Hello world
Hello john
Hello joe
I greet world
I greet john
I greet joe
Goodbye world
Goodbye john
Goodbye joe
This should be the last message.
Traceback (most recent call last):
  File "/Users/crazychucky/test.py", line 36, in <module>
    raise Exception('Manually raised exception')
Exception: Manually raised exception

There are many possible variations on this. You could, for instance, create your objects first, then pass a list of them to the context manager. Just keep in mind that the idea of a context manager is to manage a "context" that includes setup and teardown; creating your object(s) in the setup step can help keep things tidy.

Answered By: CrazyChucky
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.