View on GitHub

Methodics of Python Intro

Practical materials for learning Python

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

Быстрый поиск


Вещественные числа

В данной главе большая часть обсуждения будет идти именно о вещественных числах и связанных с ними операциях. Если для обычных чисел их хранение реализовано в 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) — бесконечность, как аналог самого большого по модулю числа, а также nanNot 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'>  
>>>

Отдельное внимание хочется уделить представлению строк. Вместе с обычными символами в строках существует множество управляющих символов:

Данные символы в обычном формате вывода через 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  
>>>

Строковые методы

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

Среди строковых методов есть:

>>> 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'
>>>