16

This is mainly a "good python style" question.

I have a module which is using a number of constants that feels should be grouped.

Lets say we have Dogs and cat and each of them have number of legs and favorite food.

Note that

  1. we want to model nothing but those constants about Dogs and Cats
  2. quite probably we'll have more animals in the future.
  3. those constants wont be used outside of the current module.

I thought about the following solutions:


Constants at module level

DOG_NUMBER_OF_LEGS = 4
DOG_FAVOURITE_FOOD = ["Socks", "Meat"]
CAT_NUMBER_OF_LEGS = 4
CAT_FAVOURITE_FOOD = ["Lasagna", "Fish"]

They seem not really grouped, but I think it is the solution I prefer.


Classes as namespaces

class Dog(object):
  NUMBER_OF_LEGS = 4
  DOG_FAVOURITE_FOOD = ["Socks", "Meat"]
class Cat(object):
  NUMBER_OF_LEGS = 4
  FAVOURITE_FOOD = ["Lasagna", "Fish"]

I don't like this solution as we'll have class that we wont use and they can be actually instantiated.


Dictionary of constants

ANIMALS_CONFIG = { 
   "DOG" : { 
     "NUMBER_OF_LEGS" : 4,
     "FAVOURITE_FOOD" : ["Socks", "Meat"]
   },
   "CAT" : { 
     "NUMBER_OF_LEGS" : 4,
     "FAVOURITE_FOOD" : ["Lasagna", "Fish"]
   }
}

I also thought about adding submodules but I dont really want to expose those internal constants


what is the most pythonic way to do it / how would you do it?

Mario Corchero
  • 5,257
  • 5
  • 33
  • 59
  • 2
    You could also have a class `Animal` with `dog` and `cat` instances of that class. – syntonym Apr 22 '15 at 08:49
  • 4
    Or you could also use [named tuples](https://docs.python.org/2.7/library/collections.html#collections.namedtuple). – syntonym Apr 22 '15 at 08:50
  • How will you use them in your code? Will you iterate or select? Where would the selection parameter come from? – Peter Wood Apr 22 '15 at 08:52
  • No iteration nor anything like that. Just calling my constant with the full name. Either DOG_NUMBER_OF_LEGS , ANIMALS_CONFIG["DOG"]["NUMBER_OF_LEGS"] or Dog.NUMBER_OF_LEGS – Mario Corchero Apr 22 '15 at 09:00
  • @MarioA.CorcheroJiménez just to confirm, the **only** behaviour you want from the the objects is to hold those two values and access them by key and/or attribute? – jonrsharpe Apr 22 '15 at 09:35

5 Answers5

12

I would go for a fourth option, preferring a collections.namedtuple:

Animal = namedtuple('Animal', 'number_of_legs favourite_food')

You then create instances like:

DOG = Animal(4, ['Socks', 'Meat'])
CAT = Animal(4, ['Lasagna', 'Fish'])

and access the values externally as:

from animals import CAT

print CAT.number_of_legs

There's really no point having classes if you don't need to create any methods, and I think the form of access above is neater than e.g.:

from animals import animals

print animals['CAT']['number_of_legs']

namedtuples, like vanilla tuples, are immutable, too, so you can't accidentally reassign e.g. CAT.number_of_legs = 2somewhere.

Finally, the namedtuple is a lightweight data structure, which may be important if you're creating lots of animals:

>>> import sys
>>> sys.getsizeof({'number_of_legs': 4, 'favourite_food': ['Lasagna', 'Fish']})
140
>>> from collections import namedtuple
>>> Animal = namedtuple('Animal', 'number_of_legs favourite_food')
>>> sys.getsizeof(Animal(4, ['Lasagna', 'Fish']))
36
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
4

There's no one-size-fits-all answer, it really depends on how many constants you have to handle, how you use them, if having polymorphism dispatch makes sense or not and the phase of the moon.

Now in your example, since you have two or more sets of constants with a similar structure and meaning, I'd go for the "classes as namespaces" solution.

FWIW preventing a class from being instanciated is not that difficult:

>>> class Foo(object):
...    def __new__(cls):
...       return cls
... 
>>> Foo()
<class '__main__.Foo'>

But documentation should be enough - remember that your "constants" are only constants by convention anyway.

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
2

in more recent versions (>= 3.7) i would suggest you use a dataclass for that and making it frozen. This not only prevents any of the members from being changed but it also allows the constants to be used as dictionary keys (as they become hashable).

and in python >= 3.10 you may even want to set slots=True which prevents the addition of new members.

this way you also get the typing right:

from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Animal:
    number_of_legs: int
    favourite_foods: tuple[str, ...]

DOG = Animal(number_of_legs=4, favourite_foods=('Socks', 'Meat'))
CAT = Animal(number_of_legs=4, favourite_foods=('Lasagna', 'Fish'))

print(CAT)                # Animal(number_of_legs=4, favourite_foods=('Lasagna', 'Fish'))
print(DOG.number_of_legs) # 4
hiro protagonist
  • 44,693
  • 14
  • 86
  • 111
0

I'd go for the dictionary approach, as it feels more readable and better organized, but I believe it's a matter of personal preference.

Using classes also doesn't seem like a bad idea if you use a class with only static variables and methods like this and you never instantiate them.

Community
  • 1
  • 1
Davide
  • 301
  • 1
  • 8
0

Another way of using NamedTuples:

If there are some set of constants and its always nice to group them. It will be even more nice, if we can access them on IDE as we type them. Dictionary is not a solution as IDE cannot show recommendations as you type.

from collections import namedtuple
    
_coverage_comments = namedtuple('COVERAGE_COMMENTS',['no_progress', 'coverage_improved'])
    
COVERAGE_COMMENTS = _coverage_comments(no_progress="No Progress", coverage_improved="Improved")
    
print(COVERAGE_COMMENTS.coverage_improved)
Mic
  • 345
  • 1
  • 3
  • 9