Detecting empty function definitions in python
Asked Answered
Z

4

14

I need to detect whether a function is an empty definition or not. It can be like:

def foo():
    pass

or like:

def foo(i, *arg, **kwargs):
    pass

or like:

foo = lambda x: None

What is the most elegant way to detect them using the 'inspect' module? Is there a better way than this:

def isEmptyFunction(func):
    e = lambda: None
    return func.__code__.co_code == e.__code__.co_code
Zingaro answered 29/11, 2012 at 7:8 Comment(5)
You can use generate_tokens and check if tokens start with "def" or "lambda". There after, look for ":" and a "pass" or "None" immediately afterwards. Not clean, but I guess this should work.Tapdance
Yet another way would be to use ast module and parse the function string. I think you can work out the details.Tapdance
The way you're doing it looks fine.Dutiable
Curiosity only: Why? Is lambda x: x if x is None else None an empty function?Stickseed
I have an array of functions and I want to execute the first non-empty function and then stop. (These functions may return None.)Zingaro
T
4

The method you propose does not quite work because empty functions that have docstrings have a slightly different bytecode.

The value of func.__code__.co_code for an empty function with no docstring is 'd\x00\x00S', while the value of it for a function with a docstring is 'd\x01\x00S'.

For my purposes, it works just to add the additional case to test for:

def isEmptyFunction(func):
    def empty_func():
        pass

    def empty_func_with_doc():
        """Empty function with docstring."""
        pass

    return func.__code__.co_code == empty_func.__code__.co_code or \
        func.__code__.co_code == empty_func_with_doc.__code__.co_code
Tranche answered 11/7, 2014 at 4:5 Comment(2)
Unfortunately the byte code for a function with just a doc string looks the same as a function that returns a constant. So this is not sufficient.Hsiuhsu
@ChristopherBarber : that's true and the reason why I added my own answerRhodian
R
2

To answer the original question: I don't think there is a better way, but definitely a more resilient one.

Building on top of this answer by @kcon:

def isEmptyFunction(func): 
    def empty_func(): 
        pass

    def empty_func_with_doc(): 
        """Empty function with docstring.""" 
        pass 

    return func.__code__.co_code == empty_func.__code__.co_code or \
        func.__code__.co_code == empty_func_with_doc.__code__.co_code

which fails for the following:

def not_empty_returning_string():
    return 'not empty'

isEmptyFunction(just_return_string) # True

as well as for lambdas:

not_empty_lambda_returning_string = lambda x: 'not empty'

isEmptyFunction(not_empty_lambda_returning_string) # True

I built an extended version which also checks constants with the exception of docstrings:

def is_empty_function(f):
    """Returns true if f is an empty function."""

    def empty_func():
        pass

    def empty_func_with_docstring():
        """Empty function with docstring."""
        pass

    empty_lambda = lambda: None

    empty_lambda_with_docstring = lambda: None
    empty_lambda_with_docstring.__doc__ = """Empty function with docstring."""

    def constants(f):
        """Return a tuple containing all the constants of a function without:
            * docstring
        """
        return tuple(
            x
            for x in f.__code__.co_consts
            if x != f.__doc__
        )

    return (
            f.__code__.co_code == empty_func.__code__.co_code and
            constants(f) == constants(empty_func)
        ) or (
            f.__code__.co_code == empty_func_with_docstring.__code__.co_code and
            constants(f) == constants(empty_func_with_docstring)
        ) or (
            f.__code__.co_code == empty_lambda.__code__.co_code and
            constants(f) == constants(empty_lambda)
        ) or (
            f.__code__.co_code == empty_lambda_with_docstring.__code__.co_code and
            constants(f) == constants(empty_lambda_with_docstring)
        )

Testing:

#
# Empty functions (expect: is_empty_function(f) == True)
#

def empty():
    pass

def empty_with_docstring():
    """this is just an example docstring."""
    pass

empty_lambda = lambda: None

empty_lambda_with_docstring = lambda: None
empty_lambda_with_docstring.__doc__ = """this is just an example docstring."""

#
# Not empty functions (expect: is_empty_function(f) == False)
#

def not_empty():
    print('not empty');

def not_empty_with_docstring():
    """this is just an example docstring."""
    print('not empty');

not_empty_lambda = lambda: print('not empty')

not_empty_lambda_with_docstring = lambda: print('not empty')
not_empty_lambda_with_docstring.__doc__ = """this is just an example docstring."""

#
# Not empty functions returning a string (expect: is_empty_function(f) == False)
#

def not_empty_returning_string():
    return 'not empty'

def not_empty_returning_string_with_docstring():
    return 'not empty'

not_empty_lambda_returning_string = lambda: 'not empty'

not_empty_lambda_returning_string_with_docstring = lambda: 'not empty'
not_empty_lambda_returning_string_with_docstring.__doc__ = """this is just an example docstring."""


all([
  is_empty_function(empty) == True,
  is_empty_function(empty_with_docstring) == True,
  is_empty_function(empty_lambda) == True,
  is_empty_function(empty_lambda_with_docstring) == True,

  is_empty_function(not_empty) == False,
  is_empty_function(not_empty_with_docstring) == False,
  is_empty_function(not_empty_lambda) == False,
  is_empty_function(not_empty_lambda_with_docstring) == False,

  is_empty_function(not_empty_returning_string) == False,
  is_empty_function(not_empty_returning_string_with_docstring) == False,
  is_empty_function(not_empty_lambda_returning_string) == False,
  is_empty_function(not_empty_lambda_returning_string_with_docstring) == False,

]) # == True
Rhodian answered 21/11, 2019 at 10:50 Comment(8)
The screw case is that a function with a doc string and no body has the same constants and byte code as a function with a doc string and a return None statement. I am not sure there is any way to detect this without looking at the actual source string (if it is available).Hsiuhsu
@ChristopherBarber: I am not quire sure if I do understand the problem. For both functions is_empty_function returns True. Which is what I would expect. Or would you consider a function with just return None a "non empty" function?Rhodian
Does return None look empty? Looks like a statement to me, even if the semantics end up being the same. My use case is that I have a custom property decorator that provides a default getter implementation if the user has not provided one, so in this case there is a clear distinction between the two cases. However, it seems the only way to tell the difference is to actually look at the source.Hsiuhsu
I don't think "looking at the source" will help you because the two ast-trees for pass and return None are very similar if not identical. But maybe we can find another way to do what you want. Can you share a gist which shows how your decorated is intended to be used?Rhodian
I was talking about looking at the actual source string. I have no idea what the AST looks like but syntactically pass and return None are not the same so I would expect the AST to be different.Hsiuhsu
The decorator I have is used just like a regular @property decorator but will provide a default getter implementation if the body of the getter either just has a pass without a doc-string or a doc-string alone. (Except that for the reasons we just discussed it also treats a doc-string followed by return None the same as the empty case).Hsiuhsu
Looking at the __code__ attributes of the getter function, I don't see anything that shows that there is any difference. I could use the source location information to see if the body is truly empty, but I don't think that will work if the code is byte compiled without source.Hsiuhsu
Now for my case, it turns out that it is highly unlikely a user will want to return None, so looking at the constants is probably good enough.Hsiuhsu
F
0

The way you're using works. A perhaps more "elegant" solution would be to have a list of functions, and in all your empty (or all your non-empty) functions you would add it to the list, and then check whether the function is in the list or not.

Fenian answered 3/12, 2012 at 4:58 Comment(0)
S
-4

Why would you do that? It looks like bad design. I would bet you wouldn't make anything faster.

python -m timeit -s'def a(): pass' -s'def b(): pass' 'if a.__code__.co_code == b.__code__.co_code: pass'
1000000 loops, best of 3: 0.293 usec per loop

python -m timeit -s 'def a(): pass' -s 'def b(): pass' 'a()'
10000000 loops, best of 3: 0.0941 usec per loop

It seems like it is magnitude slower to compare than to just do call, because there were 10 times more loops in the latter timeit. The equals operator actually is surely calls a.code.co_code.eq. So you are just making things slower.

Supinator answered 5/1, 2013 at 9:54 Comment(1)
There are situations where you would want to detect if a function is empty or not not for the purpose of not calling it.Hyponitrite

© 2022 - 2024 — McMap. All rights reserved.