.

Моя шпаргалка по Питону

Виктор Кон,   3005-2007 (2020),   kohnvict@yandex.ru
.

Внимание !! Названия разделов можно кликать. Для возврата к Содержанию нажимайте клавиши Ctrl+Home.

Содержание

1. Вместо Введения.
2. Установка программы и метод работы.
3. Об организации кода.
4. Массивы в Питоне.
5. О функциях и переменных.
6. Использование текстов в файлах и системных команд.
7. Диагностика работы программы.
8. Научная графика.
9. Расчет преобразования Фурье.
10. Шаблон интерфейса для программы научных расчетов.
11. Многооконные программы, более сложный интерфейс.
12. Меню на картинке, еще более сложный интерфейс.
13. Анимация по серии картинок, то есть быстрая презентация.
14. Анимация двумерного массива c помощью серии графиков.
15. Новый уровень программирования и Анимация двумерного массива через один файл.
16. Кто ищет, тот всегда найдет. О том, как обмануть Питон.

1. Вместо Введения

Я начинал программировать очень давно и работал на почти самых первых компьютерах, когда можно было писать только сам код компьютера и не было никаких языков. Кстати код писался достаточно просто и я даже сейчас иногда пользуюсь таким кодом. Блок кода имел 4 числа (код операции, адрес первого числа, второго числа и результата). Адреса были довольно длинные поэтому использовались буквы и каждый сам себе писал первый язык программирования. А потом очень много лет был Фортран. Мне надо было делать научные расчеты и он подходил идеально. Прошло время и появились персональные компьютеры, на которых был экран, клавиатура и так далее. И Фортран это все программировать не умел. Точнее специальные программы были написаны, но пользоваться ими было неудобно.

Надо было менять язык программирования. Это был уже 2003 год. В то время язык Питон уже существовал, но в моде был язык Java. Оба языка существовали с начала 90-х годов, но Java в то время был моднее. Лично мне он понравился тем, что работал во всех операционных системах автоматически. И в интернете тоже. И я выбрал Java. А про Питон даже не думал и им не интересовался. Еще на Фортране, тоже в начале 90-х я придумал свой собственный язык программирования и написал к нему интерпретатор. На Java мне программировать как бы понравилось, хотя язык я учил с трудом. К тому времени я уже знал Javascript, но тут было еще сложнее. Но мне не понравилось, что надо очень много писать и сам язык как бы слишком сложный, больше, чем надо.

И я снова написал интерпретатор своего языка, но уже на языке Java, и назвал его ACL. У него были примерно такие же возможности, как у Java, но писать надо было меньше и вообще все было удобно. И на этом языке я и написал очень много программ для разных целей. Я прекрасно понимал все преимущества и недостатки интерпретируемого языка. Пользовался преимуществами и старался уменьшить недостатки. А если чего не хватало, то я просто дописывал код интерпретатора на языке Java. При этом мне не надо было писать много раз одно и то же. Я полностью оценил преимущества языка Java при написании очень больших программ. И все было у меня хорошо.

Но вот наступил момент, когда по ряду причин мне стало интересно узнать -- а что же за язык такой -- Питон. До этого я также точно поинтересовался программой Gnuplot, а затем Матлаб, и ее аналогами Octave и Scilab. Матлаб мне сразу не понравился, так как был слишком примитивным. Это даже языком программирования назвать нельзя. Просто калькулятор с графикой. Аналоги еще хуже. А вот про Питон в середине 2020 года говорили, что это претендент на номер 1 среди языков по популярности. В то время он все-таки уступал Java, но быстро набирал популярность. Мол, чего там только нет. И я решил попробовать с ним познакомиться. О том, что из этого получилось, я и написал в этой статье.

2. Установка программы и метод работы

Надо сказать, что все началось с того, что я прослушал первую главу лекций Русакова из его курса про Питон. У него первые две главы бесплатные, а потом уже надо заплатить. Мне хватило первой главы. И я вслед за Русаковым сразу сделал ошибку. Я установил 32-битную версию в папку для одного пользователя, а у меня она называется русскими буквами [Виктор], так мне продавец сделал при проверке ноутбука перед покупкой. И я ее не люблю. Потом я пробовал установить Анаконду3 в общую папку и изрядно с ней намучился, даже не понимаю почему мне так не повезло. Никому не советую ее ставить. Потом я установил таки 64-битную версию в общую папку. И снова облом -- в общей папке Система не дает дополнять пакеты. Это просто ужас, и такое я встретил впервые. Пришлось ее тоже снести вслед за Анакондой. И наконец, я установил 64-битную версию в свою папку [Виктор] и все закончилось успешно.

В результате я всем советую сразу ставить 64-битную версию в персональную папку, тогда никаких проблем не будет. При этом надо обязательно отметить галочкой предложение добавить папку программы в переменную PATH. Откуда скачать дистрибутив легко узнать поиском в интернете по слову Питон, даже не буду давать адрес сайта. Интересно, что после установки программы в ОС Виндовс все файлы с расширением .py автоматически запускаются на исполнение. Но в меню по правой кнопке мыши есть еще одна строка -- Редактировать файл в IDLE. Просто выбираем ее и файл показывается в дежурном редакторе Питона, откуда его легко запустить на исполнение по клавише [F5]. Хотя для Питона существует много редакторов, как раз один из них в Анаконде3, но и этот редактор не такой уж и плохой. Он дает подсказки по операторам, отлавливает ошибки написания кода, и делает подсветку.

Я не люблю, когда меня обслуживают, поэтому этого редактора мне вполне хватило. А какие-то заготовки кода я пишу в других редакторах, в том числе и в своем собственном. А также на своем сайте в интернете. Так просто легче искать. В стандартной поставке есть не все пакеты. Нужные пакеты можно установить записав в командной строке Системы такой текст

. . . >pip3 install имя--пакета

Это все работает достаточно просто. Я сразу установил три пакета
"numpy", "matplotlib" и "Pillow".
Новые пакеты попадают в папку
"Lib\site-packages\" внутри папки программы. А папка программы находится по сложному адресу
[AppData\Local\Programs\Python\Python38\] внутри персональной папки пользователя компьютера. Казалось бы и все, что нужно знать.

Но возникает вопрос -- а что я буду делать с программой, когда я ее напишу. Как ее переносить на другой компьютер. Я не стал искать информацию в интернете, а просто сделал эксперимент. Уже установленную программу, то есть ее папку, вместе с дополнительными пакетами я скопировал на внешний винчестер. Ее объем меньше 200 Мб, это не так уж и много. А потом приготовил командный (bat) файл вот такого содержания

set path=E:\__vkS\Python38\;
pythonw.exe wfphfm.py

При этом (bat) файл был записан в ту же папку, где находится py файл с программой на Питоне. У меня программа имела внешний вид, так что ее легко было видно. И все сработало. Диск Е -- это и есть мой внешний винчестер, в переменной PATH больше ничего нет. То есть работала программа на винчестере. Значит интерпретатор Питона вполне переносимая программа и ее можно запускать даже на флешке. Это важно, если, например, делать презентацию на Питоне.

3. Об организации кода

Новичков начинают обучать с каких-то элементарных расчетов в однокомандном режиме. Но так как я человек уже опытный, то меня сразу заинтересовал вопрос о том как писать очень большие программы. Если кода много, то в одном файле работать будет неудобно. Тем более, что есть код новый, еще не проверенный и неправильный, и код старый, уже работающий как часы, и смотреть его не обязательно. Возникает проблема разбиения кода на несколько файлов. В любом интерпретируемом языке такая проблема существует и она всегда решается. Да и в компилируемом языке тоже она имеет место быть. В Питоне принято файлам с кодом давать расширение ".py". Оказывается код в любом таком файле можно выполнить в любой программе Питона. Например, у вас написан файл "mycode.py". Вы можете добавить в код основной программы такую строку

import mycode

и код из этого файла выполнится. Точно так же работает и код, который написан не вами, а кем-то другим. Очень много таких файлов дается сразу при установке программы. А если нужны новые файлы, то их легко можно установить с помощью программы pip3, которую я указал выше. У команды import есть альтернативные варианты

import numpy as np
import matplotlib.pyplot as plt
from tkinter import *

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

Если все же в импортируемых файлах есть код и вам не хочется его выполнять, то надо делать проверку такого условия

if __name__ == "__main__":

Дело в том, что любой код из файла имеет имя, которое находится в переменной __name__. Код, запущенный первым, имеет имя __main__ . А другой код имеет другое имя. Условие не выполнится и код не будет работать.

Конечно, существует проблема в том, что интерпретатор должен найти ваш файл и как-то его обработать. Ссылки на файлы с кодом в самом интерпретаторе выглядят уж очень короткими. Так получается, если в файле записывать только универсальные функции, которые всю информацию получают через аргументы и ни с кем не общаются. А так коротко, как я написал выше, получается только в том случае, если файл находится в той же папке, где и программа. Если он находится в другой папке, то надо указывать относительный путь от вашей программы до этого файла, либо прописывать папку в переменной PATH.

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

В старом варианте Питона можно было выполнять код из любого файла и без импорта с помощью команды execfile(). Но в Питон 3 эту команду уже отменили. Пока еще осталась команда exec(), которая выполняет код из текстовой строки. То есть теперь надо прочитать файл в текстовую строку и выполнить. Все это можно объединить и выполнять код из файла следующей командой

exec(open("./имяфайла.py").read())

Это работает, я проверил. Единственное ограничение -- нельзя использовать файлы с помеченой кодировкой русских символов. Питон это не понимает. Лучше всего использовать ansi кодировку или utf-8, но без метки. Юникодовские файлы тоже не годятся, у них есть метки.

4. Массивы в Питоне

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

А числовые массивы в старом смысле этого слова реализованы в дополнительном пакете "numpy", которого даже нет в стандартной поставке языка, и его надо устанавливать дополнительно, о чем я написал выше. Такие массивы, как и любые другие обозначаются как обычные переменные, а их структура данных, и размерность устанавливается при присвоении значений. Справедливости ради нужно отметить, что какой-то пакет математических расчетов в стандартной поставке Питона все же есть, но сейчас модно пользоваться именно пакетом "numpy".

Массив можно определить разными способами, вот некоторые из них

d1 = np.array([1,2,3])  # сoздает одномерный массив с 3-мя элементами
d2 = np.array([1,2,3], [4,5,6] )  # сoздает двумерный массив -- матрицу 3*3
d3 = np.ones(5)  # инициирует одномерный массив из 5-ти элементов со значением 1
d4 = np.ones((3,5))  # инициирует двумерный массив со значением 1

Замена (ones) на (zeros) меняет 1 на 0, замена (ones) на (random) меняет 1 на случайное число. Замена (ones) на (empty) просто выделяет память под массив, ничего не присваивая. Какие числа там были, такие и остаются. Это удобно, если массив потом все равно определяется и нужно просто указать его размер. Еще одна команда

d1 = np.eye(5)  # создает квадратную единичную матрицу указанной размерности 

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

d2 = np.empty((n1,n2), dtype=np.float)

Эта команда может объявить массив любой размерности и размера и она же указывает его тип в явном виде, но больше она ничего не делает. Возможные типы переменных и элементов массивов можно найти в интернете, например, по адресу , где представлен справочник по пакету (numpy). Наиболее важные типы -- это int, bool, float, complex, bytes, str. Также есть функции с таким же названием для конвертирования типов скалярных переменных. Я не буду все переписывать из интернета. Вместо слов можно указывать сокращения, можно указывать размер в байтах. Указанная ссылка поможет разобраться.

Арифметические операции с массивами делаются поэлементно. Элементы можно не указывать, только названия массивов. То же самое со стандартными функциями, они все есть. Сразу скажу для чего это нужно. Дело в том, что в интерпретируемом языке все массивы надо выполнять одной командой, которая делается интерпретатором намного быстрее, чем если бы он интерпретировал цикл по элементам массива. Все большие циклы надо спрятать и выполнять в компилируемой части программы. Иначе программа будет работать очень медленно. В моем персональном языке то же самое, другого варианта просто нет.

Есть много других операций, например, d1.dot(d2) -- это скалярное произведение матриц. Результат -- это матрица первой и последней размерности, то есть (2,3) и (3,4) дают (2,4). Промежуточные размерности должны совпадать. Индексы начинаются от нуля и указываются в квадратных скобках. Если какой-то индекс не указан, то подразумеваются все значения. Вместо конкретного значения индекса можно писать интервал в виде 2:4. При этом интервал начинается с первого значения и заканчивается ПЕРЕД вторым значением.

В многомерных массивах можно выделять направление вот так d1.max(axis=0) -- это значит вычислять массив максимальных значений при изменении первого индекса, получаем матрицу на единицу меньшей размерности. Транспонированная прямоугольная матрица пишется так d1.T, то есть после точки буква Т. Размерности матриц можно менять с помощью функции d1.reshape(2,3). А просто определить размерность массива можно так sar = ar.shape. При этом sar -- это кортеж, его длину можно узнать функцией len(sar), а элементы определяются индексом в квадратных скобках. Вообще, как говорят, возможности этого пакета довольно большие. Так он умеет представлять аудио-файл как временной массив и можно выделить определенный временной интервал из аудио файла, он может представить картинку как матрицу чисел и с ней можно манипулировать. В моем языке это тоже имеет место быть. Можно составить словарь слов для проверки правописания.

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

a = np.arange(8) # инициализация массива последовательностью целых чисел 0, 1, . . ., 7

Это очень удобно, если надо явно прописать индексы массива. Тип данных не указан, и нужно помнить, что элементы массива имеют тип целых чисел. И если вы потом присвоите какому-то элементу значение 0.5, то получите 0. Чтобы указать тип реальных чисел надо писать так

a = np.arange(8)*0.1   # инициализация массива последовательностью чисел 0.0, 0.1, . . .,0.7

Вот теперь массив будет иметь реальный тип, и если вы присвоите 0.5, то так и будет 0.5. А если вам нужно определить комплексный массив, то надо делать еще сложнее

a = np.arange(8)*(0.1+0.1j)

Только так все элементы массива будут иметь комплексные значения. Буква j (можно использовать i тоже) используется как мнимая единица. Самая общая форма данной команды такая

a = np.arange(first,last,step)

Здесь первый аргумент указывает начальное значение, второй -- значение, выше которого быть не может, третий -- шаг. И есть еще вариант

a = np.linspace(first,last,n)

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

Очень важно уметь записывать массивы в файл и считывать их из файла. При выполнении научных расчетов бывает так, что один и тот же массив много раз преобразуется по определенной схеме. И вам нужно запомнить все промежуточные варианты для последующего анализа. Это легко делать записывая массив в файлы с разными именами на каждой итерации. В пакете "numpy" это делается довольно просто

np.save('data1', ra)  

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

ra = np.load('data1.npy')  

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

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

Теперь пару слов про формат записи значений реального массива в файл, то есть в файлы с расширением (.npy). Я просто посмотрел как выглядит такой файл в режиме побайтной записи и понял, что у него первые 128 байтов заполняются заголовком, то есть простым текстом, в котором записаны кое-какие сведения о массиве, в том числе его размерность. Остальная часть файла показывает числа по 8 байтов на число. При этом, если сравнить с записью тех байтов в программе на языке Java, то легко заметить, что в системе Виндовс Питон переворачивает порядок байтов. То есть делает так, как поступает сама Виндовс. Младшие байты идут раньше, чем старшие.

А в программах на языке Java все делается как в системе Юникс независимо от операционной системы. То есть младшие байты в конце. Соответственно порядок байтов в каждом числе (то есть каждые 8 байтов) нужно поменять на зеркально инверсированный, чтобы прочитать в Java числа, записанные в Питоне и наоборот. Ну и шапку надо либо отрезать, либо добавлять в зависимости от того куда идет переход с данными. И не забывать, что числа, которые в Питоне имеют формат float, в Java называются double.

5. О функциях и переменных

Грамматика Питона тоже весьма оригинальна и первое время мне активно не нравилась. Она и сейчас не нравится, но в редакторе IDLE все записывается достаточно быстро и просто, так что можно потерпеть. Да и свой редактор можно настроить как надо. В нем нет скобок. Каждый оператор пишется на отдельной строке, даже очень короткий. А некоторые операторы пишутся на нескольких строках. Вместо скобок используется отступ и признак конца строки. При этом функции как бы просто обрезаются, они не имеют явно выписанного конца, только отступ. Как пишутся функции написано в учебниках для чайников и можно увидеть в любом файле с расширением ".py". Определение функции начинается со слова "def ".

Важно хорошо понимать область видимости переменных. Переменные никак специально не объявляются. Если переменная впервые встретилась в теле функции, то она существует только в этом теле и является локальной. А если переменная записана в теле программы, то есть вне функции, то она та же самая и в функции. При этом ее нельзя определять еще раз. Точнее, определять можно, но тогда она будет локальной. И вот если вы все же хотите переопределить значение внешней переменной, только в этом случае ее надо объявить как "global", то есть написать

def click(text):
    global inptxt

Тут как бы вариантов нет, иначе будет неоднозначность. Но про это надо помнить и не забывать. Это тоже источник ошибок.

Функции писать разумно и полезно. Так программа легче читается и код можно сделать более компактным. А при программировании интерфейса функции просто необходимы как способ реакции на действия пользователя. Я лично для себя разработал минимальный интерфейс для научных программ, который включает в себя три кнопки с названиями [Help], [Input], [Execute] и простой текстовый редактор в окне определенного размера. Изначально в окне редактора могут быть записаны входные данные программы и комментарии к ним. Можно сразу кликать 3-ю кнопку и выполнить программу. После этого можно кликнуть вторую кнопку и снова увидеть входные данные. Их можно отредактировать и снова кликнуть третью кнопку. А первую кнопку можно кликать в любой момент, чтобы получить инструкцию к программе в том же окне.

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

6. Использование текстов в файлах и системных команд

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

mf = open("путь к файлу", "w")
mf.write("текст для записи")
mf.close()

При этом, если файла не было, то он создается, а если был, то переписывается. Если нужен файл в папке программы, то пишем просто имя файла, а если нет, то надо писать относительный или абсолютный путь к файлу. Есть и другие режимы работы, но это уже надо смотреть в более подробном описании Питона. Да и они редко кому нужны. Сложность в том, что разные версии Питона имеют разный набор команд, и можно вычитать в интернете такое, что не работает в 3-й версии Питона. Здесь я пишу только то, что сам проверил и это работает. Считать записанный текст можно таким кодом

mf = open("путь к файлу")
text = mf.read()
mf.close()

То есть отличия небольшие. При открытии файла режим чтения не указан, так как он используется по умолчанию. Но если кому хочется, то можно приписать "r" вместо "w", как было в предыдущем примере. В этом случае файл считывается целиком. Если вам нужна лишь часть текста, то выделить часть можно уже средствами работы с текстом. На самом деле есть способ считывать лишь часть символов из файла, но если файлы небольшие, то нет смысла этим пользоваться.

Запись текста в файл -- весьма полезная процедура еще и вот по какой причине. В любом языке, и Питон не исключение, есть возможность запускать внешние программы. Но намного проще запускать командные файлы. В операционной системе (ОС) Виндовс любой файл с расширением ".bat" считается командным и исполняется ОС. Питон позволяет записать такой файл динамически, а потом выполнить его. Вот как это делается

import os
mf = open("run-pro.bat", "w")
mf.write("infocomp.htm\npause")
mf.close()
k = os.system('run-pro.bat')

Что тут происходит. Для выполнения системных операций надо выполнить (import) пакет os. Затем записываем файл, в котором указан ".htm" файл и команда остановки на следующей строке. А потом команда "os.system()" выполняет то, что записано в файле. Увидев название ".htm" файла система автоматически запустит браузер и покажет этот файл. Это тоже весьма полезно при чтении описания программ, так как наиболее продвинутые описания можно приготовить в виде форматированного вэб сайта.

Команды ОС Виндовс я описал в серии статей на своем сайте и на сайте proza.ru . Хотя в Питоне тоже можно много чего сделать, но операции с файлами, такие как копирование, переименование и много чего еще, часто удобнее делать через ОС, так как это более универсальный и прямой способ. После завершения запущенной программы команда возвращает код завершения. Если все успешно, то будет 0. Эту информацию часто можно и не использовать и запускать команду без присваивания возвращаемого значения.

Надо отметить, что модуль "os" имеет много возможностей, не все из них одинаково полезные, но некоторые могут пригодится. Например

k = os.path.getsize('путь к файлу')

позволяет определить размер файла в байтах. Иногда это бывает полезно для контроля записи файлов с данными. Если размер файла не соответствует тому, который был при записи заказан, значит произошла ошибка или еще какое недоразумение. Надо разбираться. Вместо команды "getsize" можно использовать и другие, например, "getatime" время последнего доступа к файлу, "getmtime" -- время последнего изменения файла, "getctime" -- время создания файла.

7. Диагностика работы программы

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

import time
import numpy as np
k1 = time.time()
for k in range(10000):
    x = np.exp(k/1000)
k2 = time.time()
k3 = k2-k1
print(k1, k2, k3)

Функция "time.time()" выдает время от начала эпохи в секундах, поэтому абсолютное значение времени не имеет смысла, но вот разница -- это именно то, что нужно. Интересно, что Питон выдает разницу во времени точнее, чем само время и точность вроде как ничем не ограничена. Это немного странно, но комментариев нет. Вот как выглядит выдача результат в указанном примере

1590823634.6564617 1590823634.6704705 0.014008760452270508

Тут в первых двух числах 7 знаков после запятой, а у разницы 18 знаков. Впрочем разницу можно умножить на 1000, взять целое число а потом снова разделить на 1000 и определить время с точностью до в миллисекунд. Можно указать еще одну полезную команду, правда не очень понятно как она работает в Питоне. Это команда остановки работы программы на время в секундах. Вот как она записывается

time.sleep(2)

Такая команда остановит работу программы на 2 секунды. При использовании многопоточных программ это бывает необходимо для того, чтобы остановить один поток и дать возможность выполниться другому потоку. Но это также может быть полезно при запуске внешних программ, когда надо дождаться окончания их работы. Если время работы внешней программы примерно известно, то это самый простой путь. А если нет, то останавливаем, проверяем наличие файла, если нет, то снова останавливаем и так далее, пока файл не появится.

8. Научная графика

Для научной графики в Питоне есть несколько пакетов, но наиболее широко используется система matplotlib, в частности ее пакет pyplot, который стандартно импортируется с помощью команды

import matplotlib.pyplot as plt

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

plt.imshow(za,extent=[-a,a,-b,b])
plt.show()

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

plt.title('Заголовок')
plt.xlabel('название оси Х (cm)')
plt.ylabel('название оси Y (cm)')

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

plt.plot(x1,f1, x2,f2, ...)

вместо первой команды "imshow". Здесь x1 -- массив значений аргумента первой функции, f1 -- массив значений первой функции. Можно указывать несколько функций простым перечислением, а можно в цикле указывать по одной функции, не важно. В описании пакета можно найти и другие возможности. Впервые такая техника и такой способ графики появились, наверно, в программе Gnuplot в системе Юникс в то время, когда системы Виндовс еще даже в проекте не существовало. И за последние 30 лет она повсеместно развивается и используется в разных вариантах.

Но очень часто такого скудного набора не хватает и хочется делать более сложные рисунки, точнее управлять процессом и указывать что и как надо рисовать. Тогда нужно писать больше команд. Как это обычно бывает, средств и способов рисования намного больше, чем это необходимо. А информацию о том, как ими пользоваться, найти не всегда получается быстро. Вот и я пока знаю далеко не все, да всего наверно и знать то не обязательно. Однако какие-то базовые принципы работы с графикой могу показать. Сначала я покажу код более сложного графика одномерных функций, а потом объясню что делает каждая команда

fig = plt.figure()
fig.set_figwidth(14)
fig.set_figheight(5)
fig.set(facecolor='#fdfdfd')
ax1 = fig.add_subplot(121)
ax1.plot(xa,ya, linestyle='-', linewidth=1, color='#0000ff')
ax1.minorticks_on()
ax1.tick_params(labelsize=14)
k = 122
ax2 = fig.add_subplot(k)
ax2.plot(xa,ya, color='#ff0000')
plt.tight_layout(h_pad = -0.88)
plt.show()

Итак начнем. В 1-й строке мы в переменной fig явным образом задали объект рисунка. Про классы и объекты я подробно писать не буду, это отдельный разговор. Теперь мы можем определить свойства этого объекта. Во 2-й и 3-й строках задаются горизонтальный и вертикальный размеры рисунка. Уже хорошо, и очень часто это важно. Правда единицы, в которых задаются размеры, весьма странные, но это уже не так важно, все можно настроить эмпирическим путем проб и ошибок. В 4-й строке задается цвет рисунка. Он не обязательно может быть белым. Цвет можно задавать разными способами, но наиболее универсальный это задание RGB (компоненты красного, зеленого и синего) в 16-тиричной системе, когда имеется 256 градаций от 00 до ff для каждого цвета. Эти же параметры можно задать и в первой строке в аргументе, но это большого значения не имеет. Все равно надо делать свою универсальную функцию.

В 5-й строке определяется область осей первого графика. Дело в том, что рисунок -- это просто контейнер, и в нем можно расположить много графиков, причем даже один поверх другого. Иногда это бывает важно. Расположение графиков можно задавать разными способами. Здесь указан метод add_subplot(), в котором в качестве аргумента указывается три числа 121 без разделения. На самом деле это одно число, у которого каждый десятичный разряд имеет самостоятельное значение. Первые два разряда слева указывают число строк и столбцов, на которые условно можно разбить область рисунка. При этом прямоугольник рисунка с заданными размерами условно разделяется на матрицу одинаковых рисунков меньшего размера. А третье число показывает какое именно место в этой матрице рисунков будет использоваться для рисования данного графика.

Очевидно, что при таком правиле максимально можно задать только 339, то есть 9 рисунков. Конкретно в указанном примере заданы 2 рисунка по горизонтали. Если у вас всего один рисунок , то он задается аргументом 111. Область осей -- это тоже объект с названием ax1. И ему тоже можно определить его свойства. Как раз функция plot() в 6-й строке указывает что и как надо рисовать на этих осях. У нее есть два обязательных аргумента xa и ya -- одномерные массивы аргумента и функции, и может быть много необязательных аргументов, значения которым можно присваивать прямо при вызове функции. В частности в указанном примере определяются тип линии, толщина линии и цвет. Но могут быть еще и маркеры. Уже этого набора достаточно, чтобы сделать график более привлекательным.

Но у нас все еще оси графика определяются автоматически (по умолчанию) из значений аргумента и функции. Это часто бывает удобно, но не всегда. Так, например, по умолчанию тики рисуются только там, где есть числовые значения. Это не всегда красиво. Часто делаются еще более мелкие тики между крупными. Поэтому в 7-й строке указано сделать на графике мелкие тики. В 8-й строке переопределяется один из параметров тиков, а именно, размер числовых подписей. Размер задается в единицах pt, которые как раз используются в постскрипте и при указании размеров всех шрифтов. Про них легко получить информацию в интернете. Конкретный размер легко подобрать простым тестированием.

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

Далее 9-11 строки описывают второй график. Тут уже аргумент функции add_subplot() задан переменной, но она обязательно должна иметь три десятичных разряда, иначе будет ошибка. Такая возможность важна при автоматическом построении графика самой программой, когда код пишет не пользователь, а программа. Это часто бывает в программах с интерфейсом. Предпоследняя 12-я строка тоже очень важная. Дело в том, что стандартно между краями области рисунка и графиками имеются довольно большие поля пустого пространства. Это и некрасиво, и неэффективно используется экран. В данной команде указано уменьшить горизонтальную ширину этих пустых полей. А вертикальные уменьшаются автоматически, так как графики увеличиваются в размере с сохранением аспектного отношения. Значение подобрано так, чтобы поля были минимальные. Часто аргумент можно не указывать, но команду без аргумента все равно разумно писать, иначе будет некрасиво.

Последняя 13-я строка делает весь рисунок. На всякий случай сообщу, что указанные выше функции title(), xlabel(), ylabel() теперь относятся к объектам осей, то есть ax1 и ax2, также точно, как и plot(). Их тоже можно использовать. И еще. У нас все еще оси размечаются автоматически. Это тоже можно изменить, но делается это более сложным образом. Я просто покажу что нужно добавить к тексту описания рисунка.

import matplotlib.ticker as ticker
ax1.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax1.xaxis.set_minor_locator(ticker.MultipleLocator(0.5))
ax1.yaxis.set_major_locator(ticker.MultipleLocator(0.2))
ax1.yaxis.set_minor_locator(ticker.MultipleLocator(0.1))

То есть в самом начале программы нужно добавить импорт еще одного пакета, и потом с помощью указанных команд можно определить расстояния между длинными и короткими рисками на осях. А команду minorticks_on() нужно убрать. И все равно значение первой длинной риски определяется автоматически. Казалось бы такое простое дело делается таким сложным образом. Не знаю почему так сложно, но что есть, то и есть.

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

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

Однако, продолжим. Функция add_subplot() не является единственной для задания области графика. Есть и другие способы. Вот еще один.

box = [0.25, 0.5, 0.25, 0.25]
ax3 = fig.add_axes(box)   

Можно вместо box сразу указать в аргументе правую часть первой строки. Здесь 4 числа означают X,Y,W,H, то есть координаты левого нижнего угла графика и его ширину и высоту в долях от общего размера рисунка. Кстати, размер рисунка не совпадает с размером окна рисунка. Размер окна больше, я писал об этом выше, и о том как этим управлять, и тут учитываются только размеры рисунка. Этот способ более универсальный, но нужно считать координаты и размеры. Новые графики делаются поверх тех, которые были нарисованы раньше.

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

import os
def save(name,fmt):
    pwd = os.getcwd()
    iPath = './pictures/{}'.format(fmt)
    if not os.path.exists(iPath):
        os.mkdir(iPath)
    os.chdir(iPath)
    plt.savefig('{}.{}'.format(name, fmt))
    os.chdir(pwd)
save('pic-5-1', 'png')   

Здесь первую строку надо поставить в начало программы. Функцию можно записать в отдельный файл и тоже делать импорт, а последняя строка как раз спасает картинку в файл. При этом, если вы не хотите ее смотреть на экране, то не надо писать команду plt.show(), а вместо нее надо записать plt.close(). Дело в том, что если картинка показывается, то она как бы и закрывается. А если не показывается, то она не закрывается и остается "лежать на полке" с номером 1. Новая картинка рисуется с номером 2 и если вы захотите ее показать, то вам покажут сразу две картинки. Иногда это бывает удобно, но на самом деле самый правильный вариант -- это все картинки спасать в файлы и потом смотреть из файлов. У новой функции два аргумента, которые вполне очевидны. Первый аргумент -- это имя файла, второй -- это тип сжатия (кодирования) картинки в файл. Короче можно сказать, что это тип файла. Самое интересное находится в коде функции и он не вполне универсальный. Его можно переписывать. Ниже я напишу что там делается.

Сначала в переменную pwd спасается путь к текущей папке, той самой, где находится данная программа, которая выполняет этот код. Далее в переменной iPath формируется новый путь, который состоит из пути данной программы (это точка ./) папки picture (она должна быть на компьютере, иначе будет ошибка) и новой папки, имя которой совпадает с типом файла в аргументе функции. Затем проверяется наличие этой папки, и если ее не существует -- она создается. Это надо пояснить. Создается всегда только одна папка, то есть та, которая будет иметь новое имя. Эта папка должна быть внутри папки picture. Если нет папки picture, то команда mkdir должна создать эту папку, а внутри нее еще одну папку. Но она так делать не умеет. Это тоже можно сделать, но в два приема. Сначала создать первую папку, а потом внутри нее вторую. В этом коде предполагается, что первая папка уже существует.

После того, как папка создана, если ее не было, система переходит в эту новую папку. И дальше выполняется команда plt.savefig(). Тут и выше используется одно свойство текстовой переменной. Эти переменные -- тоже объекты класса и у них есть свои свойства. В частности есть свойство format(). Но об этом можно почитать в другом месте. Тут как раз ничего менять не надо, просто копируете код себе и все дела. А потом система возвращается в ту папку, в которой была в самом начале. Если вам такие сложности не нужны, то можно сразу использовать команду plt.savefig() и файл картинки появится в той же папке, где находится программа. Но это как раз не всегда удобно.

Очевидно, что все, о чем указано выше, относится и к картинкам распределения двумерных функций, достаточно команду plot() заменить на imshow(). Но тут есть один нюанс, который состоит в том, что просто цветная картинка никак не устраивает. Хотелось бы иметь возможность задавать карту цветов, и также нужна шкала, показывающая соответствие между цветами и значениями функции. Я пока нашел только один способ сделать это достаточно просто, и в этом случае функция imshow() не используется. Я снова приведу код готовой программы, а потом объясню что он делает. Итак, вот код.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
a = 4
n = 128
xa = np.linspace(-a,a,n)
ya = np.exp(-xa*xa)
za = np.empty((n,n), dtype=np.float)
for i in range(0,n): za[i] = ya*ya[i]
fig = plt.figure(figsize=(5,4))
N = 50
ba = np.linspace(np.min(za), np.max(za), N+1)
cmap = mpl.cm.get_cmap('RdBu', N) 
ax = fig.add_subplot(111)
ax.minorticks_on()
ax.tick_params(labelsize=14)
cs = ax.pcolor(xa,xa,za, cmap=cmap) 
cbar = fig.colorbar(cs, ax=ax) 
cbar.ax.tick_params(labelsize=14)
cbar.ax.minorticks_on()
ticks = ba
ax.set_title(u'Карта цветов по заданной шкале')
plt.tight_layout()
plt.show()

Теперь обсудим. В первых 6-ти строках просто вычисляется двумерный массив функции при одинаковых значениях аргумента по обеим осям. О том, как это делается, написано выше. Затем определяется объект рисунка. Но здесь, в отличие от ранее написанного кода, размеры рисунка сразу написаны в аргументе конструктора. Так компактнее, но кому как нравится. Затем задается размер шкалы цветов в 50 градаций в переменной N. В массиве ba задается набор чисел с постоянным шагом от минимального до максимального значения функции. И вот в 10-й строке самое важной -- задается карта цветов на N разных переходов от красного до синего. Параметр 'RdBu' можно интерпретировать как Red down Blue up.

Затем определяется область графика и рисуется цветная карта нашего массива функцией pcolor(). У нее аргументами стоят значения аргумента по осям X, Y и затем значения функции, а в конце уже определенная карта цветов. Затем определяется новый объект colorbar() с аргументами в виде объекта цветной карты и ее области графика. При этом вертикальный размер шкалы цветов получается равным вертикальному размеру карты. Для разметки оси передается массив ba, который описывает диапазон значений двумерной функции. Последние три строчки кода уже описаны выше. И дело сделано, теперь рисунок выглядит как надо. Осталось только понять какие еще карты цветов можно задавать и какими способами.

Все заранее приготовленные карты цветов можно посмотреть вот по этой ссылке . Это удивительно, но все карты очень плохие. Я давно заметил какие ужасные карты цветов показывают люди. Из всех заранее готовых карт заслуживают внимания только 'gray' и 'hot'. Интересно, что в самом конце указанной статьи дается ссылка на скачивание кода на Питоне, который показывает все готовые карты. Но лично я на своем языке ACL давно использую собственную карту цветов, в которой переход идет от черного до белого через синее, красное, зеленое и желтое. Моя карта по стилю похожа на 'hot' и 'gnuplot2', только цветов больше.

В Питоне тоже можно приготовить собственную карту цветов и делается это достаточно просто, причем разными способами. Наиболее универсальный -- это массив цветов на 256 элементов. Больше просто нельзя, так как картинки записываются в файл по системе: одна компонента цвета -- один байт. То есть цветные по три байта на пиксель. Делается это таким кодом

from matplotlib.colors import ListedColormap
cmap = ListedColormap([
 . . . 
"#ffffe2", "#ffffe5", "#ffffe9", "#ffffec", "#ffffef", "#fffff1", "#fffff4", "#fffff6"])

Здесь многоточие означает все другие цвета из набора размером 256. Для экономии места я показал только последние 8. Я уже сделал универсальную функция для показа двумерной карты цветов со шкалой и нужными параметрами в своей библиотеке. И даже написал к ней описание, но работа пока не закончена и библиотеку я не публикую. Как-нибудь позднее она появится на моем сайте.

9. Расчет преобразования Фурье

В большом числе научных расчетов используется преобразование Фурье. Это интеграл в бесконечных пределах от произведения комплексной функции на экспоненту, аргумент которой является чисто мнимым произведением аргумента функции и и аргумента фурье-образа. Аргумент функции одновременно является переменной интегрирования. Например, самый распространенный случай -- это когда аргументом функции является координата реального пространства x, тогда аргумент фурье-образа обозначается как q и является координатой в обратном пространстве, а в экспоненте стоит знак минус. Есть любители использовать знак плюс. Это как всегда и везде, есть белые и красные, верующие и атеисты и так далее. Но Питон использует знак минус.

С другой стороны, есть процедура Быстрого Преобразования Фурье (БПФ), которая очень быстро вычисляет сумму такой же конструкции, но с некоторыми отличиями. Во-первых, индекс суммирования начинается от 0 и идет до N-1, где N -- число точек массива функции. Во-вторых, в аргументе экспоненты есть еще множитель (2*np.pi/N). Здесь конструкцией np.pi записано число ПИ, то есть 3.14159265, как это записывается в Питоне при использовании пакета numpy as np. Такая сумма -- вовсе не интеграл Фурье, как его используют в математической физике. Но функция

fq = np.fft.fft(fx)

вычисляет именно такую сумму. Здесь fx и fq -- комплексные массивы, имеющий N элементов. Первый вычисляется предварительно, второй вычисляет Питон. Если вычислить массив fx по значениям функции в симметричных пределах с постоянным шагом d в реальном (x) пространстве, то результат в обратном (q) пространстве будет иметь странный вид. У него отрицательные значения будут справа, а положительные слева. Вообще говоря, известно, что процедура БПФ оптимально работает когда значение N равно целой степени числа 2. Тогда правое и левое легко делится и имеет одинаковое число точек. Но Питон об этом не пишет, у него N может быть любым, это странно.

Чтобы исправить ситуацию с неправильной упаковкой массива результата Питон предлагает дополнительные функции сдвига, fftshift и ifftshift. В результате выполнения операции fftshift правая и левая стороны массива меняются местами. Но значения функции все равно очень сильно осциллируют. А более точно -- они на каждом шаге меняют знак. И это совсем не то, что является реальным образом Фурье исходной функции. Однако модуль этой функции оказывается почти правильным с точностью до нормировки.

Интересно, что если не делать сдвиг, а сразу сделать обратное БПФ, вот так

fx = np.fft.ifft(fq)

то Питон возвращает исходную функцию. То есть сумма имеет все свойства интеграла Фурье. Но снова облом если перед вычислением обратного БПФ возвести функцию в квадрат. Понять в чем отличие интеграла в симметричных пределах от суммы в несимметричных пределах позволяет формула, к которой можно преобразовать интеграл Фурье. Пусть аргумент функции в реальном пространстве представляет собой систему точек с шагом d в симметричных пределах и с числом N. Математически эта система точек выглядит так x[k] = d*(k+k0), k = 0:N-1, k0 = (1-N)/2. А аргумент функции в обратном пространстве отличается от выписанного тем, что шаг d заменяется на шаг d1 = (2*np.pi/(N*d)).

Как нужно вычислять интеграл? Область интегрирования разбиваем на отрезки шириной d и интеграл по каждому отрезку заменяем на значение функции в центре отрезка, умноженное на d. Это метод прямоугольников для расчета интеграла. Отрезок надо брать настолько маленьким, чтобы функция на его размере изменялась линейно. Если функция меняется линейно, то интеграл по отрезку получается правильно. В результате, вместо интеграла получаем сумму, причем по несимметричному индексу. Осталось подставить аргументы в экспоненту и результат выглядит как показано ниже.

Формула для А написана в предположении, что N/4 -- целое число. Видно, что в этой формуле, действительно, вычисляется сумма от произведения правильной экспоненты на исходный массив функции, но дополнительно есть еще множитель, который, грубо говоря, меняет знак на каждом шаге. Это есть плата за переход от симметричных пределов к несимметричным. Легко заметить, что этот множитель эффективно приводит к сдвигу индекса m на величину (1-N)/2. Этот сдвиг должен компенсировать тот сдвиг, который сумма делает изначально.

Процедура Питона этот множитель не учитывает, поэтому у нее массив остается сдвинутым. Питон пытается исправить ситуацию с помощью дополнительной процедуры сдвига, которая указана выше. Учитывает ли она сдвиг на половину индекса? Численные эксперименты показывают, что нет. Но сам по себе сдвиг не дает правильный ответ. Результат, который выдает Питон, меняет знак на каждом шаге, потому что не учтен еще один множитель dAC[m]. Только после умножения на этот массив можно получить правильный массив значений преобразования Фурье исходной функции.

Таким образом, в любом случае и всегда надо помнить, что интеграл Фурье и процедура БПФ -- это не одно и то же. Последнее замечание -- при использовании процедуры fft Питона для вычисления аккуратного обратного преобразования Фурье массив надо делить на d, так как деление на 2пи делается в формуле, а деление на N делает процедура Питона. В заключение я должен сделать оговорку, что ссылаясь на Питон я имел в виду пакет (numpy). Возможно в Питоне есть и доугие способы вычисления интеграла Фурье. Но в этом нет необходимости. Если вычислять интеграл по указанной формуле ответ получается правильный без всяких сдвигов и не портится в любых комбинациях вычисления прямого и обратного преобразования. Формула для обратного преобразования такая же, только все функции, кроме исходной надо заменить на комплескно-сопряженные.

10. Шаблон интерфейса для программы научных расчетов

Прежде, чем показывать код, я хочу сделать пару замечаний. Здесь весь код стандартный. Единственное, что надо изменить -- это тексты в файлах, в которых описана помощь и входные данные, а также название окна программы. Весь научный расчет выполняет функция execute(), которая здесь выписана только в самом начале, когда значения параметров расчета из текста конвертируются в числовой массив. При этом переменная k должна соответствовать числу имеющихся в задаче параметров. В примере оно равно 1. Функция execute() только и будет разной в разных задачах. Эта функция должна будет выполнить все расчеты и показать графики, а также записать файлы. Программа позволяет выполнять много расчетов с разными входными данными за один вызов, и не требует от пользователя знания кода языка Питон.

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

Тем, кто не любопытный, текст дальше можно не читать, просто копируйте код себе в программу и пользуйтесь. Для тех новичков, кто еще плохо знаком с программированием, я попробую добавить более подробное пояснение. Сейчас пошла такая мода привлекать все больше людей в программирование. Причина очень простая -- компьютеры плохо продаются, многим вполне достаточно смартфона, чтобы потусоваться в сетях, посмотреть фотки и видео. И им больше ничего не надо. Но вот программировать на смартфоне все же неудобно, да и нет таких простых программ. Я сам еще в 2004 году предлагал свой язык программирования для карманного компьютера. Других просто не было.

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

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

В Питоне практически все является объектом какого-то класса и имеет свойства, которые приписаны данному классу. Часто при написании программы можно об этом даже не задумываться. Вот дети начинают говорить в среднем в 2 года, а грамматику языка изучают только в 7 лет, когда приходят в школу. Так и многие программисты пишут программы не зная грамматики, так как в этом нет большой нужды. Можно просто кого-то копировать или списывать готовый код. Если удается его пристроить и он работает, то даже не важно, что программист не знает как он работает. Я тоже так иногда поступаю. А в Питоне такая практика широко используется.

Ниже я попробую объяснить основные черты кода, который написан в конце данного раздела. Функции надо записывать вперед, а сам код идет после них. В первой строке определяется переменная root -- это объект класса Tk. Сам класс описан в пакете tkinter и нам его писать не обязательно. У этого объекта есть все свойства данного класса. Это окно нашей программы, в котором как раз и будет выполняться общение с пользователем. В частности, можно вызвать функцию title и присвоить окну имя. Затем окно надо заполнить. В окне могут находиться объекты других классов. В коде, который используется, есть объекты классов Label, Button и Text. У них есть свои свойства и их надо определить. Но прежде всего надо иметь проект заполнения окна. Есть разные методы заполнения. Наиболее простой -- это решетка (таблица) grid. В этом случае все окно представляет собой таблицу областей, разделенную на строки и столбцы.

Почему-то этот код принято писать в конце и он записан в последних трех строках программы. Там записано, что наше окно имеет структуру grid c 2 строками и 8 столбцами. Самая последняя строка кода запускает окно в работу. Работа состоит в том, что окно ждет действий пользователя и реагирует на них. После реакции оно снова ждет и все это выглядит как бесконечный цикл. Бег по замкнутому кругу. Но теперь надо вернуться в начало и описать компоненты окна. Класс Label -- это комментарии, просто текст в окне. Сначала заполняется первый ряд (отсчет идет от нуля). Ставится комментарий с пустым текстом, но с заданной шириной в первую колонку, затем кнопка, объект класса Button. Он имеет текст, можно указать каким фонтом этот текст будет напечатан.

И самое главное -- надо указать какая функция будет работать, если данную кнопку пользователь кликнет. В разных языках это организуется по разному. В Питоне придумали конструкцию типа вcтроенной функции command=lambda. Это не так важно как и что писать, важно четко передать информацию. Так вот тут сказано, что будет работать функция click(text), а в качестве аргумента ей будет передано слово Help. Сказано весьма запутанно, но автор языка волен придумывать так, как ему удобно. Главное, что все работает. Есть и другой способ сделать то же самое, но я сделал так, как было в примере, который я нашел.

Затем снова два раза записываются объекты классов Label и Button, причем у кнопки уже другой текст и в ту же функцию передается другой аргумент. И потом еще раз Label. Никакие комментарии мне на самом деле были не нужны, но если их не ставить, то кнопки будут тесно прижаты друг к другу. А я как раз этого не хотел. То есть назначение комментариев -- организовать пустое пространство и это можно делать по разному. Я сделал так, что размер крайних комментариев больше, чем центральных. Тут надо напомнить, что у кнопок есть такой параметр (padx), который указывает сколько пустого пространства надо сделать около кнопки по горизонтали. Я его не использовал, но его тоже можно использовать. Вариантов сделать как хочется всегда больше, чем один.

А во второй строке стоит один объект класса Text. Это текстовый редактор. Он имеет размеры, можно указать тип фонта, правила переноса слов, и можно записать туда первый текст. Текст в редактор записывает функция insert, у нее два аргумента. Первый указывает номер строки и столбца, куда будет записан первый символ текста. Причем строки отсчитываются от 1, а столбцы от 0. Снова вопрос -- почему так -- не ко мне. Как говорили в советской столовой "Жри, что дают". Ну и к текстовому окну пристроена линейка прокрутки. В Питоне это тоже отдельный объект класса Scrollbar, непонятно почему.

В Джаве она появляется автоматически, когда надо и ее не надо специально описывать. А в Питоне надо, и делается это довольно запутанным образом. Сначала надо описать сам объект, указать его размещение. Затем этот объект надо связать с редактором, а редактор надо связать с ним. Вот и все, что касается организации окна программы.

Далее надо записать функцию click(text), которая выполняет реакцию на действия пользователя. Пользователь может кликать кнопки и писать текст в текстовом редакторе. Сама функция click(text) очень простая. Что касается редактора текстов, то функция delete уничтожает текст в окне редактора. Про insert я уже писал, и есть еще функция get, которая передает содержимое редактора в текстовую переменную. У текстовой переменной есть много своих свойств, это отдельный разговор.

При обработке кнопки (Help) я показываю заранее приготовленный текст в браузере через операционную систему. При обработке кнопки (Input) показывается текст из файла, который при нажатии кнопки (Execute) снова записывается в файл. Наконец, при обработке кнопки (Execute) в конце концов вызывается функция execute(), которая и выполняет расчет по программе. В результате она либо показывает графики, либо записывает файлы на винчестер. А бывает, что и то и другое. В научной программе это главное. Интерфейс удобен тем, что позволяет легко менять входные данные, не пугает пользователей, которые не знают Питона, и дает подсказку, то есть информационную помощь. Также кое-какие результаты расчетов можно показать в окне редактора. Вообще говоря, данный интерфейс самый минималисткий, только самое необходимое.

А в более продвинутой программе можно еще показывать картинки, можно весь интерфейс сделать на картинке, то есть нарисовать кнопки и все остальное. Картинку можно сделать так, что программа будет возвращать место, где был сделан клик на картинке. Об этом будет написано чуть позже. Можно также делать анимации и много чего еще. Я все это умею делать на своем языке ACL, который в свою очередь интерпретируется Джавой. А в Питоне я сам новичок, я его учил не более месяца. Точнее, я его не учил, а просто узнавал как записывается то, что мне надо. В интернете на любой вопрос можно получить четкий ответ. Главное знать что спрашивать. Как в старой хулиганской песне "И спрашивает тихо, куда бы мне пойти, где можно было б лихо, время провести". Вот, наконец и код.

 
from tkinter import *
import numpy as np

def execute():
    global inptxt
    Sa = inptxt.split("\n")
    k=1
    id = 0.1*np.arange(k)
    for i in range(0,k): 
        Sb = Sa[i].split("|")
        id[i] = eval(Sb[0])
    дальше пишем код программы для решения  научной задачи

def click(text):
    global inptxt
    if text == 'Help':
        mf = open("clickme.bat", "w")
        mf.write("info.htm\n")
        mf.close()
        k = os.system("clickme.bat")
    if text == 'Input':
        mf = open("inp.txt")
        inptxt = mf.read()
        mf.close()
        text1.delete(1.0, END)
        text1.insert(1.0, inptxt)
    if text == 'Execute':
        inptxt = text1.get(1.0, END) 
        mf = open("inp.txt", "w")
        mf.write(inptxt)
        mf.close()
        text1.delete(1.0, END)
        text1.insert(1.0, 'Program is running\n')
        execute()

root = Tk()
root.title("Program First")

label = Label(root, text='', width=18)
label.grid(row=0, column=0, sticky="nsew")
b1 = Button(root, text='Help', command=lambda text='Help': click(text), font=("Courier", 12, "bold"))
b1.grid(row=0, column=1, sticky="nsew")

label = Label(root, text='', width=4)
label.grid(row=0, column=2, sticky="nsew")
b2 = Button(root, text='Input', command=lambda text='Input': click(text), font=("Courier", 12, "bold"))
b2.grid(row=0, column=3, sticky="nsew")

label = Label(root, text='', width=4)
label.grid(row=0, column=4, sticky="nsew")
b3 = Button(root, text='Execute', command=lambda text='Execute': click(text), font=("Courier", 12, "bold"))
b3.grid(row=0, column=5, sticky="nsew")
label = Label(root, text='', width=18)
label.grid(row=0, column=6, sticky="nsew")

text1 = Text(root,height=40,width=100,font=("Courier", 12), wrap=WORD)
text1.grid(row=1, column=0, columnspan=7, sticky="nsew")
scrollbar = Scrollbar(root)
scrollbar.grid(row=1, column=8, sticky="nsew")
scrollbar['command'] = text1.yview
text1['yscrollcommand'] = scrollbar.set
text1.insert(1.0, 'Click [Help] to know how it works\nClick [Input] to begin\n')

root.grid_rowconfigure(2, weight=1)
root.grid_columnconfigure(8, weight=1)

root.grid_rowconfigure(2, weight=1)
root.grid_columnconfigure(7, weight=1)
root.geometry("+10+10")

root.mainloop()

11. Многооконные программы, более сложный интерфейс

В предыдущем разделе написано как сделать главное окно программы определенного вида на базе класса Tkinter. Это один из главных классов программы, он поставляется сразу в дистрибутиве, и его не надо скачивать дополнительно. Есть еще и другие классы, Питон вообще представляет собой коллекцию разработок, сделанных в разных системах и переписанных для интерпретатора Питона. И в каждой разработке свои идеи как организовать программирование. В Tkinter идея такой организации сводится к тому, что в программе должно быть всего одно главное окно. Могут быть и другие окна, но уже не главные.

Главное окно, как написано в предыдущем разделе, должно быть объектом класса Tk(). Стандартно его обозначают именем root. И оно имеет определенные преференции, в частности оно пишется в теле программы. И его описание является главным элементом этого тела. А дополнительные окна правильно писать как объекты класса Toplevel(). И это даже можно делать в теле функции. И тут надо напомнить, что у окон есть еще одно полезное свойство geometry(), которое позволяет задать ширину и высоту окна, а также его положение на экране. Ширину и высоту задавать нежелательно, если окно плотно упаковано, а вот отступы от левого верхнего угла экрана очень важны. Именно они заданы в предыдущем разделе для главного окна программы. Это строка, в которой после плюса идет горизонтальный отступ, а потом после плюса вертикальный. Почему так -- вопрос не ко мне. В языках не всегда понятно почему что-то пишется так, а не иначе.

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

def adpro(k,fni,fs):
    global imgs
    ap01 = Toplevel()
    ap01.geometry('+'+str(fs)+'+'+str(fs))
    ap01.title(fni)
    imgs[k] = PhotoImage(file=fni)
    la = Label(ap01,image=imgs[k])
    la.pack()

Это очень простая функция, которая открывает окно, задает геометрию и название окна, затем переопределяет картинку и вставляет ее в объект класса Label, про который я писал в предыдущем разделе и все это плотно упаковывает. Аргументы такие: номер картинки в списке, имя файла, где записана картинка, отступ от левого верхнего края. В данном случае они равны по горизонтали и вертикали. Список картинок описан как глобальный.

И вот это как раз является самым главным моментом всей технологии, который шокирует своей непредсказуемостью. Хотя картинка полностью описывается именно в функции как объект класса PhotoImage(file=fni) с указанием файла, где она записана, ее нельзя тут определять. Точнее определять то можно, и даже интерпретатор ошибки не заметит. Но окно картинку не покажет. Тут ее можно только переопределять. Почему так сделано -- трудно сказать. Одной из причин может быть то, что интерпретатор рисует окна не сразу, а потом, когда функция закончила свою работу. А все переменные, которые определены в функции, исчезают вместе с окончанием ее работы. Ведь они локальные, а не глобальные.

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

fni = './pic/icon.png'
imgs = []
for i in range(16):
    imgs.append(PhotoImage(file=fni))

То есть этот как бы совершенно бесполезный код надо добавить к тому коду, который описан в предыдущем разделе. Но и тут фокус. Это надо ставить только после объявления объекта класса Tk() и не раньше. На эту ошибку интерпретатор показывает. Хотя класс PhotoImage() вроде как в выглядит независимо. Интересно, что так как вы не знаете сколько картинок сразу попросит пользователь, то надо объявлять не одну картинку, а сразу список на всякий случай. Потому что если функция будет переопределять только одну картинку, то она и появится в последнем окне, а остальные окна будут пустыми.

Вот такие чудеса присутствуют в Питоне. И об этом мало кто пишет, а догадаться невозможно. Поэтому программирование таких систем -- не такая уж простая задача в простом языке, каким считается Питон. В Джаве это все делается намного проще и нагляднее. Я так и не смог найти в интернете текст, где об это написано. Только на одном форуме был намек и ... дальше я сумел догадаться имея кое-какой опыт.

12. Меню на картинке, еще более сложный интерфейс

Дополнительных окон в программе, вообще говоря, может быть много и разных. В предыдущей главе описано как открывать окна с картинками, например, графиками. При этом предполагалось, что такие окна закрывает сам пользователь, кликая крестик в правом верхнем углу. В этом разделе описано более сложное окно, которое имеет более разнообразный функционал при общении с пользователем. Очень часто возникает ситуация, когда надо сделать выбор из многих вариантов. Например, один из 99. Это можно сделать разными способами, например, открыть окно ввода, в котором пользователь наберет номер, потом кликнет кнопку [OK] и программа этот номер прочитает. Но при этом надо пользоваться клавиатурой, что не быстро. И нет подсказок к выбору.

Можно сделать по другому -- организовать таблицу кнопок в виде матрицы 11*9 и на каждой кнопке написать какой-то текст. Для пользователя уже лучше, но плохо для программиста. Кодировать 99 кнопок не очень то быстро и будет много текста. Есть, однако третий способ, который часто применяется во многих языках -- показать картинку в виде набора кнопок или как-то еще, и сделать так, чтобы программа возвращала координаты курсора мыши, где был сделан клик. Эти координаты можно пересчитать в номер и задача решена. Но нужен дополнительный функционал. После того как выбор сделан, картинка должна исчезнуть, так как она больше не нужна. Но исчезнуть не совсем. Она может еще понадобится при выполнении следующего выбора.

Сделать такую картинку в Питоне, наверно, можно разными способами, в том числе используя ООП и классы. Я придумал способ как это сделать без ООП, все в том же классе Tkinter. Этот способ я и объясню в данном разделе. Итак, снова придется в главном коде программы (в главном окне) открыть глобальные переменные. В прошлом разделе мы открыли картинки. Одна из них нам понадобится и тут. И еще надо открыть три переменные kmp, wmp, hmp и новое окно mp01, вот так

kmp = 1
wmp = 1
hmp = 1

root = Tk()
- - - 
mp01 = Toplevel()
mp01.withdraw()
- - -
root.focus()
root.mainloop()

Тут окно открыто как объект класса Toplevel(), а функция withdraw() сразу убирает его с экрана. И дополнительно root.focus() делает так, чтобы основное окно программы было активным. В главной программе больше делать ничего не надо, все остальное можно записать в функциях. Самая главная функция выглядит так

def mppro(k,fni,fs):
    global imgs,mp01,kmp,wmp,hmp
    mp01.deiconify()
    if kmp == 0:
        mp01.geometry(str(wmp)+'x'+str(hmp)+'+'+str(fs)+'+'+str(fs))
    if kmp == 1:
        mp01.title(fni)
        mp01.geometry('+'+str(fs)+'+'+str(fs))
        imgs[k] = PhotoImage(file=fni)
        la = Label(mp01,image=imgs[k])
        la.pack()
        mp01.bind('',getXY)
        mp01.update()
        s = mp01.geometry().split('+')[0].split('x')
        wmp = int(s[0])
        hmp = int(s[1])

У нее такие же аргументы, как у функции предыдущего раздела. В списке глобальных переменных указаны картинки, тоже как раньше и те переменные, которые мы только что объявили. Функция deiconify() возвращает наше новое окно на экран. Далее все зависит от значения переменной kmp. Первоначально она равна 1. В этом случае окно надо оформить. Указывается заголовок окна (title), его положение на экране (geometry), определяется картинка из файла, вставляется в объект класса Label(), упаковывается. Это все было и раньше. Теперь новое. Функция bind() указывает, что пока это окно будет активным, то при каждом нажатии левой кнопки мыши (то есть клик) нужно запускать функцию getXY(). Это как раз то, что нам необходимо, чтобы окно передавало информацию от пользователя.

Следующая функция update() тоже очень важная. В Питоне так сделано, что если функцию geometry() запускать с аргументом в виде текстовой строки, то она передает информацию интерпретатору. А если с пустым аргументом, то она возвращает информацию. И даже не только ту, которую мы задавали, но и ту, которую она сама вычисляла при упаковке окна. Нам нужна информация о размере окна, мы ее не задавали, она сама получилась как раз в тот момент, когда выполняется функция update(). А потом нам надо получить размеры окна. Это делается сложным разбором аргумента функции geometry(), которая его возвращает при нулевом заданном аргументе. Мы сначала разделяем его по символу (+), тут же берем первый элемент списка и разделяем его по символу (х). Первые два элемента нового списка и есть ширина и высота окна. Осталось поменять у них тип с текста на целое число и запомнить.

Дело сделано, мы сформировали окно, показали его и запомнили его размеры. И теперь ждем когда пользователь кликнет мышкой на этом окне. Вот, наконец это произошло. Теперь нам надо написать функцию getXY(). Она выглядит так

def getXY(event):
    global kmp,mp01 
    xc=event.x 
    yc=event.y
    kmp = 0
    mp01.withdraw()
    mpresp(xc,yc)

У этой функции должен быть единственный аргумент event (событие). Его создает интерпретатор. У него есть много свойств, но нас интересуют только два: x и y. Мы их передаем в свои вннутренние переменные. Это совсем не обязательно, просто так легче читать код. Затем меняем значение переменной kmc с 1 на 0. Снова убираем окно с экрана. Один раз мы это уже делали в основной программе, теперь тут, в функции. Глобальные переменные работают всюду одинаково. И затем вызываем новую функцию, которая будет обрабатывать координаты клика. Данная функция полностью стандартная. Ее можно написать один раз на всю жизнь. А вот новая функция в каждой программе будет разная. Поэтому ее удобно выделить. В новой функции первое, что надо сделать -- это преобразовать координаты курсора в номер кнопки. Например, вот так

def mpresp(x,y):
    k2 = int(y/108)
    k1 = int(x/192)
    n = 1+k1+k2*5
    - - -

В данном примере показан конкретный случай, когда картинка имела размер 960*648 и образовывала матрицу (6*5) кнопок размером 192*108. В другом случае надо просто поменять числа. После того, как номер кнопки определен можно делать все то, что вам необходимо по клику кнопки с этим номером. А на картинке кнопки можно рисовать бесконечным набором вариантов, как вам позволяет ваш ум, честь и совесть. Осталось только объяснить, а что будет в следующий раз, когда мы снова вызовем функцию mppro(k,fni,fs). В следующий раз она пойдет по первому сценарию при kmp = 0. И снова покажет окно, но уже с правильными размерами, а все свойства, которые мы назначили окну в первый раз, у этого окна сохраняются, и оно будет работать точно так же. Вот и все.

13. Анимация по серии картинок, то есть быстрая презентация

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

Это вполне возможно на любом языке программирования, не является исключением и Питон. Но я решил усложнить задачу. То есть сделать управляемую анимацию. В самом начале пусть будет пошаговый режим, то есть первая картинка стоит. По клику мышкой в SW (юго-запад) квадрате картинки, она меняется на один кадр. Еще один клик и еще один кадр. И так далее. Если кликнуть мышкой в NE (северо-восток) квадрате, то картинки меняются автоматически через заданный интервал времени. Это и есть анимация. Она бесконечная, то есть после окончания все начинается с начала. Если кликнуть мышкой в NW (северо-запад) квадрате, то пауза, все снова стоит и ждет. И, наконец, клик в SE (юго-восток) квадрате останавливает анимацию и убирает окно. Я просто сначала покажу полный код небольшой программы, которая это делает, а потом объясню что этот код означает. Итак, вот код

from tkinter import *
import time

def click(text):
    global wan,han
    wan = 1000
    han = 600
    anpro(0,21,'ts',10,0.2)

def getStop(event):
    global kan,kas,kao,wan,han
    x = event.x
    y = event.y
    if y < int(han/2):
        if x < int(wan/2): kas = 0
        else:
            kas = 1
            kao = 0
    else:
        if x < int(wan/2):
            kas = 1
            kao = 1
        else: kan = 0
    
def anpro(n0,n,fn,fs,ti):
    global an01,can,kan,kas,kao,kar,wan,han
    kan = 1
    kas = 1
    kao = 1
    an01.deiconify()
    an01.focus()
    if kar == 1:
        an01.geometry(str(wan+4)+'x'+str(han+4)+'+'+str(fs)+'+'+str(fs))
        an01.bind('',getStop)
        can = Canvas(an01, width=wan, height=han)
        can.pack()
        kar = 0
    i = -1
    while kan == 1:
        if kas == 1:
            i += 1
            if i == n: i = 0
            if kao == 1: kas = 0
            fni = './pic/'+fn+str(i).zfill(3)+'.png'
            an01.title(fni)
            img = PhotoImage(file=fni)
            can.create_image(0,0,anchor=NW,image=img)
        an01.update()
        time.sleep(ti)
    an01.withdraw()

kar = 1
kan = 1
kas = 1
wan = 1
han = 1
kao = 1
root = Tk()
root.geometry('+1100+600')
b1 = Button(root, text='Help', command=lambda text='Help': click(text), font=("Courier", 12, "bold"))
b1.pack()
an01 = Toplevel()
can = Canvas(an01, width=wan, height=han)
an01.withdraw()
root.mainloop()

А теперь разберем что тут написано. Начнем, как обычно, с конца. Шесть глобальных переменных, все исходно имеют значение 1. Это потому что не используем ООП, то есть собственные классы. Приходится работать с глобальными переменными. Исходно задан пошаговый режим. Затем сформировано главное окно программы с единственной кнопкой, имя которой [Help] можно заменить на любое другое. Тут все как раньше. Также нужно объявить глобальными новое окно и канву рисования в этом окне. И окно снова закрыть. Тоже как раньше. Иначе все будет работать только один раз и при повторном клике вызова функции мы ничего не получим. Функция click(text) задает размеры окна, равные размеру картинок в глобальные переменные wan, han. Размер картинок легко узнать с помощью сторонних программ и обычно они известны, так как готовятся пользователем. Важное условие -- как в кино, все картинки должны иметь одинаковые размеры. Обычно их делают с помощью программы в цикле, а если нет, что есть много редакторов картинок и все можно изменить так, чтобы размеры совпадали. Затем она вызывает новую функцию, указывая ее аргументы. А вот эта новая функция как раз и делает то, что мы сейчас обсуждаем.

Начнем с аргументов. Первый аргумент n0 -- это начальный номер картинки, затем n -- это число картинок, третий аргумент fn -- это часть имени файла с картинкой. В данной программе сделано так, что файлы с картинками находятся в папке (./pic/), имеют расширение (.png), а перед точкой номер, который начинается от n0 и имеет три разряда, причем пустые левые заменяются на 0. Полное имя файла записывается в переменную fni внутри цикла и условия. При желании это все можно изменить. Следующий аргумент fs -- это сдвиг окна от левого верхнего края экрана, снова как раньше. Наконец, последний параметр -- время показа одной картинки в секундах.

Этой информации достаточно для работы. Далее надо восстановить исходные значения глобальных переменных, так как они могли быть изменены при предыдущих вызовах. Затем открываем ранее закрытое дополнительное окно, ставим на него фокус. И тут важно учитывать -- первый ли раз мы используем функцию. Если первый, то задаем размеры, размещаем его на экране как надо, привязываем к нему событие в виде клика левой кнопки мышки с указанием функции. Это все мы уже проходили. А вот теперь новое. Нужно поставить в окно графическое полотно как раз с размерами, равными размерам картинок. И упаковать его.

Это полотно мы тоже уже ставили. его надо делать глобальным иначе оно второй раз работать не будет. Тут просто м уточняем это аргументы. Это все предварительная работа. А теперь цикл пока не изменится значение переменной kan. Счетчик картинок меняется только при условии, что переменная kas равна 1. И сами картинки тоже меняются только при этом условии. Это происходит при изменении имени файла с картинкой.

Имя файла очередной картинки формируется так, что в нем меняется номер. Это имя файла ставится в заголовок окна. Потом из этого файла вынимается картинка и ставится на графическое полотно. Тут первые два аргументы -- координаты сдвига картинки от левого верхнего угла. На это указывает параметр anchor, он равен NW а это север-запад по английски. Ну и надо указать на картинку. А вот обновление в окне надо сделать постоянно, это мы уже тоже проходили. Иначе оно не будет реагировать на клики пользователя. И задержать работу программы на некоторое время. Об этом я уже писал выше. Вот и все.

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

Но если клик сделан в SW квадрате, то просто восстанавливаются исходные значения переменных kas и kao -- это пошаговый режим, картинка меняется только один раз и надо снова кликать. Если клик сделан в NE квадрате, то это режим анимации, когда kao = 0. А клик в NW квадрате делает kas = 0 и это режим паузы. Вообще говоря, режим паузы можно было бы не делать, так как режим пошагового просмотра тоже останавливает анимацию. Но он все-таки делает один шаг, а пауза его не делает. Такая небольшая разница между ними есть. Вообще говоря, еще бы было неплохо сделать так, чтобы откатить анимацию назад. Но этого пока нет.

14. Анимация двумерного массива c помощью серии графиков

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

Второй способ состоит в том, что графики рисуются и картинки спасаются в файлы, а потом картинки показываются из файлов. Этот способ хорош тогда, когда картинки рисует чужая программа, и их просто никак нет в памяти компьютера, а только в файле. Ниже я опишу как раз второй способ, потому что его можно использовать через функцию, рассмотренную в предыдущем разделе. А здесь достаточно описать как создавать эти картинки. Итак, пусть у нас есть двумерный массив (fa) и известны пределы изменения его аргумента по оси X, как a и b. И что аргумент меняется с постоянным шагом. Я снова покажу весь код, а потом объясню, что и как он делает.

import numpy as np
import vkohnpy.stf as vkst

sfa = fa.shape
ny = sfa[0]
nx = sfa[1]
xa = np.linspace(a,b,nx)
cma = np.amax(fa)
cmi = np.amin(fa)
faa = np.ones(nx)*cma
fia = np.ones(nx)*cmi
ya = np.empty((3,nx),dtype=float)
ya[0] = fia
ya[1] = faa
lwa = [1,1,1]
ca = ['#ffffff','#ffffff','#0000ff']
for i in range(ny):
    ya[2] = fa[i]
    fn ='fn'+str(i).zfill(3)
    vkst.pffun2(xa,ya,lwa,ca,10,6,fn,0)

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

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

15. Новый уровень программирования и Анимация двумерного массива через один файл

Перед тем, как начать этот раздел, я хочу объяснить, что записываю этот сайт не как ученик по программированию на Питоне, а как дневник своей собственной работы. Я начал с нуля, потом чему-то научился и понял, что в Питоне есть ошибки, хотя бы с процедурой FFT. Потом я стал пробовать делать то, что давно умею на Джаве. Но там все сразу делается в ООП (объектно ориантированное программирование), а в Питоне можно писать большую программу вообще без классов. То есть используя готовые классы, написанные либо в самом дистрибутиве Питона, либо скачанные дополнительно.

Но при этом, если писать окна и графику через пакет Tkinter, нужно обязательно использовать глобальные переменные, иначе окна будут пустые и будет невозможно открыть дважды одно и то же окно. И чем больше окон надо открыть, тем больше этих глобальных переменных необходимо вводить. В какой-то момент начинаешь понимать, что с этими глобальными переменными необходима какая-то организация. И я придумал некий вариант такой организации. Разумеется -- это первое решение, которое пришло в голову. В будущем может быть будут и другие идеи. Но я пишу дневник, то есть то что придумал здесь и сейчас.

Итак, я решил, что функции, которые используют глобальные переменные как бы не являются совсем библиотечными. Ведь они общаются между собой и у них есть общие переменные. Они работают часто в связке, а библиотечные функции обычно полностью независимы. С другой стороны, они все же стандартные и их можно много раз использовать. По этой причине их лучше записывать в отдельный файл. Но не импортировать его, а просто прочитывать в самом начале и все. Об организации кода я писал в 3-м разделе. Можно прочитать файл в память и потом выполнить команды из памяти. Это работает.

Далее, я решил все глобальные переменные записать в список pa = [ ] и просто использовать элементы списка. В чем большой минус Питона? В том, что его автор не любит символ (;) и никак его не использует. Это как мусульмане не пьют водку, потому что Пророк не велел. Это просто смешно, что надо три буквы (a=b) писать на отдельной строке. Список позволяет записать на одной строке сразу все параметры, сколько бы их не было. Уже плюс. Также я решил использовать только целые значения. А реальные параметры просто умножать на 1000 перед записью и потом умножать на 0.001 перед испоьзованием. Этого вполне достаточно.

Так параметры легко определить в самом начале. Но их надо переопределять. Для это я сделал специальную функцию chpa(lst) аргументом которой является еще один список. В этом списке подряд идут пары чисел, первое в паре -- номер элемента списка pa, второй -- то значение на которое его надо изменить. Снова можно на одной строке изменить все параметры, которые нужно менять. Также я объединил две функции -- показа меню на картинке и анимацию -- в одну функцию анимации, в которой функция обработки клика работает по разному в зависимости от значения pa[15]. Для этого пришлось чуть переделать код, написанный в 13 разделе. А именно, заменить can.pack() на can.place(x=0,y=0).

Дело в том, что окно win тоже было глобальное до того, как я и от этого отказался. И оно помнило о тех объектах, которые в него ставили. Функция pack() правильно работает (так, как нам надо) только первый раз. А в следующий раз оно ставит объект так, как будто там все еще есть предыдущий объект, хотя был выход из функции и локальный объект автоматически уничтожен. Я долго не мог понять почему так сделано, но потом просто поставил принудительное размещение в начало и все стало нормально работать. Код функций со списком читать сложнее, чем было раньше, но это легко исправить подсказкой, которую можно записать в отдельном файле. Все равно уже написанные функции читать не надо, они работают и этого достаточно.

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

import numpy as np
import vkohnpy.stf as vkst
from tkinter import *
import time
exec(open('./proacf0.txt').read())

def click(text):
    global pa,fa
    nx = 201
    ny = 21
    a = 10.0
    xa = np.linspace(-a,a,nx)
    b = 5.0
    xca = np.linspace(-b,b,ny)
    aa = np.linspace(0.5,1.,ny)
    fa = np.empty((ny,nx),dtype=float)
    for i in range(ny):
        xta = xa-xca[i]
        fa[i] = aa[i]*np.exp(-0.2*xta*xta)
    anfa2d(-a,a,fa)

pa = [0,8,10,500,1000,600,1,1,1,1,0,0,0,0,0,1]
root = Tk()
root.geometry('+1100+600')
b1 = Button(root, text='Help', command=lambda text='Help': click(text), font=("Courier", 12, "bold"))
b1.pack()
root.focus()
root.mainloop()

Как видим, код программы стал совсем коротким. Это потому, что сначала делается импорт библиотек, а потом прочитывается файл со стандартными функциями. Этот файл можно просто скачать себе, вот ссылка . Если вам не интересно, то его даже можно вообще не смотреть. Он все делает как надо. Но если хотите чему-то научиться, то я про него тоже расскажу. А пока скажу, что в команде exec() написано так, что этот файл должен находиться в той же папке, где и программа, которая его использует. Это важно.

Теперь снова спустимся вниз. Там стоит стандартный код главного окна с одной кнопкой. И не заказано никаких внешних окон, которые тут же убирались. Вообще-то при запуске программы раньше было видно, что эти окна возникают и потом исчезают. Теперь вообще ничего нет. А функция click (text) просто вычисляет двумерный массив, который и показывается в анимационном окне через один файл. А потом вызывает стандартную функцию anfa2d(a,b,fa), у которой первые два аргумента -- это начальное и конечное значения аргумента функций, а третий аргумент -- двумерный массив многих функций от одного аргумента. Число точек на графике программа узнает из размерности это массива. В Питоне она записывается в файл.

Можно спорить что лучше -- аргументы или глобальные переменные. В моем собственном языке ACL все процедуры вызываются без аргументов, так как у меня вся память глобальная. А процедуры -- это просто кусок текста, который надо повторить много раз и всего-то. Даже скобки писать не надо. А в Питоне скобки писать надо, даже пустые. И аргументы есть, поэтому иногда их все же лучше использовать. Функция anfa2d() находится в указанном файле. У нее появились существенные изменения, а именно, новое окно оформлено как локальная переменная, сразу указывается и размер окна и его положение. Полотно рисования Canvas раньше упаковывалось, теперь принудительно ставится с самого начала. Так проще и правильнее. А далее пишется тот код, который я уже объяснял выше для рисования одной кривой на заранее заданных осях. И потом в цикле вызывается функция pffun2() из моей библиотеки, которая записывает файл, затем он прочитывается и ставится на графическое полотно.

К сожалению, моя функция использует библиотеку Matplotlib, в которой я не умею записывать график в память, только в файл. Это резко снижает скорость анимации, но все же для графиков как бы много и не надо. Хотя, конечно, бывает, что и надо. А сейчас, даже если поставить нулевую задержку (она указывается параметром pa[3] в миллисекундах), то пауза все равно есть, хоть и не очень большая. И тем не менее, теперь внешнее окно сделано почти профессионально.

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

16. Кто ищет, тот всегда найдет. О том, как обмануть Питон.

В этом разделе я буду информировать о тех нестандартных приемах программирования на Питоне, которые я сам придумал и которые активно использую. Я большой любитель интерпретируемых языков программирования. Я придумал свой язык программирования ACL такого типа и активно им пользуюсь. Также активно использую Постскрипт, Джаваскрипт тоже из этой серии. И вот теперь Питон. Но у Питона есть такие особенности, которых нет у других языков, и которые сильно портят жизнь. Я пока отмечу две такие особенности.

Неприятность номер 1

Во всех руководствах по Питону написано, что его надо скачать и потом установить. И путь (переменную PATH) не забыть прописать. Более того, если установить Питон в общую папку, то возникают проблемы с установкой дополнительных пакетов. То есть надо обязательно устанавливать в папку конкретного пользователя. Я работаю в Виндовс и речь только об этой системе. И это уже перебор. Из соображений безопасности скоро вообще запретят жить. Ведь жить вредно, от этого умирают. И как быть? Ну, против лома нет приема. Первый раз и на первый компьютер так приходится и делать. Я написал об это во второй главе. Но вот вы установили у себя интерпретатор Питона и записали дополнительные пакеты. И все отлично работает.

А вот теперь можно заниматься обманом. Скачиваем папку интерпретатора как есть на внешний винчестер (или даже на флешку). У меня она имеет название [Python38]. И все, можно работать даже с флешки на любом компьютере. Ничего и нигде больше устанавливать не надо. Потому что программа интерпретатора никак не привязана к операционной системе. Там просто записано как и что запускать, если использовать подсказки в меню по правой кнопке мыши. А это все можно организовать в командном (bat) файле. Итак, допустим вы знаете где находится папка [Python38]. Допустим, она находится на флешке (E:\) в корневой папке и на диске (C:\) другого компьютера.

Тогда надо открыть новый файл с названием (run-pro.bat) и с таким текстом
set path=E:\Python38\;%path%
python.exe ваша-программа.py
И ваша программа заработает при двух условиях (1) если файл программы находится в текущей (активной) папке (2) если программа имеет внешний вид (то есть окно). Но у меня почему-то в таком режиме программа работает, но неадекватно заканчивает. Выполнив всю работу одного прохода, она не возвращается в окно, а самоустраняется. Причину я пока не понимаю. Как раз поэтому я не пользуюсь таким способом в Питоне. Хотя на языке Джава как раз такой способ отлично работает без всяких проблем.

На Питоне надо запускать среду общения с пользователем IDLE. Для этого открываем новый файл с названием (run-idle.bat) и с таким текстом
set path=E:\Python38\;%path%
python.exe E:\Python38\\Lib\idlelib\idle.py
Здесь мы вместо собственной программы запускаем программу интерпретатора Питона. Так как активная папка наверняка другая, то надо указать полный путь к этой программе. В результате запускается интерактивная среда работы с Питоном. В ней можно сразу писать команды Питона и они сразу будут выполняться при нажатии клавиши [Enter]. А если надо запустить файл, то выбираем в меню (File\\Open), находим свой файл и он открывается на редактирование. Если редактировать не надо, то сразу нажимаем клавишу [F5] и программа запускается в работу. И в таком режиме она работает адекватно и не самоустраняется.

Это файл можно приготовить заранее и все будет работать очень четко и быстро без всякой установки и на любом компьютере. На флешке, возможно, программа будет чуть тормозить, но на другом компьютере все будет как на родном. Самое главное, что программа уже имеет сколько угодно дополнительных пакетов и ничего снова не надо скачивать и устанавливать. Фактически так программа и работала еще лет 10 назад. Просто новые версии ОС сильно зажимают пользователя и приходится заниматься обманом. Ведь все делается в угоду дураку, который будет работать на компьютере не зная и не понимая что он делает. За автомобиль без прав садиться запрещают, а за компьютер можно -- главное продать побольше. Вот и вся механика. А чтобы дурак что-нибудь не сломал, ему все запрещают.

Неприятность номер 2

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

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

Пользователь уж точно умеет читать, у нас теперь грамотность поголовная. И он вполне способен прочитать небольшую инструкцию о том что ему надо делать. А в интерпретируемом языке код программы -- это просто текст, который можно легко написать. Так почему не предложить пользователю просто написать кусок текста программы, который потом сразу будет исполняться. Без всяких изменений. Понятно, что такой кусок должен быть предельно простым, типа A=234. Тут пользователю всего-то надо поменять число. Это ничем не отличается от окна ввода с подписью.

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

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

txt = 'a=1; b=2; c=3;'
txt = txt.replace('; ','\n')
exec(txt)
pa = [a,b,c]
print(pa) 

Если прогнать такую программу, то она напечатает [1, 2, 3]. То есть переменные a, b и c получили таки свои значения, хотя они были записаны в неправильном для Питона коде. И вот как раз такой прием очень удобно использовать в тот момент, когда пользователю предлагается определить значения параметров для работы программы. Более того, если кому противно писать код в такой грамматике, которую предлагает Питон, то можно писать код в текстовую строку в любой грамматике. Потом его обработать и выполнить с помощью команды exec(txt). Это самое сильное преимущество интерпретируемых языков.

Впрочем такие же штуки можно проделывать и в компилируемых языках до процесса компиляции. Вообще обработка текстов -- это мощная техника. Только на одних заменах я сделал программы, которые превращают текст с очень простой разметкой в вэб-сайт (html+css+js) или в электронную книгу в формате fb2. Часто тексты удобно писать так, что вместо какого-то длинного термина ставить один символ из редко используемых и потом за один клик сделать замену по всему тексту. А если составить словарь, то можно много терминов так писать.

.