__setattr__ versus __slots__ for constraining attribute creation in Python

1.1k views Asked by At

I've been reading about how make Python classes less dynamic, specifically by not allowing users to dynamically create new attributes. I've read that overloadding __setattr__ is a good way to do this , and __slots__ is not the way to go. One post on this last thread actually suggests that __slots__ can break pickling. (Can anyone confirm this?)

However, I was just reading the whatsnew for Python 2.2, and the attribute access section actually suggests using __slots__ for the very purpose of constraining attribute creation, not just for optimization like others have suggested. In terms of Python history, does anyone know what the original intention of __slots__ was? Is constraining variable creation a feature or a bug to be abused? How have people seen __slots__ used in practice? Have many people seen __setattr__ overloaded to restrict attribute creation? Which one is best? If you are more familiar with one method or the other, feel free to post the pros and cons of the method you know. Also, if you have a different way of solving the problem, please share! (And please try not to just repeat the downsides of __slots__ that have been expressed in other threads.)

EDIT: I was hoping to avoid discussion of "why?", but the first answer indicates that this is going to come up, so I'll state it here. In the project in question, we're using these classes to store "configuration information", allowing the user to set attributes on the objects with their (the users') parameters, and then pass the objects off to another part of the program. The objects do more than just store parameters, so a dictionary wouldn't work. We've already had users accidentally type an attribute name wrong, and end up creating a new attribute rather than setting one of the attributes that the program expects. This goes undetected, and so the user thinks they're setting the parameter but doesn't see the expected result. This is confusing for the user, and hard to spot. By constraining attribute creation, an exception will be thrown rather than pass by silently.

EDIT 2, re pickling: These objects will be something that we will want to store in the future, and pickling seems like a good way to do this. If __slots__ is clearly the best solution, we could probably find another way to store them, but pickling would definitely be of value, and should be kept in consideration.

EDIT 3: I should also mention that memory conservation isn't an issue. Very few of these objects will be created, so any memory saved will be negligible (like 10s of kilobytes on a 3-12 GB machine).

2

There are 2 answers

2
kindall On BEST ANSWER

My suggestion for your use case is to use __setattr__ and issue a warning when the attribute name is not recognized using Python's warnings module.

4
ThiefMaster On

Why are you trying to restrict developers from doing that? Is there any technical reason besides "I don't want them to do it" for it? If not, don't do it.

Anyway, using __slots__ saves memory (no __dict__) so it's the better solution to do it. You won't be able to use it if some of your code needs the object to have a __dict__ though.

If there is a good reason to restrict it (again, are you sure there is one?) or you need to save that little bit of memory, go for __slots__.


After reading your explanation you might want to use __setattr__, possibly combined with __slots__ (you need to store the attribute whitelist somewhere anyway, so you can as well use it to save memory). That way you can display some more helpful information such as which similar attribute are available. A possible implementation could look like this:

class Test(object):
    __slots__ = ('foo', 'bar', 'moo', 'meow', 'foobar')

    def __setattr__(self, name, value):
        try:
            object.__setattr__(self, name, value)
        except AttributeError:
            alts = sorted(self.__slots__, key=lambda x: levenshtein(name, x))
            msg = "object has no attribute '{}', did you mean '{}'?"
            raise AttributeError(msg.format(name, alts[0]))

The levenshtein() I tested it with was implementation #4 from this site. In case of not so smart users you might want to make the error even more verbose and include all close matches instead of just the first one.

You can further improve the code by creating a mixin class containing just the __setattr__ method. That way you can keep the code out of your real classes and also have a custom __setattr__ if necessary (just use super(...).__setattr__(...) in the mixin instead of object.__setattr__)