Программируем трехмерную графику с Irrlicht . Часть 8

August 8, 2007

Эффективное программирование 3d-приложений с помощью Irrlicht и jython. Часть 8



В прошлый раз мы учились выводить на экран надписи в irrlicht, изучали методы работы со строками текста (модуль string). Сегодня мы отложим немного в сторону irrlicht и продолжим изучать собственные средства python|jython. Сложность наших примеров будет с каждым разом возрастать и нам необходимы способы оперировать большим количеством сложноорганизованной информации. Т.е. нам нужен механизм группировки множества переменных в централизованные группы и нужны способы выполнять ВЗАИМОСВЯЗАННЫЕ действия сразу над множеством этих переменных.

Например, если у нас есть объект машинка, то мы не можем ее представлять как множество переменных, например, задающих координаты, цвет, скорость, количество бензина - ПООТДЕЛЬНОСТИ. Мы должны объединить эти переменные в единое целое и работать как с единым целым, так, если координаты машинки изменились, то очевидно, что количество топлива должно автоматически уменьшиться. Итак, наша сегодняшняя тема - это работа со списками и классами.

Но сначала мы вернемся еще немного назад и вспомним, как в части 6 мы работали с пользовательскими функциями. Нам осталось разобраться с тем, как функция взаимодействует с переменными, которые находятся (т.е. были созданы вне ее тела). Вот пример для рассмотрения.
  1. import sys
  2. import math 
  3. # подключаем модуль math в нем есть функция извлечения квадратного корня - sqrt
  4. # создаем функцию вычисляющую корни квадратного уравнения
  5. def calcRoots (a,b,c):
  6.     d = b**b - 4*a*c # вычисляем определитель
  7.     if d > 0: # и сравниваем его с “0”
  8.         return (
  9.             (-b + math.sqrt(d))/(2*a), (-b - math.sqrt(d))/(2*a)
  10.         )
  11.     elif d == 0:
  12.         return ((-b )/(2*a))
  13.     else:
  14.         return ()
  15. # создаем функцию выполняющую модификацию двух переданных переменных a,b
  16. def modify (a , b):
  17.     a = 11
  18.     b = 12
  19.     g = a + b # здесь мы пытаемся изменить некоторую переменную g
  20. # создав эти две функции, вызовем их    
  21. print calcRoots(1, 6 ,3)
  22. print type( calcRoots(1, 6 ,3) )
  23. a = 1 
  24. # создаем три переменные с такими же именами как и те которые используются внутри функции modify
  25. b = 2
  26. g = 3
  27. modify (a, b)
  28. print ' a = ' , a
  29. print ' b = ' , b
  30. print ' g = ' , g
  31. print ' d = ' , d
Вот результат выполнения программы.
  1. (104.98611021793498, -110.98611021793498)
  2. <type 'tuple'>
  3.  a =  1
  4.  b =  2
  5.  g =  3
  6.  d =
  7. Traceback (most recent call last):
  8.   File "a5.py", line 35, in ?
  9.     print ' d = ' , d
  10. NameError: name 'd' is not defined
Начнем анализ вышеприведенного кода с рассмотрения функции modify. Эта функция получает в качестве своих параметров две переменные a,b. Внимание, до вызова функции я в теле программы создал эти переменные со значениями 1 и 2, также была создана переменная g, но, и это важно, которая не передается внутрь функции modify. После вызова modify, внутри которой я присваиваю всем трем переменным (двум переданным как аргументы функции и еще одной g) новые значения, я вывожу чему же равны эти переменные. Они не изменились, так a = 1, b =2 , g = 3. Отсюда вывод, что функция не может изменить те переменные, которые были созданы вовне ее. Если функция создаст какую-то переменную, то это будет ее локальная переменная, которая исчезнет, как только функция завершит работу. Так, например, произошло с переменной d, которая была создана внутри функции calcRoots. Попытка распечатать d за пределами функции calcRoots привела к ошибке.

Более того, пример с функцией calcRoots, ищущей корни квадратного уравнения ставит перед нами нетривиальную проблему. Как известно у квадратного уравнения всегда есть два корня. Просто если детерминант равен нулю, то они совпадают, а если D < 0, то корни лежат в плоскости комплексных чисел (в примере я упростил, положив как в школьном курсе математике, что если D < 0, то корней нет). Следовательно, мне необходим способ вернуть из функции два, один, ноль чисел, в зависимости от того чему равен дискриминант. Написать друг за дружкой два оператора return, надеясь, мол, сколько return-ов, столько и возвращаемых переменных, не пойдет. Как только сработает первый return, то выполнение функции тут же прекратится.

На этот случай в python есть такой тип данных как tuple. Именно это слово будет напечатано после вызова: print type( calcRoots(1, 6 ,3) ). Так мы получили . Функция type получающая в качестве параметра некоторое значение определяет название типа данных этой переменной. Вообще-то tuple - это частный случай списка. Список – это просто множество каких-то элементов, чисел, строк, чего угодно. У списка, равно как и у tuple, можно узнать его длину, а также получить по порядковому номеру его элемент. Список можно изменять, а вот tuple менять нельзя. В примере ниже я не сразу распечатываю результат вызова calcRoots а сохраняю в переменной tu:
  1. tu = calcRoots(1, 6 ,3)
  2. print tu # печатаем всю tuple сразу
  3. print 'tuple size = ' , len(tu) # печатаем количество элементов списка tuple
  4. print 'element of tuple # 0 = ' , tu [0] # печатаем первый элемент списка, нумерация идет от нуля
  5. tu [0] = 'hello' # попытка присвоить новое значение – изменить элемент tuple
  6. print tu # распечатаем что же там изменилось в tuple
В результате мы получим ошибку, именно при попытке изменить содержимое tuple.

Так же как и при работе со строками в части 7, мы не можем модифицировать существующую tuple или строку, а только создать новую tuple|строку.
  1. Traceback (most recent call last):
  2.   File "a5.py", line 29, in ?
  3.     tu [0] = 'hello'
  4. TypeError: object does not support item assignment
Для того чтобы создать tuple нужно всего лишь написать в круглых (именно в круглых скобках) скобках список значений. Например, (1,2,3,4) – вот пример tuple из четырех чисел, или так: (1,2,3, “hello world”).

Как вывод: tuple это список значений, любых типов данных, который не может быть изменен, но позволяет получить его длину и любой элемент с помощью порядкового номера.

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

Однако предположим, что эта функция может быть использована для рисования и квадратов, ведь это частный случай прямоугольника, только стороны в нем равны. Может, проще было бы иметь такую функцию, которая получала бы центр прямоугольника, его высоту, а параметр широты был бы не обязателен. Тогда бы функция, проверив сколько параметров было реально передано ей при вызове, рисовала бы либо квадрат, либо прямоугольник. Вот пример функции находящей максимальное значение из трех чисел, или из двух.
  1. def find_max (a, b, c = False): 
  2. # аргумент функции c необязателен и имеет значение по-умолчанию False – ложь. 
  3. Какое значение вы выберете не важно, главное в том, 
  4. чтобы вы всегда могли бы определить, было ли значение этой переменной передано 
  5. при ее вызове или же нет, брать на роль значения по-умолчанию любое число, 
  6. в именно этой задаче, несколько рискованно
  7.     if a < b:
  8.         d = b
  9.     else:
  10.         d = a
  11.     if c != False:
  12.         if c > d:
  13.             return c
  14.         else:
  15.             return d
  16.     else:
  17.         return d
  18.  
  19. print find_max (20, 6) # вызываем поиск максимального числа среди двух чисел
  20. print find_max (20, 6, 30) # вызываем поиск максимального числа уже среди всех трех аргументов
Следующим шагом развития функции поиска максимального числа была бы возможность получать произвольное количество аргументов и искать среди них максимальное.
  1. def find_anything (kind = 'max', *values):
  2.     if len(values) == 0:
  3.         return # нет возвращаемого значения
  4.     found = values [0]
  5.     i = 1
  6.     while i < len(values):
  7.         if (kind == 'max'):
  8.             if (found < values[i]):
  9.                 found = values [i]
  10.         else:
  11.             if found > values [i]:
  12.                 found = values[i]
  13.         i = 1 + i
  14.     return found
  15.  
  16. print find_anything('max', 4, 7, 2 , 4 , 9 , 7 , 6   ) 
  17. # ищем максимальное значение в списке
  18. print find_anything('min', 4, 7, 2 , 4 , 9 , 7 , 6   )
  19.  # а теперь ищем минимальное
В этом примере первый параметр функции имеет значение по-умолчанию слово “max”, количество же остальных параметров не определено и все они помещаются в список с именем values, перед именем такой переменной обязательно нужно ставить символ *. Также очевидно, что такие переменные накопители всего, должны идти самыми последними в списке аргументов функции.

Развивающее задание № 6. Наша программа “угадай число” растет, так что, несмотря на свою простоту, дает нам уже необходимость задуматься над таким понятием как оптимизация кода. Очевидно, что одна и та же задача может иметь разное количество решений. Например, мой прием с вычислением x^y через последовательные умножения x*x*..x - не лучший и тем более не единственный. Известно, что x^y = exp(y * ln(x)), попробуйте например такой код (должны получиться одинаковые числа):
  1. >>> print math.pow(6, 7.3) , ‘ --- ’, math.exp (7.3 *math.log(6))
  2. 479186.007364 --- 479186.007364
Следовательно, нам нужен механизм позволяющий оценить насколько качественен выбранный нами алгоритм от других. Понятие качества алгоритма очень сложное, оно состоит из ряда компонент и эти компоненты не равнозначны, и в разных ситуациях эти доли важности могут меняться. Например, в простейшем случае качество тем выше, чем меньше время выполнения и количество затраченных ресурсов, скажем, оперативной памяти. Но если у Васи в компьютере быстрый процессор и мало памяти, то ему гораздо важнее чтобы наша программа потребляла меньше памяти. Для Пети, у которого слабый процессор, но много памяти, все наоборот.

Начнем с того, что оценим время выполнения некоторого алгоритма. В простейшем случае, вы можете узнать текущее системное время (с максимальной точностью до миллисекунд). Затем сохранить эту величину в переменной, например start. После выполнить тестируемый алгоритм, и снова замерить системное время. Разница между этими двумя замерами и будет искомой величиной.
  1. import time
  2. >>> time.time()
  3. 1165770365.921
Здесь используется функция time в модуле time, которая возвращает число равное, количеству секунд от сотворения мира. Это целая часть числа, и возможно возвращается дробная часть, в зависимости от операционной системы.

В качестве задания для игры “угадай число” введите учет времени затраченного на угадывание секретного числа, как во время очередного тура, так и всей игры (серии туров). После окончания очередного тура следует сообщить не только количество попыток угадать число, но и время, в течение которого шел этот тур игры.

Другой прием оценки времени выполнения алгоритма – использование модуля profile, например, так:
  1. import profile
  2. # --- весь код, который был в задаче расчета экспоненты ранее в части 5 ---
  3. profile.run ("exponenta (2, 0.001)")
В качестве параметра функции run из модуля profile задается вычисляемое выражение. А результатом вызова функции run будет распечатанная таблица, в которой перечисляются то, какие функции были вызваны, сколько раз они были вызваны, а также, сколько времени было затрачено в среднем на вычисление кода этих функций.



Здесь
 ncalls - количество вызовов функции, 
 tottime - полное время выполнения кода функции 
 (без времени нахождения во вложенных функциях), 
 percall - тоже, в пересчете на один вызов,
 cumtime - аккумулированное время нахождения в функции, 
 вместе с вложенными вызовами других функций из этой функции. 
 Последний столбец содержит имя файла, номер строки с функцией и имя функции.
Развивающее задание № 7. Продолжаем совершенствовать нашу программу “Угадай число”. Теперь я хочу, чтобы вы добавили возможность сохранения истории вводившихся чисел в ходе очередного тура. И по завершению игры программа не только сообщала количество попыток и затраченное на тур время, но и предлагала пользователю получить перечисление всех ответов (чисел) которые он давал. Для того чтобы вы могли решить эту задачу нужно ответить на вопрос: где хранить данные об вводимых числах. До этого момента времени если мы создавали какую-то переменную, то в ней хранилось только одно значение: строка или число не важно, но только одно. Теперь в переменной надо хранить произвольное количество элементов, и мы ведь в общем случае незнаем даже сколько раз игрок будет пытаться угадать число, а, следовательно, сколько значений будет храниться внутри одной переменной. Правда, мы уже сталкивались с таким понятием как tuple.

Tuple – представляет собой список элементов произвольной длины любого типа, но я ведь говорил, что tuple – представляет собой неизменяемый список. Как же мы будем в него добавлять новые элементы? Tuple – это частный случай общего типа данных списка, и, именно, список может быть изменен. Создать список можно также как и tuple, просто перечислив элементы списка друг за другом через запятую, но элементы должны находиться не внутри круглых скобок, а именно внутри квадратных.

Список может содержать элементы любого типа, в том числе еще один список, который в свою очередь может содержать, почему бы, не еще один список. Список создается помимо перечисления его элементов также с помощью функции range (A,B), которая получает два параметра: A – начальный элемент списка, B – конечный, список будет заполнен числами от A до B. Возможен еще третий необязательный аргумент функции – задающий размер шага приращения.
  1. >>> range(1,10)
  2. [1, 2, 3, 4, 5, 6, 7, 8, 9]
  3. >>> range(1,10,2)
  4. [1, 3, 5, 7, 9]
Для доступа к элементам списка используются порядковые номера, или индексы, отсчет начинается от нуля, если же вы указываете отрицательный индекс, то отсчет начинается от конца списка.
  1. >>> print range (1,10)[0]
  2. 1
  3. >>> print range (1,10)[2]
  4. 3
  5. >>> print range (1,10)[-2]
  6. 8
Элементы списка можно изменять. Для этого в квадратных скобках вы должны указать индекс элемента, который будет изменен. Возможно, также указать индекс не одного элемента, а целого отрезка от и до, разделив их символом “:”.
  1. >>> li = range(1,10)
  2. >>> print li
  3. [1, 2, 3, 4, 5, 6, 7, 8, 9]
  4. >>> li [4] = 100 # изменяем пятый элемент списка на 100
  5. >>> print li # проверяем – элемент действительно изменился
  6. [1, 2, 3, 4, 100, 6, 7, 8, 9]
  7. >>> li [7:]=["apple", "orange", "grapes"]
  8.  # все элементы, начиная с восьмого до конца списка будут заменены на новый список из названий фруктов
  9. >>> print li
  10. [1, 2, 3, 4, 100, 6, 7, 'apple', 'orange', 'grapes'] 
  11. >>> li [7:9]=["red", "green", "blue"] 
  12. # теперь замене подлежат элементы списка от восьмого до десятого
  13. >>> print li
  14. [1, 2, 3, 4, 100, 6, 7, 'red', 'green', 'blue', 'grapes']
  15. >>> li [:2]=[-1, -2, -3] 
  16. # снова заменяем, но уже от начала списка до третьего элемента
  17. >>> print li
  18. [-1, -2, -3, 3, 4, 100, 6, 7, 'red', 'green', 'blue', 'grapes']
  19. Часто ставится задача определения того, содержит ли список некоторый элемент. 
  20. Для этого служит оператор in, вот пример:
  21. >>> print 3 in range(1,10)
  22. True # истина – действительно число 3 находится в списке чисел от 1 до 10
  23. >>> print "apple" in range(1,10)
  24. False # ложь яблок в этом списке отсутствует
Для работы со списками можно использовать цикл while, мы его уже применяли для работы со списком argv в задании работы с параметрами командной строки, только тогда термин список явно не прозвучал. Теперь я покажу еще один прием с организацией цикла for.
  1. import sys
  2. import math
  3.  
  4. li = [] 
  5. # вначале я создаю пустой список куда будут помещаться вводимые пользователем числа
  6.  
  7. while 1: # бесконечный цикл будет продолжаться до тех пор, 
  8. пока пользователь не введет число равное нулю, 
  9. остальные же числа будут добавляться в конец списка
  10.     print 'Введите число, число 0 для завершения ввода набора чисел'
  11.     num = float(sys.stdin.readline())
  12.     if (num == 0):
  13.         break # надо прервать выполнение цикла
  14.     li = li + [ num ]; 
  15. # для того чтобы добавить новый элемент в конец существующего списка 
  16. используется прием с конкатенацией или объединением списков, 
  17. важно что если бы я не поместил бы число num внутрь квадратных скобок, 
  18. то машина python сказала бы что мол ошибка, 
  19. нельзя к списку добавить число. А вот к списку добавить другой список, 
  20. пусть даже из одного элемента можно вполне.
  21.  
  22. if len(li) > 0 : 
  23. # если количество введенных элементов больше нуля 
  24. то ищем среди них максимальный элемент, 
  25. алгоритм поиска максимального или минимального элемента очень прост, 
  26. надо предположить что искомый элемент например первый,
  27. а затем перебрать все оставшиеся элементы списка и проверить 
  28. действительно ли очередной элемент списка больше или меньше 
  29. чем предполагавшийся максимальным элемент, если условие нарушается 
  30. то переменной max хранящей в себе предполагаемое максимальное значение 
  31. нужно присвоить новое значение, аналогично при поиске минимального элемента списка
  32.     max = li [0]
  33.     for item in li: 
  34. # цикл for говорит что надо с помощью переменной item перебрать все элементы в (in) списке li
  35.         print "li [] = " , item
  36.         if max < item:
  37.             max = item
  38.     print 'максимальный элемент списка = ', max
  39. else:
  40.     print 'список пуст в нем невозможно найти максимальный элемент'
В следующий раз продолжим работу с irrlicht и рассмотрим как создать приложение реагирующее на действия пользователя: ввод с клавиатуры и мышь. Для этого нам придется изучить такую возможность python|jython как классы.