Это первый курс по университетской информатике: основы программирования
, преподаётся с использованием Python 3. Не требует предварительных знаний программирования. Каждая программа в
в этом раздаточном материале завершена и готова к запуску — наберите её, запустите, измените одну строку и предскажите
что изменится перед повторным запуском. Цикл «предсказание-запуск» составляет всю суть навыка.
Курс представляет собой лестницу:
Модули 1.1 и 1.3 формируют основы механики: значения и типы, выборка, итерация.
Модуль 1.4 упаковывает работу в функции.
Модули 1.5 и 1.6 добавляют стандартные коллекции и файлы с обработкой ошибок.
Модуль 1.7 — это дисциплина верха: декомпозиция задач, тестирование, отладка.
Как читать каждый модуль: короткая история объясняет, почему это важно; картинка или правило
идут до любого синтаксиса; затем точные правила; затем проработанные примеры, становящиеся всё сложнее,
каждый со следом, который можно проверить, запустив код; затем ошибки, которые люди на самом деле
допускают; затем трёхстрочное резюме. Каждый пример здесь можно проверить вторым способом —
запустив его. Чтение кода без запуска похоже на чтение нот без того, чтобы
услышать музыку.
1.1
Значения, типы и ввод/вывод
Приложение для покупок показывает "4.20" — это текст или число? Если это текст, сложение двух
цен даст "4.203.50"; если это число, оно даст 8.7. Что именно произойдёт, решает
тип, и весь модуль 1.1 посвящён тому, чтобы учиться видеть типы до того, как
вы запустите что-либо.
Значение имеет тип, а тип определяет, что с ним можно делать: int (целые
числа), float (десятичные), str (текст), bool (True/False). Функция type(...)
называет тип любого значения — задавайте её свободно во время обучения.
age = 21 # int
price = 4.20 # float
name = "Ada" # str
open_ = True # bool
print(type(age)) # <class 'int'>
Переменные — это имена, привязанные к значениям; присваивание age = 22 перепривязывает имя —
оно никогда не редактирует старое значение. Ментальная модель, которую стоит сохранить на протяжении всей учёбы:
Имена — это ярлыки на коробках; выражения читают из коробок и создают новые
Вычисление выражения — это «открыть коробки, вычислить, закрыть новую коробку»: count * 3.50
при count = 6 открываются коробка int 6 и коробка float 3.50, а Python выполняет смешивание
правило повышает разрядность int — результатом становится float21.0, никогда не int 21.
Превращение явное: int("21") является 21, но "4" + 2 — это TypeError — Python
отказывается угадывать, имели ли вы в виду "42" или 6. Выводите с помощью print(...); вводите с
input(...), который всегда возвращает строку, поэтому числа нужно преобразовывать:
Разобранное решение 1 (кассовый аппарат кафе).count = int(input("Coffees? ")), затем
print("Total:", count * 3.50). При 6 кофе этот код выведет Total: 21.0 — это .0
помечает число с плавающей точкой, потому что int * float повышает разрядность до float. Отслеживание того, какой тип получается на выходе
из выражения, составляет основную привычку unit 1.1. Проследите его по состоянию: input возвращает
строку "6"; int(...) открывает новую коробку, содержащую int 6; умножение
создаёт свежую коробку float со значением 21.0; print отображает её.
Разобранный пример 2 (классическая ошибка, исправленная). Принтер баннеров пытается сложить счётчик
со строкой:
Запустите его, и Python выдаст TypeError: can only concatenate str (not "int") to str —
правило отказывается гадать. Исправление явно указывает преобразование:
print("Sold " + str(count) + " today"), или более чистый вариант print("Sold", count, "today"), which inserts the spaces for you. The lesson to carry: + означает сложение
для чисел, конкатенацию для строк и никогда не тихое смешивание.
Типичные ошибки. Забывание о том, что input() всегда возвращает строку, поэтому
input() * 2 повторяет текст, а не удваивает число. Написание 4.20 + 6 и
ожидание ответа в формате int — любое дробное число в выражении делает результат дробным. Доверие
0.1 + 0.2 == 0.3 (это False; двоичное округление — единица 1.2 показывает исправление).
Резюме: четыре базовых типа определяют каждую операцию; имена — это метки, присваивание перепривязывает;
выражения смешивают типы с помощью приведения через числа с плавающей точкой; преобразования выполняются явно и
input() требует одного.
Турникет в парке развлечений должен взимать плату по-разному для детей, взрослых и пожилых людей — одна
программы, несколько путей. Выбор — это способ, которым программа принимает решения: if, elif,
else выполняет блок только при выполнении условия.
Возраст 70, студент: первый истинный ветвь определяет цену
Условием является любое логическое выражение; сравнения (==, !=, <, >=) и
логика (and, or, not) его формируют. Два правила делают каждое решение читаемым:
Ветви проверяются сверху вниз; первая истинная ветвь выигрывает, остальные
пропускаются.
Отступы — это не просто оформление — они И есть блок. Один лишний пробел меняет, какие
строки относятся к ветви.
temp = 37.5
if temp >= 38:
print("Fever")
elif temp >= 36 and temp < 38:
print("Normal")
else:
print("Low")
Разобранный пример 1 (цена за вход в кино). При 12 или 65+ плата составляет 5, студенты платят 7,
всем остальным 10:
age = int(input("Age? "))
student = input("Student? y/n ") == "y"
if age < 12 or age >= 65:
price = 5
elif student:
price = 7
else:
price = 10
print("Pay", price)
Порядок имеет значение: побеждает первая истинная ветвь, поэтому проверка возраста предшествует
проверке студента — 70-летний студент должен платить 5, и именно такой порядок это обеспечивает. Запустите
код со значениями 70 и y, чтобы увидеть, что срабатывает левый путь на блок-схеме.
Разбор примера 2 (диапазоны оценок, оптимизированный). Тот же каркас, но теперь в виде цепочки
условий, где каждое elif требует всего одного сравнения, поскольку предыдущие проверки уже
не прошли:
score = int(input("Score? "))
if score >= 80:
band = "A"
elif score >= 70:
band = "B"
elif score >= 60:
band = "C"
else:
band = "F"
print("Band:", band)
На уровне 79 первое условие не выполняется, второе выполняется: результат Band: B. Переход к
заметке: для проверки elif score >= 70 также не нужно проверять and score < 80 — достижение этой
строки уже доказывает, что балл ниже 80. Доверие механизму последовательного выполнения (fall-through) в этом случае
делает цепочки короткими; повторная проверка всех условий — это более длинная и неверная версия для новичков.
версия.
Сравнение чисел с плавающей точкой требует осторожности: 0.1 + 0.2 == 0.3 может быть равно False из-за округления в двоичной системе
округлению), поэтому сравнивайте с допустимой погрешностью: abs(x - y) < 1e-9.
Распространенные ошибки. Написание = там, где подразумевалось == — классическая ошибка, и Python
запрещает её в условиях (SyntaxError) именно потому, что она встречается слишком часто. Проверка
частного случая после общего, из-за чего общее условие всегда выигрывает. Использование
elif с условием, которое никогда не будет достигнуто, так как более ранняя ветвь уже
обработала этот случай.
Резюме: условия выбирают пути по принципу «первая истинная побеждает», отступ определяет блок кода; порядок
тесты проверяют от частного к общему; числа с плавающей точкой сравниваются через допустимую погрешность, никогда не по ==.
1.3
Итерация: повторение действия
Обработка 5,000 счетов вручную — это работа на всю жизнь; с помощью цикла это программа. Итерация
повторяет работу: цикл for проходит по известной последовательности, а цикл while выполняется, пока выполняется
условие. Главный вопрос при проектировании всегда таков: знаете ли вы точное количество шагов или же
условие определяет завершение?
total = 0
for i in range(1, 101): # 1..100 — range stops BEFORE its end
total += i
print(total) # 5050
Используйте for, когда количество шагов известно; используйте while, когда условие решает об остановке. Для каждого
while требуются три элемента, видимые в коде: начальное значение переменной, само условие и
действие внутри цикла, которое приближает к выходу из него — иначе это бесконечный цикл.
цикл**.
Таблица трассировки для факториального цикла
n = 5
factorial = 1
while n > 1:
factorial *= n
n -= 1 # the step toward exiting
print(factorial) # 120
Таблица трассировки — это обязательная практика курса для отладки циклов: прослеживайте
переменные по кругу за каждую итерацию, сначала на бумаге. Для факториального цикла:
(n=5, f=1) → (4, 5) → (3, 20) → (2, 60) → (1, 120). Составьте таблицу вручную, затем
добавьте print(n, factorial) внутрь цикла и позвольте Python проверить вас.
Разобранное решение 2 (поиск с использованием break, усложнённый вариант). Найдите первое число, кратное
7, которое больше 100 — количество шагов неизвестно, условие определяет завершение:
n = 100
while True:
if n % 7 == 0:
break # found it — leave the loop now
n += 1
print(n) # 105
Трассировка короткая: 100, 101, …, 104 все проходят проверку; 105 проходит, а break
прерывается. Оператор break досрочно покидает цикл; оператор continue пропускает текущую итерацию и переходит к следующей. Оба являются
средства читаемости кода — цикл, который должен сканировать данные, пока что-то не найдёт, затем остановится, читает
лучше всего с break. Дальнейшее наращивание, удвоение денег: под ставку 5% годовых, сколько времени
берёт ли 1000 для достижения 2000?
balance = 1000.0
years = 0
while balance < 2000.0: # the condition itself is the goal
balance *= 1.05
years += 1
print(years, round(balance, 2)) # 15 2078.93
Пятнадцать лет, и цикл останавливается в момент изменения условия — таблица трассировки
показала бы пересечение баланса через отметку на 2078.93, а не точное попадание на 2000.
Распространённые ошибки. Изменение списка (модуль 1.5) или переменной цикла, по которой вы
for-итерируете. range(1, 100) при условии 100 — конец не включён.
Цикл while без шага выхода или с закомментированным шагом: условие никогда не меняется,
и программа зависает.
Резюме: for для известного количества циклов, while для условий; каждый цикл имеет начало,
условие и шаг; трассировка на бумаге, затем вывод; break и continue для
чистого раннего выхода.
1.4
Функции: определение задачи одним именем
Одна и та же расчётная формула цены встречается в пяти местах; правило налога меняется; четыре из
пяти обновляются. Функция упаковывает задачу под имя, чтобы она была написана
один раз и вызывалась везде: параметры на входе, возвращаемое значение на выходе. def
определяет; вызов выполняет.
def total_price(count, unit=3.50):
"""Return the total for count items at unit price."""
return count * unit
print(total_price(6)) # 21.0
print(total_price(6, unit=4.0)) # 24.0
Определение задаёт параметр (count, unit); вызов передаёт
аргумент (6). Значение по умолчанию (unit=3.50) делает аргумент необязательным.
return возвращает значение и завершает вызов; вывод на экран — это не возврат: функция без
return возвращает None, а вызывающий код, пытающийся выполнить над ним арифметику,
получает ошибку TypeError.
Вложенные вызовы: вызывающая функция приостанавливается до получения значения обратно
Каждый вызов получает собственную локальную область видимости: имена внутри неё невидимы
снаружи и исчезают после завершения вызова. Трассировка вложенных вызовов выглядит так:
вызывающий приостанавливается → вызываемый работает → возвращаемое значение попадает к вызывающему.
Разобранный пример 1 (заказ с учётом комиссии).
def add_fee(amount):
"""Return amount plus a fixed 3 pence fee."""
return amount + 3
def order_total(unit_pence, quantity):
"""Return the order total: subtotal plus the fixed fee."""
return add_fee(unit_pence * quantity)
print(order_total(250, 4)) # 1003
Контракт в docstring имеет значение: целые пенни на входе, целые пенни на выходе. Трассировка
order_total(250, 4): subtotal 1000, add_fee возвращает 1003 — и кадр
диаграмма выше показывает паузу и передачу управления.
Разобранный пример 2 (реальный алгоритм внутри небольшой функции, расширенный).is_prime(n) возвращает True, если ничего из 2 до $\sqrt{n}$ не делит n:
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n: # only test up to the square root
if n % d == 0:
return False # found a divisor — answer known, stop
d += 1
return True
print([p for p in range(2, 30) if is_prime(p)])
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Здесь встречаются две особенности работы функций: ранний return return завершает вызов, как только ответ
становится известен (переменная-флаг не нужна), а граница цикла d * d <= n берётся из
вывода — если для $n = a \times b$ подходит $a \le b$, то и для $a \le \sqrt{n}$, поэтому проверка за пределами $\sqrt{n}$
проверяет делители, которые уже были проверены. Запустите код: список содержит ровно простые числа меньше 30.
Типичные ошибки. Печать результата внутри функции вместо его возврата, из-за чего результат
нельзя использовать повторно. Забытый return return на одной из ветвей — этот путь молча возвращает
None. Попытка доступа к локальным переменным функции снаружи; они недоступны после завершения
вызова.
Резюме: return def один раз, вызовите где угодно; параметры поступают, одно значение выходит через return return;
ранний возврат завершает вызов; локальные переменные существуют и уничтожаются вместе с вызовом; строки документации описывают
контракт.
Касса кафе требует заказы дня в правильном порядке, меню по названиям, местоположение, которое
не должно смещаться, и уникальные напитки сегодняшнего дня. Четыре задачи, четыре контейнера — выбор
среди них есть архитектурное решение — единица измерения 1.5.
Список — это упорядоченная, изменяемая последовательность: marks = [72, 85, 61], нумерованная
начиная с 0, срезаемая через marks[1:3]. Словарь связывает ключи со значениями:
price = {"flat white": 3.5, "tea": 2.8}, читаемый и записываемый по ключу. Кортеж
это фиксированная последовательность (53.1, -1.5) — подходит для координат, которые не должны изменяться. A
множество хранит уникальные элементы без порядка: {"a", "b"}.
marks = [72, 85, 61]
marks.append(90) # [72, 85, 61, 90]
best = max(marks) # 90
average = sum(marks) / len(marks) # 77.0
Алиасинг: два имени, один список
Присваивание не копирует: a = [1]; b = a; b.append(2) делает a[1, 2] — список
является общим, имена — нет. Этот алиасинг — снова коробки из модуля 1.1, теперь
с двумя тегами в одной строке, а также объясняет следующее правило: мутация списка во время
прохода по нему пропускает элементы — пройдите по копии (for m in marks[:]) или создайте
новый список.
Метод словаря .get(key, 0) возвращает 0 при первом появлении — этот приём
делает подсчёт однострочником. Проверьте вручную: слово "tea" встречается три раза в orders,
и напечатанный счёт это подтверждает.
Разобранный пример 2 (лучший хит дня, усложнённый). Опираясь на подсчёт,
укажите самый частый элемент и его количество:
best = max(tally, key=tally.get)
print(best, tally[best]) # tea 3
Аргумент key=tally.get заставляет max сравнивать элементы по их частоте появления, а не
по алфавиту — тот же приём key= сортирует любую коллекцию по любому критерию. Усложнение важно: реальные программы цепляют эти инструменты вместе (подсчёт, затем ранжирование), вместо того чтобы
подчеркивает важность последовательности операций: реальные программы цепляют эти инструменты вместе (сначала подсчёт, затем ранжирование), а не
сканируют orders дважды.
Выбор типа коллекции — это архитектурное решение: порядок + дубликаты → список; поиск по имени → словарь; фиксированная группировка → кортеж; уникальность → множество.
имя → словарь; фиксированная группировка → кортеж; уникальность → множество.
Предположение, что tally[item] += 1 копирует список; на самом деле он создаёт ссылку на него.
(KeyError) — .get является исправлением. Ожидание того, что множество или словарь сохранят порядок.
делает подсчёт безопасным; b = a превращает ранжирование в однострочник.
Резюме: четыре контейнера для четырех задач; назначение делит, никогда не копирует; .get
делает подсчет безопасным; key= делает ранжирование однострочником.
Кафе закрывается, свет гаснет — а утренний подсчет выживает только если
программа записала его на диск. Файл существует дольше программы. with open(path) as f: opens, closes on exit, and reads text; open(path, "w") записывает
(и сначала очищает — классическая ошибка).
with open("log.txt", "w") as f:
f.write("opened\n")
with open("log.txt") as f:
for line in f:
print(line.rstrip()) # rstrip removes the trailing newline
Блок with — это блок безопасности: что бы ни произошло внутри него, файл будет закрыт.
Разобранный пример .rstrip() (отсутствующий файл настроек). Чтение размера текста из файла настроек, который может отсутствовать или содержать не целое число:
Ошибки — это значения, которые Python выбрасывает как исключения: FileNotFoundError,
ValueError, KeyError, TypeError. try/except перехватывает именованный тип — никогда не
голый except:, который скрывает реальные ошибки.
Разобранный пример 1 (отсутствующий файл настроек). Чтение размера текста из файла
настроек, который может отсутствовать или не содержать целое число:
try:
with open("settings.txt") as f:
size = int(f.read().strip())
except FileNotFoundError:
size = 12 # no file yet: a sensible default
except ValueError:
size = 12 # the file holds "big", not a number
print("Text size", size)
Запустите его три раза — со 16 в файле, без файла и со big в
файле — и он каждый раз выводит корректный размер из трёх разных путей. Это и есть форма надёжного кода: одна разумная
результат, несколько дорог к нему. Обрабатывайте ожидаемую ошибку там, где вы можете отреагировать на
нее; позвольте остальным аварийно завершить работу.
**Разбор примера 2 (надежный цикл ввода, усложненный). Спрашивайте до тех пор, пока пользователь не введет
целое число:
Передайте ему x, затем 4: первый проход вызывает исключение, перехватывается, выводит сообщение, а
while True запрашивает снова; второй проход успешно выполняет int(...), и return выходит
из цикла и функции за один шаг. Здесь объединяются три элемента: цикл (1.3),
контракт функции (1.4) и исключение (1.6) — именно в этом заключается суть
усложнения.
Типичные ошибки.open(path, "w") на неверном пути: файл создается или
очищается еще до того, как вы заметите проблему. Перехватывать Exception слишком широко и продолжать выполнение с
состояние. Забывание .rstrip(), поэтому вывод отображается с двойными пробелами.
Краткое содержание: with open(...) для чтения и письма; "w" очищает первым;
исключения — это значения; ловите ожидаемый тип по имени, делайте разумный откат, позволяйте
остальному упасть.
Две команды пишут одно приложение; одна выпускает его за неделю, другая тонет месяц. Разница
не в скорости печати — дело в том, что код быстрой команды состоит из небольших
кусков, каждый из которых можно проверить на корректность отдельно, тогда как медленная команда одновременно отлаживает всё приложение.
Юнит-1.7 — это дисциплина.
Декомпозиция разбивает задачу на функции по одной задаче каждая — каждая небольшая
достаточно для отдельного тестирования. Тестирование сравнивает фактический выход функции с
ожидаемым, на границах: ноль, один, пустой, отрицательный, огромный.
Юнит-assert, который никогда не падает, — это успешный тест. Пишите тесты до написания
тела, когда возможно — они являются контрактом, переведенным в исполняемый код, а цикл выше
(красный → зеленый → рефакторинг) — это привычка, работающая непрерывно.
Разобранный пример 1 (расширение тестов вместе с функцией). Расширьте order_total с помощью
10% скидки сверх £10 — тест пишется сначала, на новой границе:
Проверьте средний тест вручную: subtotal 1003, скидка в десять процентов дает 902.7, и
int(...) округляется вниз до 902. Обратите внимание на первый assert также: при 200 пенни и четырёх
чашках общая сумма остаётся 803 — граничные тесты доказывают, что скидка применяется
и что она не срабатывает преждевременно.
Разобранный пример 2 (бисекция бага с помощью print, усложнённая). Подсчёт из 1.5
выводит {'tea': 3}, но строка бестселлера печатает неправильный напиток. Метод:
(1) воспроизвести с наименьшим входящим, вызывающим ошибку; (2) распечатать значения на границе
между доверенной частью и подозрительной частью — здесь, подсчёт перед max;
(3) исправить одну вещь; (4) перезапустить тесты. Перемещение print посередине кода на каждом запуске сокращает область поиска вдвое: четыре print находят баг в программе из шестнадцати строк, однако
каждый запуск сокращает поиск вдвое: четыре вывода находят баг в программе из шестнадцати строк, однако
хорошо бы он ни скрывался.
Отладка — это поиск различия между тем, что вы считаете, делает код, и тем, что он делает на самом деле; print на границе доверия/подозрения является инструментом для этого.
и то, что он делает — вывод на границе доверия/подозрения является инструментом.
Типичные ошибки. Тестировать только «счастливый путь»: именно на границах (ноль, один, пустое, отрицательное) программы ломаются. Писать неделю кода до первого теста,
отрицательные) — это места, где программы ломаются. Писать неделю кода до первого теста,
затем отлаживать гору. Исправлять две вещи одновременно, так что ни один результат не имеет смысла.
чем-либо.
Резюме: декомпозируйте до тех пор, пока каждый компонент тестируется отдельно; assert-ируйте границы сразу же после создания функции; пишите тест заранее, когда это возможно; отлаживайте методом бисекции с print на границах доверия — это навык, на котором строится вся программа обучения.
функция существует; пишите тест первым, когда это возможно; отлаживайте бисекцией с помощью print
на границах доверия — это навык, на котором строится вся учебная программа.
Интерактивные уроки по этой теме
Пройдите его шаг за шагом с упражнениями мгновенной проверки.