views:

274

answers:

5

As python does not have concept of constants, would it be possible to raise an exception if an 'constant' attribute is updated? How?

class MyClass():
    CLASS_CONSTANT = 'This is a constant'
    var = 'This is a not a constant, can be updated'

#this should raise an exception    
MyClass.CLASS_CONSTANT = 'No, this cannot be updated, will raise an exception'

#this should not raise an exception    
MyClass.var = 'updating this is fine'

#this also should raise an exception    
MyClass().CLASS_CONSTANT = 'No, this cannot be updated, will raise an exception'

#this should not raise an exception    
MyClass().var = 'updating this is fine'

Any attempt to change CLASS_CONSTANT as a class attribute or as an instance attribute should raise an exception.

Changing var as a class attribute or as an instance attribute should not raise an exception.

A: 

If you really want to have constant that can't be changed then look at this: http://code.activestate.com/recipes/65207/

Mohamed
+1  A: 

Start reading this:

http://docs.python.org/reference/datamodel.html#customizing-attribute-access

You basically write your own version of __setattr__ that throws exceptions for some attributes, but not others.

S.Lott
it still will be possible to update attribute by accessing: `MyClass.CLASS_CONSTANT`. And I think this is the OP's problem.
SilentGhost
"possible" is not the same as the specific example the OP posted. "possible" isn't the issue unless the programmers involved are malicious sociopaths. It's documented as a constant; catching honest mistakes outside unit testing is the only purpose for this.
S.Lott
+2  A: 

You could do something like this: (from http://www.siafoo.net/snippet/108)

class Constants:
  # A constant variable
  foo = 1337

  def __setattr__(self, attr, value):
    if hasattr(self, attr):
      raise ValueError, 'Attribute %s already has a value and so cannot be written to' % attr
    self.__dict__[attr] = value

Then use it like this:

>>> const = Constants()
>>> const.test1 = 42
>>> const.test1
42
>>> const.test1 = 43
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __setattr__
ValueError: Attribute test1 already has a value and so cannot be written to
>>> const.test1
42
EPiddy
`>>> Constants.foo = 45; >>> Constants.foo 45`
SilentGhost
Yeah, apparently __setattr__ on a class only protects access to instance attributes, not class attributes. So as EPiddy suggested you basically need to make sure the object you're using an instance of a class instead of a class object itself.
Phil
+3  A: 

Customizing __setattr__ in every class (e.g. as exemplified in my old recipe that @ainab's answer is pointing to, and other answers), only works to stop assignment to INSTANCE attributes and not to CLASS attributes. So, none of the existing answers would actually satisfy your requirement as stated.

If what you asked for IS actually exactly what you want, you could resort to some mix of custom metaclasses and descriptors, such as:

class const(object):
  def __init__(self, val): self.val = val
  def __get__(self, *_): return self.val
  def __set__(self, *_): raise TypeError("Can't reset const!")

class mcl(type):
  def __init__(cls, *a, **k):
    mkl = cls.__class__
    class spec(mkl): pass
    for n, v in vars(cls).items():
      if isinstance(v, const):
        setattr(spec, n, v)
    spec.__name__ = mkl.__name__
    cls.__class__ = spec

class with_const:
  __metaclass__ = mcl

class foo(with_const):
  CLASS_CONSTANT = const('this is a constant')

print foo().CLASS_CONSTANT
print foo.CLASS_CONSTANT
foo.CLASS_CONSTANT = 'Oops!'
print foo.CLASS_CONSTANT

This is pretty advanced stuff, so you might prefer the simpler __setattr__ approach suggested in other answers, despite it NOT meeting your requirements as stated (i.e., you might reasonably choose to weaken your requirements in order to gain simplicity;-). But the techniques here might still be interesting: the custom descriptor type const is another way (IMHO far nicer than overriding __setattr__ in each and every class that needs some constants AND making all attributes constants rather than picking and choosing...) to block assignment to an instance attribute; the rest of the code is about a custom metaclass creating unique per-class sub-metaclasses of itself, in order to exploit said custom descriptor to the fullest and achieving the exact functionality you specifically asked for.

Alex Martelli
Perfect! I absolutely love your answers to py questions. I wish SO had an invite feature, so I could ping you for my py questions. Thanks.
Ingenutrix
+2  A: 

You can use a metaclass to achieve this:

class ImmutableConstants(type):
    def __init__(cls, name, bases, dct):
        type.__init__(cls, name, bases, dct)

        old_setattr = cls.__setattr__
        def __setattr__(self, key, value):
            cls.assert_attribute_mutable(key)
            old_setattr(self, key, value)
        cls.__setattr__ = __setattr__

    def __setattr__(self, key, value):
        self.assert_attribute_mutable(key)
        type.__setattr__(self, key, value)

    def assert_attribute_mutable(self, name):
        if name.isupper():
            raise AttributeError('Attribute %s is constant' % name)

class Foo(object):
    __metaclass__ = ImmutableConstants
    CONST = 5
    class_var = 'foobar'

Foo.class_var = 'new value'
Foo.CONST = 42 # raises

But are you sure this is a real issue? Are you really accidentally setting constants all over the place? You can find most of these pretty easily with a grep -r '\.[A-Z][A-Z0-9_]*\s*=' src/.

Ants Aasma
Foo().CONST = 42 does not raise?
Ingenutrix
Yeah you can override the constant on an instance. If that is a problem, the metaclass initializer could install a similar setattr to the class. I didn't try to make it complete because I fail to see why it would be necessary. It's easy enough to spot mistakes by static analysis (e.g. grep) and it's impossible to make it secure without a proper sandboxing feature in the interpreter. Even if you use a C extension to create the constant, you can overwrite the memory location with ctypes, or if you use an external const service, override the service location.
Ants Aasma
I plan to opt for 'Alex Martelli' answer as I can subclass from with_const and use const() [ class foo(with_const):], pls see his solution for details. but I think it is a matter of style and some other dev might prefer your style. I hope you will fix the 'Foo().CONST does not raise' problem in your answer, so that if some one uses your solution, they will not have that problem. I think your answer has merit and I would like to up vote it, if your solution solves the question.
Ingenutrix
btw, I am a beginner at python, and I am sure you made valid points in your previous comment (which are way beyond my current understanding of python). I was looking for a python level solution to the problem, mostly to avoid other absolute newbies I work with to accidentally overwrite a variable whose value should not change. Thanks for your contribution. Both your solution and Alex Martelli's have help broaden my knowledge on python.
Ingenutrix
Added the check for instances and made the constant check customizable by subclassing the metaclass. If your intent is to catch unintentional errors I still maintain that it would be better to just search the code for direct assignments to constants. Either mine or Alex's checks can be bypassed if you want to, and the checks introduce complexity and overhead to your code. Whereas static checks keep the complexity separate and reveal errors at commit time as opposed to runtime.
Ants Aasma
Classic case of nothing is correct before you test, had the check the wrong way around in assert_attribute_mutable.
Ants Aasma
+1, new code clears my 4 test cases, voted up. Thanks.
Ingenutrix