7

Take the following class

class Person(object):

def __init__(self, first_name, last_name):
    self.first_name = first_name
    self.last_name = last_name

How can I prevent the following usage?

p1 = Person('Foo', 'Bar')
p1.firstname='Fooooooo'

The code above will execute successfully in Python, however, a mistake was made with the name of the property, i.e. its missing _ between first and name

UPDATE: This sounds like "Monkey Patching", why do I want to do this?

My intention is to simply help avoid the user from setting the wrong property, have the code execute, and see unexpected behavior and not be aware of the mistake immediately.

What does the Pythonic way recommend here?

Danish
  • 3,708
  • 5
  • 29
  • 48
  • I think this is an example of what people call "monkey patching", and I think it's one of the things people love about Python. So it sounds a bit like you're fighting the language. – BenDundee Apr 29 '13 at 14:38
  • 1
    If you really must, `__slots__` can achieve what you're looking to do. However, it really shouldn't be used for this ... I would consider that `__slots__` abuse. Ultimately, this is the reason why you need to provide a consistent API and good documentation. If a user gets a bug, they'll figure out that they didn't use your API correctly. – mgilson Apr 29 '13 at 14:40
  • 3
    @BenDundee: monkeypatching is mostly about adding / modifying behaviour at runtime. While relying on the same mechanisms, the above example is definitly a typo, not a monkeypatch ;) – bruno desthuilliers Apr 29 '13 at 14:43
  • 2
    Run PyLint on your code and it will alert you to that line. – interjay Apr 29 '13 at 14:43
  • See some of the answers to this question: http://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python . They don't precisely answer your question, but they can lead you in the right direction. – Robᵩ Apr 29 '13 at 14:50
  • Python is a scripting language, it is not meant to write code that needs to restrict things from those using it. – Inbar Rose Apr 29 '13 at 14:51
  • FWIW, the inimitable Jeff Atwood was on the record as [opposing monkey patching](http://www.codinghorror.com/blog/2008/07/monkeypatching-for-humans.html). I don't know whether his opinion has changed since that article from 2008. But @brunodesthuilliers is right; same mechanism, but typos aren't monkey patches. – Mike Sherrill 'Cat Recall' Apr 29 '13 at 14:51
  • Agree, sounds like an awful hack. I favor the user finding the bug on his own, using pylint, a debugger or by hand. I could see a potential benefit of this if running asynchronous code, so that the error is raised long after the mistaken assignment. – Felipe Apr 29 '13 at 15:35

1 Answers1

10

First of all, it's almost always a bad idea to do such a thing. If only reason why you want that is making sure you don't make typos - there are better tools for that (think IDE or pylint). If you are a 100% positive that you need such a thing, here are two ways to do it:

First way - you can do this with using __setattr__ method. See python __setattr__ documentation

class Person(object):

    def __init__(self, first_name, last_name):
        self.__dict__['first_name'] = first_name
        self.__dict__['last_name'] = last_name
    def __setattr__(self, name, value):
        if name in self.__dict__:
            super(Person, self).__setattr__(name, value)
        else:
            raise AttributeError("%s has no attribute %s" %(self.__class__.__name__, name))

and output:

In [49]: a = Person(1, 2)

In [50]: a.a = 2
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/usr/local/lib/python2.7/dist-packages/django/core/management/commands/shell.pyc in <module>()
----> 1 a.a = 2

/usr/local/lib/python2.7/dist-packages/django/core/management/commands/shell.pyc in __setattr__(self, name, value)
      8             super(Person, self).__setattr__(name, value)
      9         else:
---> 10             raise AttributeError("%s has no attribute %s" %(self.__class__.__name__, name))

AttributeError: Person has no attribute a

Alternatively, you can do this using __slots__(python __slots__ documentation):

class Person(object):
    __slots__ = ("first_name", "last_name")

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

output:

In [32]: a = Person("a", "b")

In [33]: a.first_name
Out[33]: 'a'

In [34]: a.a = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/usr/local/lib/python2.7/dist-packages/django/core/management/commands/shell.pyc in <module>()
----> 1 a.a = 1

AttributeError: 'Person' object has no attribute 'a'

The first way is more flexible as it allows hacking this code even further through using __dict__ directly, but that would be even more wrong than it is now. Second approach preallocates space for certain number of instance variables (references), which means less memory consumption.

Krzysztof Bujniewicz
  • 2,407
  • 21
  • 16
  • While (almost - this should call `super.__setattr__`, not `super.__setitem__`) technically correct ("the worst kind of correct"), this is a perfect exemple of what NOT to do in Python. Fighting the language is only a waste of time and a major PITA. – bruno desthuilliers Apr 29 '13 at 14:47
  • 1
    sorry about setitem, corrected that. I agree that most of the time it is wrong, but one case i have used it - and found it useful - is creating an ORM (you fight the language there anyway). And in the long run, it was not a waste of time. – Krzysztof Bujniewicz Apr 29 '13 at 14:53