Easy to learn, hard to master.
– Известно как закон Бушнилла/Нолана, используется Blizzard Ent. в качестве дизайн принципа.
Этот принцип отлично подходит для разработки игр: с первого уровня до последнего игрок должен развиваться и превозмогать, чтобы получать радость от преодоления. И если на первом уровне это должно быть просто, ведь игрок только начал, то к финальным раундам это должно быть почти невозможно, чтобы даже при том же геймплее и сеттинге игрок испытал радость от прохождения, достижения.
В программировании принято говорить о низком или высоком пороге вхождения в язык / технологию. Это как раз про "easy to learn". В Python порог низкий – уже вскоре после старта изучения вы можете писать программы, которые даже будут полезными кому-то (и да, зарабатывать на этом). Но при этом часто забывают вторую часть – "hard to master". Если хотите быть мастером – не обольщайтесь этими ранними "ачивками" – здесь вам повезло. Но для создания действительно серьёзных программных продуктов, для пользы людям, для уважения коллег... ну и денег, как следствие... вам нужно изучать дальше, понимать больше!
Я вас уверяю, эта книга не сделает из вас хорошего разработчика. Реальная промышленная разработка отличается от "книжных знаний" и не зациклена только на языке программирования. Именно поэтому мы поговорим здесь и о культуре кода, и о поддержке/развитии проекта, и о реализациях классических абстракций... И всё же, ни одна книга не может сделать из вас разработчика. Только вы!
Первая программа
Talk is cheap. Show me the code.
Линус Торвальдс
Приятно начинать разговор с кода. Это прекрасный предмет для обсуждения. Даже если человек не близок к программированию, он пытается проявлять терпение, или даже уважение в общении.
while True:
command = input('Операция> ')
if command == '+':
first_operand = input('операнд 1> ')
second_operand = input('операнд 2> ')
first_operand = int(first_operand)
second_operand = int(second_operand)
print("Результат=", first_operand + second_operand)
elif command == '*':
first_operand = input('операнд 1> ')
second_operand = input('операнд 2> ')
first_operand = int(first_operand)
second_operand = int(second_operand)
result = 0
for num in range(0, first_operand):
result += second_operand
print("Результат=", result)
else:
print("Неизвестная операция")
Если вы уже программировали на одном из императивных языков, скорее всего, вам понятно в общих чертах, что тут происходит. С другой стороны, даже если у вас и не было подобного опыта, но вы хоть немного знаете английский – суть кода вам также ясна.
Это калькулятор, поддерживающий сложение и умножение. Операций мало, операция умножения сделана... странно, да и будут проблемы с отрицательными или не целочисленными операндами. И тем не менее, код довольно легко читать. А если знать хотя бы базовые конструкции и типы данных Python, то можно и доработать до приемлемого!
Базовые типы данных Python
Не откладывая в "долгий ящик", давайте разбираться с теми самыми типами! Сразу после – разберём и управляющие конструкции.
Логический тип данных
И сразу на первой строке нас встречает значение "True" – истина. Относится это значение к логическому типу данных. Также именуется как булевый тип данных или булеан – в честь английского математика логика Джорджа Буля.
Значений у логического типа всего 2: True и False – истина и ложь. Как можно догадаться, используется он в случае, когда явление или предмет может находиться только в двух состояниях. Например, "выключен – включён", "равен – не равен" и тому подобное.
Упражнение 1.1. Какие из перечисленных явлений/предметов можно описать в коде булевым значением?
- Состояние выключателя;
- Размер ноги;
- Время суток;
- Наличие освещения в комнате;
- Пол животного.
Исторически в программировании при выборе одного из нескольких альтернативных действий используется двоичная логика. "Если утверждение верно, то выполняется первое альтернативное действие, иначе – второе". Здесь также в качестве результата утверждения используется логическое значение.
Рассмотрим несколько утверждений в интерпретаторе Python3:
>>> 1 + 2 == 3
True
>>> 2 > 1
True
>>> 1 <= 1
True
>>> 1 < 1
False
Сравнение на равенство производится оператором "==", на "меньше или равно" – оператором "<=". Результаты сравнений очевидны из курса школьной арифметики.
С другой стороны, а истинна ли строка? Или число само по себе, без утверждения, что оно больше/меньше другого? Это непростой вопрос, и ответим мы на него лишь в следующей части книги. Пока же ограничимся парой наблюдений:
>>> bool("Привет!")
True
>>> bool("")
False
>>> bool(1337)
True
>>> bool(0)
False
>>> bool(-1)
True
Эксперимент показал, что:
- Строки Python воспринимает как истинные утверждения, если только они не пустые (нулевой длины).
- Числа интерпретируются также как истина, за исключением нуля.
Также, мы использовали конструктор логических значений – bool
. Передав ему
"в скобки" какое-либо значение, мы получим его представление в логическом виде.
Целые числа
Сам же наш "калькулятор" складывает и умножает целые числа или "инты" – калька с английского "int" (integer). О целых числах мы знаем также из арифметики. А по аналогии с конструктором логических значений, мы можем также провести некоторые эксперименты с целыми числами:
>>> int(1)
1
>>> int(-1)
-1
>>> int("-1")
-1
>>> int("Привет!")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'Привет!'
>>> int("-1.123")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '-1.123'
>>> int(-1.123)
-1
>>> int(True)
1
>>> int(False)
0
После очевидных приведений чисел 1, -1 к числу, мы получили из строки "-1"
число -1 – конструктор умеет приводить текстовое представление чисел к числовым
значениям. Строка же, которая не содержит целое число, не может быть приведена,
поэтому мы получили ValueError
– ошибку переданного значения.
Более того, строка с вещественным числом (десятичная дробь) также вызывает ошибку.
А вот с самим вещественным числом конструктор int
поступает иначе – отбрасывает
дробную часть.
Рассмотренные же нами выше True и False преобразуются в 1 и 0 – соответственно. Это от части логично, учитывая, что сами эти числа обратно преобразуются в исходные True и False.
Длинная целочисленная арифметика
Во многих языках программирования, где не реализована длинная арифметика, существуют специализированные типы данных для целых чисел разного размера, например: int16_t, int32_t, int64_t. Числа здесь указывают на то, сколько битов памяти будет выделено на хранение значения. Так к примеру, int16_t способен хранить целые числа в отрезке от -32768 до 32767.
Что же будет, когда мы к 32767 прибавим ещё 1? Предположим, что мы используем классическое представление целого числа: первый бит отвечает за знак: 0 – для для положительных, 1 – для отрицательных; а дальше – с младшего бита с каждым числом увеличиваем на 1. Тогда в битовом представлении эта операция выглядит так:
0111111111111111
+ 0000000000000001
= 1000000000000000
То есть мы получим первое отрицательное число – -1.
Ответ 1.1. Какие из перечисленных явлений/предметов можно описать в коде булевым значением?
- Состояние выключателя – если мы исключаем сосотояние "неизвестно" – вполне можно использовать True/False;
- Размер ноги – обычно размеров более чем два, однако, если мы говорим о "детской" ноге или "ноге взрослого", может подойти и boolean;
- Время суток – время также имеет много больше, чем два состояния. Однако и тут есть AM|PM - до полудня и после полудня;
- Наличие освещения в комнате – в целом освещённость измеряется в люменах (вещественное число), однако сам факт освещённости может быть bool;
- Пол животного – в вопросах пола животных мы как общество не дошли до гендеров – так что возможно ограничиться двумя вариантами, что есть bool.*
Упражнение 1.2. Аналогичным образом проведите сложение целых 16-ти битных чисел -32768 и -1.FF
Такой подход к хранению чисел обусловлен ограниченностью памяти в компьютере. Как бы мы не представляли числа, в компьютере они хранятся в виде битов памяти, а потому мы должны ограничивать максимальные размеры используемых чисел, чтобы экономить память.
Или же не должны? Проведём эксперимент в нашем интерпретаторе Python:
>>> 2 ** 100000
9990020930143845079440327643300335909804291390541816917715
... пропущено несколько десятков экранов результата ...
6223208402597025155304734389883109376
– забавно, ведь для хранения значения этого числа нам бы понадобился int100000_t! А всё потому, что Python для хранения целых чисел и операций над ними использует длинную арифметику. Подробнее о том, как это делает CPython, можно прочитать в исходном коде, файл "Objects/longobject.c". Мы же рассмотрим упрощенную модель для понимания этого подхода.
Рассмотрим случай без-знакового сложения – так как для отрицательных чисел мы можем аналогично примеру выше выделить специальный бит. Также начнём с 16 бит:
1111111111111111
+ 1111111111111111
= 1111111111111110
Дабы не мелочиться – сложим самое большое число, представимое в 16-ти битах с ним же. При этом у нас теряется 1 старший бит. Процессор, который и совершал сложение, установит флаг переноса в 1 – он отслеживает подобные переполнения.
Далее – время перестроить структуру данных, которая хранит результирующее число. Если мы будем хранить число не в виде 16-ти бит (которыми оперирует процессор), а в виде массива из подобных кусочков, то нам при обнаружении установленного флага нужно перенести этот "потерянный" бит в следующий элемент массива. Если следующего элемента массива пока нет – создать его и таки перенести. В результате получим:
1111111111111111
+ 1111111111111111
= 0000000000000001 1111111111111110
Если же нужно провести операцию сложения/вычитания на подобных числах, то поэлементно совершаем операцию, не забывая смотреть на флаг. При вычитании – обратно – если получилось отрицательное значение, нужно забрать 1 байт из следующего элемента массива.
Для умножения используем команду процессора, позволяющую получить в 2 раза больший по выделенной памяти результат.
Таким образом, Python снимает с нас заботу о размерности чисел, однако, потенциально может "съесть" всю память под одно число. Также стоит понимать, что операции на длинных числах заметно медленнее, что иллюстрирует и описанный выше пример.
Числа с плавающей точкой
Получив общее представление о целых числах, самое время задуматься о не целых –
вещественных, или же числах с плавающей точкой (или запятой – в США принято
записывать дробную часть десятичной дроби через точку). В Python они относятся
к типу данных float
. Также используется название "флоат" – калька с английского.
Вернёмся к нашим экспериментам в консоли Python:
>>> float('1.23')
1.23
>>> float(True)
1.0
>>> float(1)
1.0
>>> float('inf')
inf
>>> float('-inf')
-inf
>>> float('nan')
nan
– наш пример с преобразованием строки '1.23'
в число наконец-то заработал!
С True
результат уже предсказуемый после рассмотрения int
. Очевидным
образом получаем из целого 1
число с плавающей точкой 1.0
. А вот дальше –
интереснее: строки 'inf'
, -inf
и nan
Python в рамках построения флоата
преобразует в "бесконечность", "минус бесконечность" и "не число".
Также в отличии от int
различаются "-0.0" и "0.0":
>>> float('-0')
-0.0
>>> float('0')
0.0
>>> int('0')
0
>>> int('-0')
0
Это происходит также из-за ограничений точности при хранении чисел с плавающей
точкой. То есть -0.0
обозначает очень маленькое отрицательное число.
Но насколько ограничена эта точность? Если это какие-то редкие случаи, то не так это и важно, но если это проявляется даже на малых числах – об этом надо знать и помнить!
>>> 1.2 / 0.4
2.9999999999999996
>>> 0.1 + 0.2
0.30000000000000004
IEEE-754
То есть теряется точность даже не на уровне "копеек"... Поэтому давайте поймём, почему это так происходит.
Традиционно вспоминаем, что чисел в компьютере не существует, а есть байты, которые как-то представляются в виде чисел. Так что ответ на вопрос "почему всё так плохо с флоатами?" лежит в объяснении устройства этих самых флоатов. Подробно это описывается в стандарте Institute of Electrical and Electronics Engineers за номером 754 (и 874 – для произвольной базы системы счисления).
Итак, у нас есть 64 бита на современных процессорных архитектурах под число
с плавающей точкой. И да, float
в отличии от int
ограничен не размером
вашей планки памяти, а 64-ю битами. Проверить это довольно просто:
>>> 1.12 ** 10000
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: (34, 'Result too large')
>>> 112 ** 10000
15143515339910936940906232189494294899467662719426625349
... несколько экранов чисел ...
14072369468888712121486394470158647160954081509376
Из чего же состоят те самые 64-ре бита? Знак, мантисса и экспонента. Ну, что делать со знаком, мы уже знаем – отдадим под него аж целый бит. А вот 2 других слова требуют расшифровки... Это не просто, поэтому продемонстрирую на примере. "Следите за руками"!
Возьмём число 155.625
, приведём его к двоичному виду:
- 155.625 = 128 + 16 + 8 + 2 + 1 + 0.5 + 0.125
- = 2 ** 7 + 2 ** 4 + 2 ** 3 + 2 ** 1 + 2 ** 0 + 2 ** -1 + 2 ** -3
- Выписываем биты, в которых мы 2 возводили в степень (до точки – с нулевой и
далее, после - с -1 и далее):
10011011.101
.
Приводим к виду, при котором до точки остаётся только 1:
1.0011011101 * 2 ** 7
. Каждый сдвиг точки влево увеличивает степень двойки на 1.
7 = 111 в бинарном виде.
Тогда мантисса = 1.0011011101, а экспонента = 111. Само же число получается:
число = знак, мантисса * 2 ** экспонента
– это общий алгоритм для разной битности, для 64-х – 11 бит на экспоненту, 52 бита на мантиссу.
Само собой, здесь могут происходить ошибки переполнения, равно как и ошибки округления (которые для стандарта и не ошибки вовсе). Особенно, когда возникают периодические дроби, как на пример при делении 1 на 3. Или же при использовании десятичной части, которая плохо раскладывается в степени двойки...
В дополнении к этой главе рассмотрены альтернативные подходы к хранению дробных чисел. В общем виде проблема хранения дробных чисел не решена (например, число Пи так и не вычислено до сих пор), а алгоритма сложения двух действительных чисел так и не существует (они могут быть бесконечными).
Упражнение 1.3. Проверьте египетский треугольник 0.03, 0.04, 0.05 на прямоугольность. Добейтесь правильного с точки зрения науки смысла.*
Комплексные числа
Помимо действительных чисел с ограниченной точностью Python предлагает инструментарий для работы с комплексными числами. Их вы можете встретить при работе с гармоническими вычислениями, в общей алгебре, топологии и теории множеств.
Также есть прикладная необходимость в теории кодирования, физике квантовых частиц и т.д. В общем, довольно полезная абстракция, но не каждому.
Базируется это множество чисел на множестве вещественных чисел. А значит имеет всё те же проблемы в программировании:
>>> (1 + 2.0j) ** 1000
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: complex exponentiation
>>> (1 + 2.0j) ** 100
(-6.443164690985892e+34-6.113241307762508e+34j)
Кстати, заметьте, что мнимая часть обозначается j
, а не i
, как мы
привыкли в русской школе математики. Говорить о нём долго смысла мало –
всё те же болячки, что и у вещественных чисел.
Ответ 1.3. Проверьте египетский треугольник 0.03, 0.04, 0.05 на прямоугольность. Добейтесь правильного с точки зрения науки смысла.
Проблема состоит в том, что мы начинаем заниматься нецелочисленной арифметикой. С IEEE-754 наше уравнение
a*a + b*b = c*c
не работает. Но мы можем решить данный вопрос с определённой степенью допущения:a*a + b*b - c*c < 0.00001
Упражнение 1.4. Создайте число из единицы мнимой части.
Учитывая среднего заказчика калькулятора, реализовывать и поддерживать комплексную арифметику мы не будем.
Зачем "ничто" в Python?
Представьте, пишите вы веб-сайт, а на странице спрашиваете пользователя:
"Хотите подписаться на нашу рассылку?". Само собой, под ответ у нас заведена
переменная is_subscribed
со значением... А вот какое значение нужно задать
этой переменной до того как пользователь ответил на этот вопрос?
Само название переменной is_subscribed
подсказывает, что её тип – bool
.
Так что, ставим False
? Но тогда как мы отличим: пользователь ответил
"нет, спасибо", или же он ещё не определился?
Может мы неверно выбрали тип данных? Возьмём, например, int
– там 0
пусть
соответствует "не определился", 1
– "нет", а 2
– "подписался". Тогда
возникает путаница: напишем в коде if is_subscribed:
– считая, что переменная
логическая (название будет смущать) – проспамим всем: тем, кто хотел, и кто ясно
сказал, что не заинтересован. Да и если мы вдруг задаём вопрос не "да / нет",
а, например, "каков Ваш возраст?"...
Можно завести две переменные: is_answered
и is_subscribed
– ответил ли,
как ответил – соответственно. Это неудобно: одно простое значение мы размазали
по двум местам – такой код сложнее дорабатывать.
Тут нам на помощь приходит тип NoneType
, имеющий одно единственное значение –
None
– "ничто". Именно его и стоит использовать, если пользователь ещё
не определился. Во-первых, это семанично. Во-вторых, это устоявшийся подход –
не выдумывайте своих "велосипедов", пока есть стандартные средства!
Посмотрим, как ведёт себя None
:
>>> bool(None)
False
>>> int(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
>>> None > 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '>' not supported between instances of 'NoneType' and 'int'
>>> None == None
True
>>> None == False
False
>>> 0 is None
False
>>> 0 is not None
True
>>> 0 is (not None)
False
None
приводится к bool
, и имеет логическое значение False
. В виде
int
-а и прочих числовых типов None
не представим. Равен себе и не равен
False
, к которому приводится.
Для сравнения значения с None
стоит использовать
ключевое слово is
(проверяет тот же ли объект слева от is
, что и справа).
Также есть ключевое слово / конструкция is not
– объекты слева и справа разные.
Заметьте, что x is not None
и x is (not None)
имеют совершенно разный смысл!
Ответ 1.4. Создайте число из единицы мнимой части.
1j
Ведь просто
j
– это переменная Python, которая, вероятно, на данный момент не определена.
Переменные в Python
Но в день печали, в тишине, Произнеси его тоскуя; Скажи: есть память обо мне, Есть в мире сердце, где живу я.
А.С. Пушкин "Что в имени тебе моем?"
Как и во многих других языках программирования, в Python значения принято хранить в переменных. Переменная – это прежде всего имя, смысл того значения, на которое она указывает.
pi = 3.14
radius = 100
perimeter = 2 * pi * radius
Заметьте, каждое осознанное действие в коде можно как-то назвать или же прокомментировать. И важнее правильно выбирать названия переменных, чем унавоживать код коментариями. Тот же код с коментариями, но с плохими названиями переменных:
a = 3.14 # approximate π
b = 100 # radius of the circle
c = 2 * a * b # the length of the circumference
Возможно из-за небольшого примера вам показалось, что разницы особо нет.
Но чем больше код, тем больше вам нужно помнить, какая переменная за
какое значение отвечает. Предст ставьте, что a
и b
вы определили на предыдущем
экране кода, а экраном ниже вам надо использовать эти переменные...
Поэтому рекоммендую сразу выбирать названия переменных,
которые отражают суть значений, на которые они будут ссылаться.
Таким образом, мы подходим к главной проблеме прикладного программирования: именованию. Чаще всего код пишется 1 раз и дорабатывается много раз. Каждая доработка требует чтения этого кода. И чем проще и приятнее этот код читать, тем проше его поддерживать, дорабатывать. Ну а самый простой способ сделать под читаемее - использовать подходящие по смыслу переменные.
Отдельно стоит упомянуть именование переменных, состоящих из двух и более слов. В Python для разделения слов в переменной (чтобы проще читалось) используется нижняя черта. Например,
long_name_example = "Вот так"
Объекты Python
Мы живы, пока нас помнят Искренне, не лживо! Мы живы, пока нас помнят И любят! – Тогда мы живы!!!
Нина Зимина
Python - объектно-ориентированный язык программирвоания. Более того, все данные в Python являются объектами. Что это даёт нам? На первый взгляд доволно мало: просто многие низкоуровневые операции выполняются единообразно. Однако, это уже очень приятно. В частности, выделение пямяти и освобождение её происходят незаметно для нас. Однако, полезно базово понимать, как Python определяет, когда вам понадобится объект, а когда он уже не нужен.
Когда создавать объект - довольно очевидно - когда мы в коде опереляем значение, ведь все значения - это объекты. А вот удаляет Python, когда последнее опомянание об объекте пропадает. Каждая переменная, каждый способ обратиться к объекту увеличивает счётчик ссылок, который хранится внутри объекта. Если же счётчик ссылок станет равен нулю, то при следующей сборке мусора, Python освободит память, которую использовал объект.
К слову, вы сами можете посмотреть, как это работает, используя функцию getrefcount
из модуля sys
:
>>> import sys
>>>
>>> hello = "Good news everyone!"
>>> print(sys.getrefcount(hello))
2
Во-первых, обратите внимание на название функции getrefcount
. Правила именования функций
не отличаются от именования переменных. Поэтому предлагаю придумать своё,
более правильное название функции для получения значения счётчика ссылок объекта.
Во-вторых, почему "2", а не "1", ведь переменная, что ссылается на объект строки `"Good news everyone!" одна? На самом деле, внутри функции также создаётся переменная, которая ссылается на этот объект строки, ведь внутри функции также нужно как-то обращаяться к этому объекту.
Так мы можем ещё немного поэкспериментировать:
>>> greeting = hello
>>> print(sys.getrefcount(hello))
3
>>> del(hello)
>>> print(sys.getrefcount(hello))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'hello' is not defined
>>> print(sys.getrefcount(greeting))
2
Мы создадим переменную greeting
, в которую присвоим hello
. На самом деле,
мы возьмём объект, на который ссылалась переменная hello
, и запишем его в соответствие
переменной greeting
, увеличив на 1 счётчик ссылок объекта. Далее мы удаляем
переменную hello
- больше её нет для Python. Но объект и его 2-ое имя
всё ещё есть.
Важно отделять понятие "переменной" от понятия "объект", как прозвище человека от него самого.
С другой стороны оператор =
- это скорее оператор связывания переменной и
объекта, чем привычное присвоение из языков подобных C
. Так например,
>>> hello = greeting = "Ho-ho-ho!"
>>> hello
'Ho-ho-ho!'
>>> greeting
'Ho-ho-ho!'
>>> hello = (greeting = "Ho-ho-ho!")
File "<stdin>", line 1
hello = (greeting = "Ho-ho-ho!")
^
SyntaxError: invalid syntax
>>> "Ho-ho-ho!" == (greeting = "Ho-ho-ho!")
File "<stdin>", line 1
"Ho-ho-ho!" == (greeting = "Ho-ho-ho!")
^
SyntaxError: invalid syntax
Мы можем в одной строке присвоить значение сразу двум переменным. Но сам
оператор =
не возвращает значения, более того, синтаксически - это неверная
конструкция. Если же вы хотите использовать более привычный оператор присвоения,
который возвращает присвоенное значение, следует использовать "моржовый
оператор" - :=
:
>>> "Ho-ho-ho!" == (greeting := "Ho-ho-ho!")
True
Он доступен начиная с 3.8 версии Python. Обычно хватает стандартного =
, однако,
в некоторых ситуациях "моржовый" удобнее. Я постараюсь это продемонстрировать
далее на конкретных примерах.
Условный оператор
Как и во многих других императивных языках программирования, в Python условный
оператор if
выглядит следующим образом:
if условие_1:
действие_1
elif условие_2:
действие_2
else:
действие_3
Блоки elif
и else
необязательны.
Проверка условий идёт сверху вниз, пока одно из них не окажется верным. Как только найдено верное, выполняется соответствующее ему действие, а программа выходит из условного оператора. Например,
if 1 == 1:
print("Первое условие")
elif 2 == 2:
print("Второе условие")
else:
print("Третее условие")
выведет только "Первое условие", хотя и второе верно.
Что может броситься в глаза и вызвать некий диссонанс, так это отсутствие
фигурных скобок вокруг действия и круглых вокруг условия. Также может показаться слегка
странным ключевое слово elif
. К примеру, на том же C
это выглядело бы так:
if (1 == 1) {
puts("Первое условие");
}
else if (2 == 2) {
puts("Второе условие");
}
else {
puts("Третее условие");
}
Функция puts
здесь использована как наиболее близкая к Python-овской print
в данном контексте – вывести строку, после чего добавить перевод строки.
Частое использование дополнительных вариантов для ветвления подтолкнуло
создателей Python к введению специального ключевого слова, которое явняелся
"склейкой" конструкции else if
– elif
.
В Python также условия можно обрамлять круглыми скобками, особенно, если условие занимает несколько строк. Однако, если у нас простое условие (к чему и стоит программисту стремиться), то незачем захламлять текст программы ненужными символами.
Пример уместного использования скобок вокруг условия:
comment_count = int(input())
lower_position = comment_count % 10
if lower_position == 1:
print(comment_count, " комментарий")
elif (
lower_position == 2
or lower_position == 3
or lower_position == 4
):
print(comment_count, " комментария")
else:
print(comment_count, " комментариев")
Подумайте, как записать 3 условия из elif
одним.
В блоке elif
мы разбили строку условия на 3 строки для удобства чтения.
Так они единообразно воспринимаются. Мы также могли это сделать с помощью
экранирования символа переноса строки:
elif lower_position == 2 \
or lower_position == 3 \
or lower_position == 4:
print(comment_count, " комментария")
Python требует, чтобы условия и команды не разбивались переводом строки.
Если же вы не смогли написать более изящный код, и вам необходимо разбить
на несколько строк команду – можно воспользоваться сибмолом \
для
разбиения строки на несколько. Согласитесь, это выглядит уродливо?
К тому же действие сливается с условием... Поэтому советую для сложных
условий использовать круглые скобки. Это правило работает не только для
условий, позднее мы увидим, где это можно также использовать для улучшения
читаемости кода.
Фигурные скобки в Python также решили устранить. Раз уж хорошей практикой стало делать отступы для обозначения блоков кода, вложенности логики, то почему бы этого не требовать, почему бы не на них и пологаться?
Так в операторе if-elif-else
для обозначения блока когда, отвечающего за действие,
мы просто добавляем отступ. Отступ может быть сделан несколькими пробелами или
символами табуляции, но хорошим тоном в сообществе Python считается использование
4-х пробельных символов.
Также, вы можете обратить внимание, что для многострочного условия я также использовал дополнительный отступ, ведь условие – также дополнительная логическая вложенность.
Саму же вложенность старайтесь держать низкой. С каждым отступом, с каждым вложенным блоком читателю нужно держать в голове всё больше контекста. Будьте краткими. Пишите проще.
Тернарная операция
Довольно спорная синтаксическая конструкция, но будет правильно рассказать и о ней.
Бывает нужно в зависимости от какого-то условия присвоить переменной то или иное значение. Для этих целей была придумана тернарная условная операция (от лат. "ternarius" — "тройной").
В некоторых языках программирования вы могли встретить его в виде:
var age = 18;
var adult = age >= 18 ? "совершеннолетний" : "несовершеннолетний";
В Python же это выглядит так:
age = 18
adult = "совершеннолетний" if age >= 18 else "несовершеннолетний"
Всё те же if
, else
, но уже другая синтаксическая конструкция. Может показаться,
что она безобидная... И в каком то смысле так и есть, пока мозг свеж,
а условие ограничивается одной строчкой, да вложенности нет. Принципиально
же отличие в ходе разбора такой конструкции мозгом. До этого мы читали
слева направо, теперь мы должны сначала разобраться в условии из центра строки,
а потом только понять, когда выполнится начало строки.
Старайтесь не путать читателя, не затруднять его работу. Тем более в мелочах.
Циклы
Те, кто пытается приблизить конец, могут его отсрочить. Те, кто хочет отсрочить конец, могут его приблизить. Возможно, этот мир только Яйцо следующей кальпы? Леин вокин? Ты не дашь родиться новому миру?
The Elder Scrolls V
Одна из задач программирования – автоматицация рутинных задач. Часто это какие-то повторяющиеся действия, которые нужно производить согласно алгоритму. И тут без циклов не обойтись – ведь именно в них мы выполняем действия итерация за итерацией.
Самым простым циклом можно считать while
– мы выполняем работу, пока
верно определённое условие. Один из простейших алгоритмов, которые
итеративно приближают нас к ответу (результату) – алгоритм Эвклида для
поиска наибольшего общего делителя:
- Если числа равны, значит мы получили НОД – возвращаем результат.
- Из большего числа вычитаем меньшее.
- Большее число заменяем на результат вычитания.
- Переходим к пункту 1.
В чистом виде наш while
. На Python это будет выглядеть так:
while a != b:
if a > b:
a -= b
else:
b -= a
print(a)
Всё, что нам надо было сделать – это инвертировать условие из пункта 1,
чтобы получить условие для while
– сколько ещё нужно крутиться в цикле.
Чуть менее удобный пример для while
– получение факториала числа.
То есть произведения всех натуральных чисел от 1, включая само число:
number = int(input())
factorial = 1
while number > 0:
factorial *= number
number -= 1
print(factorial)
Тут нам нужно, во-первых, позаботиться о подготовке начальных данных
(factorial = 1
). Во-вторых, поддерживать изменение переменной, от которой
зависит условие и логика исполнения.
Чуть лучше с этим справятся for
и range
. Первый предлагает нам
итеративный проход по чему-то с применением команд, описанных в блоке.
Второй же создаёт полуинтервал, по которому мы можем итеративно двигаться.
Нам всё также нужно инициализировать переменную factorial
, но дальше
всё идёт читаемее:
number = int(input())
factorial = 1
for current_number in range(number, 0, -1):
factorial *= current_number
print(factorial)
Подумайте, как убрать ещё и factorial
.
Чтобы базово понять, как работают for
и range
, забежим немного вперёд
и узнаем о list
– списке в Python. Его используют, когда нужно несколько
объектов объединить, чтобы после единообразно обрабатывать. Например,
список имён персонажей:
names = ['Fry', 'Leela', 'Zoidberg', 'Bender']
for name in names:
print("Hello, ", name, "!")
То есть, for
берёт поочерёдно элементы из списка (или подобного списку объекта)
и выполняет с каждым элементом действие, описанное в блоке. На самом деле, "кроличья
нора" куда глубже – мы вернёмся к этому через несколько тем.
range
же порождает объект, подобный списку. Принимает 3 параметра:
- с какого числа начинаем,
- каким числом заканчиваем (не включая),
- с каким шагом мы идём.
С помощью конструктора списков приведём range
к списку и взглянем на его
содержимое:
>>> range_example = list(range(6, 0, -1))
>>> print(range_example)
[6, 5, 4, 3, 2, 1]
Поэкспериментируйте с начальным/конечным значением полуинтервала и шагом. Попробуйте дробный шаг.
Равно как и в случае условного оператора, постарайтесь минимизировать
вложенность for
и while
в ваших программах. Чем глубже отступ,
чем глубже логическая вложенность, тем сложнее читать Вашу программу!
break, continue
Во время работы цикла могут возникнуть такие ситуации, когда нужно либо полностью остановить работу цикла, либо незаметлительно перейти к следующей итерации.
Для примера, когда нужно остановить работу цикла, возьмём нашу первую программу-калькулятор. Опуская детали, её можно доработать следующим образом:
while True:
command = input('Операция> ')
if command == '+':
...
elif command == '*':
...
+ elif commain == 'quit':
+ break
else:
print("Неизвестная операция")
Для обработки пользовательского входа мы сделали вечный цикл, не предусмотрев
варианты окончания. Однако, благодаря команде break
мы можем единообразно
добавить команду "quit", которая закончит выполнение данного кода, выйдет
из цикла.
Частый способ использования break
– прерывание потенциально вечного цикла
обработки данных.
Другой, менее радикальный способ управления потоком исполнения в цикле – continue
.
Здесь мы лишь пропускаем исполнение текущей итерации цикла. Полезно это в случаях,
если нам нужно применить это единообразное действие не ко всем элементам
исходного списка, а лишь к части. Так, например, мы можем вывести только
чётные числа:
for number in range(0, 100):
if number % 2:
continue
print(number)
Зная, как работает range
, напишите этот код короче.
И условий может быть масса. К примеру, если вам нужно сделать рассылку на всех пользователей, кто не логинился с прошлого года, чей баланс не нулевой, кто был подписан на какой-то товар, то код будет примерно следующим:
for user in users:
if user.last_login > datetime.now() - timedelta(years=1):
continue
if user.balance <= 0:
continue
if product not in user.subscribes:
continue
send_mail(user)
Это лишь синтетический пример, но Вы можете представить, сколько таких задач в мире интернет-коммерции!
Также заметьте, как в данном примере автор поборол сложные условия и излишнюю вложенность кода, при этом создав единообразный подход к фильтрации пользователей.
Внезапный else
Как-то в ревью своего кода я увидел: "Тут же не if, поправь - не будет работать". А код там был примерно следующий:
for user in users:
...
if user.is_great():
competition.winner = user
break
...
else:
log.warning("No winner selected")
competition.winner = None
Среди пользователей по каким-то показателям выбирался победитель. Если такого
нет - предупреждение в логи, и "ничто" в победителя. И этот код верный.
Другое дело, мой коллега не знал, что у while
и for
есть ещё и ветка
else
, которая срабатывает, когда не сработал break
(ну и return
, о котором
узнаем позже).
В общем, я вам рассказал о том, что в while
и for
есть ветка else
.
Использовать её или нет – думайте сами. Я после возражения моего, к слову,
team-lead'а до сих пор опасаюсь использовать эту конструкцию, дабы
не нарушать и без того шаткое душевное равновесие сих персон.