В данной главе мы подробно рассмотрим работу с самыми популярными типами данных — числами и строками.
Быстрый поиск
- Вещественные числа
- Вычисления в рациональных числах
- Случайные и псевдослучайные числа
- Строки
- Строковые методы
- Форматные строки
Вещественные числа
В данной главе большая часть обсуждения будет идти именно о вещественных числах и связанных с ними операциях. Если для обычных чисел их хранение реализовано в 64-битном формате для малых чисел и в безразмерном формате для больших 2**64, то вещественные числа хранятся в формате стандарта IEEE 754, выделяющим отдельно знаковый бит, мантиссу и экспоненту. Данный классический метод хранения вещественных чисел позволяет с неплохой точностью дискретизировать бесконечное вещественное представление чисел.
Кроме этого при данном вещественном представлении реализовано несколько удобных для работы особенностей. Первая из таких особенностей — независимость знакового бита и представления модуля значения вещественного числа, благодаря чему появляется отдельное число -0 (которого в целых числах быть просто не может по определению):
>>> -0
0
>>> -0.0
-0.0
>>> -0.0 is 0.0
<python-input-43>:1: SyntaxWarning: "is" with 'float' literal. Did you mean "=="?
False
>>> -0.0 == 0.0
True
>>>
Кроме этого в библиотеке math для определённых комбинаций бит в вещественном числе определены специальные значения: inf (и -inf) — бесконечность, как аналог самого большого по модулю числа, а также nan — Not A Number, как аналог не являющегося числом значения.
>>> from math import inf, nan
>>> inf
inf
>>> inf / 2
inf
>>> 2 / inf
0.0
>>> 2 / -inf
-0.0
>>> nan
nan
>>> nan + 1
nan
>>> nan / 2
nan
>>> 2 / nan
nan
>>>
Вообще math — огромная библиотека с разными алгоритмами, часть из которых нацелены на работу именно с вещественными числами. Погружаться в неё с головой отдельно мы не будем, однако использовать некоторые функции — более чем. Так, например, хочется ещё раз подсветить основную проблему всех компьютерных реализаций вещественных чисел — реально вещественных чисел реализовать невозможно просто потому, что невозможно хранить бесконечное число знаков, которое по определению есть в вещественных числах. Вследствие этого дискретного ограничения множество операций с вещественными числами очень сильно теряет в точности даже при, казалось бы, простых вычислениях.
>>> 1.1 + 2.2
3.3000000000000003
>>> 1.1 + 2.2 == 3.3
False
>>>
Для сравнения вещественных чисел правильнее использовать специализированные функции приближённого равенства, например, math.isclose:
>>> a, b, c = 1.1, 3.3, 4.4
>>> a + b == c
True
>>> a, b, c = 1.1, 2.2, 3.3
>>> a + b == c
False
>>> from math import isclose
>>> isclose(a + b, c)
True
>>>
Вычисления в рациональных числах
В случае, когда нет необходимости прибегать к математическим гигантам типа SciPy, NumPy или Data Science вычислениям (Pandas, TensorFlow, scikit-learn), удобно пользоваться представлениями вещественных чисел с помощью целочисленных представлений.
Первый вариант представления — описание рациональных вещественных чисел в виде целочисленных дробей, для этого используется модуль fractions. Объекты Fraction представляют из себя пары числитель-знаменатель, над которыми строится обычная целочисленная арифметика.
>>> from fractions import Fraction
>>> Fraction(1, 7)
Fraction(1, 7)
>>> type(Fraction(1, 7).numerator)
<class 'int'>
>>> type(Fraction(1, 7).denominator)
<class 'int'>
>>> a = Fraction(1, 8)
>>> a * a
Fraction(1, 64)
>>> a * 2
Fraction(1, 4)
>>> a * 2.2
0.275
>>>
Точность таких вычислений ничем не ограничена (вернее, ограничена способностями вашего устройства хранить безразмерные целые), а операции возможны как между дробями, так и между дробью и вещественным.
>>> Fraction(5, 7)
Fraction(5, 7)
>>> float(Fraction(5, 7))
0.7142857142857143
>>>
>>> Fraction(11, 10)
Fraction(11, 10)
>>> float(Fraction(11, 10))
1.1
>>> Fraction(11, 10) + Fraction(22, 10)
Fraction(33, 10)
>>> float(Fraction(11, 10) + Fraction(22, 10))
3.3
>>>
Инициализация Fraction возможна из пары значений, а также с помощью строк с записью вещественного числа или дроби.
>>> Fraction(2, 3)
Fraction(2, 3)
>>> Fraction("4.65")
Fraction(93, 20)
>>> Fraction("137/159")
Fraction(137, 159)
>>>
А вот создание Fraction от вещественных чисел хоть и возможно, но считается моветоном, поскольку Fraction получает значение какой-то точности, и эту точность пытается приблизить наилучшим образом:
>>> Fraction(1.1)
Fraction(2476979795053773, 2251799813685248)
>>>
>>> a = Fraction(1.1)
>>> bin(a.numerator)
'0b1000110011001100110011001100110011001100110011001101'
>>> bin(a.denominator)
'0b1000000000000000000000000000000000000000000000000000'
>>>
Второй вариант представления вещественных, позволяющий работать уже не только с рациональными вещественными — это моделирование числа с помощью десятичных цифр. Такой вариант всё ещё не даёт возможности делать реально вещественные числа, однако позволяет работать с абсолютно любой точностью этих значений (ограничением, опять же, выступают лишь аппаратные возможности устройства). Модуль, реализующий такую логику работы со значениями, называется decimal.
>>> from decimal import Decimal
>>> Decimal("1.1")
Decimal('1.1')
>>> d = Decimal("1.1")
>>> d ** 15
Decimal('4.177248169415651')
>>> d ** 225
Decimal('2057567827.546977662121286421')
>>>
Для более тонкой настройки вычислений и, в частности, точности значений в модуле предусмотрен специальный объект Context. В нём можно настроить правила округления, отлавливаемые исключения, а также задать точность вычислений. Однако следует помнить, что поскольку вычисления проводятся над десятичными цифрами, скорость вычислений линейно возрастает с увеличением количества знаков.
>>> from decimal import getcontext
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> getcontext().prec = 100
>>> d ** 225
Decimal('2057567827.546977662121286420820117535896306179309900875668263201766183058170822068171433263963595925')
>>>
Объекты Decimal поддерживают множество математических операций, что делает их достаточно мощным инструментом для вычислений:
>>> d = Decimal("1.1")
>>> dir(d)
['__abs__', '__add__', '__bool__', '__ceil__', '__class__', '__complex__', '__copy__', '__deepcopy__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__round__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', 'adjusted', 'as_integer_ratio', 'as_tuple', 'canonical', 'compare', 'compare_signal', 'compare_total', 'compare_total_mag', 'conjugate', 'copy_abs', 'copy_negate', 'copy_sign', 'exp', 'fma', 'from_float', 'imag', 'is_canonical', 'is_finite', 'is_infinite', 'is_nan', 'is_normal', 'is_qnan', 'is_signed', 'is_snan', 'is_subnormal', 'is_zero', 'ln', 'log10', 'logb', 'logical_and', 'logical_invert', 'logical_or', 'logical_xor', 'max', 'max_mag', 'min', 'min_mag', 'next_minus', 'next_plus', 'next_toward', 'normalize', 'number_class', 'quantize', 'radix', 'real', 'remainder_near', 'rotate', 'same_quantum', 'scaleb', 'shift', 'sqrt', 'to_eng_string', 'to_integral', 'to_integral_exact', 'to_integral_value']
>>> d.sqrt()
Decimal('1.048808848170151546991453513679937598475271857681503984875755763580005925501100691419385288933194418')
>>>
Случайные и псевдослучайные числа
Применение чисел в языке очевидно. Но если заострять внимание на конкретных модулях, позволяющих реализовывать потоковую работу с числами, нельзя не упомянуть о случайных числах. Случайные числа, вообще говоря, делятся на две совершенно разные категории: реально случайные числа, которые могут быть использованы в криптографии, и псевдослучайные числа, основная задача которых давать достаточное распределение значений для, например, тестирования или решения задач. Реально случайные числа получаются с помощью обращений к специальным устройствам генерации, встроенным в железо, с помощью модуля secrets или вызова os.getrandom. Для псевдослучайных чисел разработан свой модуль random, о котором и пойдёт речь далее.
Классическими функциями генерации псевдослучайных чисел выступают генератор из случайного распределения random() или генератор значения из диапазона randrange():
>>> from random import *
>>> random()
0.27464863480150226
>>> random()
0.3691120282224577
>>> random()*10 + 20
29.32248891932194
>>>
>>> randrange(1, 5)
4
>>> randrange(1, 5)
2
>>> randrange(1, 5)
2
>>> [randrange(2, 8) for i in range(15)]
[3, 3, 7, 3, 7, 5, 7, 7, 2, 4, 3, 2, 2, 4, 4]
>>>
Основная особенность модуля — работа с псевдослучайными последовательностями воспроизводима. Для задания стартового значения генерации (зерна) используется специальная функция seed(). Одно и то же зерно позволяет генерировать одинаковые псевдослучайные последовательности.
>>> seed(123)
>>> [randrange(2, 8) for i in range(15)]
[2, 4, 2, 5, 4, 2, 2, 5, 6, 6, 4, 4, 2, 3, 3]
>>> [randrange(2, 8) for i in range(15)]
[4, 6, 4, 7, 3, 3, 2, 5, 2, 6, 5, 2, 2, 4, 7]
>>> [randrange(2, 8) for i in range(15)]
[5, 2, 2, 2, 7, 3, 3, 2, 4, 5, 6, 5, 4, 5, 2]
>>>
>>> seed(123)
>>> [randrange(2, 8) for i in range(15)]
[2, 4, 2, 5, 4, 2, 2, 5, 6, 6, 4, 4, 2, 3, 3]
>>> [randrange(2, 8) for i in range(15)]
[4, 6, 4, 7, 3, 3, 2, 5, 2, 6, 5, 2, 2, 4, 7]
>>> [randrange(2, 8) for i in range(15)]
[5, 2, 2, 2, 7, 3, 3, 2, 4, 5, 6, 5, 4, 5, 2]
>>>
Для решения часто встречаемых задач со «случайными» действиями в модуле предусмотрены специальные методы. Так, например, choice() может использоваться для случайного выбора элемента из последовательности, sample() — для случайного выбора без возвращения:
>>> choice("qwerty")
'e'
>>> choice("qwerty")
'e'
>>> choice("qwerty")
't'
>>> choice("qwerty")
'r'
>>> sample(range(10, 100), 10)
[36, 87, 91, 77, 82, 50, 11, 60, 92, 75]
Для разового множественного выбора с возвращением может использоваться функция choises(). Кроме того, она позволяет добавлять веса к объектам, чтобы управлять вероятностями выбора значений:
>>> def two(s):
print(s, sorted(s), sep="\n")
>>> two(choices(range(1, 5), k=20)) # Равные веса
[4, 4, 3, 2, 2, 4, 3, 4, 4, 4, 1, 1, 3, 2, 1, 4, 3, 3, 1, 2]
[1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4]
>>> two(choices(range(1, 5), range(1, 5), k=20)) # Чем больше, тем вероятнее
[3, 2, 4, 4, 3, 2, 3, 3, 4, 1, 3, 4, 4, 3, 4, 4, 4, 4, 4, 2]
[1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
>>> two(choices(range(1, 5), cum_weights=range(1, 5), k=20)) # Равные кумулятивные веса
[1, 1, 2, 3, 1, 2, 3, 1, 4, 3, 4, 1, 3, 3, 2, 4, 4, 1, 2, 4]
[1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4]
>>> two(choices(range(1, 5), cum_weights=[1, 2, 5, 6], k=20)) # 3 в три раза чаще
[2, 1, 1, 3, 1, 2, 3, 3, 1, 3, 3, 2, 4, 4, 2, 3, 4, 3, 3, 1]
[1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4]
>>> two(choices("aoenlt", (2, 2, 2, 1, 1, 1), k=17))
['t', 'n', 'a', 'l', 'a', 'a', 'a', 'n', 'e', 'e', 'e', 'o', 'o', 'e', 'o', 'a', 't']
['a', 'a', 'a', 'a', 'a', 'e', 'e', 'e', 'e', 'l', 'n', 'n', 'o', 'o', 'o', 't', 't']
Строки
Подробнее обсудим строки, представляющие из себя уникальные объекты.
Для начала рассмотрим базовые особенности строк. В Python строки представляются символами Unicode — каждый символ кодируется двумя байтами. При этом поддерживается ASCII-оптимизация: если символы могут быть закодированы однобайтово — так и будет организовано.
>>> import sys
>>> a = "qwe"
>>> sys.getsizeof(a)
44
>>> a += "r"
>>> sys.getsizeof(a)
45
>>> a += "Э"
>>> sys.getsizeof(a)
68
>>> a
'qwerЭ'
>>> len(a)
5
>>> a += "i"
>>> sys.getsizeof(a)
70
>>>
Чуть забегая вперёд и вновь обращаясь к словарю globals(), затронутому в прошлой главе, можно заметить, что поскольку все идентификаторы хранятся в строковом виде, в качестве имён переменных могут выступать и строки с русскими символами:
>>> переменная = 100500
>>> a = 10
>>> a + переменная
100510
>>> globals()['переменная']
100500
>>>
Строки задаются четырьмя способами — {одинарными или тройными} {двойными или одинарными} кавычками.
>>> type('one')
<class 'str'>
>>> type("two")
<class 'str'>
>>> type('''three''')
<class 'str'>
>>> type("""four""")
<class 'str'>
>>>
Отдельное внимание хочется уделить представлению строк. Вместе с обычными символами в строках существует множество управляющих символов:
- Перенос строки со сдвигом каретки (
\n) - Табуляция (
\t) - Сдвиг каретки без переноса строки (
\r) - И т.д.
Данные символы в обычном формате вывода через print отрабатывают как управляющие, выполняя свою непосредственную задачу
>>> s = '''ONE \n \t\t three \r two'''
>>> print(s)
ONE
two three
>>>
Однако при обращении к строковой переменной из интерпретатора выводится непреобразованная raw-строка
>>> s
'ONE \n \t\t three \r two'
>>>
Причина этого — разные методы, вызываемые в этих случаях: __str__ для вывода print и __repr__ для возврата значения в интерпретаторе.
>>> print(s.__str__())
ONE
two three
>>> print(s.__repr__())
'ONE \n \t\t three \r two'
>>>
Для работы с raw-строкой можно также явно задать её с помощью специального идентификатора в начале строки. Это же позволяет избавиться от бесконечно накапливающихся символов \:
>>> print("qwert\nqwerty")
qwert
qwerty
>>> print("qwert\\nqwerty")
qwert\nqwerty
>>> print(r"qwert\\nqwerty")
qwert\\nqwerty
>>>
Продолжая разговор о строках, нельзя не упомянуть, что они удовлетворяют протоколу последовательностей. Более того, поскольку типа char в понимании символьного типа в Python не существует, строки фактически состоят из строк, что позволяет вести бесконечную итерацию по ним.
>>> s = "qwertyui"
>>> s[2]
'e'
>>> s[2:5]
'ert'
>>> s + "asdfgh"
'qwertyuiasdfgh'
>>> s[5][0][-1][0]
'y'
>>>
Главное отличие строк от других последовательностей — поиск принадлежности не только элемента, но и подстроки с помощью алгоритма Кнута — Морриса — Пратта за линейное время:
>>> "qwe" in "asfdgbsndgqwezdbsbc"
True
>>> s = "asfdgbsndgqwezdbsbc"
>>> s.index("qwe")
10
>>>
Строковые методы
Непосредственно строковых методов огромное количество, мы лишь быстро пройдём по группам некоторых из них, и отдельно рассмотрим особенно интересующие нас.
Среди строковых методов есть:
- Упрощение поиска по префиксам-суффиксам;
- Аналитика элементов строки;
- Определение свойств символов в строке;
- Выравнивания;
- Преобразования строк;
- Замена (s.replace)
>>> lst = ["Hello", "Help", "supelp"]
>>> print(*((s.startswith("He"), s.endswith("elp")) for s in lst))
(True, False) (True, True) (False, True)
>>> s = "QWeQrTY"
>>> print(s.find('e'), s.index('TY'), s.count('Q'))
2 5 2
>>> print('123'.isdigit(), 'QWE'.islower())
True False
>>> print('right'.rjust(10), 'center'.center(10), 'left'.ljust(10), sep=r'\\', end='&\n')
right\\ center \\left &
>>> print('real small text'.title(), "QKRQnotused".removesuffix('notused'))
Real Small Text QKRQ
>>> 'I never like flowers'.replace('never', 'really')
'I really like flowers'
>>>
Отдельное внимание хочется выделить трём методам. Первый — токенизатор строки partition. Получая подстроку-разделитель, он возвращает кортеж из трёх частей: подстроки до разделителя, разделителя и подстроки после разделителя.
>>> s = "qre AFSDSF SDGDSG SDGDSB"
>>> s.partition(" ")
('qre', ' ', 'AFSDSF SDGDSG SDGDSB')
>>>
Второй и третий методы идут в паре: split — для разбивания строки по подстроке-сепаратору — и join — для сборки с разделителем-строкой
>>> s = "qwer asdf zxcv qwer asdf"
>>> a = s.split()
>>> a
['qwer', 'asdf', 'zxcv', 'qwer', 'asdf']
>>> a = s.split('s')
>>> a
['qwer a', 'df zxcv qwer a', 'df']
>>>
>>> ' '.join(a)
'qwer a df zxcv qwer a df'
>>> '@@'.join(a)
'qwer a@@df zxcv qwer a@@df'
>>>
Форматные строки
Идея предобработки выводимой строки тянется ещё из Си, где каждый вывод состоял из форматной строки с описанием выводимых параметров. Ещё в Python 2 появился аналог форматных Си-строк, записываемый похожим образом и сохранившийся до сих пор:
>>> "%d %s" % (12, "qwe")
'12 qwe'
>>>
Позже появилась отдельная функция format, занимающаяся парсингом строки и формированием вывода. Она позволяла не просто указывать параметры, но и задавать их свойства в более удобном виде, а также управлять порядком вывода:
>>> "{} * {} = {}".format(2, 3, 6)
'2 * 3 = 6'
>>> "{2} * {1} = {0}".format(2, 3, 6)
'6 * 3 = 2'
>>>
Современные форматные строки (они отличаются специальным символом f перед строкой) позволяют описывать параметры прямо в строке и ведут себя практически как упрощённый eval:
>>> n = 100
>>> f"qwerqwrq {n} qwasfaf"
'qwerqwrq 100 qwasfaf'
>>> f"qwerqwrq {n+2} qwasfaf"
'qwerqwrq 102 qwasfaf'
>>> f"qwerqwrq {nn+2} qwasfaf"
Traceback (most recent call last):
File "<python-input-158>", line 1, in <module>
f"qwerqwrq {nn+2} qwasfaf"
^^
NameError: name 'nn' is not defined. Did you mean: 'nan'?
>>>
Использование форматных строк позволяет просто и удобно работать с параметрами и выводить их в удобном формате. Язык форматирования достаточно объёмный, ключевыми для базового использования элементами выступают выравнивание и заполнители:
>>> a, b, c = 5, 6, 7
>>> f"a + b * c == {a + b * c}"
'a + b * c == 47'
>>> f"{a} + {b} * {c} == {a + b * c}"
'5 + 6 * 7 == 47'
>>>
>>> a = 123.456
>>> f"{a:011}/{a:^11}/{a:<011}/{a:>011}"
'0000123.456/ 123.456 /123.4560000/0000123.456'
>>> s = "QWR"
>>> f"{s:~^10}"
'~~~QWR~~~~'
>>>
Последние изменения, связанные с f-строками расширили возможности их обработки, в частности, позволило работать с вложенными форматными структурами:
>>> a, n = 12345, 10
>>> f"{a:{n}}"
' 12345'
>>> a, c, n = 12345, '0', 10
>>> f"{a:{c}{n}}"
'0000012345'
>>> a, c, n = 12345, ' ', 10
>>> f"{a:{c}{n}}"
' 12345'
>>>