Лог №3
Из прошлого ДЗ, задача 2
Задача: в двух списках разные части имени школьников, при этом ещё и с разными искажениями: лишние пробелы и буквы в неправильном регистре.
Для каждого человека мы хотим написать одну строку, значит тут речь может идти только об одном цикле. Из имени узнать номер позиции в списке мы не можем1. Значит единственный вариант – это получать элементы из номера списка.
Значит идём циклом по номерам элементов одного из списков и надеемся, что второй список будет действительно такой же длины, как и первый:
Простые преобразования строк .strip() и .capitalize() помогут нам очистить строки и привести всё к нужному виду.
Вот эти две записи эквивалентны и отличаются только тем, что мы подставили вместо имён переменных их значения:
Раз:
Два:
1 print(names[n].strip().capitalize(), last_names[n])
Выбирайте из них ту, которая вам удобнее. Важно, чтобы вы её могли хорошо понять. (Вероятно, со временем, с привычкой, вы станете предпочитать более короткую запись, так как её смысл будет проще воспринимать сразу как цельный. А поначалу, наверное, проще воспринимается первая запись, так как она разделяет непривычное целое на простые части).
Из прошлого ДЗ, задача 3
Задание: дано число n, посчитать его факториал.
Задачу можно решать двумя подходами: инструментальным и алгоритмическим.
Инструментальный подход состоит в том, чтобы найти инструмент, который делает решение этой задачи как можно более простым:
Алгоритмический подход состоит в том, чтобы пользуясь известными уже инструментами сочинить алгоритм, который приведёт к решению задачи.
Наиболее очевидное решение2 состоит в том, чтобы сначала положить в какую-нибудь переменную 1, а потом для каждого n из нужного нам диапазона домножать значение переменной на n и класть результат обратно:
Маленькая тонкость: range(5) возвращает нам числа 0,1,2,3,4, а нам нужны числа 1,2,3,4,5. Есть два (и более) варианта борьбы с этой незадачей: либо заметить, что вторая последовательность отличается от первой прибавлением единицы (что мы и сделали в примере), либо вспомнить, что у функции range() есть от одного до трёх аргументов, и в частности, мы можем написать range(1, 6), который вернёт в точности нужный нам диапазон.
Лирическое отступление про алгоритмические и инструментальные методы
В решении настоящих задач в жизни всегда приходится соблюдать какой-то баланс между инструментальным и алгоритмическим подходом.
Часто инструментов, решающих почти нужную задачу, находятся большие горы, и много времени можно потратить только на их изучение. (Надо заметить, что это время будет потрачено отнюдь не впустую). И ещё чаще оказывается, что все инструменты делают всё криво именно в том аспекте, который нужен вам, либо хотя бы все кривы по-своему и не делают то, что нужно. (Это закон Мэрфи, а, точнее, свойство человеческого восприятия мира).
С другой стороны, есть некоторый потолок уровня сложности, которую способен осознавать человеческий мозг. Этот потолок можно и нужно отодвигать подальше – и в этом как раз идеально помогают алгоритмические (и вообще всякие олимпиадные) задачи. Но потолок всё равно где-то есть. И, хуже того, есть потолок сложности восприятия у окружающих, который, обычно, сильно ниже. Поэтому решать сложные задачи с нуля только алгоритмическими способами тоже зачастую неправильно. (Если ваша цель сделать что-то полезное, а не потренировать мозги. Тренировать мозги – дело святое!)
Поэтому правильный инженер (в очень широком смысле этого слова) всегда начинает с того, что выбирает наиболее удобные и достаточно близко подходящие к нужной цели инструменты, а затем из них складывает алгоритм, который при этом оказывается достаточно простым.3
Из прошлого ДЗ. Задача 9
Задание: написать программу, которая рисует в произвольном месте экрана радугу.
Программа будет графической. Значит, семь строчек мы знаем сразу:
Вспоминаем, что радуга состоит из цветов и находится в случайном месте:
1 import tkinter as tk
2
3 width = 500
4 height = 500
5
6 colors = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"]
7 x = random.randint(0, width)
8 y = random.randint(0, height)
9
10 root = tk.Tk()
11 canvas = tk.Canvas(root, width=500, height=500, background='cyan')
12 canvas.pack()
13 root.mainloop()
Осталось придумать, как рисовать радугу.
Снова дилемма между инструментальным и алгоритмическим путём:
можно поискать в документации tkinter и найти там штуку, которая рисует фрагмент круга. А потом, удручившись содержимым документации, поискать примеров
- а можно просто понарисовать друг поверх друга кучу кругов, а потом сверху нарисовать прямоугольник, которым закрасить всё лишнее
Мне более улыбается второй путь.
Для очередного круга нам нужно одновременно держать в руках цвет и радиус, при этом радиус есть номер цвета. Значит, проще всего пройти по номерам цветов, а потом взять в руки цвет:
1 import tkinter as tk
2
3 width = 500
4 height = 500
5
6 colors = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"]
7 x = random.randint(0, width)
8 y = random.randint(0, height)
9
10 root = tk.Tk()
11 canvas = tk.Canvas(root, width=500, height=500, background='cyan')
12 canvas.pack()
13
14 for n in range(len(colors)):
15 color = colors[n]
16 canvas.create_oval(
17 x - radius*n, y - radius*n, x + radius*n, y + radius*n, fill=color)
18
19 root.mainloop()
Упс, ничего не видно. Это потому, что мы рисуем от маленького к большому, и, потому, последний круг затирает все предшествующие.
Значит нам нужно идти по номерам в обратном порядке. Мы это можем сделать по-разному:
вспомнить, что у range есть куча аргументов: range(len(colors)-1, -1, -1)
превратить результат range в список и развернуть его:
color_numbers = list(range(len(colors)))
... in color_numbers[::-1]
или обнаружить, что в питоне сразу доступна функция reversed(), которая возвращает список задом наперёд (точнее, функция возвращает итератор – напоминаю – это такая штука, которая ведёт себя в точности так же, как список, если её запихивать в for или list, а остальные предназначения списков выполнять не умеет; зато не хранит ничего в памяти)
1 import tkinter as tk
2
3 width = 500
4 height = 500
5
6 colors = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"]
7 x = random.randint(0, width)
8 y = random.randint(0, height)
9
10 root = tk.Tk()
11 canvas = tk.Canvas(root, width=500, height=500, background='cyan')
12 canvas.pack()
13
14 for n in reversed(range(len(colors))):
15 color = colors[n]
16 canvas.create_oval(
17 x - radius*n, y - radius*n, x + radius*n, y + radius*n, fill=color)
18
19 root.mainloop()
Осталось закрасить нижнюю половину круга прямоугольником:
1 import tkinter as tk
2
3 width = 500
4 height = 500
5
6 colors = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"]
7 x = random.randint(0, width)
8 y = random.randint(0, height)
9
10 root = tk.Tk()
11 canvas = tk.Canvas(root, width=500, height=500, background='cyan')
12 canvas.pack()
13
14 for n in reversed(range(len(colors))):
15 color = colors[n]
16 canvas.create_oval(
17 x - radius*n, y - radius*n, x + radius*n, y + radius*n, fill=color)
18
19 canvas.create_rectangle(x - radius*n, y, x + radius*n, y + radius*n,
20 fill='cyan')
21
22 root.mainloop()
и обнаружить, что мы перечислили цвета задом-наперёд:
1 import tkinter as tk
2 import random
3
4 width = 500
5 height = 500
6
7 colors = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"]
8 colors = list(reversed(colors)) # мы пользуемся длиной colors ниже, поэтому
9 # его нужно превратить из итератора в список
10 x = random.randint(0, width)
11 y = random.randint(0, height)
12 rainbow = 20
13
14 root = tk.Tk()
15 canvas = tk.Canvas(root, width=width, height=height,
16 background="cyan")
17 canvas.pack()
18
19 for n in range(len(colors) - 1, 0, -1):
20 canvas.create_oval(x-n*rainbow,y-n*rainbow,
21 x+n*rainbow,y+n*rainbow,
22 fill=colors[n], outline=colors[n])
23 canvas.create_rectangle(
24 x-len(colors)*rainbow, y,
25 x+len(colors)*rainbow, y+len(colors)*rainbow,
26 fill="cyan", outline="cyan")
27
28 root.mainloop()
Из прошлого ДЗ. Задача 5. Конструкция if
Задание: дано целое число a, найти такое целое число b, что [e^b-b] = a
Эту задачу очень трудно решить аналитически, однако очень просто перебрать все числа, скажем, от 0 до a (нетрудно показать, что для a > e/2 решение, если оно существует, лежит в этом диапазоне), и для каждого из них проверить гипотезу.
Но это не интересно. Мы хотим просто получить ответ. Поэтому нам нужно сказать питону: если увидел подходящее b, напечатай его:
Но тут ещё одна печаль. Если мы не нашли подоходящего b, неплохо бы сообщить об этом.
Такого эффекта добиться чуть сложнее. Для этого нам нужно где-то хранить ответ на вопрос: нашли ли мы уже нужное b? И после того, как мы перепроверили все b, если мы так ничего и не нашли, то нужно выдать сообщение об этом.
Разумеется, вначале, до того, как мы начали искать, правда состоит в том, что мы ещё ничего не нашли.
Осталось привести сообщения к симпатичному виду, а для этого ещё раз вспомнить метод .format() у строк, здесь для него самая работка:
Вся правда про if
У конструкции if есть несколько форм.
Вначале обязательно идёт:
Условие – это всё, что угодно, что мы можем дать в качестве аргумента bool(). Например:
- 0 это False
- [] это False
- [1] это True
- "eh" это True
- "" это False
- "False" это True (это же непустая строка!)
- [False] это True (это же непустой список!)
- None это False
Соответственно, мы можем писать:
При этом подразумевается некое условное-обобщённое понятие пустоты переменной. Учитывайте, что если вы так делаете, а у вас переменная может иметь совсем разные значения (значения разных типов), то вы имеете все шансы сделать ошибку.
На днях я долго выяснял, почему в выдаче программы (которая считает расстояния между частями клетки на снимке с микроскопа) слишком много -1. Обнаружил, что у меня в переменную result попадало либо расстояние (число), либо None, если расстояние слишком большое и его считать слишком долго.
В соответствюущем месте в программе оказался код:
...
Следующая форма конструкции if:
Наконец, мы можем после if прилеплять сколько угодно блоков elif:
1 if условие:
2 тело # выполнится, если первое условие верно
3 elif условие:
4 тело # выполнится, если первое условие не верно, но верно второе
5 elif условие:
6 тело # выполнится, если первые два условия не верны, но верно третье
7 elif условие:
8 тело # выполнится, если первые три условия не верны, но верно четвёртое
И сюда же можно добавить в конец else, который будет описывать, что делать, если не подошло ни одно условие.
Типичное применение: когда мы накладываем на данные кучу мерил и ищем то, которое окажется применимо.
NB. Это пример про if, а не про то, как проверять слово на содержание в нём цифр. Содержание цифр в слове можно в питоне проверять гораздо лучше, мы этим займёмся как только возьмёмся за работу с файлами.
NB2. Эта конструкция абсолютно идентична вот такой вот уродливой конструкции:
NB3. Эта конструкция делает совсем другое, чем такая:
Первый пример про проверки пишет всегда ровно один из вариантов: первый из тех, для которых условие выполнится.
Второй третий пример про проверки может выдать от одной до трёх строк: выдаст или не выдаст первый, вне зависимости от него выдаст или не выдаст второй, вне зависимости от них (и в зависимости только от наличия буквы 'a' в слове) выдаст либо третью, либо четвёртую строку.
Мы можем вспомнить, что у списков есть метод .find(item), который возвращает нам номер элемента по тексту, но, однако, если в списке два человека с одним именем, то .find() вернёт нам оба раза номер превого элемента, и в один из разов это точно будет неправильный ответ. (1)
Есть несколько задач, которые составляют собой особый вид спорта по написанию как можно большего количества как можно более разнообразных решений. Факториал и числа Фибоначчи в их числе (2)
В жизни программисты часто делают сильный перекос в сторону одного из подхода. Автор настоящего лирического отступления грешен большими загибами в инструментальную степь. (3)