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.
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}
That's cool.
Nice!
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().
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:The above should read:
... __len__ is wrong since ...