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

Easy to use chain of dictionaries for crafting nested scopes or for a tree of scopes. Useful for analyzing AST nodes, XML nodes or other structures with multiple scopes. Can emulate various chaining styles including static/lexical scoping, dynamic scoping and Python's own globals(), locals(), nested scopes, and writeable nonlocals. Can also model Python's inheritance chains: instance dictionary, class dictionary, and base classes.

Python, 152 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
'Nested contexts trees for implementing nested scopes (static or dynamic)'

from collections import MutableMapping
from itertools import chain, imap

class Context(MutableMapping):
    ''' Nested contexts -- a chain of mapping objects.

    c = Context()           Create root context
    d = c.new_child()       Create nested child context. Inherit enable_nonlocal
    e = c.new_child()       Child of c, independent from d
    e.root                  Root context -- like Python's globals()
    e.map                   Current context dictionary -- like Python's locals()
    e.parent                Enclosing context chain -- like Python's nonlocals

    d['x']                  Get first key in the chain of contexts
    d['x'] = 1              Set value in current context
    del['x']                Delete from current context
    list(d)                 All nested values
    k in d                  Check all nested values
    len(d)                  Number of nested values
    d.items()               All nested items

    Mutations (such as sets and deletes) are restricted to the current context
    when "enable_nonlocal" is set to False (the default).  So c[k]=v will always
    write to self.map, the current context.

    But with "enable_nonlocal" set to True, variable in the enclosing contexts
    can be mutated.  For example, to implement writeable scopes for nonlocals:

        nonlocals = c.parent.new_child(enable_nonlocal=True)
        nonlocals['y'] = 10     # overwrite existing entry in a nested scope

    To emulate Python's globals(), read and write from the the root context:

        globals = c.root        # look-up the outermost enclosing context
        globals['x'] = 10       # assign directly to that context

    To implement dynamic scoping (where functions can read their caller's
    namespace), pass child contexts as an argument in a function call:

        def f(ctx):
            ctx.update(x=3, y=5)
            g(ctx.new_child())

        def g(ctx):
            ctx['z'] = 8                    # write to local context
            print ctx['x'] * 10 + ctx['y']  # read from the caller's context

    '''
    def __init__(self, enable_nonlocal=False, parent=None):
        'Create a new root context'
        self.parent = parent
        self.enable_nonlocal = enable_nonlocal
        self.map = {}
        self.maps = [self.map]
        if parent is not None:
            self.maps += parent.maps

    def new_child(self, enable_nonlocal=None):
        'Make a child context, inheriting enable_nonlocal unless specified'
        enable_nonlocal = self.enable_nonlocal if enable_nonlocal is None else enable_nonlocal
        return self.__class__(enable_nonlocal=enable_nonlocal, parent=self)

    @property
    def root(self):
        'Return root context (highest level ancestor)'
        return self if self.parent is None else self.parent.root

    def __getitem__(self, key):
        for m in self.maps:
            if key in m:
                break
        return m[key]

    def __setitem__(self, key, value):
        if self.enable_nonlocal:
            for m in self.maps:
                if key in m:
                    m[key] = value
                    return
        self.map[key] = value

    def __delitem__(self, key):
        if self.enable_nonlocal:
            for m in self.maps:
                if key in m:
                    del m[key]
                    return
        del self.map[key]

    def __len__(self, len=len, sum=sum, imap=imap):
        return sum(imap(len, self.maps))

    def __iter__(self, chain_from_iterable=chain.from_iterable):
        return chain_from_iterable(self.maps)

    def __contains__(self, key, any=any):
        return any(key in m for m in self.maps)

    def __repr__(self, repr=repr):
        return ' -> '.join(imap(repr, self.maps))


if __name__ == '__main__':
    c = Context()
    c['a'] = 1
    c['b'] = 2
    d = c.new_child()
    d['c'] = 3
    print 'd:  ', d
    assert repr(d) == "{'c': 3} -> {'a': 1, 'b': 2}"

    e = d.new_child()
    e['d'] = 4
    e['b'] = 5
    print 'e:  ', e
    assert repr(e) == "{'b': 5, 'd': 4} -> {'c': 3} -> {'a': 1, 'b': 2}"

    f = d.new_child(enable_nonlocal=True)
    f['d'] = 4
    f['b'] = 5
    print 'f:  ', f
    assert repr(f) == "{'d': 4} -> {'c': 3} -> {'a': 1, 'b': 5}"

    print len(f)
    assert len(f) == 4
    assert len(list(f)) == 4
    assert all(k in f for k in f)
    assert f.root == c

    # dynanmic scoping example
    def f(ctx):
        print ctx['a'], 'f:  reading "a" from the global context'
        print 'f: setting "a" in the global context'
        ctx['a'] *= 999
        print 'f: reading "b" from globals and setting "c" in locals'
        ctx['c'] = ctx['b'] * 50
        print 'f: ', ctx
        g(ctx.new_child())
        print 'f: ', ctx


    def g(ctx):
        print 'g: setting "d" in the local context'
        ctx['d'] = 44
        print '''g: setting "c" in f's context'''
        ctx['c'] = -1
        print 'g: ', ctx
    global_context = Context(enable_nonlocal=True)
    global_context.update(a=10, b=20)
    f(global_context.new_child())

Easy to use:

>>> d = Context()     # Create a new context
>>> d['k'] = 1        # Store a key/value just like a dict
>>> e = d.new_child() # Create a child context
>>> e['q'] = 2        # Store an item in the child context
>>> e['k']            # Look-up a key in the child, then parent
1
>>> e                 # Display the dictionary chain
{'q': 2} -> {'k': 1}

5 comments

Sunjay Varma 11 years, 2 months ago  # | flag

That's cool.

Alia Khouri 11 years, 2 months ago  # | flag

Nice!

thom neale 10 years, 11 months ago  # | flag

Really cool recipe.

Perhaps a useful enhancement would be the possibility of passing in a dict or extra_context mapping when calling Context() or c.new_child().

Miki Tebeka 9 years ago  # | flag

I think __setitem__ and __delitem__ should raise KeyError if the key is not found. Also IMO __len__ since it'll count keys shared by parent and child twice. Should be something like:

return len(set(chain.from_iterable(m.iterkeys() for m in self.maps)))
Miki Tebeka 9 years ago  # | flag

The above should read: ... __len__ is wrong since ...