functions running in list as parameter

Question:

I am making a library for creating text adventure games.

Here is my main.py code:

from txtadvlib import utils

utils.menu("Main Menu", ["play","about","quit"])
utils.getInput(prompt="user>",
               ifs=[
                 "1",
                 "2",
                 "3"
               ],
               thens=[
                 print(1),
                 print(2),
               ],
               catch="print('caught')"
              )

Here is the code I use for the library:

import os
import time
import random
import colorama
from datetime import date
from colorama import Fore

class utils:
  def menu(title,optionsArray,):
    print(title)
    cycles = 0
    for i in optionsArray:
      cycles += 1
      print(f"[{cycles}].{i}")
      
  def getInput(prompt, ifs, thens, catch):
    choice = input(prompt)
    for i in ifs:
      if choice == i:
        eval(thens[ifs.index(i)])
        break
      else:
        eval(catch)

I do not want to be using eval for every function as it requires the library user to format any functions as strings.

Here is the problem:
the functions in the thens list run immediately and then the input goes.

Here is the output:

Main Menu
[1].play
[2].about
[3].quit
1    <--- function in the list ran
2    <--- function in the list ran
user>   <--- input prompt

I have tried making the function parameters one line, and I can’t think of anything else to try.

Asked By: Rat

||

Answers:

You could expect the library user to pass in a callable, which will be called in case this option is selected:

...
import functools 

def catch():
  print('caught')

utils.getInput(prompt="user>",
               ifs=[
                 "1",
                 "2",
                 "3"
               ],
               thens=[
                 lambda: print(1),
                 functools.partial(print, 2),
               ],
               catch=catch
              )

Then you code should look sth like this:

...
  def getInput(prompt, ifs, thens, catch):
    choice = input(prompt)
    for i in ifs:
      if choice == i:
        thens[ifs.index(i)]()
        break
      else:
        catch()
Answered By: yjay

You need a different way to pass the thens list, because as you found out, calls such as print(1) do execute right away.

One way to do it is to pass a tuple of the function object and its arguments, to be called later:

thens = [
    (print, 1),
    (print, 2)
]

Because the function name print is not followed by parens (), it is not called immediately.

functools.partial is another way to do this.

Answered By: John Gordon

The basic idea is to use functions as objects, luckily Python can easily do that. E.g. you can use def to make a function, and pass it into the menu.

def on_play():
    print(1)

def on_about():
    print(2)

def on_catch():
    print('caught')

utils.getInput(prompt="user>",
               ifs=["1","2","3"],
               thens=[on_play,on_about,],
               catch=on_catch)

It’s important, that as soon as you put parentheses besides the function name, the function is called. You want to avoid that, and call it later, so omit parentheses when defining thens. At the point where you’re ready to call the function, add parentheses to the function object stored in thens, e.g. thens[i]()

def getInput(prompt, ifs, thens, catch):
    choice = input(prompt)
    try:
        i = ifs.index(choice)
        thens[i]() # now call
    except:
        catch() # now call

Sometimes you would to pass arguments to the functions, to reuse them. I like to use functools.partial for that. It takes one existing function object, and any arguments, and creates a new function object that has some arguments filled in and excluded from the original arguments list. You can achieve this, to make sure that thens[i]() can be called without any arguments, because a newly created function object via functools.partial would already fill in all necessary arguments.

from functools import partial

def on_menu(mode, username=None, game_version=None):
    if mode == 'play'
        print(1, username)
    elif mode == 'about'
        print(2, game_version)

utils.getInput(prompt="user>",
               ifs=["1","2","3"],
               thens=[
                   partial(on_menu, mode='play', username='Bob'),
                   partial(on_menu, mode='about', game_version='v1.0'),],
               catch=on_catch)

You could also use Python lambdas to make simple one-line functions instead of writing long def.

utils.getInput(prompt="user>",
               ifs=["1","2","3"],
               thens=[
                   lambda: print('Whatever'),
                   lambda: print('Game v1.0'),
               catch=on_catch)

Effectively this lambda: print('Whatever') statement also creates a function object, that is NOT yet called, it will be only when you do (lambda: print('Whatever'))(), then the message will be printed. You can also add arguments to lambdas, explore it on your own

Answered By: Alexey Larionov

The issue is with the way you are passing the list of functions thens to the getInput method. Instead of passing the functions as objects, you are passing them as evaluated expressions using eval().

To pass the functions as objects, you can define them separately and then pass them as a list. Here’s an example:

def foo():
    print("Hello from foo")

def bar():
    print("Hello from bar")

thens = [foo, bar]

Then, you can pass thens to getInput() method:

utils.getInput(prompt="user>",
               ifs=[
                 "1",
                 "2",
                 "3"
               ],
               thens=thens,
               catch=lambda: print('caught')
              )

Note that I have also changed the catch parameter to a lambda function, which will print "caught" when no match is found.

With these changes, the functions in thens list will not run immediately, and will be executed only when a matching input is provided.

Answered By: Boy Nandi
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.