Каппес Даниил Олегович

ПРАКТИКУМ PYTHON · ЗАНЯТИЕ 7

Классификация символов, форматирование, Unicode и списки

Строка научится рассказывать о себе, текст — собираться из кусков, а данные — храниться целиком, а не по одному.

Чему научимся

Из чего состоит строка

Третья группа строковых методов отвечает на вопросы о содержимом. Все они возвращают True или False.

МетодTrue, если строка непустая и…
isalnum()состоит только из букв и цифр
isalpha()состоит только из букв
isdigit()состоит только из цифр
islower()все буквы в ней строчные (небуквенное не мешает)
isupper()все буквы в ней заглавные (небуквенное не мешает)
isspace()состоит только из пробельных символов
s1 = 'abc123'
s2 = 'abc$*123'
s3 = ''
print(s1.isalnum())
print(s2.isalnum())
print(s3.isalnum())

Результат

True
False
False

Пустая строка не проходит ни одну из этих проверок: в ней нет ни букв, ни цифр, ни пробелов. А вот islower() и isupper() смотрят только на буквы: у строки 'abc1$d' метод islower() даёт True, потому что цифра и знак ему безразличны.

7.1

Перевернуть ответ

Запустите приведённые ниже фрагменты. Обратите внимание, что результат имеет тип boolTrue или False. Внесите минимальные изменения в переменные s, чтобы получить обратный результат: там, где печаталось True, должно печататься False, и наоборот.

Код

# 7.1.1
s = 'aabbAA111ccDDaa'
print(s.isalnum())
print(s.isalpha())
print(s.isdigit())

# 7.1.2
s = 'aabb!@#$11cc'
print(s.islower())

# 7.1.3
s = 'AAb!@#$11CC'
print(s.isupper())

# 7.1.4
s = '  abbc  '
print(s.isspace())

У первого фрагмента три проверки, и одной строкой все три не перевернуть: буквы и цифры одновременно не бывают. Подберите свою строку под каждую проверку.

Подсказка 1

Чтобы isalnum() дал False, достаточно одного символа, который не буква и не цифра.

Подсказка 2

Чтобы islower() стал False, нужна заглавная буква; чтобы isupper() стал True — убрать единственную строчную.

Разбор
  1. isalnum() было True — добавляем любой знак, например !.
  2. isalpha() было False из-за цифр — убираем цифры.
  3. isdigit() было False из-за букв — оставляем только цифры.
  4. islower() было True — делаем одну букву заглавной.
  5. isupper() было False из-за строчной b — делаем её заглавной.
  6. isspace() было False из-за букв — оставляем одни пробелы.
Решение
# 7.1.1 — под каждую проверку своя строка
print('aabbAA111ccDDaa!'.isalnum())   # добавили знак → False
print('aabbAAccDDaa'.isalpha())      # убрали цифры → True
print('111'.isdigit())               # оставили цифры → True

# 7.1.2
print('aaBb!@#$11cc'.islower())      # одна заглавная → False

# 7.1.3
print('AAB!@#$11CC'.isupper())       # исправили строчную b → True

# 7.1.4
print('     '.isspace())             # только пробелы → True

Задача не про код, а про точное чтение описания: islower() и isupper() не обращают внимания на цифры и знаки, а isalnum() — обращает. Одно лишнее слово в описании метода меняет ответ.

Форматирование строк

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

age = 27
txt = 'My name is Timur, I am ' + str(age)

Поэтому чаще пользуются методом format(). В строке ставят заполнители {}, а значения передают методу:

age = 27
txt = 'My name is Timur, I am {}'.format(age)
print(txt)

Результат

My name is Timur, I am 27

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

name = 'Timur'
print('My name is {0}-{0}-{0}'.format(name))

Результат

My name is Timur-Timur-Timur

С Python 3.6 есть способ ещё короче — f-строки. Перед кавычками ставят букву f, а в фигурные скобки пишут прямо имя переменной:

first_name = 'Timur'
age = 27
print(f'Hello, {first_name}. You are {age}.')

Результат

Hello, Timur. You are 27.

f-строка читается лучше всего, когда значений много: не приходится сверять порядок заполнителей со списком аргументов. Но format() тоже встречается сплошь и рядом — знать надо оба.

7.2

Собрать фразу через format

Дополните приведённый код, используя метод format(), так чтобы он вывел текст: «In 2010, someone paid 10k Bitcoin for two pizzas.» (без кавычек).

Код

s = 'In {0}, someone paid {1} {2} for two pizzas.'
print()
Формат выходных данных
Одна строка.
Вывод
In 2010, someone paid 10k Bitcoin for two pizzas.
Подсказка 1

Заполнители пронумерованы: {0}, {1}, {2} — значит, значения встанут в том же порядке, в каком переданы.

Подсказка 2

Метод применяется к строке: s.format(...).

Разбор
  1. {0} — год, {1} — количество, {2} — название валюты.
  2. Передаём их методу в этом же порядке.
Решение
s = 'In {0}, someone paid {1} {2} for two pizzas.'
print(s.format(2010, '10k', 'Bitcoin'))

Число 2010 передано числом, а не строкой, — format() сам превратит его в текст. В этом и удобство: str() писать не приходится.

7.3

Та же фраза f-строкой

Дополните приведённый код, используя f-строку, так чтобы он вывел текст: «In 2010, someone paid 10K Bitcoin for two pizzas.» (без кавычек).

Код

currency = 'Bitcoin'
amount = '10K'
year = 2010
print('In {}, someone paid {} {} for two pizzas.')
Формат выходных данных
Одна строка.
Вывод
In 2010, someone paid 10K Bitcoin for two pizzas.
Подсказка 1

Перед открывающей кавычкой нужна буква f.

Подсказка 2

В скобках пишутся имена переменных, а не номера.

Разбор
  1. Ставим f перед строкой.
  2. В первый заполнитель пишем year, во второй amount, в третий currency.
Решение
currency = 'Bitcoin'
amount = '10K'
year = 2010

print(f'In {year}, someone paid {amount} {currency} for two pizzas.')

Порядок переменных в объявлении и в строке разный — и это не мешает: f-строка подставляет по имени, а не по очереди. С format() пришлось бы следить за номерами.

Символы и их коды

В памяти компьютера всё хранится числами, и текст не исключение: каждому символу соответствует числовой код. Историческая схема кодирования — ASCII: 128 кодов на английские буквы, цифры и знаки препинания, по 7 бит на символ. Код заглавной латинской «A» — 65, «B» — 66 и так далее.

Ста двадцати восьми символов миру не хватило, и в начале 1990-х появился Юникод — таблица, куда входят знаки почти всех письменностей. Первые 128 кодов у него совпадают с ASCII, а сама таблица не ограничена и продолжает расти.

Юникод — это не кодировка, а таблица символов: она говорит, какой символ каким числом обозначен. UTF-8 — кодировка: она говорит, как эти числа хранить в памяти. Вопрос «чем UTF-8 отличается от Unicode» — вопрос с подвохом: сравнивать их нельзя.

Две встроенные функции связывают символ и его код:

print(ord('A'), ord('B'), ord('a'))
print(chr(65), chr(75), chr(110))

Результат

65 66 97
A K n

ord — от order (порядок), chr — от char (символ). Работают они в паре; вот, например, весь английский алфавит:

for i in range(26):
    print(chr(ord('A') + i), end=' ')

Результат

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

ord() принимает ровно один символ. ord('Abc') остановит программу: TypeError: ord() expected a character, but string of length 3 found.

7.4

Из кодов в символы

На вход подаются два числа a и b. Напишите программу, которая для каждого кода от a до b включительно выводит соответствующий ему символ таблицы Юникода.

Формат входных данных
Два целых числа a и b, каждое на отдельной строке.
Формат выходных данных
Символы через пробел.
Ввод
65
70
Вывод
A B C D E F
Подсказка 1

Коды идут подряд — значит, обычный range() от a до b включительно.

Подсказка 2

Чтобы символы печатались в одну строку, нужен end=' '.

Разбор
  1. Перебираем коды от a до b включительно.
  2. Каждый переводим в символ функцией chr() и печатаем.
Решение
a = int(input())
b = int(input())

for code in range(a, b + 1):
    print(chr(code), end=' ')

Попробуйте задать диапазон 1040–1071 — получите русский алфавит заглавными буквами. Латиница, кириллица и цифры лежат в таблице сплошными кусками, и это часто выручает.

7.5

Из символов в коды

На вход подаётся строка текста. Напишите программу, которая переводит каждый её символ в соответствующий код таблицы Юникода.

Формат входных данных
Одна строка.
Формат выходных данных
Коды символов через пробел.
Ввод
Hello world!
Вывод
72 101 108 108 111 32 119 111 114 108 100 33
Подсказка 1

Здесь нужны сами символы, а не их номера в строке.

Подсказка 2

Каждый символ переводится в код функцией ord().

Разбор
  1. Перебираем символы строки.
  2. Печатаем ord(c) через пробел.
Решение
s = input()

for c in s:
    print(ord(c), end=' ')

В примере хорошо виден код 32 — это пробел. Пробел ничем не отличается от других символов: у него есть код, он занимает место в строке и участвует в переборе.

Списки

До сих пор мы обрабатывали данные по одному: прочитали число — учли — забыли. Но во многих задачах нужно хранить всю последовательность целиком. В большинстве языков для этого есть массив; в Python — список.

Список — это последовательность элементов, пронумерованных с нуля, как символы в строке. Создаётся он перечислением в квадратных скобках:

numbers = [2, 4, 6, 8, 10]
languages = ['Python', 'C#', 'C++', 'Java']
print(numbers)
print(languages[0])

Результат

[2, 4, 6, 8, 10]
Python

Пустой список создаётся двумя способами: mylist = [] или mylist = list(). Функция list() умеет и превращать в список другие последовательности:

numbers = list(range(5))
chars = list('abcde')
print(numbers)
print(chars)

Результат

[0, 1, 2, 3, 4]
['a', 'b', 'c', 'd', 'e']

Со списками работает всё то же, что и со строками: len() — длина, in — проверка принадлежности, индексы (в том числе отрицательные), срезы, + и *.

numbers = [2, 4, 6, 8, 10]
print(len(numbers))
print(numbers[-1])
print(numbers[1:3])
print([1, 2] + [3, 4])
print([0] * 5)

Результат

5
10
[4, 6]
[1, 2, 3, 4]
[0, 0, 0, 0, 0]

Ещё три встроенные функции работают со списком целиком: sum() — сумма элементов, min() и max() — наименьший и наибольший.

Главное отличие списка от строки: список изменяем. s[4] = 'X' для строки — ошибка, а numbers[4] = 100 для списка — обычное дело. Менять можно и целый срез: fruits[2:5] = ['банан', 'вишня', 'киви'].

Работают и сокращённые записи += и *= — они меняют сам список:

a = [1, 2, 3, 4]
b = [7, 8]

a += b     # добавили к списку a список b
b *= 5     # повторили список b пять раз

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

7.6

Список от 1 до n

На вход подаётся одно число n. Напишите программу, которая выводит список [1, 2, 3, …, n].

Попробуйте записать решение одной строкой.

Формат входных данных
Натуральное число n.
Формат выходных данных
Список целиком, как его печатает print().
Ввод
5
Вывод
[1, 2, 3, 4, 5]
Подсказка 1

Нужная последовательность уже есть — её даёт range().

Подсказка 2

Осталось превратить её в список функцией list().

Разбор
  1. range(1, n + 1) — числа от 1 до n включительно.
  2. list(...) делает из них список, и print() печатает его со скобками и запятыми.
Решение
n = int(input())

print(list(range(1, n + 1)))

Печать списка выглядит непривычно — с квадратными скобками и запятыми. Это и есть «список целиком»; если нужен другой вид, элементы выводят по одному — этому научимся на следующем занятии.

7.7

n букв алфавита

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

Формат входных данных
Натуральное число n ≤ 26.
Формат выходных данных
Список из n букв.
Ввод
5
Вывод
['a', 'b', 'c', 'd', 'e']
Подсказка 1

Алфавит можно просто написать строкой — двадцать шесть букв подряд.

Подсказка 2

Первые n символов даёт срез, а список из них — функция list().

Разбор
  1. Берём строку с алфавитом.
  2. Отрезаем первые n символов срезом [:n].
  3. Превращаем в список.
Решение
n = int(input())
alphabet = 'abcdefghijklmnopqrstuvwxyz'

print(list(alphabet[:n]))

Алфавит можно было бы и не писать руками, а собрать через chr(ord('a') + i) в цикле. Но когда данные короткие и известны заранее, написать их прямо — честнее и понятнее, чем вычислять.

7.8

Сумма минимума и максимума

Дополните приведённый код так, чтобы он вывел сумму минимального и максимального элементов списка numbers.

Код

numbers = [12.5, 3.1415, 2.718, 9.8, 1.414, 1.1618, 1.324]
print()
Формат выходных данных
Одно число.
Вывод
13.6618
Подсказка 1

min() и max() принимают не только несколько аргументов, но и список целиком.

Подсказка 2

Складывать их результаты можно прямо внутри print().

Разбор
  1. min(numbers) — наименьший элемент, max(numbers) — наибольший.
  2. Печатаем их сумму.
Решение
numbers = [12.5, 3.1415, 2.718, 9.8, 1.414, 1.1618, 1.324]

print(min(numbers) + max(numbers))

На третьем занятии min() принимал числа через запятую, а здесь — один список. Это одна и та же функция: ей всё равно, перечислили вы значения или отдали их одним списком.

7.9

Среднее арифметическое

Дополните приведённый код так, чтобы он вывел среднее арифметическое элементов списка evens.

Код

evens = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
average = 
print(average)
Формат выходных данных
Одно число.
Вывод
11.0
Подсказка 1

Среднее — это сумма, делённая на количество.

Подсказка 2

И сумму, и количество считают готовые функции.

Разбор
  1. sum(evens) — сумма всех элементов.
  2. len(evens) — сколько их.
  3. Делим одно на другое.
Решение
evens = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

average = sum(evens) / len(evens)
print(average)

Деление здесь обычное, а не целочисленное: среднее почти всегда дробное. Даже когда оно выходит ровным, как здесь, print() покажет 11.0 — признак того, что тип вещественный.

7.10

Первые шесть простых

Дополните приведённый код, используя срезы, так чтобы он вывел первые 6 элементов списка primes.

Код

primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
print()
Формат выходных данных
Список из шести чисел.
Вывод
[2, 3, 5, 7, 11, 13]
Подсказка 1

Срез списка пишется так же, как срез строки.

Подсказка 2

Левую границу можно опустить.

Разбор
  1. primes[:6] — первые шесть элементов.
  2. Результат среза — тоже список, поэтому печатается со скобками.
Решение
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]

print(primes[:6])

Срез строки давал строку, срез списка даёт список — правило одно: срез возвращает тот же тип, что и исходная последовательность.

7.11

Список наоборот

Дополните приведённый код так, чтобы он вывел элементы списка languages в обратном порядке.

Код

languages = ['Chinese', 'Spanish', 'English', 'Hindi', 'Arabic', 'Bengali', 'Portuguese', 'Russian', 'Japanese', 'Lahnda']
print()
Формат выходных данных
Список в обратном порядке.
Вывод
['Lahnda', 'Japanese', 'Russian', 'Portuguese', 'Bengali', 'Arabic', 'Hindi', 'English', 'Spanish', 'Chinese']
Подсказка 1

Разворачивает последовательность отрицательный шаг среза.

Подсказка 2

Он работает со списком так же, как со строкой.

Разбор
  1. languages[::-1] — список в обратном порядке.
  2. Печатаем результат.
Решение
languages = ['Chinese', 'Spanish', 'English', 'Hindi', 'Arabic', 'Bengali', 'Portuguese', 'Russian', 'Japanese', 'Lahnda']

print(languages[::-1])

Срез не меняет исходный список, а создаёт новый — в languages порядок остался прежним. На девятом занятии появится метод reverse(), который, наоборот, переворачивает список на месте.

7.12

Собрать список из кусков

Дополните приведённый код, используя операторы конкатенации + и умножения на число *, так чтобы он вывел список [1, 2, 3, 1, 2, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 8, 9, 10, 11, 12, 13].

Код

numbers1 = [1, 2, 3]
numbers2 = [6]
numbers3 = [7, 8, 9, 10, 11, 12, 13]
print()
Формат выходных данных
Один список.
Вывод
[1, 2, 3, 1, 2, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 8, 9, 10, 11, 12, 13]
Подсказка 1

Посмотрите на нужный список по частям: сколько раз повторяется каждый кусок?

Подсказка 2

[1, 2, 3] идёт дважды, шестёрка — девять раз, а третий список — один.

Разбор
  1. Первый кусок: numbers1 * 2.
  2. Второй: numbers2 * 9.
  3. Третий: numbers3 как есть.
  4. Склеиваем всё через +.
Решение
numbers1 = [1, 2, 3]
numbers2 = [6]
numbers3 = [7, 8, 9, 10, 11, 12, 13]

print(numbers1 * 2 + numbers2 * 9 + numbers3)

Умножение списка на число — самый короткий способ получить повторяющиеся элементы. Особенно часто пишут [0] * n — список из n нулей, заготовку под будущие значения.

7.13

Лесенка с человечком

Напишите программу, рисующую в командной строке лесенку из 10 ступеней: она спускается слева направо, каждая следующая ступень ниже и правее предыдущей.

Затем дополните её так, чтобы при запуске задавался вопрос «На какой ступени стоит человечек? Введите число от 1 до 10», и в зависимости от ответа человечек рисовался на нужной ступени. Ступени считаются сверху: первая — самая верхняя, левая.

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

Формат входных данных
Одно число от 1 до 10.
Формат выходных данных
Лесенка из десяти ступеней с человечком на указанной.
Ввод 1
1
Вывод 1
На какой ступени стоит человечек? Введите число от 1 до 10:
  ()
 /()\
 _/\
  |_
    |_
      |_
        |_
          |_
            |_
              |_
                |_
                  |_
Ввод 2
2
Вывод 2
На какой ступени стоит человечек? Введите число от 1 до 10:
    ()
 _ /()\
  |_/\
    |_
      |_
        |_
          |_
            |_
              |_
                |_
                  |_
Подсказка 1

Ступени отличаются только отступом: у каждой следующей его на два пробела больше. У самой верхней стойки | нет — ей не к чему примыкать.

Подсказка 2

Человечка удобно не печатать отдельно, а дописывать к готовым строкам лесенки: ноги — к строке своей ступени, туловище и голову — к строкам выше.

Разбор
  1. Лесенка — десять строк: у верхней ступени только полка _, у остальных стойка и полка |_, и каждая следующая на два пробела правее.
  2. Список заводится сразу нужной длины ([''] * 12), а строки в нём заменяются по индексу: двух лишних строк в начале как раз хватает человечку на первой и второй ступени.
  3. Ноги приклеиваются прямо к строке своей ступени — человечек встаёт на её полку.
  4. Туловище идёт строкой выше и заканчивается на один символ левее ног, голова — ещё строкой выше и вровень с ногами. Нужное число пробелов считается по длине уже готовой строки.
  5. Пустые строки, оставшиеся над человечком, не печатаются.
Решение
print('На какой ступени стоит человечек? Введите число от 1 до 10:')
step = int(input())

person = ['()', '/()\\', '/\\']       # голова, туловище с руками, ноги

# двенадцать строк: десять ступеней и две про запас — на них встанет
# человечек, если он стоит на первой или второй ступени
rows = [''] * 12

for i in range(1, 11):
    if i == 1:
        rows[i + 1] = ' _'            # у верхней ступени стойки нет
    else:
        rows[i + 1] = ' ' * 2 * (i - 1) + '|_'

# ноги приклеиваются к самой ступени, туловище и голова — на строки выше
rows[step + 1] = rows[step + 1] + person[2]
rows[step] = rows[step] + ' ' * (2 * step - 1 - len(rows[step])) + person[1]
rows[step - 1] = rows[step - 1] + ' ' * (2 * step - len(rows[step - 1])) + person[0]

for row in rows:
    if row != '':                     # пустые строки над человечком не печатаем
        print(row)

Обратите внимание на '/()\\': чтобы напечатать одну обратную косую черту, в коде её пишут дважды. Одиночная \ — управляющий символ, как в \n. И на то, зачем в списке две лишние строки: человечку надо куда-то поместиться над лесенкой, и завести это место заранее проще, чем разбирать два особых случая.

Сортировка пузырьком

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

Медленные алгоритмы: пузырьковая сортировка, сортировка выбором, сортировка вставками. Быстрые: сортировка Шелла, быстрая сортировка, сортировка слиянием, пирамидальная, TimSort (её и использует Python). Разница не в проценты: на списке в миллион элементов медленные считают часами, быстрые — секунды.

Пузырьковая сортировка — учебная. Список проходят несколько раз; за один проход соседние элементы сравнивают попарно и меняют местами, если порядок неверный. За первый проход самый большой элемент «всплывает» в конец, как пузырёк в воде, — отсюда название.

Как это выглядит

a = [5, 1, 4, 2, 8]

# первый проход
[5, 1, 4, 2, 8] → [1, 5, 4, 2, 8]   # 5 > 1, меняем
[1, 5, 4, 2, 8] → [1, 4, 5, 2, 8]   # 5 > 4, меняем
[1, 4, 5, 2, 8] → [1, 4, 2, 5, 8]   # 5 > 2, меняем
[1, 4, 2, 5, 8] → [1, 4, 2, 5, 8]   # 5 < 8, не трогаем

Проходов нужно n − 1, где n — длина списка. После каждого прохода очередной наибольший элемент стоит на своём месте в конце, поэтому внутренний цикл можно каждый раз укорачивать.

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

7.14

Пузырьковая сортировка

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

Список случайных чисел можно получить так:

import random

length = 6
x = random.sample(range(0, 10), length)

Функция sample() из модуля random возвращает список указанной длины, составленный из случайно выбранных элементов последовательности.

Пример вывода

проход 1: [2, 6, 5, 7, 0, 9]
проход 2: [2, 5, 6, 0, 7, 9]
проход 3: [2, 5, 0, 6, 7, 9]
проход 4: [2, 0, 5, 6, 7, 9]
проход 5: [0, 2, 5, 6, 7, 9]
Подсказка 1

Обмен двух элементов пишется одной строкой: a[j], a[j + 1] = a[j + 1], a[j].

Подсказка 2

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

Разбор
  1. Получаем случайный список функцией random.sample().
  2. Внешний цикл делает проходы — их нужно не больше, чем len(x) - 1.
  3. Внутренний цикл сравнивает соседей и меняет их местами, если левый больше правого.
  4. Каждый обмен отмечаем флажком; если за проход обменов не было, выходим по break.
  5. После каждого прохода печатаем список.
Решение
import random

numbers = random.sample(range(0, 10), 6)   # шесть разных случайных чисел
print('исходный список:', numbers)

for i in range(len(numbers) - 1):
    swapped = False                        # обменов на этом проходе ещё не было

    # с каждым проходом хвост списка уже упорядочен, и его можно не трогать
    for j in range(len(numbers) - 1 - i):
        if numbers[j] > numbers[j + 1]:
            numbers[j], numbers[j + 1] = numbers[j + 1], numbers[j]
            swapped = True

    print(f'проход {i + 1}: {numbers}')

    if not swapped:                        # список уже отсортирован
        break

Обмен через кортеж — a, b = b, a — тот самый приём со второго занятия. Без него понадобилась бы третья переменная: присвоив numbers[j] = numbers[j + 1], старое значение вы бы уже потеряли.

Итог занятия

Главное с седьмого занятия. Методы is…() отвечают на вопросы о содержимом строки, и пустая строка не проходит ни одну проверку. Текст собирают методом format() или f-строкой — второе короче, потому что подставляет по имени. ord() и chr() переводят символ в код Юникода и обратно. Список — это последовательность, где всё то же, что у строки: индексы, срезы, len(), in, +, *; но, в отличие от строки, список изменяем.