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

This implements nonlocal in Python 2...albeit in a slightly ugly way. Tested with CPython 2.7 and PyPy.

Python, 115 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
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!!!