В данной лекции рассказывается о том, какие операции (арифметические, логические и другие) можно выполнять над переменными и константами (литералами) в Python, какие типы они должны иметь, чтобы операцию можно было выполнить, а также какие преобразования осуществляет интерпретатор, если в одной операции используются значения разных типов. Не все операции языка Python описываются в этой лекции - некоторые будут рассмотрены позднее, когда мы познакомим читателя с аспектами языка, в рамках которых они используются.
Операция - это некоторое действие над переменными и константами в языке программирования, зачастую аналогичное соответствующей математической операции. Операция принимает на вход один или несколько аргументов, называемых также операндами, и возвращает некоторый результат. Например, в операции a + 1
операндами являются переменная a
и константа 1
. Результатом, возвращаемым данной операцией, будет их сумма.
По количеству принимаемых аргументов операции в языке программирования Python делятся на две группы:
Для каждой операции определены типы данных, которые могут быть использованы для ее операндов. Если операция вызывается с переменной или константой неподдерживаемого типа, интерпретатор генерирует исключение TypeError
. Ниже, когда будут подробно рассматриваться имеющиеся в Python операции, мы будем обращать ваше внимание на то, какие типы могут иметь их операнды. Как правило, эти ограничения естественны и интуитивно понятны. Рассмотрим такой пример:
s1 = 'hello'
s2 = 'world'
s1 / s2
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-1-826caff4b057> in <module>() 1 s1 = 'hello' 2 s2 = 'world' ----> 3 s1 / s2 TypeError: unsupported operand type(s) for /: 'str' and 'str'
В этом примере мы пытаемся выполнить операцию /
(деление) над переменными с типами str
. Очевидно, что деление строки на строку лишено смысла, поэтому интерпретатор отказывается выполнять такой код и генерирует исключение.
По сути после выполнения операции (например, a + 1
), в том месте, где она была в программе, вместо нее оказывается некоторая переменная, создаваемая самим интерпретатором и содержащая результат операции. Эта внутренняя переменная, как и любая другая, имеет свой тип данных, который определяется исходя из следующего:
Для каждой конкретной операции, рассматриваемой ниже, мы расскажем, какой тип данных имеет ее результат. При этом помните о функции type
, с помощью которой вы сами можете легко получить эту информацию:
a = 1
b = 2
type(a + b) # посмотрим, какой тип имеет результат сложения двух целых чисел
int
Рассматриваемые в данном разделе операции могут применяться для операндов с числовыми типами данных, например int
, float
, complex
и Decimal
. Для большинства из представленных операций результат будет иметь тот же тип данных, что и тип операндов, участвующих в ней. Исключением является операция деления /
, при выполнении которой с целочисленными операндами (тип int
) результат будет иметь тип float
.
Название |
Синтаксис | Описание |
---|---|---|
Сложение |
x + y |
Складывает число x и число y |
Вычитание |
x - y |
Вычитает из числа x число y |
Умножение |
x * y |
Умножает число x на число y |
Деление |
x / y |
Делит число x на число y |
Целочисленное деление |
x // y |
Делит число x на число y, отбрасывая дробную часть |
Получение остатка (mod) |
x % y |
Возвращает остаток от деления числа x на число y |
Возведение в степень |
x ** y |
Возводит число x в степень y |
Минус |
-x |
Изменяет знак числа x |
С помощью этих операций можно записывать арифметические выражения любой сложности. Очевидно, что для таких составных выражений интерпретатору нужно решить, в каком порядке выполнять операции. Для этого в Python каждая операция (не только арифметическая, но и любая другая) имеет такое свойство, как приоритет. Если в одном составном выражении встречаются различные операции, то интерпретатор выполняет их в порядке уменьшения приоритета (вначале более приоритетные, затем менее). При этом, если на каком-то уровне приоритета есть несколько операций, то интерпретатор выполняет их слева направо (за редким исключением, см. пример про возведение в степень далее).
Для арифметических операций приоритеты установлены следующим образом (упорядочено от большего приоритета к меньшему):
**
-
*
, /
, //
, %
+
, -
Попробуйте самостоятельно разобраться, в какой последовательности выполнялись операции в следующем выражении:
a = 1 - 3 / 3 + 3 * 4 + -2 ** 2 / 2
a
10.0
С помощью обычных скобок ()
можно указать интерпретатору, в какой последовательности выполнять операции (так же, как это делается в математике):
a = 1 + 2 * 3
b = (1 + 2) * 3
a, b
(7, 9)
Как видите, значение переменной a
высчитывалось исходя из приоритетов операций (поэтому умножение выполнилось раньше сложения), а в выражении для переменной b
мы явно указали, что вначале должно выполниться сложение. Как и в математике, выражения в скобках можно вкладывать друг в друга, при этом вычисляются они от самых внутренних скобок к наружним.
Скобки ()
следует использовать всегда, когда выражение является достаточно сложным, содержащим несколько различных операций - это упрощает чтение исходного кода, так как не нужно вспоминать, какой приоритет имеет каждая операция, входящая в выражение. Давайте с их помощью перепишем первый пример из этого раздела, сохранив порядок выполнения операций тем же:
a = 1 - (3 / 3) + (3 * 4) + ((-(2 ** 2)) / 2)
a
10.0
Наиболее сложный момент в этом выражении заключается в вычислении -2 ** 2
. Поторопившись, можно подумать, что это будет 4 (минус 2 в квадрате), однако внимательно посмотрев на приоритеты операций, становится понятно, что операция -
выполняется после возведения в степень, поэтому вначале 2 возводится в квадрат, а затем берется минус от результата. Обратите внимание, насколько понятнее стало выражение после того, как мы расставили в нем скобки.
Для всех бинарных арифметических операций существуют комбинированные инструкции присваивания, имеющие вид "x op= y", где вместо op стоит конкретная операция. Комбинированные присваивания являются просто более короткой формой записи выражений вида "x = x op y":
a = 10
a += 1 # эквивалентно a = a + 1
b = 3
c = 5
b *= c + 1 # эквивалентно b = b * (c + 1)
a, b
(11, 18)
В заключение отметим особенность операции возведения в степень **
: если их несколько в одном выражении, то они выполняются не слева направо, а наоборот, справа налево:
a = 2 + 2 + 3 # эквивалентно (2 + 2) + 3
b = 2 ** 2 ** 3 # эквивалентно 2 ** (2 ** 3)
a, b
(7, 256)
Как известно, вся информация в компьютере хранится в двоичном виде, т.е. представляет из себя набор битов, каждый из которых может принимать одно из двух значений: 0 или 1. Такой способ представления информации был выбран не случайно - электронные схемы при работе с двоичными данными должны уметь надежно отличать друг от друга лишь два уровня напряжения: низкое (для 0) и высокое (для 1). Это значительно упрощает и удешевляет производство микросхем.
По причине того, что внутри компьютера все данные имеют двоичный формат, многие языки программирования (и Python в их числе) предоставляют операции для работы непосредственно с битами. Операнды битовых операций должны иметь целочисленный тип данных (int
или bool
), при этом тип результата будет соответствовать типу операндов. Для всех бинарных битовых операций можно использовать комбинированные инструкции присваивания, то есть вместо "x = x op y" писать "x op= y".
Название |
Синтаксис | Описание |
---|---|---|
Битовое AND (И) |
x & y |
Для каждого бита с номером n в двоичном представлении x выполняет x(n) & y(n) |
Битовое OR (ИЛИ) |
x | y |
Для каждого бита с номером n в двоичном представлении x выполняет x(n) | y(n) |
Битовое XOR (исключающее ИЛИ) |
x ^ y |
Для каждого бита с номером n в двоичном представлении x выполняет x(n) ^ y(n) |
Сдвиг влево |
x << y |
Свигает все биты в двоичном представлении x на y позиций влево |
Сдвиг вправо |
x >> y |
Свигает все биты в двоичном представлении x на y позиций вправо |
Инвертация битов |
~x |
Инвертирует (заменяет 0 на 1 и наоборот) все биты в двоичном представлении x |
Объясним теперь, как работают битовые операции AND, OR и XOR для отдельно взятых двух битов. Это удобно делать в виде таблицы:
AND |
bit1 = 0 | bit1 = 1 | OR |
bit1 = 0 | bit1 = 1 | XOR |
bit1 = 0 | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
bit2 = 0 | 0 | 0 | bit2 = 0 | 0 | 1 | bit2 = 0 | 0 | 1 | ||||||||
bit2 = 1 | 0 | 1 | bit2 = 1 | 1 | 1 | bit2 = 1 | 1 | 0 |
С помощью этой таблицы легко можно увидеть, что операция битового AND возвращает для двух битов 1 только в том случае, если оба они равны 1, а операция OR - если хотя бы один равен 1. Для операндов с типом bool
считается, что True
это 1, а False
- 0.
Приоритеты битовых операций следующие (от наибольшего к наименьшему):
~
<<
, >>
&
^
|
Рассмотрим пример, демонстрирующий работу данных операций. Заметим, что в нем мы используем функцию format
для того, чтобы выводить значения переменных в двоичном формате. Кроме того желательно, чтобы читатель имел представление о том, как десятичные числа преобразуются в двоичные и наоборот (оба эти преобразования можно найти здесь).
a = 0b1001
b = 0b0011
print('a = {:04b}'.format(a))
print('b = {:04b}'.format(b))
print('a & b = {:04b}'.format(a & b)) # здесь выполняется операция a & b, а затем ее результат передается функции format
print('a | b = {:04b}'.format(a | b))
print('a ^ b = {:04b}'.format(a ^ b))
a = 1001 b = 0011 a & b = 0001 a | b = 1011 a ^ b = 1010
Часто в программировании приходится отслеживать и в нужные моменты изменять состояние некоторого процесса. Это состояние удобно описывать набором флагов - специальных значений, у которых все биты, кроме одного, равны 0.
В качестве примера рассмотрим гипотетическую программу, предназначенную для отправки текстовых сообщений. Состояние каждого сообщения может быть описано произвольной комбинацией следующих флагов (обратите внимание, что для каждого флага мы используем свой бит в двоичном представлении!):
MSG_CREATED = 0b000001 # сообщение создано
MSG_DELIVERED = 0b000010 # сообщение доставлено получателю
MSG_READ = 0b000100 # сообщение прочитано получателем
MSG_EDITED = 0b001000 # сообщение было отредактировано
MSG_FORWARDED = 0b010000 # сообщение было отправлено еще кому-то, кроме первоначального получателя
MSG_REPLIED = 0b100000 # на сообщение был отправлен ответ
state = 0
# ... пользователь набрал текст сообщения
state |= MSG_CREATED
# ... сообщение было доставлено
state |= MSG_DELIVERED
# ... сообщение было прочитано
state |= MSG_READ
# ... на сообщение был отправлен ответ
state |= MSG_REPLIED
print('state = {:06b}'.format(state))
state = 100111
Теперь мы очень просто можем проверять, наступили или нет для сообщения определенные события:
# is_delivered не равно 0 только если флаг MSG_DELIVERED установлен, т.е. сообщение доставлено
# заметим, что нас не особо интересует точное значение is_delivered, а только то, равно оно нулю или нет
is_delivered = state & MSG_DELIVERED
# is_edited не равно 0 только если флаг MSG_EDITED установлен, т.е. сообщение было отредактировано
is_edited = state & MSG_EDITED
is_delivered, is_edited
(2, 0)
Битовые операции сдвигов используются нечасто в программах на языках высокого уровня, тем не менее рассмотрим небольшой пример, поясняющий их работу:
a = 0b0101
b = 0b1001
print('a << 1 = {:04b}'.format(a << 1))
print('b >> 2 = {:04b}'.format(b >> 2))
a << 1 = 1010 b >> 2 = 0010
Операция a << 1
сдвинула все биты в двоичном представлении переменной a
на одну позицию влево, при этом дописав справа один нулевой бит. Операция b >> 2
сдвинула все биты переменной b
вправо на две позиции, при этом младшие два бита b
были отброшены, а слева дописаны два нулевых бита.
Если вы внимательно ознакомились с преобразованием чисел в двоичную систему счисления, то для вас будет очевидным тот факт, что операция сдвига переменной влево на $n$ позиций эквивалентна умножению ее же на $2^n$:
a = 5
b = a << 3
c = a * (2 ** 3)
b, c
(40, 40)
Такой прием применяется опытными разработчиками в участках кода, где требуется максимальное быстродействие программы. Дело в том, что битовые операции наиболее быстро выполняются процессором, и выигрыш от использования сдвига влево вместо умножения может достигать десятки и сотни раз.
Язык программирования Python предоставляет стандартный набор операций сравнения с предсказуемой семантикой. Их операторы могут иметь целочисленный тип данных (int
, bool
), тип данных с плавающей точкой (float
, complex
, Decimal
), а также строковый тип данных str
, для которого сравнения выполняются лексикографически (так, как принято в словарях). Результат имеет булевый тип данных bool
и принимает значение True
, если сравнение верно и False
в противном случае. Все операции сравнения имеют одинаковый приоритет.
Название |
Синтаксис | Описание |
---|---|---|
Меньше |
x < y |
True, если x меньше y, иначе False |
Меньше либо равно |
x <= y |
True, если x меньше или равен y, иначе False |
Больше |
x > y |
True, если x больше y, иначе False |
Больше или равно |
x >= y |
True, если x больше или равен y, иначе False |
Равно |
x == y |
True, если x равен y, иначе False |
Не равно |
x != y |
True, если x не равен y, иначе False |
Мы еще раз вернемся к операциям сравнения в следующем разделе, посвященном логическим операциям, а пока ограничимся простым примером:
a = 0
b = 1
s1 = 'hello'
s2 = 'hello'
s3 = 'world'
b1 = True # для булевых значений считается, что True > False
b2 = False
a < b, a >= b - 1, s1 != s2, s1 > s3, b1 > b2
(True, True, False, False, True)
Логические операции пришли в языки программирования из таких областей науки, как дискретная математика, математическая логика и исчисление высказываний. Всего в Python три логических операции. Каждая из них в качестве аргументов принимает переменные и константы типа данных bool
и возвращает результат такого же типа. Операции and
и or
также могут использоваться и с операндами типа int
, float
и complex
(в этом случае их результат имеет соответствующий операндам тип), однако необходимость в этом возникает крайне редко, и мы рассмотрим этот частный случай отдельно в конце раздела.
Название |
Синтаксис | Описание |
---|---|---|
Логическое AND (И) |
x and y |
True, если x и y равны True, иначе False |
Логическое OR (ИЛИ) |
x or y |
True, если x или y равен True, иначе False |
Логическое NOT (НЕ) |
not x |
True, если x равен False, иначе False |
По аналогии с битовыми операциями, значения логических также удобно представлять в виде таблицы (обратите внимание на связь между операциями and
и &
, а также or
и |
- по сути они идентичны, только одни работают с логическими значениями True
и False
, а другие - с битами 0 и 1):
AND |
False | True | OR |
False | True | NOT |
False | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
False | False | False | False | False | True | True | False | |||||||||
True | False | True | True | True | True |
Приоритеты логических операций следующие (от наибольшего к наименьшему):
not
and
or
Логические операции часто применяются вместе с операциями сравнения. Рассмотрим, например, как определить, что значение переменной попадает в определенный интервал:
start = 0.0
end = 100.0
point = 33.8
# логические операции имеют самый низкий приоритет по сравнению с остальными операциями, поэтому в следующем
# выражении скобки ставить не обязательно
is_internal_point = point > start and point < end
is_internal_point, type(is_internal_point)
(True, bool)
В выражении point > start and point < end
происходит следующее:
point
чем start
(результат True
, потому что 33.8 > 0.0)point
чем end
(результат True
, потому что 33.8 < 100.0)and
(результат True
, потому что оба ее операнда равны True
)В языке Python поддеживается более простой (и более естественный) способ проверить, что значение попадает в некоторый интервал:
start = 0.0
end = 100.0
point = 33.8
is_internal_point = start < point < end
is_internal_point
True
Теперь давайте немного усложним пример предыдущий пример. Пусть нам теперь нужно определить, что point
лежит либо в интервале от $(0;10)$, либо в интервале $(90;100)$:
start1 = 0.0
end1 = 10.0
start2 = 90.0
end2 = 100.0
point = 9.1
# в скобках определяем принадлежит ли point первому или второму интервалу, затем объединяем результат
# операцией or, которая дает True, если хотя бы один из операндов равен True
is_internal_point = (start1 < point < end1) or (start2 < point < end2)
is_internal_point
True
Важной особенностью логических операций является то, что они вычисляются по так называемой короткой схеме:
and
равен False
, то правый не вычисляется (что логично, так как он уже не окажет влияние на результат операции and
).or
равен True
, то правый не вычисляется (аналогично, он уже не окажет влияние на результат операции or
)Это означает, что в примере выше сравнения в правых скобках вообще не проверялись, так как уже после вычисления результата сравнений в левых скобках оказалось, что он равен True
, а значит и результат всей операции or
будет True
.
Как мы говорили в самом начале этого раздела, логические операции and
и or
можно использовать и для значений типов int
, float
и complex
. Действуют они таким образом:
and
возвращает 0, если один из операндов 0, или значение операнда справа, если оба операнда не 0or
возвращает 0, если оба операнда 0, значение левого операнда, если он не 0, иначе - значение правого операндаa = 0
b = 1.0
c = -2
d = 1 + 2j
a and b, a or b, b and c, d or b, a or a
(0, 1.0, -2, (1+2j), 0)
Посмотрим, какой тип имеет результат логических операций в этом случае:
type(a and b), type(a or b), type(b and c), type(d or b), type(a or a)
(int, float, int, complex, int)
Строковые операции принимают в качестве операндов строковый тип данных str
и возвращают результат такого же типа. Для перечисленных ниже операций возможно использование комбинированных инструкций присваивания.
Название |
Синтаксис | Описание |
---|---|---|
Конкатенация |
x + y |
Возвращает строку, составленную из строки x и приписанной к ее концу строки y |
Дублирование |
x * y |
Возвращает строку x, продублированную y раз |
Обратите внимание, что для представленных строковых операций используются знаки +
и *
, которые выше применялись для операций сложения и умножения чисел. Интерпретатор может понять, какую операцию выполнить, по тому, какой тип данных имеют ее операнды. Например, для чисел +
означает сложение, а для строк - конкатенацию. Это не единственный пример, когда один и тот же знак используется для разных операций, как будет видно в других лекциях. При этом приоритет их остается тем же, что и для операций с числами (для строк это означает, что конкатенация имеет меньший приоритет, чем дублирование).
s1 = 'hello, '
s2 = 'world'
s3 = '!'
s4 = 'result: '
s4 += s1 + s2 + s3 * 3
s4
'result: hello, world!!!'
Срезом строки (англ. slice) в языке Python называется некоторое подмножество ее символов. Операцией получения среза является операция []
, которая имеет три формы записи:
Значения start, end и step должны быть переменными или константами с целочисленным типом данных int
. Также обратите внимание, что во второй и третьей форме записи символ с индексом end не попадает в результат (у интервала открытая правая граница)!
Символы в строке пронумерованы следующим образом (можно обращаться к ним как с помощью верхних положительных индексов, так и с помощью нижних отрицательных):
Простейшее использование операции среза заключается в получении одного символа из строки по его индексу:
s = 'ABC DEF GHI'
s[0], s[-11], s[5], s[-6]
('A', 'A', 'E', 'E')
Вторая форма записи операции взятия среза позволяет сразу получить целую подстроку:
s[4:7], s[-7:-4]
('DEF', 'DEF')
При таком способе записи мы можем опустить любой из индексов: если не указать начальный индекс, то вместо него будет использован 0, если конечный, то вместо него будет использоваться индекс на 1 больше индекса последнего элемента (в нашем примере 10 + 1):
s[:5], s[5:], s[:] # последний срез возвращает всю строку
('ABC D', 'EF GHI', 'ABC DEF GHI')
Наконец, третья форма записи позволяет получить строку, которая заполняется символами оргинальной строки, взятыми с определенным шагом:
# получаем символы от начала и до конца указанного интервала, начиная с первого и с шагом 4 (т.е. берем
# первый символ, "шагаем" на 4 позиции вперед, берем следующий символ и так далее, пока не выйдем за
# границы интервала или не достигнем его конца)
s[0:7:4]
'AD'
Если в качестве шага используется отрицательный индекс, то интерпретатор просматривает строку в обратном направлении:
# получаем строку, состоящую из символов s[6], s[5] и s[4]
s[6:3:-1]
'FED'
В третьей форме записи также можно не указывать параметры start и end. В этом случае для положительного step строка просматривается с первого символа до конца строки, а для отрицательного - с последнего и до начала:
s[::2], s[::-2]
('ACDFGI', 'IGFDCA')
Часто операция получения среза используется вместе с операцией конкатенации для изменения некоторого символа в строке:
s = 'there is a miscake in the string'
s = s[:14] + 't' + s[15:] # 'there is a mis' + 't' + 'ake in the string'
s
'there is a mistake in the string'
Несмотря на то, что язык программирования Python относится к языкам со строгой типизацией, в нем существует несколько неявных преобразований типов данных, которые считаются безопасными и не приводящими к ошибкам в программах. Безопасными считаются такие преобразования, при которых не происходит потеря важной информации, например:
Неявные преобразования типов используются интерпретатором в следующих ситуациях:
В случае, если интерпретатор Python не смог произвести преобразование типов, он генерирует исключение TypeError
.
Перечислим преобразования типов, считающиеся безопасными в языке программирование Python:
True
преобразуется в 1, а False
в 0)True
, а 0 или пустая строка - в False
)Работу преобразований второго типа мы увидим, когда будем проходить инструкцию ветвления и циклы в языке программирования Python. Пока приведем примеры преобразований первого типа:
a = 2
b = 2.5
c = a * b # a преобразуется в float (int -> float)
c, type(c)
(5.0, float)
a = 2
b = 4.6 + 8j
c = b / a # a преобразуется в complex (int -> float -> complex)
c, type(c)
((2.3+4j), complex)
Несколько более неожиданный результат, связанный со спецификой преобразования булевого типа, демонстрирует следующий пример:
a = True
b = False
c = 1
d = 5.0
r1 = a + c # c преобразуется в int (bool -> int), True -> 1
r2 = b * d # b преобразуется в float (bool -> int -> float), False -> 0
r1, type(r1), r2, type(r2)
(2, int, 0.0, float)
В конце обратим ваше внимание на то, что в языке программирования Python отсутствует неявное преобразование из строки в число, поэтому выполнение такого кода завершается генерацией исключения TypeError
:
a = 10
s = '20'
a + s
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-31-abc0a29c9581> in <module>() 1 a = 10 2 s = '20' ----> 3 a + s TypeError: unsupported operand type(s) for +: 'int' and 'str'
Несмотря на то, что для представленного выше примера неявное преобразование из строки в число скорее всего было бы тем, чего и хотел программист, зачастую оно может приводить к труднообнаруживаемым ошибкам, например, если в строке случайно окажется не цифровой символ, а буквенный.
point
равно -1.0?