Search code examples
pythonooppython-datamodel

Python: Magic method for when the object's reference count changes?


I want to a method that runs when object reference count is changed. is there a method like the following code ?

class Foo():
    def __method__(self):
        print("ojbect reference count is changed !")

a = Foo()
b = a  # runs __method__ because the reference count is increased
c = b  # runs __method__ because the reference count is increased
del b  # runs __method__ because the reference count is decreased
del c  # runs __method__ because the reference count is decreased

Note: This is a repost of this question, I'm looking for an answer compatible with Python 3.10.

Thanks!


The reference count of my object never goes below 1. Taking this as a given, I tried to implement some clean-up code for when an object is dereferenced by all user-facing code by adding __del__. That didn't work, because there's one reference left.

So, I need to add some clean-up code for the reference count hits 1.

This is an XY Problem, and I'm comfortable with that. My actual situation involves singletons, wherein ((User("my_name") == User("my_name")) and (User("my_name") != User("other_name"))) is True. To do this, there's an internally referenced dictionary that's handling all this. That's why the minimum reference count for an object is 1.

When an object is removed from the User's code, ie, the reference count reaches 1, I'd like to clean it up from my reference dictionary as well.


Solution

  • I'm afraid there's no magic method like that in Python. However, if you're looking to implement a singleton with auto-cleanup, using a WeakValueDictionary would work:

    from __future__ import annotations
    
    from threading import Lock
    from weakref import WeakValueDictionary
    
    _USERS_LOCK = Lock()
    _USERS: WeakValueDictionary[str, User] = WeakValueDictionary()
    
    
    class User:
        def __new__(self, name: str) -> User:
            with _USERS_LOCK:
                # Try to return an existing user.
                try:
                    user = _USERS[name]
                except KeyError:
                    # Create and cache a new user.
                    user = _USERS[name] = object.__new__(User)
                    user.name = name
                # All done!
                return user
            
    

    You can test this works as expected like so:

    # Users created with a unique username return new user objects.
    user1 = User("Dave")
    assert user1.name == "Dave"
    user2 = User("TheOnlyWayUp")
    assert user2.name == "TheOnlyWayUp"
    assert user1 is not user2
    
    # The users are cached.
    assert len(_USERS) == 2
    
    # Users created with the same username as an existing user return
    # the same user instance.
    user3 = User("Dave")
    assert user3 is user1
    
    # The number of cached users remains the same.
    assert len(_USERS) == 2
    
    # Deleting the users frees up the cache.
    del user1, user2, user3
    assert len(_USERS) == 0
    

    See: