[Python-ideas] For-loop variable scope: simultaneous possession and ingestion of cake
Arnaud Delobelle
arnodel at googlemail.com
Mon Oct 13 21:55:28 CEST 2008
On 13 Oct 2008, at 01:24, George Sakkis wrote:
> Since this idea didn't get much steam, a more modest proposal would
> be to relax the restriction on cells: allow the creation of new
> cells and the rebinding of func_closure in pure Python. Then one
> could explicitly create a new scope without any other change in the
> language through a 'localize' decorator that would create a new cell
> for every free variable (i.e. global or value from an enclosing
> scope) of the function:
>
> lst = []
> for i in range(10):
> @localize
> def f(): print i
> lst.append(f)
> lst.append(localize(lambda: i**2))
>
> I'd love to be proven wrong but I don't think localize() can be
> implemented in current Python.
Here is a (very) quick and dirty implementation in CPython (requires
ctypes). I'm sure it breaks in all sorts of ways but I don't have
more time to test it :) Tests follow the implementation.
------------------------- localize.py ---------------------
from itertools import *
import sys
import ctypes
from array import array
from opcode import opmap, HAVE_ARGUMENT
new_cell = ctypes.pythonapi.PyCell_New
new_cell.restype = ctypes.py_object
new_cell.argtypes = [ctypes.py_object]
from types import CodeType, FunctionType
LOAD_GLOBAL = opmap['LOAD_GLOBAL']
LOAD_DEREF = opmap['LOAD_DEREF']
LOAD_FAST = opmap['LOAD_FAST']
STORE_GLOBAL = opmap['STORE_GLOBAL']
STORE_DEREF = opmap['STORE_DEREF']
STORE_FAST = opmap['STORE_FAST']
code_args = (
'argcount', 'nlocals', 'stacksize', 'flags', 'code',
'consts', 'names', 'varnames', 'filename', 'name',
'firstlineno', 'lnotab', 'freevars', 'cellvars'
)
def copy_code(code_obj, **kwargs):
"Make a copy of a code object, maybe changing some attributes"
for arg in code_args:
if not kwargs.has_key(arg):
kwargs[arg] = getattr(code_obj, 'co_%s' % arg)
return CodeType(*map(kwargs.__getitem__, code_args))
def code_walker(code):
l = len(code)
code = array('B', code)
i = 0
while i < l:
op = code[i]
if op >= HAVE_ARGUMENT:
yield op, code[i+1] + (code[i+2] << 8)
i += 3
else:
yield op, None
i += 1
class CodeMaker(object):
def __init__(self):
self.code = array('B')
def append(self, opcode, arg=None):
app = self.code.append
app(opcode)
if arg is not None:
app(arg & 0xFF)
app(arg >> 8)
def getcode(self):
return self.code.tostring()
def localize(f):
if not isinstance(f, FunctionType):
return f
nonlocal_vars = []
new_cells = []
frame = sys._getframe(1)
values = dict(frame.f_globals)
values.update(frame.f_locals)
co = f.func_code
deref = co.co_cellvars + co.co_freevars
names = co.co_names
varnames = co.co_varnames
offset = len(deref)
varindex = {}
new_code = CodeMaker()
# Disable CO_NOFREE in the code object's flags
flags = co.co_flags & (0xFFFF - 0x40)
# Count the number of arguments of f, including *args & **kwargs
argcount = co.co_argcount
if flags & 0x04:
argcount += 1
if flags & 0x08:
argcount += 1
# Change the code object so that the non local variables are
# bound to new cells which are initialised to the current value
# of the variable with that name in the surrounding frame.
for opcode, arg in code_walker(co.co_code):
vname = None
if opcode in (LOAD_GLOBAL, STORE_GLOBAL):
vname = names[arg]
elif opcode in (LOAD_DEREF, STORE_DEREF):
vname = deref[arg]
else:
new_code.append(opcode, arg)
continue
try:
vi = varindex[vname]
except KeyError:
nonlocal_vars.append(vname)
new_cells.append(new_cell(values[vname]))
vi = varindex[vname] = offset
offset += 1
if opcode in (LOAD_GLOBAL, LOAD_DEREF):
new_code.append(LOAD_DEREF, vi)
else:
new_code.append(STORE_DEREF, vi)
co = copy_code(co, code=new_code.getcode(),
freevars=co.co_freevars + tuple(nonlocal_vars),
flags=flags)
return FunctionType(co, f.func_globals, f.func_name,
f.func_defaults,
(f.func_closure or ()) + tuple(new_cells))
------------------------ /localize.py ---------------------
Some examples:
>>> y = 3
>>> @localize
... def f(x):
... return x, y
...
>>> f(5)
(5, 3)
>>> y = 1000
>>> f(2)
(2, 3)
>>> def test():
... acc = []
... for i in range(10):
... @localize
... def pr(): print i
... acc.append(pr)
... return acc
...
>>> for f in test(): f()
...
0
1
2
3
4
5
6
7
8
9
>>> lambdas = [localize(lambda: i) for i in range(10)]
>>> for f in lambdas: print f()
...
0
1
2
3
4
5
6
7
8
9
>>> # Lastly, your example
>>> lst = []
>>> for i in range(10):
... @localize
... def f(): print i
... lst.append(f)
... lst.append(localize(lambda: i**2))
...
>>> [f() for f in lst]
0
1
2
3
4
5
6
7
8
9
[None, 0, None, 1, None, 4, None, 9, None, 16, None, 25, None, 36,
None, 49, None, 64, None, 81]
>>>
--
Arnaud
More information about the Python-ideas
mailing list