3

Let's say this is my class:

class A:
    def __init__(self):
        self.good_attr = None
        self.really_good_attr = None
        self.another_good_attr = None

Then a caller can set the values on those variables:

a = A()
a.good_attr = 'a value'
a.really_good_attr = 'better value'
a.another_good_attr = 'a good value'

But they can also add new attributes:

a.goood_value = 'evil'

This is not desirable for my use case. My object is being used to pass a number of values into a set of methods. (So essentially, this object replaces a long list of shared parameters on a few methods to avoid duplication and clearly distinguish what's shared and what's different.) If a caller typos an attribute name, then the attribute would just be ignored, resulting in unexpected and confusing and potentially hard to figure out behavior. It would be better to fail fast, notifying the caller that they used an attribute name that will be ignored. So something similar to the following is the behavior I would like when they use an attribute name that doesn't already exist on the object:

>>> a.goood_value = 'evil'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: A instance has no attribute 'goood_value'

How can I achieve this?

I would also like to note that I'm fully aware that a caller can create a new class and do whatever they want, bypassing this entirely. This would be unsupported behavior, though. Making the object I do provide just creates a fail-fast bonehead check to save time against typos for those who do leverage the object I'm providing (myself included), rather than making them scratch their heads wondering why things are behaving in unexpected ways.

jpmc26
  • 28,463
  • 14
  • 94
  • 146

3 Answers3

8

You can hook into attribute setting with the __setattr__ method. This method is called for all attribute setting, so take into account it'll be called for your 'correct' attributes too:

class A(object):
    good_attr = None
    really_good_attr = None
    another_good_attr = None

    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise AttributeError(
                '{} instance has no attribute {!r}'.format(
                    type(self).__name__, name))
        super(A, self).__setattr__(name, value)

Because good_attr, etc. are defined on the class the hasattr() call returns True for those attributes, and no exception is raised. You can set those same attributes in __init__ too, but the attributes have to be defined on the class for hasattr() to work.

The alternative would be to create a whitelist you could test against.

Demo:

>>> a = A()
>>> a.good_attr = 'foo'
>>> a.bad_attr = 'foo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 10, in __setattr__
AttributeError: A instance has no attribute 'bad_attr'

A determined developer can still add attributes to your instance by adding keys to the a.__dict__ instance dictionary, of course.

Another option is to use a side-effect of using __slots__; slots are used to save memory as a dictionary takes a little more space than just putting values directly into the C structure Python creates for each instance (no keys and dynamic table are needed then). That side-effect is that there is no place for more attributes on such a class instance:

class A(object):
    __slots__ = ('good_attr', 'really_good_attr', 'another_good_attr')

    def __init__(self):
        self.good_attr = None
        self.really_good_attr = None
        self.another_good_attr = None

The error message then looks like:

>>> a = A()
>>> a.good_attr = 'foo'
>>> a.bad_attr = 'foo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'bad_attr'

but do read the caveats listed in the documentation for using __slots__.

Because there is no __dict__ instance attribute when using __slots__, this option really closes the door on setting arbitrary attributes on the instances.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • I'm opting for `__setattr__` with a whitelist (stored in a variable `__attr_whitelist` so it gets name mangled to discourage messing with it). This seems the least intrusive and to have the least extra side effects. As a bonehead check, I don't think I need to worry too much about the access to `__dict__`. If someone is that determined, they'll just modify the code or create a different class. – jpmc26 Jul 31 '14 at 15:01
0

A more idiomatic option is to use a named tuple.

Python 3.6 and higher

In Python 3.6 and higher, you can use typing.NamedTuple to achieve this very easily:

from typing import NamedTuple, Any

class A(NamedTuple):
    good_attr: Any = None
    really_good_attr: Any = None
    another_good_attr: Any = None

More specific type constraints can be used if desired, but the annotations must be included for NamedTuple to pick up on the attributes.

This blocks not only the addition of new attributes, but also the setting of existing attributes:

>>> a = A()
>>> a.goood_value = 'evil'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'goood_value'
>>> a.good_attr = 'a value'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

This forces you to specify all the values at construction time instead:

a = A(
    good_attr='a value',
    really_good_attr='better value',
    another_good_attr='a good value',
)

Doing so is typically not a problem, and when it is, it can be worked around with the judicious use of local variables.

Python 3.5 and lower (including 2.x)

These versions of Python either do not have the typing module or typing.NamedTuple does not work as used above. In these versions, you can use collections.namedtuple to achieve mostly the same effect.

Defining the class is simple:

from collections import namedtuple

A = namedtuple('A', ['good_attr', 'really_good_attr', 'another_good_attr'])

And then construction works as above:

a = A(
    good_attr='a value',
    really_good_attr='better value',
    another_good_attr='a good value',
)

However, this does not allow for the omission of some values from calling the constructor. You can either include None values explicitly when constructing the object:

a = A(
    good_attr='a value',
    really_good_attr=None,
    another_good_attr='a good value',
)

Or you can use one of several techniques to give the argument a default value:

A.__new__.func_defaults = (None,) * 3
a = A(
    good_attr='a value',
    another_good_attr='a good value',
)
jpmc26
  • 28,463
  • 14
  • 94
  • 146
-1

make the parameter private by adding two underscores to it, ex self.__good_attr, this way someone can't set that parameter outside of the class. Then make a function that sets the __good_attr variable and have that function throw an exception if it's wrong.

notorious.no
  • 4,919
  • 3
  • 20
  • 34
  • This sounds to me like it's an Java-inspired idea. It would work, but using setters and getters is not pythonic at all. Python has ways to do this without obvious setters and getters. – kratenko Jul 31 '14 at 14:55
  • 1
    I agree it's very java/c++ inspired. I didn't know about the __setattr__ and __slots__. I just suggested the first thing that came to mind. You learn something new everyday around here :D – notorious.no Jul 31 '14 at 16:06