Using __getitem__ for both index and key access

187 Views Asked by At

I have a custom class for which it makes sense to access the attributes as if the class where either a tuple or a dictionary.

(The class is a generic class for a measure in units with subunits. For example a length in yards, feet and inches, or an angle in degrees, minutes and seconds.)

I already set up the class to be able to accept any set of attribute names at runtime, and a list of those names is stored in the class. The attributes can be accessed with dot notation. (And not changed, because I overwrote the __setattr__ method.) I then set up the class to be able to access the items from a subscript with __getitem__, and added a condition for accepting slice indexing. It occured to me that the __getitem__ method could be used as if the class where a dict, and accept the attribute name.

Here is the relevant code:

class MeasureWithSubunits():
    units = ('days', 'hours', 'minutes')
    # Class variable can be assigned as normal at runtime.
    # Assigned here as an example.
    
    def __init__(self, *args) -> None:
        # Tidy up the input
        ...
        for i, unit in enumerate(self.units):
            self.__dict__[unit] = args[i] if i < len(args) else 0
    
    def __getitem__(self, index):
        if type(index) is int:
            return self.__dict__[self.units[index]]
        elif type(index) is slice:
            return [self.__dict__[self.units[i]] for i in range(
                    index.start or 0,
                    index.stop or len(self.units),
                    index.step or 1
                   )]
        else:
            return self.__dict__[index]

    def __len__(self) -> int:
        return len(self.units)

    def __setattr__(self, name, attr_value):
        raise AttributeError("Does not support attribute assignment")

My question is, is it "wrong" to allow the square bracket access to be used in these two, almost contradictory ways at the same time? Especially given that the key access method is unnecessary, as dot access is already provided.

To avoid making this an opinion question, I would like an answer based upon the docs. (Not that I would mind a opinion that is well presented.) Alternatively, is there anything in the standard library, or libraries as popular as say numpy, that does this?

1

There are 1 best solutions below

0
jsbueno On

It is not wrong.

No, this is not in the docs - the docs don't say often what you "should not do even though it would work" (the exception to that rule is when you try using parts of the language dedicated to the typing machinery used for regular runtime stuff).

And if that semantically makes sense for your code, just go for it. Numpy and pandas for one would be a whole lot more intuitive if they'd allow this to some extent, instead of borrowing the .loc and .iloc semantics.

But, so that this is not "only opinion based" - you have to take care to do it right, so that you don't get unwanted surprises: in particular, if you implement an __iter__ method you should pick first which it will iterate: the non-numeric keys, like a regular mapping, or the contents like a tuple would do? You'd better do that explicitly - because since you have a __len__ and a __getitem__ your class is an iterable already, and can be plugged on a for loop (it will yield the values).

Also, an unrelated tip for the range function in there, the .indices method on the slice object will do the job of your hard-to-read three expressions there, and return the values to be passed to range. You can do:

return [self.__dict__[self.units[i]] for i in range(*index.indices(len(self))]

instead, and it will even handle negative indexes or step value, and possibly other corner cases you had not thought about.