2

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.

CrazyChucky
  • 3,263
  • 4
  • 11
  • 25
  • Which _specific_ release of the Python interpreter are you targeting? (The Python language documentation long made no guarantee that destructors would be called at all, so if you're targeting the language spec rather than a specific implementation, this isn't answerable). – Charles Duffy Apr 13 '21 at 21:58
  • ...there _are_ some guarantees introduced with PEP 442, but of course that only applies with a language version after that PEP was accepted (which is to say, after Python 3.4). – Charles Duffy Apr 13 '21 at 21:59
  • @CharlesDuffy I'm targeting "recent and future python 3". Code should work on whatever /usr/bin/python3 is being distributed by Linux distros right now and in the foreseeable future. – XOpenDisplay Apr 13 '21 at 22:05
  • If by "right now" you mean to include the oldest long-term support distro that hasn't ended its support lifecycle, that's different from "right now" meaning current distro releases as of today. – Charles Duffy Apr 13 '21 at 22:07
  • @CharlesDuffy I'm on 3.9.2. I'm fine with requiring >= 3.9.2. – XOpenDisplay Apr 13 '21 at 22:09
  • Sounds like you're in a good place, then -- `f = None` or `del(f)` after the loop and the immediate problem is addressed. Maybe move the logic from being module-level into a function so you have locals that go out-of-scope and don't need to worry about handles still being held at the module level. – Charles Duffy Apr 13 '21 at 23:11
  • 1
    If "what you really want" is specific to an individual datatype, btw, I would be tempted to have that type's constructor register a weak reference that you then process from a `__leave__` function when exiting the intended scope of objects of that type. Assuming you can move your logic into something more like a `close()` than a `__del__`, you can then call that close operation explicitly from the context handler's exit end. – Charles Duffy Apr 13 '21 at 23:44
  • Do you specifically need all the objects to be deleted, or do you only need to make sure a certain piece of logic (in this case printing "goodbye") is always executed on them? – CrazyChucky Apr 14 '21 at 00:01
  • 1
    @CrazyChucky I just need to run a function, in this case the one that says goodbye. And I want to do it in the "Python way", since as I understand destructors are a little frowned upon. Perhaps having a "cleanup" function is the easiest way, but I was wondering iv I could use `with` or something more popular. – XOpenDisplay Apr 14 '21 at 00:20
  • @XOpenDisplay "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". You can use a [`contextlib.ExitStack`](https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack) to add multiple cleanup functions to a single `with` context. – augurar Apr 14 '21 at 01:30

2 Answers2

8

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Thanks. Now all that's left is to find out what's the Python-appropriate way to implement what I really wanted here. – XOpenDisplay Apr 13 '21 at 22:11
  • @XOpenDisplay Maybe check out the [`atexit`](https://docs.python.org/3/library/atexit.html) module. Usually it's not a good idea to rely on `__del__()` as it's not guaranteed to be called at any particular time, or even at all. See the data model documentation for [`object.__del__`](https://docs.python.org/3/reference/datamodel.html#object.__del__) for details. – augurar Apr 14 '21 at 01:27
2

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.

CrazyChucky
  • 3,263
  • 4
  • 11
  • 25