Improved repr() for lambdas

One of the biggest reasons I don’t use lambda functions is that they are difficult to debug. They would be a bit easier to debug if they had a better repr() dependent on their origin in the code. For example, I think a lambda should have specialized __qualname__ (and sometimes __name__) in the following circumstances:

# assignment to a variable
func = lambda: ...
# named "func"

# assignment to expression
my_object.funcs['my_lambda'] = lambda: ...
# qualname is "my_object.funcs['my_lambda']"


# literal container
operators = {
    'add': lambda x, y: x+y
}
# __qualname__ is operators['add']

In general, if the qualname ends with a python identifier, __name__ should be set to that identifier, otherwise it should probably stay as <lambda>.

For lambdas used as an argument to a function, we could do something like this to indicate what the function is used for

# keyword arg
sorted(..., key=lambda: ...)
# qualname is key@sorted

# positional arg
filter(lambda: ..., ...)
# qualname is <lambda>@filter

If we want to take the idea even further, we can generate a repr() dynamically based on values of closure variables. For example

>>> add_n = lambda x: (lambda y: x + y)

>>> add_n
<function add_n(x)>

>>> add_n(2)
<function add_n(2)(y)>

>>> add_n(2)(3)
5

I think this is likely to be significantly harder than you imagine, because the lambda (function) object does not know what variable(s) it has been assigned to. I’m not even sure there’s a meaningful answer to “what is the name of this lambda”, given that objects can be assigned to multiple names at the same time.

How would you handle something like

f = lambda x: x + 1
g = f
# Should the lambda be named f or g?
del f
# What would the lambda's __name__ be now?

I suggest that if you want to take this forward, you look at the interpreter source and work out how you would implement it. If you can find a reasonable implementation strategy, then it may be worth taking further.

Or maybe, as a less ambitious proposal, the repr of a lambda could include the source file and line it was defined on? That information is currently available on the __code__ attribute of the lambda object.

Something like the following will “patch up” a lambda’s qualname, if you want to try it out:

def fix_qualname(fn):
    code = fn.__code__
    fn.__qualname__ = f"<lambda - {code.co_filename}@{code.co_firstlineno}>"

Warning: I’ve no idea if messing with __qualname__ like this at runtime is safe, so use at your own risk!

2 Likes

Slightly related: `inspect.get_source(lambda)` improvement · Issue #136521 · python/cpython · GitHub

1 Like

because the lambda (function) object does not know what variable(s) it has been assigned to

It would be done at compile time, while parsing the abstract syntax tree.

# Should the lambda be named f or g?

It would still be named f. It’s about what it’s assigned to initially.

If you can find a reasonable implementation strategy, then it may be worth taking further.

Here’s a pure-python dummy implementation using ast.NodeTransformer. This works for assignments, but could be extended to the other cases as well.

type AnyAssign = ast.Assign | ast.AnnAssign | ast.NamedExpr

# example regex to parse __qualname__ and __name__ from an expression
# i.e. foo.bar -> ('foo.bar', 'bar')
good_name = re.compile(r"^(?:(?:[a-zA-Z0-9\[\]\.]+\.)?([a-zA-Z0-9]+)|[a-zA-Z0-9\[\]\.]+)$")

def make_lambda[T](func: T, qualname: str|None, name: str|None) -> T:
    """
    Runtime magic to rename a lambda function.
    In the real implementation, the name and qualname
    would be stored in the bytecode directly.
    """
    if not qualname:
        qualname = '<lambda>' 
    if not name:
        name = '<lambda>'
    func.__name__ = name
    func.__qualname__ = qualname
    return func


class Transformer(ast.NodeTransformer):
    """Finds lambda expressions in assignments
    and renames them to the assignment target"""

    def visit_Assign(self, node):
        return self.maybe_lambda(node)
    
    def visit_AnnAssign(self, node):
        return self.maybe_lambda(node)
    
    def get_name(self, node: AnyAssign) -> tuple[str|None, str|None]:
        """Determine best candidates for __qualname__, __name__
        from an assignment node"""
        name = None
        
        if hasattr(node, 'target'):
            name = good_name.match(ast.unparse(node.target))
        
        if not name and hasattr(node, 'targets'):
            for n in node.targets:
                maybe = good_name.match(ast.unparse(n))only
                if maybe:
                    name = maybe
                    if maybe.group(1):
                        # __name__ candidate is a valid identifier.
                        break
                
        
        return (
            name.group(0) if name else None,
            name.group(1) if name else None
        )
    
    
    def maybe_lambda(self, node: AnyAssign):
        """If the node's value is a lambda, rename it to the node's target."""
        lam = node.value

        if isinstance(lam, ast.Lambda):
            name, qualname = self.get_name(node)
            # Use runtime magic to rename the lambda.
            node.value = ast.Call(
                func=ast.Name(id='make_lambda', ctx=ast.Load()),
                args=[
                    lam,
                    ast.Constant(value=name),
                    ast.Constant(value=qualname)],
                keywords=[]
            )

        self.generic_visit(node)
        return node

See also open issue: Make the repr of lambda contain signature and body expression.

2 Likes

If you’re doing this, don’t. There’s no reason to assign a lambda function to a simple name - just use def.

These can be done with a decorated function. I’ve used this pattern in a lot of projects:

def operator(f):
    operators[f.__name__] = f
    return f

@operator
def add(x, y): return x+y

You could even mutate __qualname__ in the decorator, if that helps.

3 Likes

I second this. PEP-008 recommends against this usage. One reason people use an anonymous expression to define a function is to avoid thinking of a name. If you name the function, use def to assign the name internally.

Right. Lambda expressions are intended for return expressions that are simple enough to not need debugging. They should never raise.

3 Likes

I like this:

  1. Having full source provides more information for debugging (as opposed to having only signature)
  2. Doesn’t mess with anonymity of lambda
  3. Simpler to do

If changing the __repr__ is undesirable (backwards compatibility or other reasons), ability to access it via __name__ might be sufficient.

1 Like

So on second thought, since I really only want this feature in interactive code (I never use lambdas in application code), I am going to write an IPython extension that implements custom naming and pretty-printing for lambdas.