Kodomo

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

Учебная страница курса биоинформатики,
год поступления 2014

Функции (в питоне), события (в графическом интерфейсе)

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

Функции

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

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

   1 math.sin(x)

Теперь будем писать свои

   1 def my_print():
   2     print(“hello”)

После такого определения функции мы можем её вызвать:

   1 >>> my_print()
   2 hello

При вызове любой функции происходит передача аргументов, которая заключается в том, что передаваемые значения присваиваются аргументам функции.

   1 def my_print(x):
   2     print x

Чтобы выполнить код, прописанный в функции, нужно в программе вызвать функцию и передать значения параметров

   1 >>> my_print(“ку-ку”)
   2 ку-ку

Помимо того, что функция берет на вход аргументы, она может возвращать значение (как в случае с синусом). Напишем свой пример:

   1 def glue(x, y):
   2     delim = '_'
   3     return str(x) + delim + str(y)

Результат выполнения функции можно присвоить в переменную

   1 z = glue(y, x)

Переменные, создаваемые внутри функции, являются локальными, и они не видны снаружи. То есть они «живут» только в рамках этой функции. Аргументы функции имеют в точности те же права, что и локальные переменные.

   1 word1 = "first"
   2 word2 = "second"
   3 def glue(x, y):
   4     delim = '_'
   5     return str(x) + delim + str(y)
   6  z = glue(word1, word2)
   7  z = glue(z, z)
   8 print z

Если мы в основной программе попытаемся вызвать print(delim) или print(x), то возникнет ошибка. (Эти переменные исчезли в тот момент, когда функция завершилась).

В разных функциях могут быть переменные с одинаковыми именами:

   1 def mul(x, y):
   2     return x * y
   3 
   4 def plus(x, y):
   5     return x + y
   6 
   7 a = 2
   8 b = 3
   9 c = 4
  10 print mul(a, b)
  11 print plus(c, b)

Можно вызывать одну функцию внутри другой.

   1 def plus(x, y):
   2   return x + y
   3 
   4 def mul(x, y):
   5   return x * y
   6 
   7 def dot_product(x1, y1, x2, y2):
   8   return plus(mul(x1, x2), mul(y1, y2))
   9 
  10 print dot_product(1, 2, 3, 4)

Функция, которая вызывает сама себя, называется рекурсией. Пример — числа Фибоначчи.

   1 def fibonacci(n):
   2     if n == 0 or n == 1:
   3         return 1
   4     return fibonacci(n - 1) + fibonacci(n - 2)
   5 
   6 print fibonacci(2)
   7 print fibonacci(5)

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

Теперь вынесем код, который рисует граф, в функцию. Перед отрисовкой будем чистить холст.

   1 import tkinter
   2 import random
   3 import math
   4 
   5 canvas_size = 500
   6 npoints = 5
   7 colors = ["red","orange","yellow","green","blue","cyan","magenta","light blue"]
   8 
   9 root=tkinter.Tk()
  10 canv = tkinter.Canvas(root, width=canvas_size, height=canvas_size, bg="white")
  11 canv.pack()
  12 
  13 def draw():
  14   x = []
  15   y = []
  16   for i in range(npoints):
  17       x.append(random.randint(0, canvas_size))
  18       y.append(random.randint(0, canvas_size))
  19   canv.delete("all")
  20   for i in range(npoints):
  21       for j in range(i + 1, npoints):
  22           color = random.choice(colors)
  23           canv.create_line(x[i], y[i], x[j], y[j], fill = color, width = 2)

Размер холста (а заодно и сам холст) является глобальной переменной для функции draw() и его нельзя поменять внутри функции.

Кнопки

Нужный нам кусок кода в функцию мы вынесли. Теперь пора создавать кнопку, при нажатии на которую мы хотим вызывать нашу функцию.

   1 button = tkinter.Button(root, text='draw', width=30, command=draw)
   2 button.pack()

Тут одно важное замечание. Когда мы питону говорим x = draw(), это переводится на русский так: вызови функцию draw и положи результат её вызова в переменную x. А когда мы говорим x = draw, мы говорим: переменная x теперь значит то же, что и draw. А разница всего лишь в наличии скобок!

События

Можно еще поиграть с мышкой. Для этого придется разобраться, что такое событие. Событие это нечто, что происходит. Например, когда мы подвинули курсор мыши, произошло событие Motion. Можно заставить программу как-то отреагировать на событие, осуществить в ответ какие-то действия. Например, нарисуем квадрат-липучку, который приклеился к курсору мыши. При нажатии левой кнопки мыши он отлетает от курсора на небольшое расстояние в произвольном направлении

   1 import tkinter
   2 import random
   3 import math
   4 
   5 canvas_size = 500
   6 side = 20
   7 radius = 50
   8 
   9 root = tkinter.Tk()
  10 canv = tkinter.Canvas(root, width=canvas_size, height=canvas_size, background='gray')
  11 canv.pack(fill="both", expand="yes")
  12 
  13 canv.create_rectangle(0, 0, side, side, fill='blue')
  14 
  15 def on_mouse_move(event):
  16     canv.delete("all")
  17     x = canv.canvasx(event.x)
  18     y = canv.canvasy(event.y)
  19     canv.create_rectangle(x, y, x + side, y + side, fill='blue')
  20 
  21 def on_click(event):
  22     canv.delete("all")
  23     x = canv.canvasx(event.x)
  24     y = canv.canvasy(event.y)
  25     alpha = random.randint(0, 360)
  26     x = x + radius * math.cos(math.radians(alpha))
  27     y = y + radius * math.sin(math.radians(alpha))
  28     canv.create_rectangle(x, y, x + side, y + side, fill='blue')
  29 
  30 canv.bind('<Motion>', on_mouse_move) #связать событие движения мыши на холсте с нашей функцией on_mouse_move
  31 canv.bind('<ButtonPress>', on_click) #связать событие нажатия мыши на холсте с нашей функцией on_click
  32 root.mainloop()

Объект event автоматически создается системой при возникновении события, он хранит разную информацию, связанную с событием. В данном случае мы получаем из него координаты курсора мыши в месте клика.

Надо заметить, что в объекте event хранятся координаты относительно левого верхнего угла окна, а оно не обязательно совпадает с левым верхним углом полотна (например, если над полотном нарисованы какие-нибудь кнопки). Чтобы узнать, каким координатам именно на полотне соответствуют координаты на окне в питоне есть функции canv.canvasx и canv.canvasy.

After; глобальные переменные

Для рисования фракталов нам захочется вызывать функцию, которая будет рисовать очередные сколько-то точек с некоторой периодичностью, и делать это не просто N раз, а до тех пор, пока пользователь не закроет окно. Более того, мы хотим, чтобы между тем, как эта функция вызывается, питон реагировал на разные события (самое смешное, что "отобразить на экране то, что мы питону уже сказали нарисовать" – это тоже такое событие; если мы не будем давать питону реагировать ни на какие события, то мы можем долго тратить процессор и память, но видеть перед собой пустое окно, или вообще не видеть никакого окна).

Поэтому в tkinter есть специальное событие: срабатывание таймера. Мы можем попросить питон вызвать нашу функцию спустя заданное количество миллисекунд (а до тех пор он отдыхает и реагирует на все события, которые ему интересны).

Здесь нам нужно вернуться к теме функций и глобальных переменных. Раньше мы сказали, что глобальные перменные внутри функции доступны нам только на чтение. Однако, если наша функция рисует, например, следующий шаг фрактала, то нам очень хотелось бы, чтобы она могла посчитать новые координаты и сохранить их. Для этого в питоне есть ключевое слово global, которое говорит следующее: "да, я знаю, что вот эти-то переменные глобальные, но я ОЧЕНЬ-ОЧЕНЬ хочу всё-таки в них писать".

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

Ровно поэтому в питоне есть специальное ключевое слово global – чтобы если уж вы что-то такое нехорошее вынуждены делать, это бы хотя бы было сразу видно видно.

Позже мы вам расскажем, как можно было бы в этом случае обойтись вовсе без глобальных переменных.

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

   1 from tkinter import Tk, Canvas
   2 import random
   3 
   4 canvas_size = 500
   5 x = 250
   6 y = 250
   7 delay = 10
   8 
   9 def step():
  10   global x, y
  11   x += random.randint(-1, 1)
  12   y += random.randint(-1, 1)
  13   canvas.create_line([x, y], [x + 1, y], fill='white')
  14   root.after(delay, step)
  15 
  16 root = tk()
  17 canvas = Canvas(root, width=canvas_size, height=canvas_size, background='black')
  18 canvas.pack()
  19 step()
  20 root.mainloop()

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

Если вам хочется вызывать функцию так часто, как только можно, то лучше писать не root.after(0, f), а root.after_idle(f). (В некоторых случаях событие after(0, f) может обрабатываться раньше, чем остальные события – например, перерисовка окна, – и это будет снова не то, что мы хотим).