5
\$\begingroup\$

So I saw a need for and came up with a class implementation to group constants, which I'm calling a Constant Class. I wondered if there existed other similar implementations or if there is a flaw in what I'm doing. My team is currently finding it fairly useful and thought we should see what other people thought. Also to offer out into the world if it indeed is useful to others.

The need was simple. We wanted to group constants as to allow auto-completion and also the ability to reference a list of all the values for validation. This mainly was for the development of an API backend which handle many groups of different types.

import inspect
import re


class ConstantClass(object):
    """ Container class for meant for grouped constants """

    @classmethod
    def all(cls):
        """
        Returns all constant values in class with the attribute requirements:
          - only uppercase letters and underscores
          - must begin and end with a letter

        """
        regex = r'^[A-Z][A-Z_]*[A-Z]$'
        class_items = inspect.getmembers(cls)
        constants = filter(lambda item: re.match(regex, item[0]), class_items)
        values = map(lambda constant: constant[1], constants)

        return values

class TypeNames(ConstantClass):
    TYPE_A = 'Whatever you want it to be'
    TYPE_B = 'Another example'

TypeNames.TYPE_A # returns 'Whatever you want it to be'
TypeNames.all() # returns list of constant values
TypeNames.TYPE_A in TypeNames.all() # Returns True

Enums the closest thing but it requires we type out EnumClass.CONSTANT.value (I honestly find kind of annoying) and there isn't a built-in function to get all the values. I don't know, maybe I'm splitting hairs here but I'm just curious if there is any draw-back to what I'm doing.

\$\endgroup\$
2
  • \$\begingroup\$ That sound a lot more like an Enum use-case than a Constant use-case -- can you explain a little more why you don't want to use Enum? \$\endgroup\$ Commented Apr 27, 2018 at 18:20
  • \$\begingroup\$ This was written while developing in python 2, I agree with @EthanFurman now using python 3.4 or greater Enum is the way to go as the Enum gives the possibility of typing, and the ConstantClass.all() can be achieved using list(EnumClass). \$\endgroup\$ Commented Mar 2, 2022 at 21:38

4 Answers 4

7
\$\begingroup\$

The code looks decent, but there are a couple problems with the overall design:

  • "constants" can be changed (rebound) or removed from the class

  • the constants don't have a nice repr() (very useful for debugging)

To solve these problems, and more, check out the aenum library1 -- besides advanced Enum and NamedTuple implementations, it also has a Constant2 class.

Your code above would look like:

from aenum import Constant

class TypeNames(Constant):
    TYPE_A = 'Whatever you want it to be'
    TYPE_B = 'Another example'

and in use:

>>> print(repr(TypeNames.TYPE_A))
<TypeNames.TYPE_A: 'Whatever you want it to be'>

>>> print(TypeNames.TYPE_A)
Whatever you want it to be

>>> try:
...     del TypeNames.TYPE_B
... except AttributeError as exc:
...    print(exc)
....
cannot delete constant <TypeNames.TYPE_B>

>>> try:
...     TypeNames.TYPE_B = 'oops!'
... except AttributeError as exc:
...     print(exc)
...
cannot rebind constant <TypeNames.TYPE_B>

1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

2 Looking at your all() implementation I can see that should be added to Constant.

\$\endgroup\$
2
  • \$\begingroup\$ FYI: The source repo for aenum on bitbucket seems to be unavailable at the time of writing this comment. bitbucket.org/stoneleaf/aenum :/ \$\endgroup\$
    – thoroc
    Commented Sep 2, 2020 at 14:50
  • 1
    \$\begingroup\$ @thoroc: Thanks for the nudge. The new repo is at github \$\endgroup\$ Commented Sep 2, 2020 at 19:36
2
\$\begingroup\$

Not bad at all; I would note a couple of things.

  • Drop 'Class' at the end of your class name(s)
  • You don't need to explicitly inherit from object
  • I was able to make it work with dict, and save some code (Hope I didn't miss any fringe cases)

Like I say, though -- looks good. Here is my modifications for reference.

import re


class Constant:
    """ Container class for meant for grouped constants """

    @classmethod
    def all(cls):
        """
        Returns all constant values in class with the attribute requirements:
          - only uppercase letters and underscores
          - must begin and end with a letter

        """
        regex = r'^[A-Z][A-Z_]*[A-Z]$'
        return [kv[1] for kv in cls.__dict__.items() if re.match(regex, kv[0])]


class TypeNames(Constant):
    TYPE_A = 'Whatever you want it to be'
    TYPE_B = 'Another example'


print(TypeNames.TYPE_A)
for name in TypeNames.all():
    print(name)
print(TypeNames.TYPE_A in TypeNames.all())

Yields

Whatever you want it to be
Whatever you want it to be
Another example
True
\$\endgroup\$
2
  • \$\begingroup\$ Constant suggests an instance is a constant variable itself. How about ConstantContainer? \$\endgroup\$
    – Daniel
    Commented May 10, 2018 at 7:25
  • \$\begingroup\$ This change breaks the ability for a class to inherit, so class ClassA(Constant), would pass its constant values on to ClassB(ClassA) but not be accessible in .all() which broke some of our use case. \$\endgroup\$ Commented Jun 7, 2018 at 14:41
0
\$\begingroup\$

Looks pretty clean to me, I'll definitely be using this! If I'm allowed to nitpick:

  • According to PEP-8, top-level class definitions should be preceded by two blank lines.PEP-8: Blank Lines

  • There's no need to assign the result of map(lambda constant: constant[1], constants) to a variable. Just return it immediately.

  • If you want, you can add a ConstantClass.__contains__(). This violates rule 2 from the Zen of Python, though:

    Explicit is better than implicit.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ cool, fixed the two blank lines. I assigned it to variable just to make it more explicit at least for this explanation. Cool tip about the __contains__(), I'll have to think about that. \$\endgroup\$ Commented Apr 27, 2018 at 16:49
  • 1
    \$\begingroup\$ Two comments about __contains__: (1) having __contains__ and using in is explicit -- in was created for containment checking, and __contains__ was created so that containment checking could be optimized; (2) __contains__ won't help in this case because you would have to use it on TYPE_A or TYPE_B (as in if some_var in ConstantClass.TYPE_A) which, while you could make it work, would not be apparent as to what it was actually doing. \$\endgroup\$ Commented Apr 27, 2018 at 17:19
  • \$\begingroup\$ @Ethan Furman (1) Do you think? What does it mean for something to be 'in' a class? It makes more sense to ask 'is it in the class' attribute list?' (ConstantClass.all(), or dir(ConstantClass) by default). (2) How so? ConstantClass.__contains__() can act just like ConstantClass.all() (returning x in values instead of values). \$\endgroup\$
    – Daniel
    Commented Apr 27, 2018 at 17:34
  • 3
    \$\begingroup\$ @Coal_: (1) Matter of fact I do. ;) Generally, it means that attribute/method/etc is defined in that class, but that meaning can be further restricted, and is, in Enum (for example); (2) It can, but putting __contains__ on the class does not add in compatibility to the class -- it would have to be on the metaclass for that to work. \$\endgroup\$ Commented Apr 27, 2018 at 17:43
0
\$\begingroup\$

You can compile the regex beforehand.

Instead of:

    regex = r'^[A-Z][A-Z_]*[A-Z]$'
    class_items = inspect.getmembers(cls)
    constants = filter(lambda item: re.match(regex, item[0]), class_items)

I would do:

    regex = re.compile(r'^[A-Z][A-Z_]*[A-Z]$')
    class_items = inspect.getmembers(cls)
    constants = filter(lambda item: regex.match(item[0]), class_items)

And since this will not change from one call to the other, you can also make REGEX a module constant

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.