2

I have a class representing parameter options for my algorithm. Creating an object initializes all options to some predefined values. I can then change a parameter by simply assigning a different value to the corresponding attribute:

opts = AlgOptions()
opts.mutation_strength = 0.2

However, I sometimes make a mistake when typing the attribute name, for example:

opts = AlgOptions()
opts.mutation_str = 0.2

Python just creates a different attribute and continues running, producing unexpected results which are difficult to debug.

Is there an elegant way to avoid this potential pitfall? It feels tedious to always double check all attribute names, compared to languages such as C# which would interpret this situation as an error. Few things come to my mind, but have some caveats:

1) I can make setter methods for parameters, but that seems like a lot of boilerplate code. That way if I try to invoke a method with a wrong name, I will get a meaningful excepion.

2) I can create a list of all attributes inside the class, and perform a check if there is any class attribute whose name is not in the list. However, I am not sure where and when would I perform the check, and it seems like a clumsy solution anyway.

3 Answers 3

4

You can achieve this by using slots, which basically prevents creating of attributes not declared in __init__:

>>> class Foo:
...     __slots__ = ["bar", "baz"]
...     def __init__(self, bar, baz):
...         self.bar = bar
...         self.baz = baz
...
>>> foo = Foo(1, 2)
>>> foo.asdf = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'asdf'
>>> foo.bar
1
>>> foo.bar += 5
>>> foo.bar
6

Note that using __slots__ has some disadvantages discussed in the documentation.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, this seems like a clean way to do it!
2

So generally it's best to just avoid making typos ;) but here is one (slightly hacky) solution. Basically subclass the NoSetAttr class and use the set_new_attr method to set new attributes in the class.

class NoSetAttr:
    def __setattr__(self, attr, value):
        getattr(self, attr)  # will raise error if not found
        super().__setattr__(attr, value)
    def set_new_attr(self, attr, value):
        super().__setattr__(attr, value)

class ExampleClass(NoSetAttr):
    def __init__(self, x):
        self.set_new_attr('x', x)

inst = ExampleClass(12)
inst.x  # 12
inst.x = 23
inst.x  # 23
inst.y = 2  # raises attribute error

You could probably turn the base class into a class decorator @no_set_attr and then annotate your classes which may be easier.

Comments

1

Here's a possible way to allow only certain attributes to be set:

class AlgOptions():

    def __str__(self):
        return str(self.__dict__)

    def __setattr__(self, k, v):
        allowed = ('mutation_strength',)
        if k in allowed:
            self.__dict__[k] = v
        else:
            raise AttributeError(f"attribute {k} not allowed")


opts = AlgOptions()
opts.mutation_strength = 0.2
try:
    opts.mutation_str = 0.2
except Exception as e:
    print('Sorry about this:', e)

print(opts)

Personally I would never use something like this on my code. I don't like these type of methods that try to "fight" the language... remember, python isn't c#.

2 Comments

Not part of the question, but you're __str__ function is an awful solution. It will fail if you have set any attribute to something that isn't a dict, list, float, int, float or None (or if the former two contain anything that isn't). Why not just use return str(self.__dict__) if you want that behaviour?
@FHTMitchell You're absolutely right. A lot of times when creating&debugging these throwaway snippets i tend to use json.dumps instead favouring str+prettyprint. Usually when dealing with non-json serializable datatypes I'll write my own encoder/decoder or using another libraries for serialization. Anyway, with your suggestion you get rid of the json dependency in the snippet so I'll just change my answer, thanks to point it out, +1 ;)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.