4

I am using PyQt and want to create a menu based on a list of strings.

The problem is that when I want to call 'addAction', it requires a call-back function (for each string) that does not take any arguments.

For simple menus, this would be fine: e.g.

menu.addAction("Open", self.open)
menu.addAction("Exit", self.quit)

However, I want to just use a single function and have the 'action string' passed in as an argument.

I am wondering if python can do something like this:

def f(x, y):
    print x + 2*y



# These 2 variables are of type: <type 'function'>
callback1 = f
callback2 = f(x=7, *)
# NB: the line above is not valid python code.
# I'm just illustrating my desired functionality

print callback1(2,5)  # prints 12
print callback2(5)    # prints 17

Here is my code snippet:

def printParam(parameter):
    print "You selected %s" % parameter


# parameters = list of strings

menu_bar = QMenuBar()
menu = menu_bar.addMenu('Select Parameter')
for parameter in parameters:
    # This line will not work because the action does not pass in
    # any arguments to the function
    menu.addAction(parameter, printParam)

Any suggestions greatly appreciated

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Ciaran
  • 553
  • 5
  • 14
  • "Use a closure". A closure is simply a function that closes over a free variable in the enclosing scope. –  Jul 13 '11 at 17:03
  • @pst: No, that'd be reinventing the wheel given the presence of `functools.partial`. Plus "closure" is a much broader concept ;) –  Jul 13 '11 at 17:07
  • @delan :) What do you mean by closure being a broader concept? –  Jul 13 '11 at 19:07

5 Answers5

6

functools.partial() allows you to supply some of the arguments ahead of time. It will allow you to make custom callbacks as you want.

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
unholysampler
  • 17,141
  • 7
  • 47
  • 64
4

You can use a closure, like this:

def printParam(parameter):
    def callback():
        print "You selected %s" % parameter
    return callback

for parameter in parameters:
    menu.addAction(parameter, printParam(parameter))
Thomas K
  • 39,200
  • 7
  • 84
  • 86
  • 1
    As I wrote before, if you just need to apply a function partially, just using `functools.partial` is shorter and (arguably) simpler. A higher order function like this is a good choice if you need more complex logic though. –  Jul 13 '11 at 17:11
  • Yes, `functools.partial` does the job here. But there's something beautiful about closures. – Thomas K Jul 13 '11 at 23:30
2

Yes, it's called partial application. The standard library has a class to apply a function partially, functools.partial. You'd write partial(f, x=7) or partial(f, 7) (since it's a positional argument and you don't need to skip over any positional arguments) in your example.

1

Use functools.partial to do the following (easier to demonstrate on your simple example):

import functools
callback2=functools.partial(f, 7)
callback3=functools.partial(f, y=5) #if you want to pass `y` instead of `x`

If you always want to pass a particular argument, then as others have pointed out you could use a closure:

def f(x):
    def g(y):
        print x + 2*y
    return g
callback1=f
callback2=f(7)
callback1(2)(5) #prints 12
callback2(5) #prints 17
murgatroid99
  • 19,007
  • 10
  • 60
  • 95
  • Note that with this, you have to write `callback2(y=5)`, otherwise you get an error about receiving multiple values for `x` when you call `callback2(5)`. Easier in this case to write `functools.partial(f, 7)` – Ismail Badawi Jul 13 '11 at 17:05
  • @isbadawi Noted and fixed. Thanks. – murgatroid99 Jul 13 '11 at 17:12
0
def my_menu_func(self, string):
   # whatever your function does with that parameter

then for setting up the menu:

x = "Open"
a = eval("lambda : my_menu_func('"+x+"')")
menu.addAction(x, a)

I tried using a lamba directly inside addAction, but that forms a closure containing the variable x, so any subsequent local changes to x would alter the parameter passed in the call to my_menu_func. Doing it with the eval you can iterate over a list of strings and create a whole menu for example by:

for x in List_of_Menu_Items:

do the same thing.

phkahler
  • 5,687
  • 1
  • 23
  • 31
  • Wasn't my vote but I'd guess it's because of the 'eval' part of your answer. – aukaost Jul 15 '11 at 11:25
  • But without the eval it creates a function that calls my_menu_func with a reference to the local variable x instead of the value "Open". So on subsequent calls, you don't get what you think. – phkahler Jul 18 '11 at 00:29