This implements nonlocal in Python 2...albeit in a slightly ugly way. Tested with CPython 2.7 and PyPy.
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 | import inspect, types, dis
__all__ = ['export_nonlocals', 'nonlocals']
# http://www.jonathon-vogel.com/posts/patching_function_bytecode_with_python/
def find_code(code, f):
i = 0
while i < len(code):
if f(code, i):
return i
elif code[i] < dis.HAVE_ARGUMENT:
i += 1
else:
i += 3
# http://nedbatchelder.com/blog/201301/byterun_and_making_cells.html
def make_cell(value):
return (lambda x: lambda: x)(value).func_closure[0]
globals().update(dis.opmap)
def export_nonlocals(*vars):
def func(f):
code = map(ord, f.func_code.co_code)
varnames = list(f.func_code.co_varnames)
names = list(f.func_code.co_names)
cf=lambda c,i:c[i] in (LOAD_FAST,STORE_FAST) and varnames[c[i+1]] in vars
while True:
idx = find_code(code, cf)
if idx is None:
break
code[idx] = LOAD_NAME if code[idx] == LOAD_FAST else STORE_NAME
var = varnames[code[idx+1]]
code[idx+1] = len(names)
try:
code[idx+1] = names.index(var)
except ValueError:
names.append(var)
for i, var in enumerate(filter(varnames.__contains__, names)):
varnames[varnames.index(var)] = '__anon_var_%d' % i
rescode = types.CodeType(f.func_code.co_argcount, f.func_code.co_nlocals,
f.func_code.co_stacksize,
f.func_code.co_flags^0x01,
''.join(map(chr, code)), f.func_code.co_consts,
tuple(names), tuple(varnames),
f.func_code.co_filename, f.func_code.co_name,
f.func_code.co_firstlineno,
f.func_code.co_lnotab, f.func_code.co_freevars,
f.func_code.co_cellvars)
return types.FunctionType(rescode, dict(f.func_globals, __ns=True),
f.func_name, f.func_defaults, f.func_closure)
return func
def nonlocals(*vars):
def func(f):
caller = inspect.stack()[1][0]
caller_vars = caller.f_globals
caller_vars.update(caller.f_locals)
code = map(ord, f.func_code.co_code)
varmap = {}
freevars = list(f.func_code.co_freevars)
freec = len(freevars)
freeoffs = len(f.func_code.co_cellvars)
varnames = list(f.func_code.co_varnames)
closure = list(f.func_closure or [])
names = list(f.func_code.co_names)
consts = list(f.func_code.co_consts)
fglobals = {'__nonlocal_plocals': caller.f_locals}
names.extend(fglobals.keys())
plocals_pos = len(names)-1
offs = 0
def cf(c, i):
if c[i] in (LOAD_FAST, STORE_FAST) and varnames[c[i+1]] in vars:
return True
elif c[i] in dis.hasjabs:
c[i+1] += offs
while True:
idx = find_code(code, cf)
if idx is None:
break
code[idx] = LOAD_DEREF if code[idx] == LOAD_FAST else STORE_DEREF
var = varnames[code[idx+1]]
code[idx+1] = len(freevars)
try:
code[idx+1] = freevars.index(var)
except ValueError:
freevars.append(var)
code[idx+1] += freeoffs
if code[idx] == STORE_DEREF and caller_vars.get('__ns') == True:
const_id = len(consts)
try:
const_id = consts.index(var)
except ValueError:
consts.append(var)
code.insert(idx, DUP_TOP)
code[idx+4:idx+4] = [
LOAD_GLOBAL, plocals_pos, 0,
LOAD_CONST, const_id, 0,
STORE_SUBSCR
]
offs += 4
nlocals = len(freevars)-freec+f.func_code.co_nlocals
closure.extend(map(make_cell, map(caller_vars.__getitem__,
freevars[freec:])))
rescode = types.CodeType(f.func_code.co_argcount, nlocals,
f.func_code.co_stacksize, f.func_code.co_flags,
''.join(map(chr, code)), tuple(consts),
tuple(names), tuple(varnames),
f.func_code.co_filename, f.func_code.co_name,
f.func_code.co_firstlineno,
f.func_code.co_lnotab, tuple(freevars),
f.func_code.co_cellvars)
return types.FunctionType(rescode, dict(f.func_globals, **fglobals),
f.func_name, f.func_defaults, tuple(closure))
return func
|
This module is a nonlocal patch for Python 2.7 and PyPy. Save it as nonlocals.py. Here's a first example:
from nonlocals import *
def outer():
var = 0
@nonlocals('var')
def inner():
print var
var = 1
print var
inner()
outer()
That prints:
0
1
Without nonlocals, this prints:
Traceback (most recent call last):
File "ex1.py", line 11, in <module>
outer()
File "ex1.py", line 10, in outer
inner()
File "ex1.py", line 7, in inner
print var
UnboundLocalError: local variable 'var' referenced before assignment
However, right now, changes to var
made in inner
don't propagate to outer
. Example:
from nonlocals import *
def outer():
var = 0
@nonlocals('var')
def inner():
print var
var = 1
print var
inner()
print var
outer()
That prints:
0
1
0
This is largely because of the way Python does certain things. In order to get around it, use export_nonlocals
:
from nonlocals import *
@export_nonlocals('var')
def outer():
var = 0
@nonlocals('var')
def inner():
print var
var = 1
print var
inner()
print var
outer()
export_nonlocals
sets up variables so they can be modified from inner
. Now, it prints:
0
1
1
How it works:
export_nonlocals
changes all local variable references in a way so that they modified by other scopes by replacing LOAD_FAST and STORE_FAST codes that modify the given variables to LOAD_NAME and STORE_NAME, moving the variable reference to external names, and using bitwise NOT to change the compile flags. nonlocals
works by changing LOAD_FAST and STORE_FAST codes that modify the given variables with LOAD_DEREF and STORE_DEREF and modifying outer
's locals if export_nonlocals
was used.
This is very hackish and plays with CPython internal implementation stuff that PyPy just happens to be compatible with. YOU WERE WARNED!!!