2

Based on Disable global variable lookup in Python (and my own answer there), I have had problems when using a function with optional parameters such as in this minimal example:

import types

def noglobal(f):
    return types.FunctionType(f.__code__, {})

@noglobal
def myFunction(x=0):
    pass

myFunction()

Essentially, it fails like this:

Traceback (most recent call last):
  File "SetTagValue.py", line 10, in <module>
    myFunction()
TypeError: myFunction() missing 1 required positional argument: 'x'

Why is x suddenly treated as a required parameter?

bers
  • 4,817
  • 2
  • 40
  • 59

2 Answers2

4

It's because you didn't properly copy the function. If you take a look at the signature of types.FunctionType you'll see that it accepts 5 arguments:

class function(object)
 |  function(code, globals, name=None, argdefs=None, closure=None)
 |
 |  Create a function object.
 |
 |  code
 |    a code object
 |  globals
 |    the globals dictionary
 |  name
 |    a string that overrides the name from the code object
 |  argdefs
 |    a tuple that specifies the default argument values
 |  closure
 |    a tuple that supplies the bindings for free variables

You didn't pass any argdefs, so the function has no optional arguments any more. The correct way to copy the function is

types.FunctionType(f.__code__,
                   {},
                   f.__name__,
                   f.__defaults__,
                   f.__closure__
                   )

However, this leads to another problem: Cutting off access to globals also cuts off access to builtins. If you try to use print or open or dict or something similar in myFunction, you'll get a NameError. So the really correct way to write your decorator is this:

import builtins
import types

def noglobal(f):
    return types.FunctionType(f.__code__,
                              {'__builtins__': builtins},
                              f.__name__,
                              f.__defaults__,
                              f.__closure__
                              )
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • Thanks, `{'__builtins__': builtins}` is a good idea. I actually use `globals().copy()` to allow all globals defined so far (in particular, builtins, imports and other functions). Or is there a smarter way to achieve that, too? – bers Mar 04 '19 at 14:51
  • @bers I don't see the point in doing that. You don't know where this function is coming from, you don't know what its globals are. Why are you giving it access to the globals of the module where the `noglobal` decorator is defined? That makes little sense. – Aran-Fey Mar 04 '19 at 14:53
  • I know where the function is coming from - I wrote it myself :) I just want to avoid using a globally defined variable (within `if __name__ == "__main__"`) by accident. – bers Mar 04 '19 at 15:02
2

If you want to preserve the default argument values you need to pass them as well:

import types

def noglobal(f):
    return types.FunctionType(f.__code__, {}, f.__name__, f.__defaults__)

@noglobal
def myFunction(x=0):
    pass

myFunction()

You can pass one last closure parameter to types.FunctionType, which you may also want to inherit from f.__closure__ if you want to keep functions with a closure working.

jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • Thank you! Could you please add where to find the signature of `types.FunctionType`? I have of course looked for it before, but had not found it. – bers Mar 04 '19 at 14:46
  • 2
    @bers It's not in the official documentation, because it's a CPython implementation detail. You can do `help(types.FunctionType)` though. – Aran-Fey Mar 04 '19 at 14:49