Kodomo

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

Лог №3

Из прошлого ДЗ, задача 2

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

Для каждого человека мы хотим написать одну строку, значит тут речь может идти только об одном цикле. Из имени узнать номер позиции в списке мы не можем1. Значит единственный вариант – это получать элементы из номера списка.

Значит идём циклом по номерам элементов одного из списков и надеемся, что второй список будет действительно такой же длины, как и первый:

   1 names = [" аНЯ", "боРИС", "  ВАся", "гЕна"]
   2 last_names = ["Борисова", "Васильев",
   3               "Геннадиев", "Денисовский"]
   4 
   5 for n in range(len(names)):
   6     name = names[n]
   7     name_cleared = name.strip()
   8     name_capitalized = name_cleared.capitalize()
   9     print(name_capitalized, last_names[n])

Простые преобразования строк .strip() и .capitalize() помогут нам очистить строки и привести всё к нужному виду.

Вот эти две записи эквивалентны и отличаются только тем, что мы подставили вместо имён переменных их значения:

Раз:

   1     name = names[n]
   2     name_cleared = name.strip()
   3     name_capitalized = name_cleared.capitalize()
   4     print(name_capitalized, last_names[n])

Два:

   1     print(names[n].strip().capitalize(), last_names[n])

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

Из прошлого ДЗ, задача 3

Задание: дано число n, посчитать его факториал.

Задачу можно решать двумя подходами: инструментальным и алгоритмическим.

Инструментальный подход состоит в том, чтобы найти инструмент, который делает решение этой задачи как можно более простым:

   1 import math
   2 print(math.factorial(5))

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

Наиболее очевидное решение2 состоит в том, чтобы сначала положить в какую-нибудь переменную 1, а потом для каждого n из нужного нам диапазона домножать значение переменной на n и класть результат обратно:

   1 factorial = 1
   2 for n in range(5):
   3     factorial = factorial * (n+1)
   4 print(factorial)

Маленькая тонкость: 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 root = tk.Tk()
   7 canvas = tk.Canvas(root, width=500, height=500, background='cyan')
   8 canvas.pack()
   9 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 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 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 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 решение, если оно существует, лежит в этом диапазоне), и для каждого из них проверить гипотезу.

   1 import math
   2 a = 100
   3 for b in range(a):
   4   print('for b =', b, ' answer is ', (int(math.e ** b - b) == a))

Но это не интересно. Мы хотим просто получить ответ. Поэтому нам нужно сказать питону: если увидел подходящее b, напечатай его:

   1 import math
   2 a = 20
   3 for b in range(a):
   4   if int(math.e ** b - b) == a:
   5     print('for b =', b, ' [e^b - b] == ', a)

Но тут ещё одна печаль. Если мы не нашли подоходящего b, неплохо бы сообщить об этом.

Такого эффекта добиться чуть сложнее. Для этого нам нужно где-то хранить ответ на вопрос: нашли ли мы уже нужное b? И после того, как мы перепроверили все b, если мы так ничего и не нашли, то нужно выдать сообщение об этом.

Разумеется, вначале, до того, как мы начали искать, правда состоит в том, что мы ещё ничего не нашли.

   1 import math
   2 a = 200
   3 found_b = False
   4 for b in range(a):
   5   if int(math.e ** b - b) == a:
   6     print('for b =', b, ' [e^b - b] == ', a)
   7     found_b = True
   8 
   9 if not found_b:
  10   print('Found no such b')

Осталось привести сообщения к симпатичному виду, а для этого ещё раз вспомнить метод .format() у строк, здесь для него самая работка:

   1 import math
   2 a = 17
   3 found_something = False
   4 for b in range(a):
   5     if int(math.e ** b - b) == a:
   6         text = "Found b = {0} such that [e^{0}-{0}] = {1}"
   7         print(text.format(b, a))
   8         found_something = True
   9 if not found_something:
  10     print("Could not find suitable b")

Вся правда про if

У конструкции if есть несколько форм.

Вначале обязательно идёт:

   1 if условие:
   2   тело

Условие – это всё, что угодно, что мы можем дать в качестве аргумента bool(). Например:

Соответственно, мы можем писать:

   1 if x:
   2   print(x, "is not empty")

При этом подразумевается некое условное-обобщённое понятие пустоты переменной. Учитывайте, что если вы так делаете, а у вас переменная может иметь совсем разные значения (значения разных типов), то вы имеете все шансы сделать ошибку.

На днях я долго выяснял, почему в выдаче программы (которая считает расстояния между частями клетки на снимке с микроскопа) слишком много -1. Обнаружил, что у меня в переменную result попадало либо расстояние (число), либо None, если расстояние слишком большое и его считать слишком долго.

В соответствюущем месте в программе оказался код:

   1 if not result:
   2   result = -1 # чтобы в итоговой таблице не встречалось слово None, которое зря мозолит глаза
   3 print(..., result, ...)

...

Следующая форма конструкции if:

   1 if условие:
   2   тело # выполнится, если условие удовлетворено
   3 else:
   4   тело # выполнится, если условие не удовлетворено

Наконец, мы можем после if прилеплять сколько угодно блоков elif:

   1 if условие:
   2   тело # выполнится, если первое условие верно
   3 elif условие:
   4   тело # выполнится, если первое условие не верно, но верно второе
   5 elif условие:
   6   тело # выполнится, если первые два условия не верны, но верно третье
   7 elif условие:
   8   тело # выполнится, если первые три условия не верны, но верно четвёртое

И сюда же можно добавить в конец else, который будет описывать, что делать, если не подошло ни одно условие.

Типичное применение: когда мы накладываем на данные кучу мерил и ищем то, которое окажется применимо.

   1 if '0' in word:
   2   print('number ' + word)
   3 elif ' ' in word:
   4   print('words ' + word)
   5 elif 'a' in word:
   6   print('letters ' + word)
   7 else:
   8   print('random-stuff ' + word)

NB. Это пример про if, а не про то, как проверять слово на содержание в нём цифр. Содержание цифр в слове можно в питоне проверять гораздо лучше, мы этим займёмся как только возьмёмся за работу с файлами.

NB2. Эта конструкция абсолютно идентична вот такой вот уродливой конструкции:

   1 if '0' in word:
   2   print('number ' + word)
   3 else:
   4   if ' ' in word:
   5     print('words ' + word)
   6   else:
   7     if 'a' in word:
   8       print('letters ' + word)
   9     else:
  10       print('random-stuff ' + word)

NB3. Эта конструкция делает совсем другое, чем такая:

   1 if '0' in word:
   2   print('number ' + word)
   3 if ' ' in word:
   4   print('words ' + word)
   5 if 'a' in word:
   6   print('letters ' + word)
   7 else: # относится только к проверке 'a' in word
   8   print('random-stuff ' + word)

Первый пример про проверки пишет всегда ровно один из вариантов: первый из тех, для которых условие выполнится.

Второй третий пример про проверки может выдать от одной до трёх строк: выдаст или не выдаст первый, вне зависимости от него выдаст или не выдаст второй, вне зависимости от них (и в зависимости только от наличия буквы 'a' в слове) выдаст либо третью, либо четвёртую строку.

  1. Мы можем вспомнить, что у списков есть метод .find(item), который возвращает нам номер элемента по тексту, но, однако, если в списке два человека с одним именем, то .find() вернёт нам оба раза номер превого элемента, и в один из разов это точно будет неправильный ответ. (1)

  2. Есть несколько задач, которые составляют собой особый вид спорта по написанию как можно большего количества как можно более разнообразных решений. Факториал и числа Фибоначчи в их числе (2)

  3. В жизни программисты часто делают сильный перекос в сторону одного из подхода. Автор настоящего лирического отступления грешен большими загибами в инструментальную степь. (3)