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.
4 Answers 4
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
library 1 -- besides advanced Enum
and NamedTuple
implementations, it also has a Constant
2 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
.
-
\$\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– thoroc2020年09月02日 14:50:59 +00:00Commented Sep 2, 2020 at 14:50
-
1\$\begingroup\$ @thoroc: Thanks for the nudge. The new repo is at
github
\$\endgroup\$Ethan Furman– Ethan Furman2020年09月02日 19:36:11 +00:00Commented Sep 2, 2020 at 19:36
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
-
\$\begingroup\$
Constant
suggests an instance is a constant variable itself. How aboutConstantContainer
? \$\endgroup\$Daniel– Daniel2018年05月10日 07:25:42 +00:00Commented 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\$Jonathan Veit– Jonathan Veit2018年06月07日 14:41:24 +00:00Commented Jun 7, 2018 at 14:41
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.
-
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– Jonathan Veit2018年04月27日 16:49:03 +00:00Commented Apr 27, 2018 at 16:49
-
1\$\begingroup\$ Two comments about
__contains__
: (1) having__contains__
and usingin
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 onTYPE_A
orTYPE_B
(as inif 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– Ethan Furman2018年04月27日 17:19:20 +00:00Commented 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()
, ordir(ConstantClass)
by default). (2) How so?ConstantClass.__contains__()
can act just likeConstantClass.all()
(returningx in values
instead ofvalues
). \$\endgroup\$Daniel– Daniel2018年04月27日 17:34:09 +00:00Commented 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 addin
compatibility to the class -- it would have to be on the metaclass for that to work. \$\endgroup\$Ethan Furman– Ethan Furman2018年04月27日 17:43:38 +00:00Commented Apr 27, 2018 at 17:43
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
Enum
use-case than aConstant
use-case -- can you explain a little more why you don't want to useEnum
? \$\endgroup\$