Базовые типы данных: числа, строки и булевы значения
Повторение работы с неизменяемыми базовыми типами и их ключевыми методами.
Добро пожаловать в третий урок курса! Сегодня мы переходим от самых основ к уровню Intermediate. Тема нашего урока — базовые типы данных: целые числа, вещественные числа, строки и булевы значения. Казалось бы, что может быть проще? Вы уже знаете, как написать x = 5 или name = 'Alice'. Однако профессиональная разработка требует понимания того, что происходит под капотом. В Python все является объектом. Когда вы присваиваете переменной число, вы не просто выделяете ячейку памяти с битами, вы создаете сложную структуру на языке C (если мы говорим о CPython), которая включает в себя счетчик ссылок, указатель на тип и само значение. Это понимание критически важно для написания оптимизированного кода. В языках со статической типизацией, таких как C++ или Java, примитивные типы (например, int или double) хранятся непосредственно в стеке и занимают фиксированный объем памяти (например, 4 или 8 байт). В Python примитивов в традиционном понимании не существует. Каждое число, каждая строка — это полноправный объект со своими методами и атрибутами. Это дает колоссальную гибкость, но требует определенных затрат памяти и процессорного времени. Мы детально разберем концепцию неизменяемости (immutability). Неизменяемость означает, что после того, как объект создан в памяти, его состояние нельзя изменить. Любая операция, которая на первый взгляд «изменяет» число или строку, на самом деле создает совершенно новый объект в другой ячейке памяти и перенаправляет ссылку вашей переменной на этот новый объект. Почему Гвидо ван Россум (создатель Python) принял такое решение? Во-первых, неизменяемые объекты потокобезопасны. Вам не нужно блокировать доступ к числу, если несколько потоков пытаются его прочитать, потому что никто не сможет его изменить. Во-вторых, это позволяет использовать неизменяемые типы в качестве ключей для словарей (хэш-таблиц), так как их хэш-сумма никогда не изменится на протяжении всей их жизни. В этом уроке мы применим методики Microlearning (короткими смысловыми блоками) и Active Recall (постоянные проверки памяти), чтобы эти фундаментальные концепции навсегда закрепились в вашем арсенале.
Концепция Неизменяемости (Immutability) и функция id(). Чтобы доказать, что базовые типы в Python действительно неизменяемы, мы воспользуемся встроенной функцией id(). Функция id() возвращает уникальный идентификатор объекта в памяти (в CPython это буквально адрес ячейки оперативной памяти). Представьте, что у вас есть переменная a = 10. Вы прибавляете к ней единицу: a = a + 1. Если бы тип int был изменяемым, адрес памяти остался бы прежним, а значение внутри ячейки поменялось бы на 11. Но если вы проверите id(a) до и после сложения, вы увидите два совершенно разных числа. Это означает, что старый объект (число 10) остался в памяти, а для числа 11 был создан новый объект. Переменная a просто получила новую «бирку» со ссылкой на новый объект. Что же происходит со старым объектом 10? Если на него больше не ссылается ни одна переменная, в дело вступает сборщик мусора (Garbage Collector). Он отслеживает так называемый «счетчик ссылок» (Reference Count). Как только счетчик падает до нуля, сборщик мусора освобождает память. Это автоматическое управление памятью делает Python невероятно удобным, но оно же может стать причиной утечек памяти в сложных циклических структурах (хотя для чисел и строк это не актуально). Понимание этой механики является краеугольным камнем для разработчика уровня Intermediate. Это объясняет, почему конкатенация строк в цикле (когда вы пишете string += new_part тысячу раз) работает так медленно: на каждой итерации Python вынужден выделять новую память и копировать туда старую строку плюс новую часть. Вместо этого профессионалы используют списки (которые изменяемы) для сбора частей, а затем объединяют их методом .join(). Мы рассмотрим эту оптимизацию позже, но фундамент для нее закладывается именно здесь, в понимании неизменяемости. Давайте посмотрим на код, иллюстрирующий поведение функции id().
a = 10
print(f'Изначальный id: {id(a)}')
a = a + 1
print(f'id после сложения: {id(a)}')
# Вывод покажет разные числа. Значит, объект изменился полностью.
Почему в Python базовые типы (числа, строки) сделаны неизменяемыми?
Целые числа: тип int и произвольная точность. В большинстве классических языков программирования, таких как C, Java или C#, целые числа имеют жесткие ограничения по размеру. Например, 32-битное целое число (int) может хранить значения от -2 147 483 648 до 2 147 483 647. Если вы попытаетесь прибавить единицу к максимальному значению, произойдет так называемое «переполнение» (integer overflow), и число магическим образом превратится в отрицательное. В Python 3 такого понятия не существует. Тип int в Python обладает произвольной точностью (arbitrary-precision). Это означает, что размер числа ограничен только объемом доступной оперативной памяти вашего компьютера. Вы можете легко возвести 2 в 10000-ю степень, и Python честно вычислит и сохранит это гигантское число. Как это реализовано на уровне интерпретатора? Внутри CPython (стандартной реализации Python) целое число больше не является просто одной машинной ячейкой. Вместо этого большое число разбивается на части (цифры по основанию 2^30), и эти части хранятся в виде динамического массива. При выполнении математических операций Python применяет алгоритмы сложения или умножения массивов, аналогичные тому, как мы считаем столбиком в школе, только намного быстрее. Конечно, за эту магию приходится платить производительностью. Арифметические операции над целыми числами в Python выполняются медленнее, чем в C, потому что интерпретатору постоянно приходится управлять массивами памяти, а не просто отправлять биты в регистры процессора. Для подавляющего большинства задач веб-разработки, автоматизации или анализа данных эта разница незаметна. Но если вы пишете высоконагруженные математические алгоритмы или движки для игр, эта особенность может стать узким местом. В таких случаях программисты Python используют библиотеку NumPy, которая реализует массивы чисел фиксированного размера на C, обходя ограничения стандартного типа int в Python. Давайте воспользуемся функцией sys.getsizeof(), чтобы посмотреть, как растет потребление памяти при увеличении числа.
import sys
# Обычное маленькое число
small_num = 42
print(f'Размер числа 42: {sys.getsizeof(small_num)} байт') # Обычно 28 байт
# Гигантское число
huge_num = 2 ** 1000
print(f'Размер числа 2^1000: {sys.getsizeof(huge_num)} байт') # Обычно 160 байт
Кэширование малых чисел (Small Integer Caching). Мы уже обсудили, что каждое число в Python — это объект, создание которого требует выделения памяти. Представьте, что в вашем коде постоянно используются нули, единицы, счетчики для циклов от 1 до 100. Если бы Python каждый раз выделял новую память под эти числа, производительность языка резко бы упала. Чтобы избежать этого, создатели CPython внедрили гениальную оптимизацию: кэширование малых целых чисел. При запуске интерпретатора Python автоматически создает массив объектов для всех целых чисел в диапазоне от -5 до 256 включительно. Эти объекты остаются в памяти на протяжении всей работы программы. Когда вы пишете a = 100, интерпретатор не создает новый объект числа 100. Он просто берет готовую ссылку на объект из кэша. Если вы напишете b = 100, переменные a и b будут указывать на один и тот же адрес в памяти. Вы можете проверить это с помощью оператора is. Оператор is проверяет не равенство значений, а равенство идентификаторов объектов (указывают ли они на одну и ту же ячейку памяти). А вот если вы возьмете число вне этого диапазона, например, 257, то поведение изменится (хотя современные версии Python иногда оптимизируют это на уровне компиляции отдельного блока кода). Если выполнить x = 257 и y = 257 в интерактивной оболочке (REPL), то x is y вернет False, потому что для каждого числа был создан свой уникальный объект. Эта особенность часто встречается на собеседованиях на позицию Junior/Intermediate Python разработчика. Вас могут спросить: «Почему a=256 и b=256 дают a is b == True, а для 257 это False?». Ответ кроется именно в этом диапазоне кэширования [-5; 256]. Почему выбран именно такой диапазон? Анализ множества программ на Python показал, что числа от -5 до 256 используются в коде чаще всего (размеры массивов, коды символов ASCII, статусы ошибок и т.д.), поэтому их предварительное создание дает максимальный прирост скорости при минимальных затратах памяти. Давайте закрепим это на практике.
# Демонстрация кэширования чисел от -5 до 256
a = 256
b = 256
print(f'256 is 256: {a is b}') # True
x = 257
y = 257
print(f'257 is 257: {x is y}') # False (если запускать в интерактивной консоли построчно)
# Внимание: если запустить этот код из файла (скрипта),
# обе переменные 257 могут стать одним объектом,
# так как компилятор Python оптимизирует константы в рамках одного блока.
Почему сравнение a = 257; b = 257; a is b может вернуть False в интерактивной консоли Python?
Какая функция в Python возвращает уникальный адрес объекта в памяти (идентификатор)? Введите только название функции с круглыми скобками.
Системы счисления: двоичная, восьмеричная и шестнадцатеричная. Целые числа в Python можно задавать не только в привычной нам десятичной системе счисления. В программировании, особенно при работе с сетями, цветами (в веб-разработке), битовыми масками и системным программированием, крайне важно уметь работать с другими базами. Python предоставляет удобный синтаксис для этого через специальные префиксы. Чтобы записать двоичное число (binary), используется префикс 0b или 0B. Двоичная система состоит только из нулей и единиц. Например, число 5 в десятичной системе — это 101 в двоичной. В коде вы напишете: binary_five = 0b101. Восьмеричная система (octal) использует префикс 0o или 0O (ноль и буква 'o') и цифры от 0 до 7. Шестнадцатеричная система (hexadecimal) — одна из самых популярных в IT — использует префикс 0x или 0X. Она включает цифры от 0 до 9 и буквы от A до F (где A=10, B=11, C=12, D=13, E=14, F=15). Например, белый цвет в вебе обозначается как #FFFFFF, что в Python можно записать как color_white = 0xFFFFFF. Что самое важное: независимо от того, в какой системе счисления вы задали число, в памяти Python оно все равно сохраняется как обычный объект int. Если вы попытаетесь вывести на печать (print) число 0xFF, Python автоматически преобразует его в десятичный формат и выведет 255. Чтобы сделать обратное преобразование — из десятичного числа получить строку с другой системой счисления — используются встроенные функции bin(), oct() и hex(). Обратите внимание: эти функции возвращают именно строки (str), а не числа. Например, hex(255) вернет строку '0xff'. Это важно понимать при манипуляциях с данными. Также стоит упомянуть побитовые операции: сдвиги (<<, >>), логическое И (&), логическое ИЛИ (|) и исключающее ИЛИ (^). Они работают с числами на уровне их двоичного представления и выполняются невероятно быстро процессором. В современных проектах побитовые операции часто используют для хранения множества флагов доступа (permissions) в одном целом числе, экономя огромное количество памяти.
decimal_num = 255
# Преобразование в другие системы (результат - строки)
print(bin(decimal_num)) # '0b11111111'
print(oct(decimal_num)) # '0o377'
print(hex(decimal_num)) # '0xff'
# Задание чисел в разных системах (тип останется int)
hex_num = 0xA # 10
bin_num = 0b10 # 2
# Побитовое ИЛИ
print(hex_num | bin_num) # 10 | 2 = 1010 | 0010 = 1010 = 10
Какой префикс используется в Python для записи числа в шестнадцатеричной системе счисления?
Флеш-карточки
Какая встроенная функция переводит целое число в двоичную строку?
Нажмите, чтобы увидеть ответ
bin()
Нажмите, чтобы вернуться
Какая функция вернет строку '0xff' при передаче числа 255?
Нажмите, чтобы увидеть ответ
hex()
Нажмите, чтобы вернуться
Как записать число 10 в двоичном виде в коде Python?
Нажмите, чтобы увидеть ответ
0b1010
Нажмите, чтобы вернуться
Вещественные числа (float) и проблема стандарта IEEE 754. Переходим к одному из самых коварных типов данных в программировании — числам с плавающей точкой. В Python вещественные числа (дроби) представлены типом float. Например, pi = 3.14. Важно знать, что в Python нет типа double (чисел двойной точности), как в C или Java. Точнее, встроенный тип float в Python уже реализует стандарт чисел двойной точности (64 бита) согласно международному стандарту IEEE 754. Этот стандарт описывает, как дроби должны храниться в бинарном (двоичном) виде процессора. Число разбивается на три части: бит знака (1 бит), экспонента (11 бит) и мантисса (52 бита). Эта архитектура позволяет хранить как невероятно огромные числа (до 1.8 * 10^308), так и бесконечно малые. Но здесь скрывается огромная проблема: компьютер работает с двоичной системой. В десятичной системе мы не можем точно записать дробь 1/3 (получается 0.33333...). В двоичной системе мы не можем точно записать многие десятичные дроби, например, 0.1 или 0.2. Двоичное представление числа 0.1 — это бесконечная периодическая дробь (0.0001100110011...). Поскольку память ограничена 64 битами, процессор просто обрубает хвост этой бесконечной дроби. В результате число 0.1 в компьютере хранится не ровно как 0.1, а как 0.100000000000000005551115123125... Это приводит к знаменитому багу новичков. Если вы напишете в Python (и почти в любом другом языке): 0.1 + 0.2 == 0.3, вы получите False. Почему? Потому что из-за погрешности округлений 0.1 + 0.2 выдаст 0.30000000000000004. Как с этим бороться? Никогда не сравнивайте вещественные числа напрямую через оператор ==. Вместо этого проверяйте, находится ли разница между числами в пределах крошечной погрешности (так называемый эпсилон). Начиная с версии Python 3.5, для этого есть готовая удобная функция в стандартной библиотеке: math.isclose(a, b). Она учитывает погрешности и правильно сравнивает вещественные числа. Понимание этой математической ловушки — абсолютный must-have для перехода на уровень Intermediate.
import math
result = 0.1 + 0.2
print(f'Результат сложения: {result}')
print(f'0.1 + 0.2 == 0.3: {result == 0.3}') # Вернет False!
# Правильное сравнение вещественных чисел:
is_equal = math.isclose(result, 0.3)
print(f'Использование math.isclose: {is_equal}') # Вернет True
Почему выражение 0.1 + 0.2 == 0.3 возвращает False в Python?
Модули Decimal и Fraction для идеальной точности. Раз уж мы выяснили, что тип float не подходит для точных вычислений, возникает резонный вопрос: как писать финансовые приложения, системы биллинга, банковское ПО? Представьте, что из-за погрешности в 0.000000001 копейки на миллионах транзакций банк теряет реальные деньги. Для таких задач стандартный float строго запрещен (Senior-разработчик не пропустит такой код на код-ревью). На помощь приходит встроенный модуль decimal и его класс Decimal. Этот модуль реализует арифметику с плавающей точкой, которая работает так же, как люди считают на бумаге (в десятичной, а не двоичной системе). Он гарантирует точные результаты без микро-погрешностей. Однако есть два важных нюанса использования Decimal. Во-первых, вы должны передавать значения в Decimal в виде строк (или целых чисел). Если вы напишете Decimal(0.1), вы сначала создадите кривой двоичный float, а потом Decimal честно сохранит этот кривой хвост со всеми нулями и четверкой на конце. Правильно писать Decimal('0.1'). Во-вторых, арифметика Decimal работает в десятки раз медленнее, чем обычный float, потому что все вычисления происходят программно на уровне языка Python, а не аппаратно на уровне сопроцессора. Поэтому float используется для научных вычислений, 3D-графики и машинного обучения (где микроскопическая точность не важна, а скорость критична), а Decimal — исключительно для денег. Еще один полезный модуль — fractions и его класс Fraction. Он позволяет работать с обыкновенными дробями (числитель и знаменатель). Если вам нужно поделить пиццу на 3 части и сохранить это математически идеально, вы пишете Fraction(1, 3). В отличие от 0.333333..., класс Fraction сохранит структуру одной трети и при умножении на 3 вернет ровно единицу, без всяких 0.999999. Знание этих модулей и умение выбирать правильный тип данных под конкретную задачу отличает начинающего от уверенного разработчика (Intermediate).
from decimal import Decimal
from fractions import Fraction
# Правильное использование Decimal (через строку)
dec_1 = Decimal('0.1')
dec_2 = Decimal('0.2')
dec_result = dec_1 + dec_2
print(f'Decimal 0.1 + 0.2: {dec_result}') # Выведет ровно 0.3
print(dec_result == Decimal('0.3')) # True
# Использование Fraction (числитель, знаменатель)
fract_1 = Fraction(1, 3) # Одна треть
print(f'Fraction(1,3) * 3 = {fract_1 * 3}') # Выведет ровно 1
Задание
Мини-задача: Валидация финансовых данных. Представьте, что вы пишете модуль расчета налогов для интернет-магазина. Вам нужно гарантировать точность до копейки.
- Импортируйте модуль decimal.
- Прочитайте цену товара из строковой переменной '199.99' и сохраните её как объект Decimal.
- Прочитайте налог '0.2' (20%) также как объект Decimal.
- Умножьте цену на налог и сложите с изначальной ценой. Убедитесь, что результат не содержит погрешностей float.
Булевы значения (bool): Ложь, Истина и наследование от int. Булев тип данных имеет всего два возможных значения: True и False (обязательно с большой буквы, в отличие от JavaScript или C++). На первый взгляд, это простейший флажок для конструкций if/else. Однако в Python логический тип скрывает в себе интересный архитектурный секрет: класс bool является прямым наследником класса int (целых чисел). Это означает, что True под капотом — это просто целое число 1, а False — это целое число 0, обернутые в специальный класс для удобства вывода на экран. Благодаря этому наследованию вы можете выполнять с логическими значениями любые математические операции. Например, вы можете написать True + True, и Python абсолютно легально вернет вам число 2. Вы можете умножить строку на False (она обнулится, став пустой строкой), или умножить на True (строка останется прежней). Эта особенность активно используется для написания компактного кода, который часто можно встретить в библиотеках вроде Pandas или NumPy. Например, если у вас есть список результатов тестов [True, False, True, True], и вы хотите узнать, сколько тестов прошло успешно, вам не нужно писать цикл с условием if. Вы можете просто вызвать функцию суммирования sum(results), и она сложит единицы и нули, вернув число 3. Однако, не стоит злоупотреблять этой фичействовать этим в бизнес-логике. Согласно Дзену Python (Явное лучше неявного), математические операции над логическими значениями ухудшают читаемость кода для других разработчиков. Сеньоры рекомендуют использовать эту фичу только в специфических кейсах агрегации данных. Давайте разберем понятие Truthiness (истинность) объектов. В Python абсолютно любой объект можно проверить в логическом контексте (например, подставив его в условие if). Если объект расценивается как False, он называется «Falsy», все остальные — «Truthy». В Python Falsy значениями являются: само значение False, число ноль любого типа (0, 0.0, Decimal('0')), пустые последовательности (пустая строка '', пустой список [], кортеж (), словарь {}, множество set()), а также специальный объект None. Все остальные объекты, даже содержащие отрицательные числа или пробел внутри строки, считаются True. Это избавляет от необходимости писать конструкции вроде if len(my_list) > 0: — достаточно написать просто if my_list:.
# Математика с True и False (bool наследуется от int)
successes = [True, False, True, True]
total_success = sum(successes) # 1 + 0 + 1 + 1
print(f'Успешных тестов: {total_success}') # 3
# Truthiness (истинность) пустых структур данных
empty_list = []
if empty_list:
print('Список не пуст!')
else:
print('Список пуст! (Falsy)') # Выведется эта строка
# Внимание на пробел!
space_string = ' '
if space_string:
print('Строка с пробелом - это True!') # Выведется эта строка
Что выведет функция sum() для списка [True, True, False] ?
Короткое замыкание логических операторов (Short-Circuit Evaluation). При работе с булевыми значениями в сложных условиях крайне важно понимать, как работают операторы and и or. В Python реализована оптимизация, известная как ленивое вычисление, или короткое замыкание. Суть в том, что интерпретатор вычисляет логическое выражение слева направо и останавливается ровно в тот момент, когда результат всего выражения становится очевидным. Например, в выражении A and B, если A является ложным (False), то не имеет значения, чему равно B — общее выражение все равно будет ложным. Поэтому Python даже не будет пытаться вычислять правую часть B. Аналогично с оператором or: в выражении A or B, если A является истинным (True), то все выражение точно будет истинным, и вычисление B пропускается. Это не просто оптимизация скорости. Это механизм, который программисты используют для защиты от ошибок. Классический пример: вам нужно проверить свойство объекта, но вы не уверены, существует ли сам объект. Если вы напишете if user.is_active:, а переменная user равна None, программа упадет с ошибкой AttributeError. Но если вы примените короткое замыкание: if user and user.is_active:, программа будет работать безопасно. Если user равен None (что расценивается как Falsy), оператор and споткнется на первой же проверке и немедленно вернет False, даже не пытаясь обратиться к свойству user.is_active, тем самым избежав фатальной ошибки. Еще одна интересная особенность: операторы and и or в Python возвращают не обязательно True или False. Они возвращают само значение последнего вычисленного операнда. Выражение 'apple' and 'banana' вернет 'banana' (так как первый истинный, проверяем второй, он тоже истинный, возвращаем его). Выражение '' or 'default' вернет 'default'. Это часто используется для назначения значений по умолчанию: username = input_name or 'Гость'.
# Пример возврата значений операторами and / or
# or возвращает первое 'истинное' значение
user_input = ''
default_name = 'Аноним'
name = user_input or default_name
print(name) # Выведет 'Аноним', так как '' расценивается как False
# and возвращает первое 'ложное' значение (или последнее, если все истинны)
result = 'Python' and 0 and 'Developer'
print(result) # Выведет 0, так как на нем споткнулся and
# Защита через короткое замыкание
data = None
# Ошибки не будет, правая часть не выполнится
if data and data['key'] == 'value':
pass
Какое значение будет присвоено переменной x в выражении: x = [] or 'Success' or 100?
Строки (str): Внутреннее устройство и Unicode. Переходим к мощнейшему инструменту любого программиста — строкам. В Python тип str — это неизменяемая последовательность символов. Важно понимать, что в Python 3 все строки по умолчанию являются юникод-строками (Unicode). Вам больше не нужно, как в Python 2, ставить префикс 'u' перед строкой или мучиться с кодировкой ASCII. Unicode — это мировой стандарт, который содержит в себе символы практически всех языков мира, математические символы, иероглифы и даже эмодзи. Каждому символу присвоен уникальный номер (code point). Однако хранение символов Unicode в оперативной памяти — задача нетривиальная. Символы английского алфавита помещаются в 1 байт (8 бит), кириллица требует 2 байта, а китайские иероглифы или эмодзи — 4 байта. Как Python управляет памятью строк? До версии 3.3 интерпретатор использовал громоздкие решения, выделяя избыточную память. Но затем был принят PEP 393 (Flexible String Representation). Теперь Python анализирует строку при ее создании. Если строка состоит только из английских букв (ASCII), Python выделит ровно по 1 байту на каждый символ, экономя огромное количество оперативной памяти. Если в строке появляется хотя бы одна русская буква, вся строка перестраивается так, чтобы каждый символ занимал по 2 байта. А если вы добавите в текст смайлик 😊, то под каждый символ (включая пробелы и латиницу) будет выделено по 4 байта! Это скрытая механика, которая позволяет Python оставаться быстрым и экономичным, прозрачно управляя сложным стандартом Unicode. В качестве разработчика вы должны об этом знать: если у вас огромный массив текстовых данных на английском языке размером в гигабайты, случайное добавление одного эмодзи в этот текст увеличит потребление оперативной памяти в 4 раза! Для работы с символами Unicode в Python есть две встроенные функции: ord(char) возвращает числовой код символа (например, ord('A') вернет 65), а chr(code) возвращает символ по его номеру (например, chr(65) вернет 'A').
# Демонстрация работы с Unicode
letter_a = 'A'
smiley = '😊'
print(f"Код символа 'A': {ord(letter_a)}")
print(f"Код символа '😊': {ord(smiley)}")
# Обратное преобразование
print(f"Символ с кодом 128512: {chr(128512)}") # Выведет 😀
# Внимание к памяти (PEP 393)
import sys
# Только ASCII - 1 байт на символ (плюс база объекта ~49 байт)
print(sys.getsizeof('abc')) # ~52 байта
# Появление кириллицы - 2 байта на КАЖДЫЙ символ
print(sys.getsizeof('abcв')) # ~82 байта
# Появление эмодзи - 4 байта на КАЖДЫЙ символ
print(sys.getsizeof('abcв😊')) # ~96 байт
Флеш-карточки
Какая функция возвращает числовой код символа Unicode?
Нажмите, чтобы увидеть ответ
ord()
Нажмите, чтобы вернуться
Какая функция возвращает символ по его числовому коду Unicode?
Нажмите, чтобы увидеть ответ
chr()
Нажмите, чтобы вернуться
Сколько байт минимум требует символ эмодзи в памяти Python?
Нажмите, чтобы увидеть ответ
4 байта
Нажмите, чтобы вернуться
Интернирование строк (String Interning). Мы уже разбирали кэширование малых чисел. Похожий, но более сложный механизм работает и для строк — он называется интернированием. В целях экономии памяти и ускорения сравнений интерпретатор CPython может использовать один и тот же объект памяти для одинаковых строк. По умолчанию Python автоматически интернирует (сохраняет в специальный внутренний словарь-кэш) строки, которые выглядят как валидные идентификаторы (имена переменных): то есть те, которые состоят только из латинских букв, цифр и знаков подчеркивания, и не содержат пробелов или спецсимволов. Если вы создадите две переменные a = 'hello_world' и b = 'hello_world', они будут указывать на один и тот же объект в памяти (a is b вернет True). Но если в строке есть пробел a = 'hello world', автоматическое интернирование не сработает, и будут созданы два разных объекта (a is b вернет False). Зачем это нужно знать? Сравнение строк — частая операция. Если две строки интернированы, интерпретатору не нужно сравнивать их посимвольно (что занимает время O(n)). Он просто сравнивает их адреса памяти (идентификаторы), что происходит мгновенно (O(1)). Если вы пишете парсер логов, анализатор текста или NLP-систему (Natural Language Processing), где часто встречаются и сравниваются одни и те же слова (токены), вы можете принудительно интернировать строки с помощью функции sys.intern(). Это может радикально ускорить ваше приложение и сэкономить мегабайты памяти, так как 1000 одинаковых слов будут указывать на одну ячейку, вместо создания 1000 копий. Это типичный пример оптимизации на уровне Intermediate, которая отличает профессиональный код от любительского. Тем не менее, не стоит интернировать все строки подряд — сам словарь интернирования занимает память, и добавление в него новых элементов требует ресурсов.
import sys
# Автоматическое интернирование (без пробелов, только буквы/цифры/_)
s1 = 'python_3'
s2 = 'python_3'
print(f"s1 is s2: {s1 is s2}") # True
# Без автоматического интернирования (из-за пробела)
s3 = 'python 3'
s4 = 'python 3'
# Внимание: при выполнении как скрипта может оптимизироваться компилятором!
# В REPL (консоли) это вернет False
print(f"s3 is s4: {s3 is s4}")
# Принудительное интернирование
s5 = sys.intern('python 3 data')
s6 = sys.intern('python 3 data')
print(f"s5 is s6: {s5 is s6}") # Точно будет True
Что дает принудительное интернирование строк с помощью sys.intern() при обработке большого текста?
Эволюция форматирования строк: от % до f-strings. В Python есть несколько способов вставить значения переменных внутрь строки. Их эволюция — это история развития самого языка. Самый старый метод, пришедший из языка C, это оператор % (называемый Си-стайл форматированием). Вы пишете: 'Привет, %s' % 'Мир'. Здесь %s означает строку, %d — целое число, %f — вещественное. Этот метод устарел, сложен в поддержке (когда переменных много, легко запутаться в их порядке) и официально не рекомендуется к использованию в новом коде. Затем в Python 2.6 появился метод .format(). Он принес фигурные скобки и возможность передавать аргументы по имени: 'Привет, {name}'.format(name='Мир'). Этот метод гораздо мощнее, он позволяет выравнивать текст, форматировать числа с отступами и до сих пор широко используется. Но настоящая революция произошла в Python 3.6 с внедрением f-строк (Literal String Interpolation, PEP 498). F-строки — это не просто синтаксический сахар, это выражения, вычисляемые во время выполнения (runtime). Вы ставите букву f перед кавычками, и внутри фигурных скобок можете писать абсолютно любой валидный код Python: вызывать функции, обращаться к методам, выполнять арифметику. Например: f'2 + 2 = {2 * 2}'. Почему f-строки лучше? Во-первых, они феноменально читаемы. Во-вторых, они работают быстрее, чем метод .format() или оператор %. Под капотом интерпретатор CPython компилирует f-строку в эффективную серию операций конкатенации строк на уровне языка C, избегая накладных расходов на вызов методов. Начиная с Python 3.8, во f-строки добавили знак равенства = для отладки. Конструкция f'{user_age=}' выведет на экран user_age=25. Это избавляет от необходимости писать print('user_age:', user_age). В современном коде (на уровне Middle и Senior) использование f-строк является золотым стандартом оформления.
name = 'Алексей'
age = 30
salary = 150000.5
# 1. Устаревший стиль (не использовать)
old_style = "Имя: %s, Возраст: %d" % (name, age)
# 2. Метод .format() (хорош для сложных шаблонов или старых версий)
format_style = "Имя: {0}, Возраст: {1}".format(name, age)
# 3. f-строки (Современный стандарт)
f_string = f"Имя: {name.upper()}, Возраст: {age + 1}"
# Форматирование чисел через f-строки (разделение тысяч пробелом и 2 знака после запятой)
# Синтаксис: {значение:формат}
f_money = f"Зарплата: {salary:,.2f}".replace(',', ' ')
print(f_money) # Выведет: Зарплата: 150 000.50
# Отладка (появилась в Python 3.8+)
print(f"{age=}") # Выведет: age=30
Какой префикс нужно поставить перед строкой 'Мой возраст {age}', чтобы переменная age подставилась автоматически? Напишите только одну английскую букву.
Методы строк: Очистка, Поиск и Трансформация. Поскольку строки неизменяемы, каждый метод строки всегда возвращает новую строку, оставляя оригинал нетронутым. Методов много, но разработчику уровня Intermediate нужно свободно владеть базовым арсеналом. Начнем с очистки данных (Data Cleaning). При получении информации от пользователя или парсинге веб-страниц, текст часто содержит лишние пробелы, переносы строк или табуляции по краям. Метод .strip() удаляет пробельные символы с обоих концов строки. Его собратья .lstrip() и .rstrip() чистят только слева или справа. Трансформация регистра: .lower() и .upper() делают все буквы строчными или заглавными. Для нормализации данных (например, при сравнении email адресов) всегда приводите текст к .lower(). Метод .capitalize() делает заглавной только первую букву всей строки, а .title() — первую букву каждого слова. Поиск и замена: метод .replace(old, new, count) заменяет подстроки. Если нужно заменить множество разных символов, новички пишут цепочку: .replace('a','').replace('b','')... Это ужасно неэффективно, так как на каждом шаге создается новая строка в памяти. Профессионалы используют связку методов .translate() и str.maketrans(dict). Вы создаете таблицу перевода (маппинг), а затем .translate() меняет все символы за один проход на уровне C-кода. Это работает в 10 раз быстрее цепочки реплейсов! Разделение и объединение: методы .split(separator) и .join(iterable) — лучшие друзья разработчика. .split(',') разобьет строку из CSV файла в список элементов. А ', '.join(['a', 'b']) склеит список строк в одну. Использование .join() — это единственно верный и производительный способ объединения множества строк в цикле, обходящий проблему неизменяемости (когда конкатенация через + создает избыточные копии памяти).
raw_data = " Пользователь: admin@mail.com \n "
# 1. Очистка и приведение регистра (chaining - объединение в цепочку)
clean_email = raw_data.strip().split(': ')[1].lower()
print(clean_email) # 'admin@mail.com'
# 2. Быстрая множественная замена через translate
bad_string = "Текст с ненужными; символами: и знаками!"
# Создаем таблицу: ключ - что менять, значение - на что менять (None = удалить)
translation_table = str.maketrans({'!': None, ';': None, ':': None})
clean_string = bad_string.translate(translation_table)
print(clean_string) # 'Текст с ненужными символами и знаками'
# 3. Эффективная сборка строки
words = ['Python', 'is', 'awesome']
# Метод вызывается у строки-разделителя!
result = ' '.join(words)
print(result) # 'Python is awesome'
Почему при объединении большого количества строк рекомендуется использовать метод ''.join(list) вместо оператора '+' в цикле?
Срезы (Slicing) в строках. Так как строки в Python — это последовательности, мы можем обращаться к отдельным символам по их индексу. Индексация в Python начинается с нуля. Более того, Python поддерживает отрицательную индексацию: -1 — это последний символ строки, -2 — предпоследний и так далее. Это невероятно удобно: не нужно вычислять длину строки через len(s) - 1, чтобы получить хвост. Но настоящая магия заключается в срезах (Slices). Синтаксис среза выглядит так: строка[start : stop : step].
1. start — индекс, с которого начинается срез (включается).
2. stop — индекс, на котором срез заканчивается (НЕ включается!).
3. step — шаг извлечения элементов (по умолчанию 1).
Если вы опустите start, Python начнет с начала (0). Если опустите stop, возьмет до конца. Например, s[1:4] возьмет символы с 1 по 3. s[:3] возьмет первые три символа. s[-3:] возьмет три последних символа. Шаг (step) позволяет извлекать каждый второй или третий символ. Но самое популярное применение шага — это реверс (переворот) строки. Конструкция s[::-1] читается как «взять строку от начала до конца, но двигаться с шагом минус один (в обратном порядке)». Это самый быстрый и питоничный способ перевернуть строку (или список). Важно помнить: срез строки всегда создает новую строку (копию), выделяя под нее отдельную память. В отличие от некоторых библиотек вроде NumPy, где срезы массивов являются «видами» (views) на те же данные, встроенные срезы строк и списков в Python делают полные копии. Поэтому срез огромной строки в 1 Гигабайт big_text[:] мгновенно съест еще 1 Гигабайт оперативной памяти.
text = "Python_Developer"
# Извлечение символов
print(text[0]) # 'P'
print(text[-1]) # 'r' (последний)
# Базовые срезы [start:stop]
print(text[0:6]) # 'Python' (символы с 0 по 5)
print(text[:6]) # 'Python' (аналогично)
print(text[-9:]) # 'Developer' (последние 9 символы)
# Использование шага [start:stop:step]
print(text[::2]) # 'Pto_eeoe' (каждый второй символ)
# Переворот строки (классика собеседований)
reversed_text = text[::-1]
print(reversed_text) # 'repoleveD_nohtyP'
Задание
Проект-симуляция: Разработка валидатора и нормализатора пользовательских данных (Data Validator). Вы пишете бэкенд для формы регистрации.
- Сценарий: К вам приходят «грязные» данные из HTML формы: username = ' Aleksey! ', age = ' 25 ', has_promo = 'True'.
- 1. Очистите username от пробелов по краям, удалите знак восклицания (через replace) и приведите все буквы к нижнему регистру.
- 2. Преобразуйте строку age в целое число (int), предварительно убрав пробелы.
- 3. Преобразуйте has_promo в булево значение. (Внимание: bool('False') вернет True! Вам нужно написать логическое сравнение: has_promo == 'True').
- 4. Используйте f-строку, чтобы вывести красивое приветствие.
# Исходные данные (сырые строки)
raw_username = ' Aleksey! '
raw_age = ' 25 '
raw_promo = 'True'
# 1. Нормализация строк
clean_username = raw_username.strip().replace('!', '').lower()
# 2. Приведение типов (Casting)
age_int = int(raw_age.strip())
# 3. Булева логика (Нельзя просто сделать bool(str), так как непустая строка всегда True)
has_promo = (raw_promo.strip() == 'True')
# 4. Форматирование результата
result_message = f"Пользователь {clean_username} ({age_int} лет) успешно зарегистрирован. Промокод: {has_promo}"
print(result_message)
Приведение типов (Type Casting) и возможные ошибки. В проекте-симуляции выше мы затронули важнейшую тему — преобразование типов. Python имеет строгую динамическую типизацию. «Строгую» означает, что интерпретатор не будет пытаться угадать, что вы имели в виду, и не сложит строку и число молча (как это делает JavaScript). Конструкция '2' + 2 в Python вызовет ошибку TypeError. Вы обязаны явно приводить типы. Для этого используются конструкторы типов: int(), float(), str(), bool(). Однако процесс приведения (кастинга) таит в себе множество подводных камней, из-за которых падают приложения. Рассмотрим int('25') — все пройдет гладко. Но что если строка придет с пробелами? int(' 25 ') — тоже сработает, метод int под капотом сам игнорирует краевые пробелы. А вот если вы попытаетесь преобразовать вещественное число в строке: int('25.5'), вы получите фатальную ошибку ValueError: invalid literal for int(). Конструктор int требует, чтобы строка содержала только цифры (разрешен знак минуса спереди). Если вам нужно извлечь целое число из такой строки, вы должны сделать двойной каст: сначала во float, затем в int: int(float('25.5')) (дробная часть .5 при этом просто отбрасывается, без округления). Отдельного внимания заслуживает кастинг в bool(). Мы уже обсуждали Truthy и Falsy значения. Главная ошибка новичка (Junior-разработчика) при работе с API — это получение строки 'False' с сервера и применение к ней bool('False'). Строка 'False' не является пустой строкой, ее длина больше нуля. Поэтому bool('False') вернет True! Это приводит к катастрофическим багам в бизнес-логике. Правильный способ проверки строковых флагов: value.lower() == 'true' (если значение true) или использование функции strtobool из модуля distutils.util (хотя в новых версиях Python его выносят в другие места). Запомните: явное лучше, чем неявное. Всегда проверяйте контент перед конвертацией, используя строковые методы вроде .isdigit() для защиты от ValueError.
Что вернет выражение bool('False') в Python?
Ситуация из жизни: Code Review (Диалог Senior и Junior). Чтобы закрепить материал, представим классическую рабочую ситуацию. Junior-разработчик написал функцию для форматирования цены товара для интернет-магазина. Функция принимает цену (число), скидку (дробь) и возвращает строку.
Код Джуниора:
def format_price(price, discount):
final_price = price - (price * discount)
return "Цена: " + str(final_price) + " руб."
Комментарий Сеньора на Code Review:
«Привет! Твой код выполнит задачу, но он далек от стандартов (Pythonic way) и содержит риски.
1. Ты используешь конкатенацию строк через '+'. Это нечитаемо и неэффективно по памяти. Используй f-строки.
2. При вычислении скидки через float у нас вылезут хвосты стандарта IEEE 754. Если price=100.0, а discount=0.15, final_price может стать 85.00000000000001. Клиент испугается такой суммы.
3. В f-строке используй спецификатор формата :.2f, чтобы жестко ограничить вывод двумя знаками после запятой (копейки) и отбросить микро-погрешности float для вывода на экран, либо перепиши математику на модуль Decimal, если это данные для биллинга.
Исправь, пожалуйста».
Исправленный код:
def format_price(price, discount):
final_price = price * (1 - discount)
return f"Цена: {final_price:.2f} руб."
Этот небольшой диалог аккумулирует знания всего сегодняшнего урока: мы применили знания о проблемах вещественных чисел, отказались от неоптимизированной конкатенации в пользу современных f-строк и использовали встроенное форматирование. Именно такой аналитический подход требуется на уровне Intermediate.
Флеш-карточки
В чем главная проблема объединения строк через оператор '+' в цикле?
Нажмите, чтобы увидеть ответ
Строки неизменяемы. На каждом шаге цикла выделяется новая память и копируется весь предыдущий текст, что катастрофически снижает производительность (алгоритмическая сложность O(N^2)).
Нажмите, чтобы вернуться
Как преобразовать строку '3.14' в целое число (int), не получив ValueError?
Нажмите, чтобы увидеть ответ
Сделать двойное приведение: сначала во float, потом в int. Например: int(float('3.14'))
Нажмите, чтобы вернуться
Что делает выражение f"{variable=}" в Python 3.8+?
Нажмите, чтобы увидеть ответ
Выводит на экран имя переменной и ее значение. Используется для быстрой отладки, например: variable=42
Нажмите, чтобы вернуться
Подведение итогов урока. Поздравляем! Вы проделали огромную работу, разобрав внутреннее устройство базовых типов данных в Python. Давайте кратко резюмируем ключевые концепции. Мы узнали, что числа и строки являются неизменяемыми (immutable) объектами. При любой операции модификации Python создает новые объекты в памяти, а сборщик мусора очищает старые. Мы разобрали тип int и узнали, что в Python 3 он не имеет ограничений по размеру памяти, а числа от -5 до 256 кэшируются при старте программы для оптимизации. Мы погрузились в проблему стандарта IEEE 754 и поняли, почему 0.1 + 0.2 != 0.3, научившись использовать math.isclose и модуль Decimal для финансовых расчетов. Булевы значения True и False оказались всего лишь наследниками чисел 1 и 0, что позволяет использовать их в математике. Мы изучили «истинность» объектов (Truthy/Falsy) и научились защищать код с помощью короткого замыкания логических операторов. И, наконец, мы детально разобрали строки (str): как работает Unicode, как CPython оптимизирует хранение символов через PEP 393, что такое интернирование строк через sys.intern(), и почему метод .join() и f-строки — это лучший выбор профессионального разработчика. В качестве финального задания (Project-Based Learning) мы написали валидатор данных, объединивший логику всех пройденных типов. Этот фундамент (Scaffolding) позволит вам писать не просто работающий код, а код безопасный, читаемый и оптимизированный по памяти и скорости. В следующем модуле мы перейдем к сложным коллекциям: спискам, словарям и множествам, где концепция изменяемости (mutability) проявит себя во всей красе. Обязательно выполните все проверочные тесты этого урока для закрепления эффекта Active Recall. До встречи на следующем уроке!