The Zen of Python tells us:
Namespaces are one honking great idea -- let's do more of those!
Python already has an excellent namespace type, the module, but the problem with modules is that they have to live in a separate file, and sometimes you want the convenience of a single file while still encapsulating your code into namespaces. That's where classes are the usual solution, but classes need to be instantiated and methods need to be defined with a self
parameter.
C++ has "namespaces" for encapsulating related objects and dividing the global scope into sub-scopes. Can we do the same in Python?
With a bit of metaclass trickery and the new ChainMap type from Python 3.3, we can!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | from types import ModuleType, FunctionType
from collections import ChainMap
class _NSChainedDict(ChainMap, dict):
pass
class Namespace(type):
def __new__(meta, name, bases, dict):
mod = ModuleType(name, dict.get("__doc__"))
for key, obj in dict.items():
if isinstance(obj, FunctionType):
obj = meta.chained_function(meta, obj, mod)
mod.__dict__[key] = obj
return mod
def chained_function(meta, func, mod):
d = _NSChainedDict(mod.__dict__, func.__globals__)
newfunc = FunctionType(func.__code__, d)
newfunc.__doc__ = func.__doc__
newfunc.__defaults__ = func.__defaults__
newfunc.__kwdefaults__ = func.__kwdefaults__
return newfunc
# === Test code ===
a = 999
b = 4
def spam(n=None):
raise RuntimeError('no spam for you!')
class meals(metaclass=Namespace):
a = 2
def repeat(word, count):
return ' '.join([word]*count)
def ham(n=1):
return repeat('ham', n)
def spam(n=3):
return repeat('spam', n)
def breakfast():
"""Return yummy and nutritious breakfast"""
template = "%s with a fried egg on toast and %s"
return template % (spam(), spam(1))
def lunch():
"""Return delicious and healthy lunch"""
template = "cheese, tomato and %s sandwiches"
return template % spam(a)
def dinner():
"""Return tasty and wholesome dinner"""
template = "roast %s garnished with %s"
return template % (ham(), spam(b))
assert meals.breakfast() == 'spam spam spam with a fried egg on toast and spam'
assert meals.lunch() == 'cheese, tomato and spam spam sandwiches'
assert meals.dinner() == 'roast ham garnished with spam spam spam spam'
|
Some features of the namespace objects:
Although you define a namespace using the class
keyword, the object created is actually a module. Being a module, the functions inside it are actual functions, not methods. That means you don't give them a self
argument.
Because the functions are functions, and not methods, it is simple to use them while the namespace is being built. Here is a toy example:
>>> class toy(metaclass=Namespace):
... def helper(x):
... return 2**x
... a = helper(1)
... b = helper(8)
...
>>> toy.a
2
>>> toy.b
256
>>> toy.helper(5)
32
With classes, you normally have the choice of making the helper a function, in which case it doesn't work once the class is created, or a method, in which case it doesn't work until the class is created.
Inside the functions, the scope is:
- local variables
- variables in the namespace
- global variables
- builtins
So in the test example above, meals.lunch
automatically sees meals.a
without needing to prefix it with self
or meals
. But since meals.b
does not exist, the global b
is automatically used instead.
Some limitations:
This recipe requires the ChainMap mapping from Python 3.3. If you're using an older version, you may be able to use or adapt Recipe 305268 instead.
Inside the namespace functions, keywords
global
,nonlocal
anddel
do not work as expected.This recipe abuses the
class
keyword. It would be better to have dedicated syntax for creating a namespace within a module.
C++ uses the syntax:
namespace identifier
{
entities
}
which would correspond to hypothetical Python syntax:
namespace name:
indented code block