Welcome, guest | Sign In | My Account | Store | Cart

"Namespaces are one honking great idea -- let's do more of those!" -- The Zen of Python

For when you want a simple, easy namespace, but you don't want it cluttered up with Python's object machinery.

Python, 104 lines
  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
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
"""namespace module"""

__all__ = ("Namespace", "as_namespace")

from collections import Mapping, Sequence

class _Dummy: ...
CLASS_ATTRS = dir(_Dummy)
del _Dummy


class Namespace(dict):
    """A dict subclass that exposes its items as attributes.

    Warning: Namespace instances do not have direct access to the
    dict methods.

    """

    def __init__(self, obj={}):
        super().__init__(obj)

    def __dir__(self):
        return tuple(self)

    def __repr__(self):
        return "%s(%s)" % (type(self).__name__, super().__repr__())

    def __getattribute__(self, name):
        try:
            return self[name]
        except KeyError:
            msg = "'%s' object has no attribute '%s'"
            raise AttributeError(msg % (type(self).__name__, name))

    def __setattr__(self, name, value):
        self[name] = value

    def __delattr__(self, name):
        del self[name]

    #------------------------
    # "copy constructors"

    @classmethod
    def from_object(cls, obj, names=None):
        if names is None:
            names = dir(obj)
        ns = {name:getattr(obj, name) for name in names}
        return cls(ns)

    @classmethod
    def from_mapping(cls, ns, names=None):
        if names:
            ns = {name:ns[name] for name in names}
        return cls(ns)

    @classmethod
    def from_sequence(cls, seq, names=None):
        if names:
            seq = {name:val for name, val in seq if name in names}
        return cls(seq)

    #------------------------
    # static methods

    @staticmethod
    def hasattr(ns, name):
        try:
            object.__getattribute__(ns, name)
        except AttributeError:
            return False
        return True

    @staticmethod
    def getattr(ns, name):
        return object.__getattribute__(ns, name)

    @staticmethod
    def setattr(ns, name, value):
        return object.__setattr__(ns, name, value)

    @staticmethod
    def delattr(ns, name):
        return object.__delattr__(ns, name)


def as_namespace(obj, names=None):

    # functions
    if isinstance(obj, type(as_namespace)):
        obj = obj()

    # special cases
    if isinstance(obj, type):
        names = (name for name in dir(obj) if name not in CLASS_ATTRS)
        return Namespace.from_object(obj, names)
    if isinstance(obj, Mapping):
        return Namespace.from_mapping(obj, names)
    if isinstance(obj, Sequence):
        return Namespace.from_sequence(obj, names)
    
    # default
    return Namespace.from_object(obj, names)

About Object Namespaces

Every object in python has a namespace, the mapping of attributes to values for that object; and the "dot" notation (obj.attr) gives you direct access to those attributes. This is facilitated through the attribute access mechanism (__getattribute__(), et al.).

The list of names in an object's namespace are exposed by its __dir__() special method, effectively making it the "object namespace protocol". To pull that list from the object, you use dir(obj).

External Namespaces

Some objects also represent separate secondary namespaces. We can call these "external" namespaces, in contrast to the "internal" object namespace. Mappings (like dict) and sequences (like list or str) expose their external namespace through indexing operators.

For modules and classes the external namespace is more abstract and gets mixed in with the internal one. Just like with the internal namespace, the objects in the external namespace for modules and classes are exposed through attribute access.

The Problem

If you want a quick-and-easy attribute-based namespace currently, you have a few simple options:

class ns:
    x = 1
    y = 2
ns.x
# prints 1
ns.z = 2

# or

class Object: ...
ns = Object()
ns.x = 1
ns.y = 2

These define an external namespace containing the names "x" and "y". That's just what you wanted. However, ns is also cluttered up with all the normal object attributes:

dir(ns)
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x', 'y']

A Solution

This recipe demonstrates a way to have an external namespace accessible through attribute access (like normal objects do), but still keep it separate from the internal one (like dicts do). The trade-off is that the internal namespace is no longer accessible through attribute access.

The trick is to use Python's awesome flexibility to give you that simple namespace without all the normal object attributes getting in the way. In the recipe, the Namespace class is a dict subclass that exposes its keys as attributes. The as_namespace() function converts an object into a new Namespace object. It's especially useful as a class decorator.

Examples

spam = {'x': 1, 'y': 2}
spam = Namespace(spam)
spam
# Namespace({'x': 1, 'y': 2})

dir(spam)
# ['x', 'y']

spam.x
# 1

spam.x = 3
spam.x
# 3

@as_namespace
class spam:
    x = 1
    y = 2

spam
# Namespace({'x': 1, 'y': 2})

Namespace Instances Aren't Quite Dictionaries

Even though Namespace is a subclass of dict, none of dict's instance attributes are available on Namespace objects. This is due to the use of __getattribute__(). This does not impact the use of special methods by operators and builtins since those are looked-up on the class and not the instance.

Just to clarify, here are the methods you might normally use on a dict instance that are not available on Namespace instances:

clear, copy, fromkeys, get, items, keys, pop, popitem, setdefault, update, values

The special methods are likewise hidden. However, you can look up any of the attributes on the class to access them. Here are three examples of how you could do so:

dict.copy(ns)
type(ns).get(ns, "x")
Namespace.__getitem__(ns, "x")

Also, the Namespace class has some static methods to make it easier to access the internal namespace of a Namespace instance.

Warning: dict(ns) doesn't work for Namespace objects. Instead, use the dict unpacking syntax: dict(**ns). The dict constructor looks for the keys attribute on the passed object to see if it is a mapping (__getattribute__() precludes that here).

6 comments

WAC 10 years ago  # | flag

Student: Could this be used to store metadata?

Eric Snow (author) 10 years ago  # | flag

Anywhere you could use a dict, you could use a Namespace. The difference is that the Namespace does not directly expose its __dict__ or use the normal name lookup. To address that, the Namespace class exposes several static methods.

Wolfgang Scherer 9 years, 6 months ago  # | flag

Oh, I forgot ... key access is actually posssible. Since I cannot edit my post, I created a recipe of my own at http://code.activestate.com/recipes/578122-abusing-modules-as-namespaces/

So you can delete my previous comment.

Martin Miller 9 years, 3 months ago  # | flag

I assume you meant class _Dummy: pass in line 7 not class _Dummy: ....

Eric Snow (author) 9 years, 3 months ago  # | flag

... is a literal for Ellipsis. In Python 3 you can use ... anywhere. In this case it would be the same as putting a string literal there, an integer, or any other expression.

In Python 3.2, calling dir() on a Namespace instance fails.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __dir__() must return a list, not tuple

Changing line 24 to return a list instead fixes this.
--ap

Created by Eric Snow on Sat, 1 Oct 2011 (MIT)
Python recipes (4591)
Eric Snow's recipes (39)

Required Modules

  • (none specified)

Other Information and Tasks