Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I use a decorator to mutate the local scope of a function in Python?

Is there any way of writing a decorator such that the following would work?

assert 'z' not in globals()

@my_decorator
def func(x, y):
   print z

EDIT: moved from anwser

In answer to hop's "why?": syntax sugar / DRY.

It's not about caching, it's about calculating z (and z1, z2, z3, ...) based upon the values of x & y.

I have lots of functions which do related things, and I don't want to do have to write

z1, z2, z3=calculate_from(x, y)

at the beginning of every single function - I'll get it wrong somewhere. If this were c I'd do this with cpp (if this were lisp, I'd do this with macros ...), but I wanted to see if decorators could do the same thing.

If it helps, I'd almost certainly call the decorator "precalculate_z", and it certainly wouldn't be part of any public API.

I could probably get a similar effect from using the class infrastructure as well, but I wanted to see if it was doable with raw functions.

like image 747
Toby White Avatar asked Feb 26 '09 16:02

Toby White


1 Answers

Echoing Hop's answer

  1. Don't do it.
  2. Seriously, don't do this. Lisp and Ruby are more appropriate languages for writing your own custom syntax. Use one of those. Or find a cleaner way to do this
  3. If you must, you want dynamic scoped variables, not lexically scoped.

Python doesn't have dynamically scoped variables, but you can simulate it. Here's an example that simulates it by creating a global binding, but restores the previous value on exit:

http://codepad.org/6vAY8Leh

def adds_dynamic_z_decorator(f):
  def replacement(*arg,**karg):
    # create a new 'z' binding in globals, saving previous
    if 'z' in globals():
      oldZ = (globals()['z'],)
    else:
      oldZ = None
    try:
      globals()['z'] = None
      #invoke the original function
      res = f(*arg, **karg)
    finally:
      #restore any old bindings
      if oldZ:
        globals()['z'] = oldZ[0]
      else:
        del(globals()['z'])
    return res
  return replacement

@adds_dynamic_z_decorator
def func(x,y):
  print z

def other_recurse(x):
  global z
  print 'x=%s, z=%s' %(x,z)
  recurse(x+1)
  print 'x=%s, z=%s' %(x,z)

@adds_dynamic_z_decorator
def recurse(x=0):
  global z
  z = x
  if x < 3:
    other_recurse(x)

print 'calling func(1,2)'
func(1,2)

print 'calling recurse()'
recurse()

I make no warranties on the utility or sanity of the above code. Actually, I warrant that it is insane, and you should avoid using it unless you want a flogging from your Python peers.

This code is similar to both eduffy's and John Montgomery's code, but ensures that 'z' is created and properly restored "like" a local variable would be -- for instance, note how 'other_recurse' is able to see the binding for 'z' specified in the body of 'recurse'.

like image 186
Aaron Avatar answered Oct 24 '22 08:10

Aaron