Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 779
culebrón Avatar asked Sep 28 '17 23:09

culebrón


2 Answers

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)
like image 168
ShadowRanger Avatar answered Nov 20 '22 07:11

ShadowRanger


Well, here's a hacky approach:

'<locals>' in f.__qualname__

It seems brittle to me, though.

Another approach is to play with the Frame, but I like that even less, I think:

In [1]: import inspect

In [2]: def deco(f):
   ...:     try:
   ...:         frame = inspect.currentframe()
   ...:         print(frame.f_back.f_locals is globals())
   ...:     finally:
   ...:         del frame
   ...:     return f
   ...:

In [3]: @deco
   ...: def g(): pass
   ...:
True

In [4]: def f():
   ...:     @deco
   ...:     def g(): pass
   ...:

In [5]: f()
False
like image 45
juanpa.arrivillaga Avatar answered Nov 20 '22 06:11

juanpa.arrivillaga