Nested contexts -- a chain of mapping objects « Python recipes « ActiveState Code (original) (raw)

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())