4

I would like to extract the docstring of a function once it has been wrapped in lambda.

Consider the following example:

def foo(a=1):
    """Foo docstring"""
    return a

dct = {
    "a": foo,
    "b": lambda: foo(2),
}

for k, v in dct.items()
    print(k, v(), v.__doc__)

I get:

a 1 Foo docstring
b 2 None

How can I reference the function called on "calling" the lambda one?

Update

Thanks for all answers:

from functools import partial

def foo(a=1):
    """Foo docstring"""
    return a

dct = {
    "a": foo,
    "b": partial(foo, 2),
}

for k, v in dct.items():
    if hasattr(v, "func"):
        print(k, v(), v.func.__doc__)
    else:
        print(k, v(), v.__doc__)
a 1 Foo docstring
b 2 Foo docstring
yellowhat
  • 429
  • 6
  • 17
  • 1
    This is an interesting question. I am not sure it is possible, since 'b' is a brand new function altogether that has no docstring. I am not sure if you can introspect into the functions that a function calls. – C_Z_ Oct 19 '21 at 15:57
  • 5
    You can't, you'd be better off using [`functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial) which preserves the wrapped function as the `func` attribute - `functools.partial(foo, 2).func.__doc__`. – jonrsharpe Oct 19 '21 at 15:58
  • 2
    You cannot. A function can call multiple functions, so how should python know which of the "child" docstrings is the right one? Best I can think of is to use [`functools.wraps`](https://docs.python.org/3/library/functools.html#functools.wraps) on the lambda, which will copy the wrapped function's docstring to the wrapping function. – 0x5453 Oct 19 '21 at 15:58
  • There is always the option to "manually" add a docstring to your lambdas, like this `dct["b"].__doc__ = "Lambda docstring"` but I do not know if this fits your actual use-case. – idinu Oct 19 '21 at 16:06

3 Answers3

1

There is no "good" way to do this. However, it is technically possible using the inspect module. Here is a very brittle and fragile implementation that fits your use case of getting the docstring of the first function called by a lambda:

import inspect
import re

def get_docstring_from_first_called_function(func):
    # the inspect module can get the source code
    func_source = inspect.getsource(func)

    # very silly regex that gets the name of the first function
    name_of_first_called_function = re.findall(r'\w+|\W+', func_source.split("(")[0])[-1]

    # if the function is defined at the top level, it will be in `globals()`
    first_called_function = globals()[name_of_first_called_function]
    return first_called_function.__doc__


def foo(a=1):
    """Foo docstring"""
    return a

b = lambda: foo(2)

print(get_docstring_from_first_called_function(b))
> Foo docstring

As I said, this implementation is fragile and brittle. For instance, it breaks instantly if the first function called is not in globals. But if you find yourself in very dire straits, you can probably hack together a solution for your use case.

If at all possible, however, you should use functools instead

import functools

def foo(a=1):
    """Foo docstring"""
    return a

b = functools.partial(foo, 2)

print(b.func.__doc__)
C_Z_
  • 7,427
  • 5
  • 44
  • 81
0

Well, trying to add features such as docs to smt which by definition should be "anonymous"... could be done only in a tricky way. I could achieve that using nested decorators.

Notice first that print('__doc__' in dir(lambda: '')) is True.

def add_doc_to_lambda(func, a, docs=''):
    # also provide custom docs parameter
    if docs == '':
        return (lambda l: (lambda _=setattr(l, '__doc__', func.__doc__): l)())(lambda: func(a))
    return (lambda l: (lambda _=setattr(l, '__doc__', docs): l)())(lambda: func(a))

def foo(a=1):
    """Foo docstring"""
    return a

dct = {
    "a": foo,
    "b": add_doc_to_lambda(foo, 2), # add_doc_to_lambda(foo, 2, 'lambda docs') # case of custom docs
}

for k, v in dct.items():
    print('>>> ', k, v(), v.__doc__)

Output

>>>  a 1 Foo docstring
>>>  b 2 Foo docstring   # lambda docs # case custom docs

Remark:

  1. add_doc_to_lambda is callable
print(dct['b'])
<function dec.<locals>.<lambda> at 0x7f6eda92ee50>
  1. Also a more straightforward way such as
l = lambda: foo(2)
l.__doc__ = foo.__doc__
print(l.__doc__)

works but assignment of lambda function to a variable is a bad practice.

cards
  • 3,936
  • 1
  • 7
  • 25
0

I suggest that a different mechanism for your lambda may be appropriate here. Anything that returns a properly wrapped FunctionType is going to have direct access to the correct __doc__. One common method would be to use functools.partial with functools.wraps:

from functools import partial, wraps

dct = {
    "a": foo,
    "b": wraps(foo)(partial(foo, 2)),
}
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264