Search code examples
pythonpython-3.xpython-decorators

How to detect if a function has been defined locally?


In Python, I have a decorator that has to skip any real work if a function is defined locally in the one that calls it. I made a simple testing script:

def fn1():
    # @my_decorator will be here
    def fn2():
        pass

    print(fn2)
    return fn2

x = fn1()
print(x)
print(x.__module__)

It prints this:

 <function fn1.<locals>.fn2 at 0x7fd61bdf3ae8>
 <function fn1.<locals>.fn2 at 0x7fd61bdf3ae8>
 __main__

As I see, Python sees that the function is defined in a local space (<locals> in the printed text), but I can't see how I can find that bit of data. I walked through the inspect module, and don't see anything similar.

I can't rely on whether the function is in globals or not.

What do I use?


Solution

  • First off, the direct approach is to check if the CO_NESTED flag is set on the function's code object:

    import inspect
    
    ...
    
    def is_nested(func):
        return func.__code__.co_flags & inspect.CO_NESTED
    
    def deco(func):
        if is_nested(func):
            # This is a nested function, return it unchanged
            return func
        ... otherwise, do your decoration here ...
    

    That said, there is another approach if what you care about is whether you've actually closed over anything. A function that doesn't use anything from the enclosing scope is nested, but not a closure, and that distinction is often important. So for example:

    def foo(x):
        def bar(y):
            pass
        return bar
    

    is not making a closure because bar makes use of no variables from the scope of the foo call. By contrast, even though it's a garbage reference, this is making a closure simply by reading the value of x from the enclosing scope:

    def foo(x):
        def baz(y):
            x
        return baz
    

    You can tell the difference between bar and baz by testing the __closure__ attribute (which is None if no nested variables have been closed over) or by checking the co_freevars attribute of the __code__ object (which is a tuple of names closed on, so if it's empty, then it's not a closure, though it may still be a nested function):

    def is_closure(func):
        return func.__closure__ is not None
        # Or using documented names, since __closure__ isn't for some reason,
        # co_freevars is a tuple of names captured from nested scope
        return bool(func.__code__.co_freevars)
    
        # Or on 3.3+, you even get a function to aid you:
        return bool(inspect.getclosurevars(func).nonlocals)