7

I was looking at the answer to this question: Is it possible to define a class constant inside an Enum?

What interested me most was the Constant class in Ethan Furman's answer.

class Constant:
    def __init__(self, value):
        self.value = value
    def __get__(self, *args):
        return self.value
    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.value)

The question was about Python 3.4 but I'm using 2.7. In the answer Ethan sets the gravitational constant as an instance variable of the class Planet like so:

G = Constant(6.67300E-11)

My testing of this class in 2.7 shows that typing just G gives me this:

Out[49]: Constant(3)

(I set it to 3 for ease of use while testing. This looks like the __repr__ output to me, please correct me if I"m wrong.)

The value is available via G.value. However, in Ethan's answer he uses

return self.G * self.mass / (self.radius * self.radius)

This obviously only works if the value is returned vs the __repr__ output. Now if I change class Constant: to class Constant(int): then type G I still get the __repr__ output but if I type G * 4 I get 12 not the error I was getting. (TypeError: unsupported operand type(s) for *: 'instance' and 'int' )

So clearly something like the int object can output a number when called. Is there a magic method I'm missing that would allow me to do this for the Constant class? Since constants could be strings, integers, or floats I'd prefer to have 1 class that handles them all vs 3 separate classes that extend those objects.

The value is also settable via G.value. Can I lock this down so the Constant class behaves likes an actual constant? (I suspect the answer is no.)

Community
  • 1
  • 1
Gabe Spradlin
  • 1,937
  • 4
  • 23
  • 47
  • Why do you want it to be constant? – Veedrac Sep 27 '14 at 05:48
  • @Veedrac Why wouldn't I want Constants? Or read only values within a class? From my reading on Python, I understand that it is counter to Python's "We're all consenting adults" philosophy. I really like Python so far but I think that philosophy is an oversight. I program for me, myself, and I - no one else will read it or use it likely. After coding in several languages for the past 15 years I've learned a few things. One of those things is that I can prevent days worth of debugging by simply designing a class where certain things just can't be changed outside of the internals of that class. – Gabe Spradlin Sep 27 '14 at 14:24
  • I was more wondering at what level you require it to be constant; my point was to check whether you were OK with a "constant through the public interface", which is the correct way of doing this, or you actually want it to be *totally* constant, which is impossible. – Veedrac Sep 27 '14 at 14:29
  • Also instead of caring whether they can set `value`, just make it private by naming it `_value`. Then you don't have to worry about p̶e̶o̶p̶l̶e̶ yourself changing it. – Veedrac Sep 27 '14 at 14:38
  • @Vedra Sorry my first couple of questions on SO regarding Python got polite responses of the form why would you want to do something so stupid? As far as constants are concerned i can live with them being read only via the public interface. I'd prefer they were true Constants and that i could write a class variable that was read only outside of the class that defines it but i understand these may not be possible in Python. – Gabe Spradlin Sep 27 '14 at 18:00

2 Answers2

2

Your class Constant should inherit from object, to be a new style Python class.

In that way Constant will be a so called descriptor. In simple terms, descriptor are a Python construct to customize the behavior of getting and setting a class attribute. They are useful when an instance of a descriptor is set as an attribute of another class.

In your example Constant is the descriptor and Planet has an attribute which is an instance of Constant. When you get the attribute G of the Planet class (self.G in you example), what you really get is what is returned by the __get__ method of the descriptor, that is the value.

Note that __get__ is invoked only when the descriptor instance is accessed by another class attribute.

So, define the class like this:

class Constant(object):
    def __init__(self, value):
        self.value = value
    def __get__(self, *args):
        return self.value
    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.value)

Then this little example:

c = Constant(3.14)
print c

class Test:
    c = Constant(3.14)

t = Test()
print t.c

Will print:

Constant(3.14)
3.14

See that when the Constant instance is printed directly, the method __repr__ will be called, but when printed as another class attribute, __get__ will be used.

You can read more on descriptors on this great article.

Augusto Destrero
  • 4,095
  • 1
  • 23
  • 25
  • From what I've tested it only works if it is a Class variable and not an Instance variable. If I move `c = Constant` to `__init__` via `self.c = Constant(...)` then I get the `__repr__` output. And it is still write-able which is fine for a constant since I can name with all CAPS. Ideally, I'm hoping to find a way to make an instance variable write-able from within the class but not to outside `__main__` script. I had something in PHP that did this quite well but from what I've read so far it isn't possible in Python. The class attribute seems to always be available for overwrite. Thx – Gabe Spradlin Sep 27 '14 at 00:06
0

Well, value is a memeber of your class Constant; so you can try making it private:

class Constant:
  def __init__(self, value):
    # This actually transforms the variable to _Constant__value, 
    # but also hides it from outer scope
    self.__value = value
  def __get__(self, *args):
    # Altough the member is theorically renamed as _Constant__value,
    # it is completely accesible from inside the class as __value
    reurn self.__value
  def __repr__(self):
    return '%s(%r)' % (self.__class__.__name__, self.__value)

Another approach could be following this recipe.

Give them a try and let me now. Hope I'd helped you!

Community
  • 1
  • 1
SonicARG
  • 499
  • 9
  • 14
  • I haven't tried the recipe yet but the code you provided did not work for me as it's own instance. I couldn't get to the value directly anymore. I suspect it's because of what @baxeico said where it must become a descriptor within a class. Thx – Gabe Spradlin Sep 27 '14 at 00:08
  • The recipe appears to work. However, it must reside in a file and be imported. It also appears to be a singleton. Importing it within a class (just below the class definition) does not restrict it to that class's namespace. Nor does putting the import line in the `__init__`. The recipe works quite nicely if you can live with these limitations. – Gabe Spradlin Sep 29 '14 at 19:29