py30.png

Command, Lambda, functools ... Voláme funkce z tlačítka

Ukázkový příklad

Nejjednoduší příklady nastávají na tutoriálních příkladech tisku, které v praxi téměř nikdy nenastávají ;-)

# -*- coding: utf-8 -*-
from tkinter import *

def tisk():
    print (1)

hlavni=Tk()

# všimněte si, že tu není "tisk()"! ale jen "tisk"!
tl=Button(text="tiskni", command=tisk)
tl.pack()

hlavni.mainloop()

Za povšimnutí rozhodně stojí, že se nepoužívá tisk(), ale jen tisk. Použijete-li tisk(), program nebude fungovat. Vyzkoušejte.

Funkce s parametrem

Častěji chceme funkci volat s nějakým parametrem Z předchozího odstavce již víme, že prosté command=tisk(10) fungovat nebude. Můžeme použít lambda funkci:

# -*- coding: utf-8 -*-
from tkinter import *

def tisk(x):
    print (x)

hlavni=Tk()

# všimněte si, že tu není jen "tisk()"! ale lambda: tisk()
tl=Button(text="tiskni", command= lambda: tisk(10))
tl.pack()

hlavni.mainloop()

Více tlačítek na jednu funkci s parametrem

Nefunkční kód může vypadat třeba takto:

from tkinter import*

def pis(co):
    print (co)

okno=Tk()
menubar = Menu(okno)
menu = Menu(menubar, tearoff=0)
cisla=[1,2,3,4,5,6,7,8,9,10]

for prvek in cisla:
    menu.add_cascade(label=prvek,command=lambda: pis(prvek))

menubar.add_cascade(label="cisla",menu=menu)
okno.config(menu=menubar)
mainloop()

Když v tom menu kliknu na jakoukoliv položku tak se vždy napíše 10? Proč? Mělo by to přece napsat to číslo na který klikám!

Řešení s lambda:

Problem je v definici lambda funkce. Promenna 'prvek' je v te funkci jako volna promenna a zkompilovana lambda vypada nejak takto:

lambda: pis(LOAD_DEREF('prvek'))

Nebo pokud je definovana na globalni urovni:

lambda: pis(LOAD_GLOBAL('prvek'))

Ale ne takto:

lambda: pis(1)
lambda: pis(2)
....

Cili ve vasem pripade v tom cyklu v podstate vznikne 10 shodnych funkci, ktere si hodnotu prvku zjistuji az za behu a ta je po skonceni cyklu rovna hodnote 10. Vice o tom najdete pres klicova slova "python closures".

Aby to fungovalo, musi se pouzit nejaky trik:

lambda p=prvek: pis(p)

nebo:

new.instancemethod(lambda p:pis(p), prvek, type(prvek))

Tim se vygenerovana instance lambda funkce vzdy svaze s konkretnim prvkem. Prvni varianta je jednodussi, ale meni signaturu funkce, coz muze nekdy vadit. Druha verze vytvori fiktivni anonymni metodu konkretniho objektu prvek (bound method), ikdyz trida int zadnou takovou metodu nema.

Ale necistsi reseni bez triku (a jeste pomerne kratke) je asi tohle:

def gen_pis_prvek(prvek):
    return lambda: pis(prvek)

for prvek in cisla:
    menu.add_cascade(label=prvek, command=gen_pis_prvek(prvek))

Řešení s modulem functools:

Od Python 2.5 lze taky využít nový standardní modul functools a jím definovanou funkci partial() -- viz dokumentace "6.6 functools -- Higher order functions and operations on callable objects. "

Příklad pak lze přepsat takto:

import functools
from tkinter import*

def pis(co):
    print (co)

okno=Tk()
menubar = Menu(okno)
menu = Menu(menubar, tearoff=0)
cisla=[1,2,3,4,5,6,7,8,9,10]

for prvek in cisla:
    menu.add_cascade(label=prvek, command=functools.partial(pis, prvek))

menubar.add_cascade(label="cisla",menu=menu)
okno.config(menu=menubar)
mainloop()