Monday, January 28, 2013

Dyamic Constants. Of the automatic kind

A quickie one -- sometimes we need named constants -
just like "True" and "False" are in Python  - and we'd like they to behave
like named constants - not just a  variable pointing to an int.

So, if the constant you want has an integer value, something that behaves like
Python's True and False is easily achievable with:


class Constant(int):
    def __new__(cls, name, value=0):
        return int.__new__(cls, value)
    def __init__(self, name, value):
        self.name = name
    def __repr__(self):
        return self.name
    __str__ = __repr__

And there you are:
>>> RED = Constant("RED", 0)
>>> RED
RED
>>> RED + 1
1
>>> print RED
RED
Then ...we stumble on the DRY principle. The constant is nice, but the call for its creation requires its name as a string, and we have to associate the resulting object with a variable with the same name. It is a problem faced by "collections.namedtuple", and ORM's all around the Galaxy.

 We could make the code in the Constant class to inject the constant name in the caller frame, yes. But that is ugly, as it kills the "explicit is better than implicit" principle, and would break static code analysis tools everywhere - one can't have code that fiddles with the calling frames and don't feel too hackish for production code anyway:
from inspect import currentframe

class Constant(int):
    def __new__(cls, name, value=0):
        return int.__new__(cls, value)
    def __init__(self, name, value):
        self.name = name
        currentframe().f_back.f_locals[name] = self
    def __repr__(self):
        return self.name
    __str__ = __repr__

And now one can do:
>>> Constant("GREEN", 1)
GREEN
>>> GREEN
GREEN
>>> print (GREEN, GREEN + 0)
GREEN 1
No more WET stuff. But unsactisfactory, nonetheless... Then you think on the ridle: what is that can inject clean names in the current namespace? Assignment statements, also some statements like "for", "with", "except" and so on....but also the "import" statement! T

here it is -- if we can get our constants to be authomaticaly created when they are imported into the current module -- there is a drawback, the import semantics does not allow one to assign values to the names being imported.

A pity - but arbitrary constants would still work when only the name as a value is important. With you, the all new, all magic....DYNAMIC AUTO CONSTANTS


import sys

class Constant(int):
    def __new__(cls, name, value=0):
        return int.__new__(cls, value)
    def __init__(self, name, value):
        self.name = name
    def __repr__(self):
        return self.name
    __str__ = __repr__


class _ConstantFactory(object):
    counter = 0
    Constant = globals()["Constant"]
    def __init__(self):
        self.counter = 0
        self._all_constants = {}
    def __getattr__(self, name):
        cls = self.__class__
        if name in self._all_constants:
            return self._all_constants[name]
        const = cls.Constant(name, self.counter)
        self.counter += 1
        self._all_constants[name] = const
        return const

sys.modules[__name__] = _ConstantFactory()
So, this code as a not-so-well-behaved module, replaces itself in sys.modules with an instance of the _ConstantFactory class .... an almost ordinary class - but which will produce a new Constant object each time an attribute is retrueved from it - and..guess what "from <module> import name1, name2" does?

>>> from ConstantFactory import RED, GREEN, BLUE
>>> RED
RED
>>> GREEN + BLUE
5
>>> print RED, GREEN, BLUE
RED GREEN BLUE

There you are: no writing names twice, no names magic appearing in the module name-space, no declarations needed.

3 comments:

  1. the only problem is that the constant integer isnt really constant, because it depends of the import order.

    :-(

    ReplyDelete
  2. This is just a quick hack - I got to this post because I am thinking on gettign to something really usable. BTW, I learned the import order is not the one typed when you have multiple names on the import ...from ...

    ReplyDelete
  3. I think that in "serious" code, the only sane way to go to avoid repeating yourself is to explicitly pass "globals()" as an argument to the constant creation.

    ReplyDelete