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.
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()
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.
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
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.
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.
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()
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.
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
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.