chriss
chriss

Reputation: 143

self[key] = value in custom python dictionary implementation does not call custom __setitem__ method

I have a behaviour that I cannot explain in my code: I want to implement a dictionary class that adds the following two behaviours to standard dictionaries:

I have somewhat achieved this behaviour by subclassing from collections.OrderedDict (to have a notion of oldest (key,item) pair) and overwriting the __setitem__ (to enforce maximum len) and __missing__ (to generate missing keys on the fly) methods:

from collections import OrderedDict
from weakref import proxy

class AttachedAutoLimitedDict(OrderedDict):
    """A special dictionary implementation that is meant to be used together with an associated object.

    When a not-existing key is requested from the dictionary it attempts to generate a value to that key by calling
    the generation method of the associated object. Furthermore the dictionary can be limited in size 
    by setting the max_len property during construction."""

    def __init__(self, associated_object, max_len: int | None = None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.associated_object = proxy(associated_object)
        if max_len is None:
            self.__setitem__ = super().__setitem__
        else:
            self.__setitem__ = self.__setitemmaxlen__
        self.max_len = max_len

    def __missing__(self, key):
        """Overwrites the __missing__ implementation. Attempts to generate a value for a missing key
        by using the key as argument to the generation method of the associated object."""
        try:
            self.__setitem__(key, self.associated_object.generation_method(key))
            return self[key]
        except AttributeError:
            super().__missing__(key)

    def __setitemmaxlen__(self, key, value):
        """This ensures that the dictionary does not grow beyond max_len"""
        # have to do it with the +1 since len(self) is incremented before the __setitem__ call
        if len(self) >= self.max_len + 1:
            # this is inherited from OrderedDict, last=False specifies FIFO popping
            self.popitem(last=False)
        super().__setitem__(key, value)

Now the implementation seems to be working like this (haven't fully tested it yet). But I had a problemt when in the implementation of __missing__ I used self[key]=self.associated_object.generation_method(key) instead of self.__setitem__(key, self.associated_object.generation_method(key)), because the custom implementation of __setitem__ was never called (and hence the restriction on len was not enforced).
Does somebody know why that happens?

Upvotes: 0

Views: 39

Answers (1)

chriss
chriss

Reputation: 143

As per @ken's suggestion in the comment, python looks up dunder methods on the class, not on the instance. Therefore dynamically assigning to __setitem__ in the instance initialization has no effect. The below works:

class AttachedAutoLimitedDict(OrderedDict):
    """A special dictionary implementation that is meant to be used together with an associated object.

    When a not-existing key is requested from the dictionary it attempts to generate a value to that key by calling
    the generation method of the associated object. Furthermore the dictionary can be limited in size 
    by setting the max_len property during construction."""

    def __init__(self, associated_object, max_len: int | None = None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.associated_object = proxy(associated_object)
        self.max_len = max_len

    def __missing__(self, key):
        """Overwrites the __missing__ implementation. Attempts to generate a value for a missing key
        by using the key as argument to the generation method of the associated object."""
        try:
            self[key]=self.associated_icon_object.get_run_data(key)
            return self[key]
        except AttributeError:
            super().__missing__(key)

    def __setitem__(self, key, value):
        """This ensures that the dictionary does not grow beyond max_len"""
        if self.max_len is not None and len(self) >= self.max_len + 1:
            self.popitem(last=False)
        super().__setitem__(key, value)


Upvotes: 0

Related Questions