5
\$\begingroup\$

I've often wanted an anonymous function that would do more than return the result of an expression. So I developed one around the exec function that supports assignments and multiline lambdas but suspect a few things are missing. Suggestions would be appreciated, please.

I've tried to keep the syntax similar to a normal lambda and also easy to debug by copying the text into a standard function.

Tested with CPython 3.6.5 and Pypy 3.5.3. To give an idea of the current version, it is capable of evaluating these functions:

λ("""var:
     for i in range(var):
         print(i, end = ",")
         print(' ', end='')
         if i == var - 1:
             print(var)
     print(var)
     rtrn = [i for i in range(var, 0, -1)]
""")(5)

0, 1, 2, 3, 4, 5
5
[5, 4, 3, 2, 1]

λ("x: [x*i for i in range(x)]; print('Final:', rtrn[-1])")(3)

Output: 6
[0, 3, 6]

λ("x, y:: from math import cos, sin; rtrn = cos(x) + sin(y)")(3, 5)

-1.9489167712635838


def lambda_(code):
    """
    Anonymous function supporting multiple line and assignments.
    For one line the form is λ("parameters : code incl assignment")(*arguments) 
      and the first exepression is assigned to rtrn which is returned
    For multiline, use triple quotes & the parameters must be on the first line,
      the : is followed directly by a line feed and then the code
    Input:
        code in the form "x, y,...: return_value_expression; other code"
    Output is assigned to variable rtrn
    """
    def build_lambda(*args):
        """ 
        All arguments are declared global as listcomps etc don't create closure
        when called using exec, so declare parameters & rtrn as globals
        Multiline will return None unless you assign rtrn a value.
        """
        val_idx, val_name = 0, []
        i, parameters = -1, []
        if ":" in code:
            for i, c in enumerate(code):  # loop until the : that ends parameters
                if c in ",:":
                    if val_name:
                        parameters.append(''.join(val_name))
                        val_name = []
                        val_idx += 1
                    if c == ":":
                        break
                elif c != " ":
                    val_name.append(c)

        # exec_code sets rtrn = None for multiline or sets rtrn = first expression for
        #    single line unless :: (used if first expression can't be evaluated)
        if code[i+1] != "\n":  # single line,
            if code[i+1] != ":":
                exec_code = ''.join(("rtrn = ", code[i+1:]))
            else:
                exec_code = ''.join(("rtrn = None; ", code[i+2:]))
        else:  # multiline
            # wont run if excess leading spaces so remove them
            lead = 0
            code_ = code[i+1:]
            while code_[lead+1] == " ":  # how many on 1st line?
                lead += 1
            for i in range(1, len(code_)):  # remove that on all lines
                if code_[i-1: i+1] == "\n ":
                    code_ = code_[: i] + code_[i+lead:]
            exec_code = "rtrn = None" + "\n" + code_  # executable code

        define_globals = ', '.join(["global rtrn"] + parameters)
        assign = [' = '.join((p, str(args[i]))) for i, p in enumerate(parameters)]
        assign = '; '.join(assign)
        cmd = '; '.join((define_globals, assign if assign else "pass", exec_code))
        if args and args[-1] == "print":
            print(cmd, "\n")
        exec(cmd)
        return rtrn

    return build_lambda

λ = lambda_  # rebind to linux ctrl+shift+u 03BB




With the added complexity it seems more reasonable to create a class splitting code generation, execution and printing. The code is posted in this question.

\$\endgroup\$
3
  • \$\begingroup\$ I fail to see any advantages over a local function definition. What is the problem at hand this implementation is trying to solve? \$\endgroup\$ – 301_Moved_Permanently May 19 '18 at 17:52
  • \$\begingroup\$ You share that opinion with the BDFL who I understand may have even wanted lambda's removed from Python 3. The purpose of this implementation is to explore the use of a less limited anonymous function. I do recognize the issues including the use of exec. I posted here because I understand that this is a good place to seek feedback, which I have received :) \$\endgroup\$ – John 9631 May 20 '18 at 0:01
  • 1
    \$\begingroup\$ You should probably post the new version of the software as a new question, then revert this and add links between the questions. That way the feedback for both versions is timely and you're more likely to get feedback on the new version. \$\endgroup\$ – l0b0 May 20 '18 at 0:25
5
\$\begingroup\$

Naming:

  • λ is not a standard Python function name ([a-z] followed by [a-z0-9_]+) and will require figuring out some magic sequence in non-Greek keyboard layouts.
  • s0, s1, s2, lead and cd should be renamed to be more intuitive. For example, based on your documentation s1 could be named something like assignments.

General:

  • Rather than adding strings and newlines together I'd use format() and join(). It's easier to follow the construction of the string that way.
  • I would not recommend using this in production code. Treating strings as code is going to result in many more runtime errors which will be harder to debug than the corresponding named function.
  • λ does not accept functions without parameters - λ('print("value")')() for example results in IndexError.
  • Code construction and execution should be split up for easier debugging. I should be able to run construct_code(my_code_string) and see what would be executed for debugging.
  • I don't have the time to test this myself, but you should be able to get rid of the global rtrn by constructing an actual function and running that instead of creating global code.
\$\endgroup\$
3
  • \$\begingroup\$ Thanks very much for the comments, @l0b0. I'll review & adjust tomorrow On the last point I still need globals for the parameters for access to list comps but I'll review rtrn. \$\endgroup\$ – John 9631 May 19 '18 at 10:12
  • \$\begingroup\$ I updated it to reflect your suggestions. I've used a more general name and bound it to λ for those who prefer. I agree with your concern about using exec in production code. Originally I assumed that in a no parameter situation, like a normal lambda, we would have a : however I've tested for users not including the colon in this version. I don't think you can get rid of the global return but will have another look when I have enough time. \$\endgroup\$ – John 9631 May 19 '18 at 23:35
  • \$\begingroup\$ I added the ability to add the string "print" to the end of the arguments to print the code but at that point I felt a class might be more appropriate so I include that (and extra tests) below. \$\endgroup\$ – John 9631 May 19 '18 at 23:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.