Rick
Rick

Reputation: 45261

Automatically delete class instance when one of its attributes becomes dead

Set Up

Say I have a Snit:

class Snit(): pass

And a Snot, which contains weak references to up to, say, four Snits:

import weakref
class Snot():
    def __init__(self,s1=None,s2=None,s3=None,s4=None):
        self.s1 = weakref.ref(s1)
        self.s2 = weakref.ref(s2)
        self.s3 = weakref.ref(s3)
        self.s4 = weakref.ref(s4)

I also have a Snot factory:

def snot_factory(snits,w,x,y,z):
    return Snot(snits[w],snits[x],snits[y],snits[z])

And a list of Snits (a snit_list as it were):

snit_list = []
for i in range(12):
    snit_list.append(Snit())

Now I make a list of Snots using the Snits in my snit_list:

snot_list = []
for i in range(3):
    snot_list.append(snot_factory(snit_list[4*i],snit_list[4*i+1],snit_list[4*i+2],snit_list[4*i+3]))

The Problem

Whoops! I don't need snit_list[3] anymore, so I'll go ahead and remove it:

snit_list.pop(3)

But now I have a Snot hanging out there with a dead Snit:

snot_list[0].s4 # <weakref at 0x00BlahBlah; dead>

This cannot stand! A Snot with a dead Snit is - obviously - total nonsense.

So I would really like for any references to the Snot to at least return as None after one or more of its Snits has been destroyed. But ideally, it would be even better for the Snot to be automatically removed from the snot_list list as well (len(snot_list) shrinks by the number of removed Snots).

What's a good way of going about this?

Clarification:

A Snot is an object that should only exist when there is a valid set of Snits ("valid" means it has the same number of defined Snits it was initialized with), with the following behavior:

  1. If any one Snit in a Snot goes away (when no strong references remain), the Snot should also go away (this is why I have set the s1, s2, etc to be weak references). Note that a Snot could have been initialized with 4, 3, 2, or 1 Snit. The number of Snits doesn't matter, the death of the Snit is what matters.
  2. If any one Snot that contains a reference to a Snit goes away, the Snit remains.
  3. OPTIONAL: When a Snot is deleted, the data structure containing the reference to the Snot object is updated as well (the Snot gets popped)
  4. OPTIONAL: When ALL the Snots that reference a certain Snit are gone, the Snit goes away too, and any data structures containing the Snit are updated as in #3 (the Snit gets popped).

So the ideal solution will allow me to set things up such that I can write code like this:

snits = get_snits_list(some_input_with_10000_snits)
snots = get_snots_list(some_cross_referenced_input_with_8000_snots)
#e.g.: the input file will say:
#snot number 12 is made of snits 1, 4, 7
#snot number 45 is made of snits 8, 7, 0, 14
do_stuff_with_snits()
snits.pop(7) #snit 7 is common to snot 12 and 45
assert len(snots) == 7998 #snots 12 and 45 have been removed

However, if this is too hard, I'd be fine with:

assert snots[12] == None
assert snots[45] == None

I am open to changing things around somewhat. For example, if it makes the design easier, I think it would be fine to remove the weak references to the Snits, or to maybe move them instead to the list of Snits instead of having the Snot members be weak refs (though I don't see how either of these changes would improve things).

I have also considered creating Snot subclasses - ClearSnot with 1 Snit, YellowSnot with 2 Snits, GreenSnot with 3 Snits, etc. I'm uncertain if this would make things easier to maintain, or more difficult.

Upvotes: 3

Views: 225

Answers (2)

Ethan Furman
Ethan Furman

Reputation: 69051

Okay, so you have a Snot with a variable amount of Snits.

class Snot(object):

    def __init__(self, *snits):
        self.snits = [weakref.ref(s) for s in snits]

    def __eq__(self, other):
        if not isinstance(other, self.__class__) and other is not None:
            return NotImplemented
        # are all my snits still valid
        valid = all(s() for s in self.snits)
        if other is None:
            return not valid  # if valid is True, we are not equal to None
        else:
            # whatever it takes to see if this snot is the same as the other snot

Actually having the class instance disappear is going to take more work (such as having dict on the class to track them all, and then other data structures would just use weak-references -- but that could get ugly quick), so the next best thing will be having it become equal to None when any of its Snits goes away.


I see that snits and snots are both lists -- is order important? If order is not important you could use sets instead, and then it would be possible to have a performant solution where the the dead snot is actually removed from the data structure -- but it would add complexity: each Snot would have to keep track of which data struture it was in, and each Snit would have to keep a list of which Snots it was in, and the magic would have to live in __del__ which can lead to other problems...

Upvotes: 1

Ethan Furman
Ethan Furman

Reputation: 69051

Nothing is truly automatic. You'll need to either have a function that you run manually to check for dead Snits, or have a function that is part of Snot that is called whenever anything interesting happens to a Snot to check for, and remove, dead Snits.

For example:

class Snot:
    ...
    def __repr__(self):
        # check for and remove any dead Snits
        self._remove_dead_snits()
        return ...
    def _remove_dead_snits(self):
        if self.s1() is None:
             self.s1 = None
        ... # and so on and so forth

The fun part is adding that call to _remove_dead_snits for every interesting interaction with a Snot -- such as __getitem__, __iter__, and whatever else you may do with it.


Actually, thinking a bit more about this, if you only have the four possible Snits per each Snot you could use a SnitRef descriptor -- here's the code, with some changes to your original:

import weakref

class Snit(object):
    def __init__(self, value):
        self.value = value  # just for testing
    def __repr__(self):
        return 'Snit(%r)' % self.value

class SnitRef(object):   # 'object' not needed in Python 3
    def __get__(self, inst, cls=None):
        if inst is None:
            return self
        return self.ref()  # either None or the obj
    def __set__(self, inst, obj):
        self.ref = weakref.ref(obj)


class Snot(object):
    s0 = SnitRef()
    s1 = SnitRef()
    s2 = SnitRef()
    s3 = SnitRef()
    def __init__(self,s0=None,s1=None,s2=None,s3=None):
        self.s0 = s0
        self.s1 = s1
        self.s2 = s2
        self.s3 = s3

snits = [Snit(0), Snit(1), Snit(2), Snit(3)]
print snits
snot = Snot(*snits)
print(snot.s2)
snits.pop(2)
print snits
print(snot.s2)

and when run:

[Snit(0), Snit(1), Snit(2), Snit(3)]
Snit(2)
[Snit(0), Snit(1), Snit(3)]
None

Upvotes: 2

Related Questions