|
|
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 ...