Kodomo

Пользователь

Tkinter, часть 2

Глобальные переменные

Определение: глобальная переменная – это переменная, определённая вне любой функции.

Мы уже много раз ими пользовались. Например, константы, которые мы используем внутри функций, являются для этих функций глобальными переменными

Предположим, мы хотим написать программу, которая бы рисовала кружок, медленно ползущий направо1:

   1 import Tkinter as tk
   2 
   3 W = H = 500
   4 R = 10
   5 
   6 def o_tawa():
   7     t = 0
   8     t = t + 1
   9     x = t % W
  10     y = H / 2
  11     sitelen.delete("all")
  12     sitelen.create_oval(x - R, y - R, x + R, y + R)
  13     ijo_lawa.after(100, o_tawa)
  14 
  15 ijo_lawa = tk.Tk()
  16 sitelen = tk.Canvas(ijo_lawa, width=W, height=H)
  17 sitelen.pack()
  18 o_tawa()
  19 ijo_lawa.mainloop()

Одна беда: круг при этом никуда не едет. Действительно, если мы присмотримся, обнаружим, что у нас почти прямым текстом сказано, что t = 1 всегда, при каждом вызове o_tawa. Беда в том, что строка t = 0 оказалась внутри функции.

Если мы вынесем t = 0 из функции:

   1 ...
   2 t = 0
   3 
   4 def o_tawa():
   5     t = t + 1
   6     ...
   7 ...

То питону это не понравится:

...
UnboundLocalError: local variable 't' referenced before assignment

Дело вот в чём: в питоне есть железное правило: глобальные переменные можно только читать. Поэтому когда мы пишем t = t + 1, питон считает, что нам и в голову не могло прийти поменять глобальную переменную t, а значит, все упоминания переменной t в нашей функции – это локальная переменная. Но тогда получается, что когда мы пишем t + 1 – это первое упоминание этой переменной, а она ещё до этого нигде не определена!

В математике есть такое правило: если нельзя, но очень хочется, то можно. Иногда оно применимо и в программировании.

Мы можем слёзно попросить питон только в этой функции разрешить нам писать только одну эту глобальную переменную – и он нам разрешит. Делает это команда global:

   1 ...
   2 t = 0
   3 
   4 def o_tawa():
   5     global t
   6     t = t + 1
   7     ...
   8 ...

Не случайно то, что в питоне запрещено в глобальные переменные писать, и нужно отдельно просить, если мы хотим это делать. Это правило – следствие большого количества шишек, набитых многими граблями на лбах великих программистов. Без него мы должны были бы, сколь велика не была бы программа, следить за тем, чтобы никакие две функции не использовали одно и то же имя переменной для разных целей. За поведением изменяемых глобальных переменных всегда сложнее следить: глядя на две соседние строчки, вы не можете быть уверены, что переменная между ними не изменилась (вы могли из одной из них вызвать функцию, которая меняет глобальную переменную). Хуже того, почти всегда правда, что переменная не изменится, и поэтому в том редком случае, когда это будет не так, вы будете мучительно ломать голову, почему же программа ведёт себя не так, как хочется.

Отсюда правило: менять глобальные переменные не стоит. Но если уж этого не избежать, то используйте команду global2

Замыкания

Если задуматься, мы могли бы и обойтись без глобальных переменных. Достаточно, чтобы функция o_tawa принимала как аргумент значение переменной t, и для следующего шага просила вызвать себя с значением t + 1.

   1 import Tkinter as tk
   2 
   3 W = H = 500
   4 R = 10
   5 
   6 def o_tawa(t):
   7     t = t + 1
   8     x = t % W
   9     y = H / 2
  10     sitelen.delete("all")
  11     sitelen.create_oval(x - R, y - R, x + R, y + R)
  12     ijo_lawa.after(100, o_tawa(t))
  13 
  14 ijo_lawa = tk.Tk()
  15 sitelen = tk.Canvas(ijo_lawa, width=W, height=H)
  16 sitelen.pack()
  17 o_tawa(0)
  18 ijo_lawa.mainloop()

Это не сработает. Когда мы говорим ...after(..., o_tawa(t)), мы сразу в этот же момент вызываем функцию o_tawa снова (а она в свою очередь сразу дойдёт до этой же строки и вызовет себя снова, и снова, и снова – и так до бесконечности: RuntimeError: maximum recursion depth exceeded

Беда в том, что after всегда вызывает функцию без аргументов. Так сделано умышленно3 потому, что в питоне это ограничение очень легко обойти.

Нам ведь всего-то нужно написать функцию, которая сможет прочитать значение переменной t, и попросить after исполнить её. Так как переменная t описана внутри функции o_tawa, то и нашу функцию нужно будет описать внутри o_tawa, чтобы она видела t:

   1 ...
   2 def o_tawa(t):
   3     ...
   4     def callback():
   5         o_tawa(t + 1)
   6     ijo_lawa.after(100, callback)

Это единственный вид ситуаций, для которых допустимо писать определение одной функции внутри другой функции.

Здесь всё просто, но всё же, если нагородить таких вложений несколько, запутать себя становится очень легко, а запутать постороннего читателя проще пареной репы.

lambda

Однако вот такие простые ситуации, когда мы определяем одну функцию внутри другой функции только ради того, чтобы передать её кому-то, настолько типичны, что для этого есть упрощённый синтаксис:

   1 ...
   2     def callback():
   3         o_tawa(t + 1)
   4     ijo_lawa.after(100, callback)
   5 ...

абсолютно эквивалентно такому:

   1 ...
   2     ijo_lawa.after(100, lambda: o_tawa(t + 1))
   3 ...

Конструкция lambda4 определяет новую безымянную функцию и возвращает нам саму функцию.

Общее определение: f = lambda a, b, c: E эквивалентно:

   1 def f(a, b, c):
   2     return E

Experts only: предупреждение о замыканиях и пространствах имён

Посмотрим на такой пример:

   1 def f():
   2     functions = []
   3     x = 1
   4     functions.append(lambda: x)
   5     x = 2
   6     functions.append(lambda: x)
   7     x = 3
   8     return functions
   9 
  10 a, b = f()
  11 print a()
  12 print b()

Казалось бы, мы хотим увидеть 1 и 2.

Дело вот в чём: когда мы определяем функцию (буде то с помощью lambda или с помощью def), питон запоминает только одно: где брать значения для переменных, которые не являются локальными.

А как это "где"? Где – это в каком пространстве имён.

Пространство имён – это таблица, в которой написано, у какой (локальной) переменной какое значение.5 Такая таблица создаётся каждый раз, когда мы вызываем функцию. Обычно она сразу после выхода из функции удаляется, но если мы внутри функции определили другую функцию, то есть если пространство имён кому-то нужно, то оно сохраняется с последними значениями всех переменных.

В нашем случае мы вызываем все функции, определённые через lambda, после того, как завершилась функция f, поэтому они видят последнее значение переменной x.

Если понятие "пространство имён" вас пугает, то простое объяснение такое: функция запоминает, о какой переменной идёт речь, но смотрит, какое у неё значение, в тот момент, когда функцию вызвали, а не в тот момент, когда мы функцию определили.

Поэтому же мы можем писать так:

   1 def f():
   2     print x
   3 x = 2
   4 f()

Питон пошёл выяснять, существует ли переменная x только в тот момент, когда мы функцию f вызвали. И только в этот момент узнал значение этой переменной, которое мы будем использовать.

Краткий обзор остальных возможностей Tkinter

Так как у нас есть прекрасный учебник Фредрика Лундха, то вместо конспектов я приведу несколько примеров со ссылками его введение. Одно маленькое напоминание: мы пока что не изучали классы в питоне (в них нет ничего сложного, но всё же это новый способ смотреть на вещи), поэтому если в примере упоминается слово class или self или __init__, то вы можете смело этот пример пропустить.

Мы смотрели на виджеты Entry, Text, http://effbot.org/tkinterbook/listbox.htm и с ними у нас был такой бессмысленный пример: окошко, в котором можно добавлять варианты в Listbox и смотреть, какой выбран:

   1 import Tkinter as tk
   2 
   3 def add():
   4     text_lines = text.get("1.0", "end").split("\n")
   5     entry_lines = [entry.get()]
   6     for line in text_lines + entry_lines:
   7         line = line.strip()
   8         if line:
   9             listbox.insert("end", line)
  10 
  11 def status():
  12     print "Entry:", entry.get()
  13     print "Text:", text.get("1.0", "end")
  14     for line_number in listbox.curselection():
  15         print "Listbox [", line_number, "]:", listbox.get(line_number)
  16 
  17 root = tk.Tk()
  18 entry = tk.Entry(root)
  19 entry.pack()
  20 text = tk.Text(root)
  21 text.pack()
  22 listbox = tk.Listbox(root) # попробуйте с selectmode="multiple"
  23 listbox.pack()
  24 add_button = tk.Button(text="Add", command=add)
  25 add_button.pack()
  26 status_button = tk.Button(text="Status", command=status)
  27 status_button.pack()
  28 root.mainloop()

Ещё мы обнаружили склады стандартных диалогов: модуль tkMessageBox и модуль tkFileDialog (про который Фредрик Лундх почему-то не написал).

С этими диалогами мы сочинили такой половинчатый текстовый микроредактор:

   1 import codecs
   2 import Tkinter as tk
   3 from tkMessageBox import askyesno
   4 from tkFileDialog import askopenfilename
   5 
   6 def load():
   7     if askyesno("Open file?", "Are you sure? If you open file, all your previous editing will be lost!"):
   8        filename = askopenfilename()
   9        with codecs.open(filename, 'r', 'utf-8') as file:
  10            text.delete("1.0", "end")
  11            text.insert("end", file.read())
  12 
  13 root = tk.Tk()
  14 text = tk.Text()
  15 text.pack(fill="both", expand="yes")
  16 load_button = tk.Button(text="Open file", command=load)
  17 load_button.pack()
  18 root.mainloop()

Я надеюсь, что я вам дал представление об основных понятиях, на которых строится подавляющее большинство графических интерфейсов, и показал, что делать интерактивные графические программы на самом деле легко!

Но, конечно, я рассказал вам далеко не всё, что есть в Tkinter. А в нём есть очень много всего полезного, вкусного и интересного. Вам придётся это осваивать самостоятельно – если того потребует от вас судьба, шило, или научное руководство. Надеюсь, теперь это вам не покажется таким уж сложным.

Вот самое полезное:

Ещё я рекомендую игнорировать главу про Variable Wrappers (BooleanVar, DoubleVar, IntVar, StringVar), и все их упоминания в примерах. Это крайне неудачная попытка перенести в питон из tcl конструкцию, которая была мощной в tcl и позволяла сэкономить 5-10 строк в небольшой программе, а в питоне наоборот ухудшает читаемость и захламляет программу лишними 10-15 строками кода. (Разве что, если вам досталась чужая программа, которая их использует, тогда придётся разобраться, что это, и как оно работает – но здесь ничего сложного нет). В питоне вместо них всегда проще вызывать метод get у соответствующего виджета.

  1. Вредный совет: если преподаватель требует, чтобы переменные назывались по-английски, пусть это будет латиница непонятного языка! It was hard to write, it must be hard to understand! (1)

  2. Вторая часть правила появилась не на пустом месте. В питоне есть множество изменяемых объектов, например, списки и словари, и пользуясь ими, можно скрытно обходить запрет на запись в глобальные переменные. Не делайте этого скрытно! Даже если технически питон разрешает вам менять глобальную переменную без дополнительных усилий, всё равно объявите её global, чтобы при беглом чтении это было заметно. (2)

  3. В родном языке tcl, из которого пришла в питон библиотека Tkinter, after умеет запоминать, с какими аргументами вызывать функцию (3)

  4. Название пришло из функционального программирования, а туда из отдела матлогики под названием лямбда-исчисление Чёрча. ФП -- это отдельная, совершенно иная (местами более красивая и выразительная) точка зрения на то, как можно писать программы (4)

  5. Если вам это напоминает словари, то это не случайно. В питоне это и есть словари. Функция locals() возвращает словарь локальных переменных текущей функции, а функция globals() -- словарь глобальных переменных модуля -- и вы даже можете эти словари менять, и при этом будут меняться значения соответствующих переменных! (5)