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\$
  • \$\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\$ – Ethan Furman Apr 27 '18 at 18:20
  • \$\begingroup\$ We use a lot of string-based types to in our api that need to be human understandable for clients so basically we didn't want the values to enumerated. I know enum is able to use string values as well but again, this solution has worked for us so far us and is minimalist at ~10 lines of code. No need to split hairs though, these solutions are all pretty close and glad to now know about aenum which was part of the reason asking. \$\endgroup\$ – Jonathan Veit May 3 '18 at 15:58
6
\$\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\$

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\$
  • \$\begingroup\$ Constant suggests an instance is a constant variable itself. How about ConstantContainer? \$\endgroup\$ – Daniel May 10 '18 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\$ – Jonathan Veit Jun 7 '18 at 14:41
1
\$\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\$
  • 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\$ – Jonathan Veit Apr 27 '18 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\$ – Ethan Furman Apr 27 '18 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 Apr 27 '18 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\$ – Ethan Furman Apr 27 '18 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, privacy policy and cookie policy

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