Введение в Python и философия дзен
Студент познакомится с принципами написания красивого кода (PEP 8) и базовым устройством языка.
Добро пожаловать на уровень Intermediate!
Вы уже знаете, как объявлять переменные, писать циклы for и while, а также создавать базовые функции. Вы умеете заставлять код работать. Однако на уровне Intermediate наша цель меняется. Теперь важно не просто написать работающий код, а создать решение, которое будет читаемым, масштабируемым и понятным другим разработчикам (и вам самим спустя полгода). В профессиональной разработке код читается в десятки раз чаще, чем пишется. Именно поэтому чистый синтаксис и архитектура выходят на первый план.
На этом курсе мы будем использовать подход Microlearning (подача информации небольшими порциями) и Active Recall (постоянное тестирование вашей памяти без подсказок), чтобы знания надежно закрепились в вашей голове. Мы также будем применять элементы Project-Based Learning, решая реальные инженерные задачи.
Первое, с чем мы столкнемся — это переход от так называемого 'спагетти-кода' (запутанного, неструктурированного кода) к Pythonic way. Что означает это выражение? Быть 'Pythonic' значит использовать встроенные возможности языка Python таким образом, как это задумывали его создатели. Это означает писать лаконично, эффективно и красиво. Вы перестанете мыслить категориями языков C++ или Java, если вы пришли оттуда, и начнете использовать мощь списковых включений, генераторов, декораторов и контекстных менеджеров.
В этом уроке мы заложим прочный фундамент: разберем философию языка (The Zen of Python), стандарты оформления кода (PEP 8) и углубимся в то, как Python работает под капотом, потому что без понимания механики управления памятью и области видимости невозможно стать Senior разработчиком.
Давайте посмотрим на простой пример. Джуниор-разработчик, которому нужно отфильтровать список и получить только четные числа, напишет цикл for, создаст пустой список, добавит условие if и будет использовать метод .append(). Это работает, но это многословно. Разработчик уровня Intermediate решит эту задачу в одну строку, используя List Comprehension, что не только короче, но и быстрее выполняется интерпретатором CPython, так как цикл оптимизирован на уровне языка C. Наша цель — научить вас видеть такие оптимизации повсюду.
Дзен Python (The Zen of Python) - Идеология языка
Прежде чем мы начнем писать сложные алгоритмы, мы обязаны понять философию языка, на котором пишем. В 1999 году программист Тим Питерс (Tim Peters) сформулировал набор из 19 руководящих принципов проектирования языка Python. Эти принципы известны как The Zen of Python.
Вы можете прочитать их в любой момент, открыв интерпретатор Python и введя команду import this. Это своеобразная 'пасхалка' и одновременно священное писание для любого Python-разработчика. Давайте детально разберем первые и самые важные афоризмы, так как они будут диктовать нам, как принимать архитектурные решения.
- Beautiful is better than ugly (Красивое лучше, чем уродливое): Код должен эстетично выглядеть. Правильные отступы, понятные имена переменных, логичные блоки кода. Уродливый код тяжело читать и еще тяжелее поддерживать.
- Explicit is better than implicit (Явное лучше, чем неявное): Это кардинальное отличие Python от магии некоторых других языков (например, Ruby или JavaScript, где происходит много неявного приведения типов). В Python, если функция возвращает данные, это должно быть очевидно. Если переменная меняет тип, это должно быть явно указано. Мы не любим скрытое поведение, которое заставляет разработчика гадать, откуда взялась та или иная переменная.
- Simple is better than complex (Простое лучше, чем сложное): Если задачу можно решить простым циклом, не нужно городить сложную архитектуру классов с множественным наследованием. Проектируйте системы настолько просто, насколько это возможно, но не проще.
- Complex is better than complicated (Сложное лучше, чем запутанное): Иногда предметная область объективно сложна (например, машинное обучение или криптография). В таком случае код будет сложным, и это нормально. Но он никогда не должен быть запутанным (complicated) — то есть содержать неоправданные хитросплетения логики.
- Flat is better than nested (Плоское лучше, чем вложенное): Если у вас есть 5 уровней вложенности циклов
forи условийif, ваш код требует рефакторинга. Читать код, уходящий вправо за пределы экрана, физически больно. Выносите логику в отдельные функции.
Понимание этих принципов — это первый шаг к прохождению собеседований на позиции Middle. Интервьюеры часто задают вопросы не только по синтаксису, но и просят вас оценить два куска кода и сказать, какой из них более 'Pythonic' и почему. И ваш ответ всегда должен опираться на принципы Дзена.
# Пример: Уродливый, неявный и вложенный код (Антипаттерн)
def process_data(d):
if d != None:
if len(d) > 0:
res = []
for i in range(len(d)):
if type(d[i]) == int:
if d[i] % 2 == 0:
res.append(d[i] * 2)
return res
return []
# Пример: Красивый, явный и плоский код (Pythonic way)
def process_data(data: list) -> list:
if not data:
return []
# Используем list comprehension - плоский дизайн, простота
return [item * 2 for item in data if isinstance(item, int) and item % 2 == 0]
Какой из принципов 'The Zen of Python' прямо призывает разработчиков избегать большого количества циклов if/for внутри других циклов if/for?
Продолжаем изучать Дзен: Читаемость и Ошибки
Двигаемся дальше по списку афоризмов Тима Питерса. Следующий критически важный принцип звучит так: Readability counts (Читаемость имеет значение). Почему это так важно? Представьте, что вы приходите на новый проект, где предыдущий разработчик использовал переменные с именами x1, x2, data_final_real. Вы потратите недели только на то, чтобы понять, за что отвечает каждая переменная. В Python считается нормой давать длинные, но максимально понятные имена переменным и функциям. Например, calculate_monthly_revenue() гораздо лучше, чем calc_rev(). Мы не экономим буквы, мы экономим время наших коллег, которые будут читать этот код.
Далее: Errors should never pass silently. Unless explicitly silenced. (Ошибки никогда не должны проходить незамеченными. Если только вы не скрыли их явно). Это прямое обращение к механизму обработки исключений (Try/Except). Одна из самых страшных ошибок новичков — написать блок try, а в блоке except написать pass, перехватив базовое исключение Exception. Что при этом происходит? Ваша программа падает или работает некорректно, но она не выдает никаких логов и сообщений об ошибке. Вы буквально затыкаете программе рот. Искать баги в таком приложении — сущий кошмар. Если вы перехватываете ошибку, вы обязаны ее обработать: записать в лог, вывести пользователю сообщение, либо выполнить резервный сценарий.
There should be one-- and preferably only one --obvious way to do it. (Должен существовать один — и, желательно, только один — очевидный способ сделать это). Это тонкий укол в сторону языка Perl, девиз которого звучит как 'There is more than one way to do it' (Есть больше одного способа сделать это). Python стремится к унификации. Если вам нужно перебрать элементы списка, очевидный способ — это for item in my_list:. Вам не нужно использовать индексы, вам не нужно использовать циклы while. Это создает единый стандарт мышления для всех разработчиков по всему миру. Когда вы смотрите на чужой код Python, он кажется вам знакомым, потому что автор использовал те же 'очевидные способы', что и вы.
И последнее на сегодня из Дзена: If the implementation is hard to explain, it's a bad idea. (Если реализацию сложно объяснить — это плохая идея). Это отличный лакмусовый тест для вашего собственного кода. Написав функцию, попробуйте объяснить ее логику вслух воображаемому джуниору. Если вы начинаете запинаться, путаться в условиях, если вам приходится рисовать сложные схемы на доске, чтобы понять, как работает ваш алгоритм — удаляйте код и пишите заново. Архитектура должна быть прозрачной.
Какую команду нужно ввести в интерактивной оболочке Python, чтобы вывести на экран все принципы 'Дзена Python'?
Флеш-карточки
Beautiful is better than ugly
Нажмите, чтобы увидеть ответ
Код должен быть эстетичным, правильно отформатированным и приятным для чтения.
Нажмите, чтобы вернуться
Explicit is better than implicit
Нажмите, чтобы увидеть ответ
Код должен явно показывать свои намерения, избегая 'магии' и скрытых преобразований.
Нажмите, чтобы вернуться
Flat is better than nested
Нажмите, чтобы увидеть ответ
Избегайте глубокой вложенности циклов и условий (if/for). Выносите логику в функции.
Нажмите, чтобы вернуться
Errors should never pass silently
Нажмите, чтобы увидеть ответ
Не используйте пустые блоки except (try/except pass). Ошибки должны логироваться или обрабатываться.
Нажмите, чтобы вернуться
Введение в PEP 8: Стандарт написания кода
Итак, мы поговорили о философии, теперь перейдем к строгим правилам. В мире Python существует документ, который важнее любой книги. Это PEP 8 (Python Enhancement Proposal 8) - Style Guide for Python Code. PEP — это предложения по улучшению языка, и документ под номером 8 посвящен стилю оформления кода. Его первоначальным автором является Гвидо ван Россум (Guido van Rossum), создатель языка Python. Почему этот документ так важен? Потому что код пишется один раз, а читается множество раз. Единый стиль оформления позволяет разработчикам не тратить ментальную энергию на парсинг синтаксиса глазами, а сразу фокусироваться на бизнес-логике.
Давайте разберем базовые принципы форматирования PEP 8. Начнем с самого святого в Python — с отступов (Indentation). PEP 8 жестко регламентирует: используйте 4 пробела для каждого уровня отступа. Никогда не используйте табуляцию (Tab), и тем более никогда не смешивайте табы и пробелы. В Python 3 смешивание табов и пробелов вызовет ошибку TabError, и ваша программа просто не запустится. Современные редакторы кода (IDE, такие как PyCharm или VS Code) автоматически конвертируют нажатие клавиши Tab в 4 пробела, поэтому вам не нужно нажимать пробел четыре раза вручную, но вы должны понимать, что происходит под капотом.
Следующее правило: максимальная длина строки — 79 символов. Это правило вызывает много споров. Оно пришло из эпохи старых мониторов и терминалов, но сохраняет актуальность и сегодня. Почему? Потому что длинные строки тяжело читать (вашим глазам приходится бегать слева направо). Кроме того, ограничение в 79 символов позволяет открыть два-три файла кода рядом друг с другом на одном современном широком мониторе, что невероятно удобно при рефакторинге. Для строк с комментариями или docstrings (строк документации) лимит еще строже — 72 символа. Если выражение не помещается в одну строку, PEP 8 рекомендует использовать неявное продолжение строк внутри круглых, квадратных или фигурных скобок, вместо использования обратного слеша \.
Третий важный аспект: пустые строки (Blank Lines). Код не должен слипаться в единую нечитаемую простыню текста. PEP 8 гласит: отделяйте определения функций верхнего уровня и классов двумя пустыми строками. А определения методов внутри класса отделяйте одной пустой строкой. Пустые строки можно использовать (умеренно) внутри функций для разделения логических блоков. Это работает как абзацы в книге — они дают глазам передышку и структурируют мысль разработчика.
# Пример правильного форматирования по PEP 8
import os
def calculate_taxes(income: float, tax_rate: float) -> float:
"""Рассчитывает сумму налога на основе дохода и ставки."""
# Логический блок 1: валидация
if income < 0 or tax_rate < 0:
raise ValueError("Значения не могут быть отрицательными")
# Логический блок 2: расчет
tax_amount = income * tax_rate
return tax_amount
class UserProfile:
def __init__(self, username: str):
self.username = username
def display(self):
print(self.username)
Согласно стандарту PEP 8, сколько пустых строк должно быть перед определением класса верхнего уровня?
PEP 8: Правила импорта модулей
Организация импортов (импортирования библиотек и модулей) — это лицо вашего файла. Когда Senior-разработчик открывает ваш код, первое, на что он смотрит, это блок импортов. Если там царит хаос, создается впечатление о неопрятности всего кода. PEP 8 устанавливает жесткие правила для импортов.
Во-первых, импорты всегда должны находиться в самом начале файла, сразу после комментариев к модулю и строк документации (docstrings), но перед объявлением глобальных переменных и констант. Вы не должны размазывать директивы import по всему телу скрипта (исключение составляют редкие случаи локального импорта внутри функции для предотвращения циклических зависимостей, но на уровне Intermediate мы стараемся избегать такой архитектуры).
Во-вторых, импорты должны группироваться в строго определенном порядке, причем между группами должна быть одна пустая строка. Порядок следующий:
1. Стандартная библиотека (Standard library imports): это встроенные модули Python, такие как os, sys, datetime, json.
2. Сторонние библиотеки (Related third party imports): это модули, которые вы установили через pip, например requests, django, numpy, sqlalchemy.
3. Локальные импорты приложения (Local application/library specific imports): это модули, написанные лично вами в рамках текущего проекта, например import my_utils или from core.models import User.
Третье правило: импорты должны быть на отдельных строках. Нельзя писать import sys, os. Правильно писать:import osimport sys.
Однако, если вы импортируете несколько объектов из одного модуля, группировка разрешена: from subprocess import Popen, PIPE. Если таких объектов слишком много и они не помещаются в лимит 79 символов, используйте круглые скобки для переноса строк.
И наконец, избегайте импортов со звездочкой (from module import *). Это настоящий антипаттерн (Bad Practice). Почему? Потому что это загрязняет ваше глобальное пространство имен (namespace). Вы не знаете, какие именно функции и классы были загружены. Если в загружаемом модуле есть функция save(), и в другом модуле есть функция save(), они перезапишут друг друга молча, и вы потратите дни на отладку. Принцип Дзена 'Явное лучше, чем неявное' здесь работает на 100%. Вы должны точно знать, откуда взялась каждая функция в вашем файле.
"""Докстринг модуля. Описание скрипта."""
# 1. Стандартная библиотека
import os
import sys
from datetime import datetime
# 2. Сторонние библиотеки (установлены через pip)
import requests
from bs4 import BeautifulSoup
# 3. Локальные модули проекта
from config import API_KEY
from services.database import db_session
Почему стандарт PEP 8 настоятельно рекомендует избегать импортов со звездочкой (from module import *)?
PEP 8: Именование переменных, функций и классов
Мы подошли к одной из самых холиварных тем в программировании — как называть вещи своими именами. PEP 8 предоставляет четкую инструкцию (Naming Conventions). Именование — это не просто дело вкуса, это способ передать метаинформацию о типе и назначении объекта, не читая его исходный код. Разные элементы языка должны иметь разный визуальный стиль.
Функции, переменные и методы (Functions, Variables, Methods): Должны быть написаны в нижнем регистре, слова разделяются символом нижнего подчеркивания (underscore) для улучшения читаемости. Этот стиль называется snake_case. Примеры: calculate_total(), user_age, db_connection. Избегайте однобуквенных имен, таких как l (строчная L), O (заглавная O) и I (заглавная i), так как в некоторых шрифтах они неотличимы от цифр 1 и 0.
Классы (Classes) и Исключения (Exceptions): Имена классов должны использовать стиль CapitalizedWords (он же CamelCase или PascalCase). Каждое новое слово начинается с заглавной буквы, без подчеркиваний. Примеры: UserProfile, DatabaseManager, NetworkError. Благодаря этому правилу вы с первого взгляда понимаете, что перед вами: функция (выполняет действие) или класс (сущность).
Константы (Constants): Константы — это переменные, значение которых не должно меняться во время выполнения программы. В Python нет встроенной защиты констант от изменения, поэтому мы используем визуальное соглашение: полностью заглавные буквы с разделением слов подчеркиванием. Стиль UPPER_CASE_WITH_UNDERSCORES. Примеры: MAX_OVERFLOW, TOTAL_RETRIES, API_SECRET_KEY. Если вы видите такую переменную в коде, вы не должны пытаться переназначить ее значение.
Внутреннее использование (Private/Protected): Если переменная, метод функции или класс предназначены только для внутреннего использования в модуле или классе и не являются частью публичного API (то есть не должны вызываться извне), их имена должны начинаться с одного нижнего подчеркивания. Пример: _internal_buffer, def _calculate_hash():. Это не делает переменную строго приватной на уровне интерпретатора, это сигнал для других программистов: 'Осторожно, это внутренний механизм, используйте его на свой страх и риск, он может измениться в будущих версиях без предупреждения'.
Конфликты с ключевыми словами: Если наиболее подходящее имя переменной совпадает с зарезервированным ключевым словом Python (например, class, id, print), PEP 8 рекомендует добавить одно подчеркивание в конец имени: class_ = 'Math', id_ = 123. Это гораздо лучше, чем искажать слово (например, klass или clss).
# Примеры именования по PEP 8
# Константы (UPPER_CASE)
MAX_USERS = 100
DEFAULT_TIMEOUT = 5.0
# Классы (CamelCase)
class NetworkConnector:
def __init__(self, host: str, port: int):
# Переменные экземпляра (snake_case)
self.host = host
self.port = port
# Внутренняя (защищенная) переменная (_snake_case)
self._connection_status = False
# Метод (snake_case)
def connect_to_server(self):
pass
# Внутренний метод
def _ping_host(self):
pass
# Конфликт с ключевым словом (trailing underscore)
def filter_users(filter_):
pass
Как согласно PEP 8 следует назвать переменную, значение которой является глобальной константой (не должно меняться)?
Флеш-карточки
snake_case
Нажмите, чтобы увидеть ответ
Используется для именования переменных, функций и методов. (пример: user_name)
Нажмите, чтобы вернуться
CamelCase / PascalCase
Нажмите, чтобы увидеть ответ
Используется для именования Классов и Исключений. (пример: UserProfile)
Нажмите, чтобы вернуться
UPPER_CASE
Нажмите, чтобы увидеть ответ
Используется для именования констант на уровне модуля. (пример: MAX_RETRIES)
Нажмите, чтобы вернуться
Одинарное подчеркивание спереди (_variable)
Нажмите, чтобы увидеть ответ
Обозначает 'защищенный' (непубличный) атрибут, метод или модуль, предназначенный для внутреннего использования.
Нажмите, чтобы вернуться
PEP 8: Рекомендации по программированию (Проверки на None и Булевы значения)
PEP 8 — это не только про пробелы и регистр букв. Это еще и рекомендации по написанию безопасного и логичного кода (Programming Recommendations). Разберем самые критичные из них, на которых часто 'горят' джуниоры во время код-ревью.
Сравнение с None. В Python None — это объект-одиночка (Singleton). Это означает, что в памяти приложения существует только один объект None. Поэтому, когда вы хотите проверить, равна ли переменная None, вы никогда не должны использовать оператор равенства ==. Вы обязаны использовать оператор идентичности is. Правильно писать: if variable is None: или if variable is not None:. Почему это важно? Потому что оператор == вызывает внутри класса магический метод __eq__(), который может быть переопределен пользователем и вернуть непредсказуемый результат. А оператор is проверяет адреса памяти, что делает проверку абсолютно надежной и работает намного быстрее.
Проверка логических значений (Boolean). Не сравнивайте переменные типа bool с True или False с помощью == или is. Если у вас есть переменная is_active = True, писать if is_active == True: — это дурной тон и нарушение PEP 8 (и здравого смысла). Правильно писать просто: if is_active:. А для отрицания: if not is_active:. Это делает код похожим на обычный английский язык (Читаемость имеет значение!).
Использование 'истинности' пустых объектов. В Python многие объекты имеют 'ложное' (falsy) значение по умолчанию. Пустые списки [], пустые строки "", пустые словари {}, ноль 0 — все они оцениваются как False в логическом контексте. Поэтому, если вам нужно проверить, что список не пустой, не пишите if len(my_list) > 0:. Это не Pythonic! Правильно писать просто: if my_list:. Это элегантно, коротко и невероятно быстро. Интерпретатор сам поймет, что если в списке есть элементы, он истинный, а если нет — ложный.
Использование методов строк вместо срезов. Если вам нужно проверить, начинается ли строка с определенного префикса (или заканчивается суффиксом), PEP 8 рекомендует использовать встроенные методы str.startswith() и str.endswith(), а не срезы строк. То есть if word.startswith('pre'): предпочтительнее, чем if word[:3] == 'pre':. Методы чище и менее подвержены ошибкам (например, при опечатке в индексах среза).
# Антипаттерны (Как делать НЕ надо)
def bad_practices(user_data, is_admin):
if user_data == None: # ОШИБКА PEP 8 (использовать is)
return
if is_admin == True: # ОШИБКА PEP 8 (просто if is_admin:)
print("Admin")
if len(user_data) > 0: # НЕ PYTHONIC (просто if user_data:)
print("Has data")
if user_data[0][:5] == 'error': # НЕ PYTHONIC (использовать startswith)
print("Error found")
# Pythonic way (Как делать надо)
def good_practices(user_data: list, is_admin: bool):
if user_data is None:
return
if is_admin:
print("Admin")
if user_data:
print("Has data")
if user_data[0].startswith('error'):
print("Error found")
Какой способ проверки того, что список `items` пустой, является наиболее корректным с точки зрения 'Pythonic way' и рекомендаций PEP 8?
Как Python работает под капотом: Интерпретатор и Память
До сих пор мы говорили о стиле. Теперь мы погружаемся в инженерию. Чтобы писать эффективный код, вы должны понимать, как Python его выполняет. Python часто называют интерпретируемым языком, но это лишь половина правды. На самом деле процесс выполнения программы делится на два этапа: компиляция в байт-код и его интерпретация.
Когда вы запускаете скрипт (например, python app.py), CPython (стандартная и самая популярная реализация языка Python, написанная на C) сначала читает ваш исходный код и компилирует его в специальный промежуточный формат — байт-код (bytecode). Вы могли замечать появление папки __pycache__ с файлами .pyc в вашем проекте. Это и есть сохраненный байт-код. Это низкоуровневые инструкции, независимые от платформы. Важно понимать: Python компилирует код не в машинный код (понятный процессору), а в инструкции для своей собственной Виртуальной Машины.
Затем в дело вступает PVM (Python Virtual Machine) — виртуальная машина Python. Она построчно читает этот байт-код и переводит его в машинные команды вашей операционной системы. Именно из-за этого дополнительного слоя абстракции (PVM) Python работает медленнее, чем компилируемые языки вроде C++ или Go. Но взамен вы получаете кроссплатформенность: один и тот же скрипт будет работать на Windows, Linux и macOS без переписывания.
Управление памятью (Memory Management). В C/C++ программист должен вручную выделять память под объекты и освобождать ее (иначе произойдет утечка памяти). В Python вам об этом думать не нужно благодаря Garbage Collector (Сборщику мусора). Основной механизм работы памяти в Python — это подсчет ссылок (Reference Counting). Каждый объект в памяти имеет счетчик. Когда вы создаете переменную x = 10, создается объект 10, и на него указывает ссылка x. Счетчик равен 1. Если вы сделаете y = x, счетчик объекта 10 станет равен 2. Как только переменные уничтожаются (например, функция завершает работу) или переназначаются, счетчик уменьшается. Когда счетчик достигает нуля, Сборщик мусора моментально удаляет объект из оперативной памяти, освобождая ресурсы. Понимание этого процесса критически важно, когда вы начнете работать с большими объемами данных: если на ваши старые данные все еще ссылаются глобальные переменные или списки, Сборщик мусора не сможет их удалить, и программа 'съест' всю оперативную память (Memory Leak).
Как называется специальная папка, которую создает интерпретатор CPython для кэширования скомпилированного байт-кода модулей, чтобы ускорить их последующую загрузку?
Флеш-карточки
CPython
Нажмите, чтобы увидеть ответ
Стандартная (и самая популярная) реализация интерпретатора Python, написанная на языке C.
Нажмите, чтобы вернуться
Байт-код (Bytecode)
Нажмите, чтобы увидеть ответ
Промежуточное представление кода, в которое компилируется исходный скрипт .py перед выполнением в Виртуальной Машине Python.
Нажмите, чтобы вернуться
Подсчет ссылок (Reference Counting)
Нажмите, чтобы увидеть ответ
Главный механизм сборки мусора в Python: когда количество ссылок на объект в памяти падает до нуля, он уничтожается.
Нажмите, чтобы вернуться
Динамическая, но Строгая типизация
Еще один важный концепт, который мы должны синхронизировать. В Python вам не нужно объявлять тип переменной при её создании. Вы не пишете int age = 20, вы пишете просто age = 20. Это называется Динамической типизацией (Dynamic Typing). Тип переменной определяется в момент выполнения программы (в рантайме), в момент присваивания ей значения. Более того, вы можете в любой момент переназначить переменную на другой тип: age = "Twenty". В языках со статической типизацией (Java, C#) это вызвало бы ошибку компиляции.
Однако, многие джуниоры путают динамическую типизацию со слабой типизацией (как в JavaScript). В Python строгая (сильная) типизация! (Strong Typing). Что это значит? Это значит, что интерпретатор не будет пытаться молча 'угадать' ваши намерения и конвертировать типы 'под капотом', если вы совершаете логическую ошибку.
Например, в JavaScript выражение "5" + 2 выдаст строку "52" (число неявно превратилось в строку). Выражение "5" - 2 выдаст число 3 (строка неявно превратилась в число). Это называется слабой типизацией, и это источник огромного количества скрытых багов.
Если вы попытаетесь сделать "5" + 2 в Python, программа моментально упадет с ошибкой TypeError: can only concatenate str (not "int") to str. Python требует явности (Дзен!). Если вы хотите сложить их как числа, вы должны явно написать int("5") + 2. Если как строки — "5" + str(2). Строгая типизация делает код намного надежнее, предсказуемее и безопаснее. Вы всегда знаете, что если типы не совпадают, Python громко заявит об этом (Errors should never pass silently), а не продолжит работать с искаженными данными.
На заметку (Type Hinting): Начиная с Python 3.5, были введены аннотации типов (Type Hints), например: def greet(name: str) -> str:. Важно понимать, что они НЕ отменяют динамическую типизацию. Интерпретатор полностью игнорирует их при выполнении кода (он не выдаст ошибку, если вы передадите число вместо строки). Аннотации используются исключительно для удобства разработчика (IDE будет подсвечивать методы строк) и для внешних инструментов статического анализа (например, MyPy), которые помогают находить ошибки до запуска кода.
# Демонстрация сильной (строгой) типизации
# В JavaScript: "Age: " + 25 -> "Age: 25"
# В Python это вызовет TypeError:
try:
result = "Age: " + 25
except TypeError as e:
print(f"Поймана ошибка: {e}")
# Правильно (Явное лучше неявного):
result_correct = "Age: " + str(25)
print(result_correct)
# Динамическая типизация позволяет менять тип на лету (но лучше так не делать):
my_variable = 10 # Сейчас это int
my_variable = "Hello" # Теперь это str
Какое утверждение о системе типов в Python является верным?
Переменные - это ярлыки, а не коробки
Чтобы понять самое важное разделение типов данных в Python (изменяемые и неизменяемые), мы должны сначала сломать стереотип, которому учат на многих базовых курсах. Часто говорят: 'Переменная — это коробочка, в которую вы кладете значение'. В языке C это так. В Python переменная — это не коробочка. Это ярлык (бирка) с именем, который привязан к объекту в памяти.
Объект существует сам по себе в оперативной памяти (где-то там, в 'куче' - heap). У объекта есть свой уникальный идентификатор (адрес в памяти), тип и значение. Когда вы пишете a = 1000, интерпретатор создает объект 1000 типа int, а затем берет 'ярлык' с именем a и привязывает его к этому объекту.
Если вы напишете b = a, Python НЕ создает копию объекта 1000! Он берет второй ярлык с именем b и привязывает его к тому же самому объекту в памяти. И a, и b указывают на один и тот же адрес. Это легко проверить с помощью встроенной функции id(), которая возвращает уникальный идентификатор объекта (в CPython это его физический адрес в оперативной памяти).
Оператор is, о котором мы говорили ранее при проверке на None, на самом деле делает очень простую вещь: он сравнивает результаты функции id() для двух переменных. Выражение a is b эквивалентно id(a) == id(b). Это называется проверкой на идентичность (указывают ли они на один и тот же объект), в отличие от оператора ==, который проверяет равенство значений (содержимое объектов может быть одинаковым, но это могут быть два разных объекта в разных участках памяти).
Почему это критически важно знать? Потому что именно эта концепция 'ярлыков' приводит к самым неочевидным багам, когда мы начинаем работать с изменяемыми типами данных (Mutable data types). Если два 'ярлыка' привязаны к одному списку, и вы измените этот список через первую переменную, вторая переменная также 'увидит' эти изменения, ведь список в памяти всего один!
# Демонстрация переменных как "ярлыков" к объектам в памяти
x = [1, 2, 3] # Создаем список в памяти, прикрепляем ярлык 'x'
y = x # Прикрепляем ярлык 'y' к ТОМУ ЖЕ объекту
print(f"ID переменной x: {id(x)}")
print(f"ID переменной y: {id(y)}")
print(f"x is y: {x is y}") # True, так как адреса совпадают
# Теперь изменим объект через переменную 'x'
x.append(4)
# Так как 'y' указывает на тот же объект, мы увидим изменения и там
print(f"Значение y: {y}") # [1, 2, 3, 4]
Какая встроенная функция в Python возвращает уникальный идентификатор (адрес в памяти) объекта, на который ссылается переменная?
Самая важная тема: Mutable vs Immutable (Изменяемые и Неизменяемые типы)
Если бы вам нужно было вынести из этого курса только одну концепцию, это должно было бы быть разделение типов на Mutable (изменяемые) и Immutable (неизменяемые). Это корень 90% багов у джуниор-разработчиков и излюбленная тема на технических собеседованиях.
Immutable (Неизменяемые) типы: числа (int, float, complex), булевы значения (bool), строки (str), кортежи (tuple) и замороженные множества (frozenset).
Свойство: После того как объект неизменяемого типа создан в памяти, его состояние (значение) невозможно изменить. Никак. Вообще.
Подождите, скажете вы. Но я ведь могу написать name = "Python", а потом name = "Python 3"! Разве я не изменил строку? НЕТ. Вы не изменили строку 'Python'. Вы создали в памяти абсолютно новую строку 'Python 3', и перевесили 'ярлык' (переменную name) со старого объекта на новый. Старый объект 'Python' остался в памяти (и вскоре будет уничтожен сборщиком мусора, если на него нет других ссылок). Если вы напишете text = "hello" и попытаетесь изменить первую букву: text[0] = "H", Python выдаст TypeError: 'str' object does not support item assignment. Строки нельзя менять по частям. Операции вроде конкатенации (+) или метод .replace() всегда создают и возвращают новые объекты, не трогая оригинал.
Mutable (Изменяемые) типы: списки (list), словари (dict), множества (set) и пользовательские классы (по умолчанию).
Свойство: Эти объекты можно изменять 'на месте' (in-place). Вы можете добавлять, удалять или менять их элементы, при этом адрес объекта в памяти (его id) остается прежним.
Когда вы делаете my_list.append(10), новый список не создается. В память существующего списка просто дописывается новый элемент. И вот тут кроется главная ловушка: если у вас есть функции, которые принимают изменяемые объекты в качестве аргументов, и вы модифицируете их внутри функции, они изменятся и снаружи функции! Это называется 'побочный эффект' (Side Effect). В функциональном программировании побочных эффектов стараются избегать, но в повседневном скриптинге на Python это обычное дело, о котором просто нужно помнить.
# Демонстрация неизменяемости (Immutability)
text = "hello"
original_id = id(text)
print(f"Original text: {text}, ID: {original_id}")
# Пытаемся 'изменить' текст (на самом деле создаем новый объект)
text = text + " world"
new_id = id(text)
print(f"New text: {text}, ID: {new_id}")
print(f"ID изменился? {original_id != new_id}") # True! Это два РАЗНЫХ объекта
# Демонстрация изменяемости (Mutability)
my_list = [1, 2, 3]
list_id = id(my_list)
print(f"\nOriginal list: {my_list}, ID: {list_id}")
# Меняем объект 'на месте'
my_list.append(4)
my_list[0] = 99
list_id_after = id(my_list)
print(f"Modified list: {my_list}, ID: {list_id_after}")
print(f"ID изменился? {list_id != list_id_after}") # False! Это ТОТ ЖЕ САМЫЙ объект в памяти
Выберите из списка только Immutable (неизменяемые) типы данных в Python:
Флеш-карточки
Immutable (Неизменяемые)
Нажмите, чтобы увидеть ответ
Объекты, состояние которых нельзя изменить после создания. Любая операция создает новый объект. (int, float, str, tuple, bool)
Нажмите, чтобы вернуться
Mutable (Изменяемые)
Нажмите, чтобы увидеть ответ
Объекты, которые можно менять 'на месте' (in-place), при этом их id() в памяти остается прежним. (list, dict, set)
Нажмите, чтобы вернуться
Побочный эффект (Side Effect) при передаче в функцию
Нажмите, чтобы увидеть ответ
Если передать Mutable объект в функцию и изменить его там (.append()), он изменится и снаружи функции. С Immutable объектами такого не произойдет.
Нажмите, чтобы вернуться
Ловушка для джуниоров: Mutable Default Argument (Изменяемый аргумент по умолчанию)
Из концепции изменяемости вытекает самая знаменитая ошибка в Python, которая 100% будет на вашем собеседовании или код-ревью. Ситуация: вы пишете функцию для регистрации пользователя. Если список ролей не передан, вы хотите по умолчанию присвоить пустой список, чтобы затем добавить туда базовую роль.
Джуниор напишет так: def add_user(name, roles=[]):. Кажется логичным? Да. Будет ли это работать? Да, первый раз. Но при втором и последующих вызовах функции без передачи списка вы с ужасом обнаружите, что новый пользователь получает роли предыдущих пользователей!
Почему это происходит? (Внимательно, это сложный момент). В Python аргументы по умолчанию вычисляются и создаются в памяти ТОЛЬКО ОДИН РАЗ — в момент, когда интерпретатор читает определение функции (на этапе парсинга файла `def ...`), а не каждый раз при вызове функции.
Когда интерпретатор видит roles=[], он создает в памяти объект списка. Этот список привязывается к функции. При первом вызове функции вы добавляете туда 'user'. Список в памяти теперь ['user']. При втором вызове функции без аргументов, Python использует ТОТ ЖЕ САМЫЙ список из памяти (ведь он изменяемый, и он был сохранен функцией). И вы добавляете туда второго пользователя. Теперь там ['user', 'admin']. И так далее. Эта общая память становится мусорным ведром.
Как это исправить? Правило (которое даже включено в линтеры кода, такие как flake8): НИКОГДА не используйте изменяемые объекты (списки, словари, множества) в качестве значений по умолчанию для аргументов функции.
Вместо этого используйте неизменяемый тип None. А внутри функции проверяйте: если аргумент равен None, тогда создавайте новый, чистый список.
Правильный Pythonic way:def add_user(name, roles=None): if roles is None: roles = []
Теперь при каждом вызове функции, если список не передан, будет выполняться блок if, который каждый раз будет создавать АБСОЛЮТНО НОВЫЙ пустой список в новой ячейке памяти. Эта ошибка настолько распространена, что понимание ее механики сразу выделяет вас среди начинающих программистов.
# Демонстрация проблемы (Антипаттерн)
def bad_add_item(item, basket=[]):
basket.append(item)
return basket
print(bad_add_item("Apple")) # ['Apple']
print(bad_add_item("Banana")) # ['Apple', 'Banana'] - Упс! Откуда тут яблоко?
# -----------------------------------------------------
# Правильное решение (Pythonic way)
def good_add_item(item, basket=None):
if basket is None:
basket = [] # Создаем НОВЫЙ список при КАЖДОМ вызове
basket.append(item)
return basket
print(good_add_item("Apple")) # ['Apple']
print(good_add_item("Banana")) # ['Banana'] - Все работает как надо!
Почему использование изменяемого объекта (например, пустого списка []) в качестве значения аргумента по умолчанию в функции является ошибкой?
Особый случай: Изменяемые элементы внутри Неизменяемых
Чтобы закрепить понимание ссылочной модели памяти в Python, давайте рассмотрим интересную ситуацию, которая на первый взгляд кажется парадоксом. Мы знаем, что Кортеж (Tuple) — это неизменяемая структура данных. Вы создаете кортеж t = (1, 2, 3), и вы не можете заменить 1 на 99. Попытка сделать t[0] = 99 выдаст TypeError. Это база.
Но что, если внутри кортежа лежит список? Списки, как мы помним, изменяемые. Давайте создадим кортеж: t = (1, 2, [3, 4]).
Вопрос на засыпку: Можем ли мы изменить содержимое списка внутри этого кортежа? Ответ: Да! Мы можем сделать t[2].append(5). И наш кортеж станет (1, 2, [3, 4, 5]).
Как же так? Разве кортеж не неизменяемый? Разве это не нарушает фундаментальные правила языка?
Нет, не нарушает. И вот почему, если мыслить концепцией 'ярлыков' и указателей:
Кортеж хранит в себе не сами объекты, а ссылки (адреса) на эти объекты в памяти. Наш кортеж хранит три ссылки:
1) Ссылка на объект int(1)
2) Ссылка на объект int(2)
3) Ссылка на объект list([3, 4]).
Неизменяемость кортежа означает только одно: он никогда не позволит вам изменить свои ссылки. Он поклялся, что его третий элемент всегда будет указывать на тот самый список с определенным адресом в памяти (id). Вы не можете написать t[2] = [9, 9] — это попытка заставить кортеж смотреть на новый объект (изменить ссылку), и это вызовет ошибку.
Но метод .append(5) не меняет адрес списка в памяти! Он проникает внутрь списка по существующей ссылке и модифицирует его на месте. Ссылка внутри кортежа осталась неизменной, кортеж свою клятву сдержал, а вот сам объект по этому адресу изменил свое внутреннее состояние. Это глубокое понимание того, как работают структуры данных в Python, и почему важно отличать саму переменную (ссылку) от объекта, на который она ссылается.
# Демонстрация кортежа со списком внутри
my_tuple = (1, 2, [10, 20])
print(f"Исходный кортеж: {my_tuple}")
# ПОПЫТКА 1: Переназначить элемент кортежа (Изменить ссылку)
# Это вызовет TypeError! Кортеж защищает свои ссылки.
try:
my_tuple[2] = [99, 99]
except TypeError as e:
print(f"Ошибка при переназначении: {e}")
# ПОПЫТКА 2: Изменить сам объект списка (Мутация in-place)
# Это сработает! Мы не трогаем ссылку, мы модифицируем объект по адресу.
my_tuple[2].append(30)
print(f"Измененный кортеж: {my_tuple}") # (1, 2, [10, 20, 30])
Что выведет на экран следующий код? t = (1, [2]) t[1].extend([3, 4]) print(t)
Область видимости (Scope) и правило LEGB
Мы переходим к следующему фундаментальному блоку — Области видимости. Когда вы пишете имя переменной x и просите Python вывести ее на экран print(x), как интерпретатор понимает, какое именно значение x нужно взять? Ведь в вашем проекте может быть сотня файлов и тысячи функций, и переменная с именем x может встречаться десятки раз. Механизм, по которому Python ищет переменные, называется разрешением имен (Name Resolution), и он строго подчиняется правилу LEGB.
LEGB — это аббревиатура от названий четырех областей видимости в порядке их поиска: Local (Локальная), Enclosing (Объемлющая), Global (Глобальная) и Built-in (Встроенная).
1. Local (Локальная): Это переменные, созданные внутри функции, которая выполняется в данный момент. Если вы внутри def my_func(): напишете x = 10, то x — локальная переменная. Python всегда ищет сначала здесь. Как только функция завершает работу, локальная область видимости уничтожается вместе со всеми переменными (собирается мусор).
2. Enclosing (Объемлющая, нелокальная): Эта область существует только если у вас есть вложенные функции (функция внутри функции — это популярный паттерн при написании Декораторов). Если вы вызываете переменную внутри внутренней функции, а ее там нет, Python ищет ее в локальной области видимости внешней (родительской) функции.
3. Global (Глобальная, уровень модуля): Это переменные, определенные на самом верхнем уровне файла (модуля) или объявленные глобальными с помощью ключевого слова global. В Python 'глобальная' переменная на самом деле локальна для текущего файла (модуля). Она не видна автоматически в других файлах, пока вы ее не импортируете. Если переменной нет ни в Local, ни в Enclosing, Python ищет ее здесь.
4. Built-in (Встроенная): Это самый последний шанс найти имя. Это предопределенные имена, встроенные в сам интерпретатор Python, например print, len, Exception, int. Они живут в специальном модуле builtins. Если интерпретатор дошел до этого уровня, не нашел имя и здесь, программа падает с ошибкой NameError (переменная не определена).
Понимание иерархии LEGB позволяет избежать глупых ошибок. Например, если вы создадите глобальную переменную с именем len = 10, вы перекроете (shadowing) встроенную функцию len(), потому что Global находится ближе, чем Built-in. Если после этого вы попытаетесь сделать len(my_list), вы получите ошибку 'int object is not callable', потому что Python найдет число 10 в Global scope и даже не будет смотреть в Built-in!
# Демонстрация правила LEGB
# (B) Built-in - встроенные функции, мы их можем использовать всегда
# (G) Global scope - Глобальная переменная модуля
x = 'Глобальная (Global)'
def outer_function():
# (E) Enclosing scope - Переменная объемлющей функции
x = 'Объемлющая (Enclosing)'
def inner_function():
# (L) Local scope - Локальная переменная
x = 'Локальная (Local)'
print(x) # Выведет 'Локальная', так как Local проверяется первой
inner_function()
outer_function()
В каком порядке (согласно аббревиатуре LEGB) интерпретатор Python ищет значение переменной?
Флеш-карточки
Local (Локальная) область видимости
Нажмите, чтобы увидеть ответ
Переменные, определенные внутри текущей функции. Они создаются при вызове функции и удаляются после ее завершения.
Нажмите, чтобы вернуться
Global (Глобальная) область видимости
Нажмите, чтобы увидеть ответ
Переменные, определенные на уровне модуля (файла) вне каких-либо функций.
Нажмите, чтобы вернуться
Built-in (Встроенная) область видимости
Нажмите, чтобы увидеть ответ
Зарезервированные имена интерпретатора Python, такие как print, len, int, id.
Нажмите, чтобы вернуться
Name Shadowing (Перекрытие имен)
Нажмите, чтобы увидеть ответ
Ситуация, когда локальная переменная имеет такое же имя, как и глобальная или встроенная (например, переменная list = []), блокируя доступ к оригинальному объекту.
Нажмите, чтобы вернуться
Модификаторы области видимости: global и nonlocal
По умолчанию, правило LEGB работает для чтения переменных. Если вы находитесь внутри функции и пишете print(x), Python найдет глобальную x и выведет ее. Но что происходит, когда вы пытаетесь изменить переменную? Здесь правила игры меняются. Любая операция присваивания (=) внутри функции по умолчанию создает новую локальную переменную, а не изменяет глобальную. Если у вас есть глобальная score = 0, и внутри функции вы пишете score = 100, вы не меняете глобальный счетчик. Вы создаете новую локальную переменную score, которая 'затеняет' (shadows) глобальную на время работы функции.
Чтобы изменить глобальную переменную внутри функции, вам нужно явно сказать интерпретатору: 'Не создавай новую переменную, используй ту, что на уровне модуля'. Для этого используется ключевое слово global.
Пример: global score. После этого заявления любая запись в score будет влиять на глобальную область видимости. Архитектурная ремарка: использование global считается плохой практикой (Anti-pattern) в 95% случаев. Функции должны быть 'чистыми' (Pure Functions) — они должны принимать данные через аргументы и возвращать результат через return. Глобальное состояние делает код запутанным и сложным для тестирования (ведь результат функции начинает зависеть от состояния среды, а не только от аргументов).
А что делать, если мы работаем с вложенными функциями? Если у нас есть внешняя функция и внутренняя функция, и мы хотим изменить переменную внешней функции из внутренней? Ключевое слово global здесь не поможет — оно проигнорирует внешнюю функцию и улетит искать переменную на уровень всего модуля. Для таких случаев в Python 3 ввели ключевое слово nonlocal. Оно говорит: 'Ищи переменную в ближайшей Объемлющей (Enclosing) области видимости, но не выходи на глобальный уровень'. Этот механизм используется при создании Замыканий (Closures) — продвинутой техники, которая позволяет внутренней функции 'запоминать' состояние внешней, даже когда внешняя уже завершила работу.
# Демонстрация global и nonlocal
counter = 0 # Глобальная переменная
def increment_global():
global counter # Явно запрашиваем доступ к записи в глобальную
counter += 1
print(f"Global counter: {counter}")
def outer():
msg = "Привет"
def inner():
nonlocal msg # Доступ к переменной из Enclosing scope
msg = "Привет из Inner!" # Изменяем внешнюю переменную
inner()
print(f"Msg после inner: {msg}")
increment_global()
outer()
Задание
Финальный проект урока: Рефакторинг 'грязного' скрипта
- Скопируйте предоставленный ниже 'спагетти-код' в вашу IDE (PyCharm или VS Code).
- Проанализируйте его на предмет нарушений PEP 8 (отступы, имена переменных, импорты).
- Найдите и исправьте логическую ошибку, связанную с Mutable Default Argument.
- Замените сложные проверки if на Pythonic way (используя неявное приведение к bool и методы startswith).
- Избавьтесь от ключевого слова global, переписав логику функции на возврат значения (return) и передачу аргументов.
- Добавьте аннотации типов (Type Hints) и docstrings к функциям.