Введение

Python — язык обманчиво простой. Мы любим его за то, что он позволяет набросать прототип за вечер: никакой тебе возни с компиляторами, строгой типизацией или управлением памятью вручную. Пишешь как думаешь, и оно работает. Кайф? Кайф.

Но у этой медали есть обратная сторона. Python прощает слишком многое. Он позволяет писать код, который выглядит рабочим, проходит базовые тесты и даже Code Review (если ревьюер устал или поторопился). Вы катите это в продакшен... а через неделю сервер ложится от переполнения памяти, или данные пользователей начинают загадочно перемешиваться.

Ошибка №1: Капкан с изменяемыми аргументами (Mutable Default Arguments)

Начнем с классики, которую любят спрашивать на собеседованиях. Казалось бы, каждый Python-разработчик знает: «Не пиши пустой список в аргументах по умолчанию!». Но знать правило и понимать физику процесса — разные вещи.

Давайте честно, у многих в проектах до сих пор проскакивает вот такой код:

def add_item_to_cart(item, cart=[]):
    cart.append(item)
    return cart

Выглядит безобидно. Логика автора проста: «Если корзину не передали, создай новую пустую и положи туда товар».

Что происходит под капотом?

Проблема в том, как Python воспринимает инструкцию def. Для интерпретатора def — это исполняемая команда. Она выполняется ровно один раз — в момент, когда Python впервые читает этот файл (момент определения функции).

Именно в этот момент Python создает объект функции и вычисляет значения аргументов по умолчанию. Он создает этот пустой список [] и «приклеивает» его к функции навсегда. Вы можете даже увидеть его своими глазами, заглянув в атрибут __defaults__:

print(add_item_to_cart.__defaults__)
# ([],) — вот он, наш список, живет внутри функции!

Каждый раз, когда вы вызываете функцию без второго аргумента, вы обращаетесь не к новому списку, а к тому самому, созданному при старте программы.

Почему это непростительно?

Локально это может не выстрелить. Вы запустили скрипт, проверили один раз — работает. Перезапустили — снова работает.

А теперь представьте, что этот код попадает в веб-сервис (Django, FastAPI — неважно). Приложение запускается и висит в памяти неделями.

  1. Приходит Юзер А, добавляет товар. Список cart внутри функции становится ['iPhone'].

  2. Приходит Юзер Б, добавляет свой товар. Он ожидает пустую корзину, но Python подсовывает ему тот же самый список-объект. Теперь в корзине ['iPhone', 'Samsung'].

Поздравляю, у вас утечка данных. Пользователи начинают видеть чужие заказы. В масштабах биллинга или банковских транзакций — это катастрофа, за которую увольняют.

Как делать правильно

Паттерн всего один — используйте None как маркер пустоты.

def add_item_to_cart(item, cart=None):
    if cart is None:
        cart = []  # Новый список создается ВНУТРИ функции при каждом вызове
    cart.append(item)
    return cart

Здесь список создается в теле функции (runtime), а не в заголовке (definition time). Каждый вызов гарантированно получает свежий, чистый объект.

Декораторы

Кстати, эта ловушка поджидает вас не только в функциях. Если вы пишете свои декораторы и инициализируете какую-то переменную в теле декоратора (а не внутри обертки wrapper), она тоже выполнится один раз при импорте модуля.

Это частая причина странных багов, когда состояние (например, кеш или счетчик вызовов) становится общим для всех задекорированных функций, хотя вы планировали их изолировать. Механика та же: код исполняется один раз при чтении файла, а «память» остается навсегда.

Ошибка №2: «Слепой» перехват, или почему ваш код бессмертен

Есть у разработчиков соблазн — сделать так, чтобы скрипт никогда не падал. Ну, знаете, это ложное чувство безопасности: «Оберну-ка я всё в try-except, и если что-то пойдет не так, программа просто пойдет дальше».

Выглядит это обычно так:

try:
    complex_data_processing()
    save_to_db()
except:  # Или except Exception:
    pass

В кругах опытных питонистов за такой код бьют по рукам линейкой. И вот почему.

1. Вы создаете зомби, которого нельзя убить

Конструкция except: (без указания типа исключения) перехватывает всё. Вообще всё. Включая системные сигналы.

Когда вы нажимаете Ctrl+C в терминале, Python генерирует KeyboardInterrupt. Когда система просит процесс завершиться, летит SystemExit. Ваш «слепой» except радостно ловит эти сигналы и говорит: «Не, мы работаем дальше». В итоге скрипт превращается в неуправляемый процесс, который приходится убивать через kill -9.

2. Вы прячете реальные баги

Представьте, что внутри блока try вы допустили опечатку. Написали usre_id вместо user_id. Python честно кидает NameError.
Но ваш except ловит его и... просто молчит. Программа работает дальше, логика ломается, данные не сохраняются, а вы смотрите в консоль и не понимаете, что происходит. Ошибок нет, но ничего не работает. Дебажить такое — сущий ад.

Реальный кейс из жизни

Однажды нам пришлось разбирать инцидент. Сервис обработки платежей неделю (!) работал «вхолостую».
Пользователи нажимали «Оплатить», сайт говорил «Спасибо», но деньги не списывались, а заказы не создавались. Техподдержка молчала, потому что мониторинг был зеленый. Ошибок в логах — ноль.

Оказалось, джуниор обернул коннект к базе данных в такой вот try-except pass. В какой-то момент пароль от базы сменили. Скрипт просто перестал подключаться к БД, молча проглатывал ошибку OperationalError и шел дальше, делая вид, что всё хорошо. Неделя потерянной выручки из-за четырех строк кода.

Как делать правильно

Правило написано: перехватывайте только то, что ожидаете.

Если вы боитесь ошибки сети — ловите ConnectionError. Боитесь кривого JSON — ловите JSONDecodeError.

import logging

try:
    save_to_db(data)
except (ConnectionError, TimeoutError) as e:
    # Мы знаем, что это может случиться, и знаем, как реагировать
    logging.error(f"База недоступна, пробуем повторить: {e}")
    retry_later(data)
except Exception as e:
    # На самый крайний случай — ловим всё остальное, НО обязательно логируем
    logging.exception("Произошла непредвиденная ошибка!")
    raise  # И пробрасываем дальше, чтобы приложение всё-таки упало (или уведомило Sentry)

Запомните: упавшее приложение лучше, чем приложение, которое молча врет, что работает.

Ошибка №3: Синхронный код в асинхронном (Sync in Async)

Сейчас все бегут на FastAPI и aiohttp. Это модно, молодежно и производительно. Выучить синтаксис несложно: приписал async перед def, await перед вызовом — и ты великолепен.

Но именно здесь кроется самая коварная ловушка для тех, кто переезжает с Django или Flask. Старые привычки умирают тяжело, и в коде появляется вот это:

import requests
import time

@app.get("/process")
async def process_data():
    # "Ну мне же надо просто дернуть стороннюю API..."
    response = requests.get("https://slow-service.com/api")
    
    # "...или подождать пару секунд"
    time.sleep(5)
    
    return {"status": "ok"}

Механика: Почему всё встало?

Чтобы понять, почему этот код — преступление, нужно вспомнить, как работает Event Loop (цикл событий).

Представьте, что ваш сервер — это один-единственный официант в ресторане (поток). В асинхронном режиме он работает гениально: принял заказ у столика №1, отдал на кухню, и пока там готовят, побежал принимать заказ у столика №2. Он не стоит и не ждет. Благодаря этому Python на ��дном ядре может держать тысячи соединений.

Но когда вы пишете requests.get(...) или time.sleep(...) внутри async def, вы заставляете этого официанта встать и ждать, пока повара пожарят котлету. Он не принимает новые заказы, он не отдает готовые блюда. Он просто стоит.

В этот момент ваш сервер «умирает» для всех остальных пользователей. Если запрос длится 5 секунд, то в течение 5 секунд сервер не ответит никому. Даже тем, кто просто запросил главную страницу. Health-check'и от Kubernetes начнут падать, балансировщик решит, что инстанс мертв, и начнет его перезагружать.

Почему это непростительно?

Потому что это убивает саму суть асинхронности. Один «тяжелый» синхронный запрос способен положить высоконагруженный сервис. Вы хотели производительности, а получили однопоточный скрипт, работающий хуже, чем старый добрый Flask на тредах.

Как делать правильно

  1. Сетевые запросы (I/O):
    Забудьте про requests. Вообще. Если вы в асинхронном мире, ваши друзья — httpx или aiohttp. Для работы с базой — asyncpg или Motor.

    import httpx
    
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://...") # Официант отдал заказ и ушел работать дальше
    
  2. Тяжелые вычисления (CPU-bound):
    Если вам нужно обработать картинку, посчитать хэш или распарсить гигантский JSON, await вам не поможет (это нагрузка на процессор, а не ожидание сети).
    Такие задачи нужно выбрасывать из Event Loop'а:

    • Вариант "Быстро и грязно": Запустить в отдельно через run_in_executor:

      loop = asyncio.get_running_loop()
      # Выполняем синхронную функцию в отдельно
      await loop.run_in_executor(None, heavy_function, arg1) 
      
    • Вариант "По-взрослому": Отправить задачу в очередь (Celery, TaskIQ, Dramatiq) и пусть её жуют отдельные воркеры. Главный сервер должен только принимать запросы.

Ошибка №4: Магия замыканий, или почему все ваши функции делают одно и то же

А вот здесь сыпятся даже опытные ребята. Это тонкий момент работы с областями видимости (scope), который выглядит как настоящий полтергейст.

Допустим, вам нужно динамически сгенерировать список функций. Например, создать 5 множителей: первый умножает на 0, второй на 1, и так далее. Вы пишете элегантный list comprehension:

# Создаем список функций: lambda x: x * 0, lambda x: x * 1 ...
multipliers = [lambda x: x * i for i in range(5)]

# Проверяем. Ожидаем: 2 * 0 = 0, 2 * 1 = 2 ...
results = [m(2) for m in multipliers]

Вы ожидаете увидеть [0, 2, 4, 6, 8].
Запускаете код.
Получаете: [8, 8, 8, 8, 8].

В этот момент хочется разбить монитор. Почему они все умножают на 4 (последнее значение i)? Вы же создавали их в цикле, когда i была разной!

Механика: Позднее связывание (Late Binding)

Проблема в том, как лямбда (или любая вложенная функция) «видит» внешние переменные.

В Python замыкания работают по ссылке, а не по значению. Когда вы пишете lambda x: x * i, функция не запоминает: «Ага, сейчас i равна нулю, запомню ноль». Она запоминает: «Мне нужно умножить x на переменную по имени i из внешней области видимости».

Функция ленива. Она не ищет значение i в момент создания. Она лезет за ним только в момент вызова.
А когда вы наконец вызываете эти функции (строкой ниже), цикл for уже давно закончился. Переменная i в глобальной области осталась висеть со значением 4. И все 5 ваших лямбд радостно лезут в одну и ту же переменную и видят там четверку.

Почему это непростительно?

Это логическая ошибка. Код валиден, ошибок нет, но логика работает совершенно не так, как задумано.
Особенно больно это бьет при создании UI-интерфейсов (например, в Tkinter или PyQt) или асинхронных колбэков, когда вы в цикле вешаете обработчики событий на кнопки:
Button 1, Button 2, Button 3...
И какую бы кнопку ни нажал юзер, срабатывает обработчик для последней.

Как делать правильно

Нам нужно заставить Python «заморозить» текущее значение i прямо в момент создания функции. Помните Ошибку №1 про аргументы по умолчанию? Там это было злом, а здесь — наше спасение.

Значения дефолтных аргументов вычисляются в момент определения функции. Используем это!

# Передаем i как аргумент по умолчанию
multipliers = [lambda x, i=i: x * i for i in range(5)]

Что здесь происходит:
При каждой итерации создается лямбда, у которой есть своя собственная локальная переменная i. Её значение берется из внешней i прямо сейчас (в момент создания) и «запекается» внутрь функции.
Теперь, даже когда внешняя i изменится, внутренняя локальная i останется той, какой была при рождении.

Ошибка №5: Игры со временем (Naive Datetime)

Если вы думаете, что время — это просто часы и минуты, вы ещё не дебажили биллинг в ночь перевода часов на зимнее время.

Новички (да и не только) обожают писать так:

from datetime import datetime

# "Просто дай мне текущее время"
created_at = datetime.now() 
# Результат: 2023-10-05 14:30:00 (и никакой информации о часовом поясе)

В документации такие объекты называют Naive (наивные). Они не знают, где они находятся: в Москве, Нью-Йорке или на Гринвиче. И это главная причина головной боли при масштабировании.

В чем проблема?

  1. Географический винегрет
    Представьте цепочку:

    • Ваш ноутбук в Москве (UTC+3).

    • Сервер приложения в Ирландии (UTC+0 или +1).

    • База данных (Postgres) настроена админом по дефолту в UTC.

    Вы сохраняете datetime.now() с сервера. В базу падает 14:00. Потом вы читаете это время скриптом с ноутбука, Python думает, что это локальное время (МСК). В итоге событие «улетает» на 3 часа в будущее или прошлое. Вы никогда не узнаете, когда на самом деле произошла ошибка.

  2. Ад с переводом часов (DST)
    В странах, где переводят часы, есть «волшебный» час осенью, когда время отматывается назад.
    Было 02:59, стало 02:00.
    Если вы используете Naive time, у вас в логах появятся две записи с меткой 02:30. Какая была раньше? Какая позже? Порядок событий восстановить невозможно. Для финансовых транзакций это приговор.

Почему это непростительно?

Потому что исправить это задним числом почти невозможно. Если у вас в базе лежит миллион записей с датой 2023-10-05 10:00:00 без таймзоны, вы не знаете: это 10 утра по Лондону или по Токио? Ваша аналитика врет, графики врут, а пользователи жалуются, что подписка истекла н�� день раньше.

Как делать правильно

Золотое правило бэкенда: Внутри системы всегда UTC. Локальное время — только для отображения пользователю.

Нам нужны Aware (осведомленные) объекты.

from datetime import datetime, timezone

# ПРАВИЛЬНО: Берем время сразу с привязкой к UTC
now_utc = datetime.now(timezone.utc)
# Результат: 2023-10-05 14:30:00+00:00 (хвост +00:00 важен!)
  1. Всегда создавайте даты с timezone.utc.

  2. В базе данных используйте тип TIMESTAMP WITH TIME ZONE (в Postgres).

  3. Если нужно показать время пользователю из Владивостока — конвертируйте UTC в его часовой пояс в самый последний момент (на фронтенде или в шаблонизаторе).

Вместо заключения

Python — дружелюбный язык. Он дает вам заряженный пистолет и не спрашивает, умеете ли вы стрелять.

  • Изменяемые дефолтные аргументы.

  • Except без указания типа.

  • Синхронный код в async.

  • Замыкания в циклах.

  • Naive datetime.

Это не просто «плохой стиль». Это баги, которые стоят денег. Проверьте свой проект прямо сейчас. Если нашли что-то из списка — вы знаете, чем заняться в ближайший спринт.

Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.

Пишите чистый код, и да пребудет с вами Zen of Python.

(P.S. Расскажите в комментариях, какой самый глупый баг ронял ваш прод? Мой фаворит — rm -rf не в той папке, но это уже совсем другая история).

Комментарии (36)


  1. Goron_Dekar
    19.01.2026 08:34

    Часто под подобными статьями про плюсы вылезают люди с коментариями типа "вот по этому плюсы плохи! Что за UB(например) в 21 веке".

    Всегда считал такие коментарии клоунадой. Мол, не знаешь - не пользуйся, зачем пришел сюда коментировать.

    К чему я это? А к тому, что разное поведение def foo(bar=1) и def foo(bar); bar=1 это плохо. Так нельзя в 21 веке.


    1. Tishka17
      19.01.2026 08:34

      А к тому, что разное поведение def foo(bar=1) и def foo(bar); bar=1 это плохо.

      А разное повдение y=x; x=x+1 и x=x+1; y=x? Это разные вещи и делают они разное. Более того, оно иногда и используется намеренно (хотя мы осуждаем).


      1. Goron_Dekar
        19.01.2026 08:34

        Да криво написал, согласен. Да и не важно, что там под копотом моей притензии - я поймал себя на том, что осуждаю сложность инструмента вместо того, чтобы сесть и разобраться, почему так сложно.


    1. Vindicar
      19.01.2026 08:34

      А ничего, что во втором случае передача параметра вообще не имеет смысла, так как его значение не используется? Какой-то у вас пример слишком синтетический, чтобы можно было понять посыл.


      1. Goron_Dekar
        19.01.2026 08:34

        Да я согласен. Просто был удивлён своим ходом мысли, и решил его записать.


        1. Vindicar
          19.01.2026 08:34

          Хотя в принципе я понимаю ваше сравнение - и то и другое вызвано ситуацией, когда "подкапотное" поведение вылезает наружу.


    1. Pand5461
      19.01.2026 08:34

      А оно с bar=1 одинаковое, вот с bar=[] будет разное.
      Вообще согласен, непонятно, зачем в Python 3 не сделали интерпретацию аргументов по умолчанию как выражений и не сделали нормальные области видимости, коль уж всё равно с Python 2 обратную совместимость сломали.


      1. Tishka17
        19.01.2026 08:34

        Это и есть выражения, просто они вычисляются в момент создания функции. Иначе бы возникли другие вопросы и другие претензии.


        1. Pand5461
          19.01.2026 08:34

          А какие бы возникли вопросы?
          Вот в Julia значения по умолчанию вычисляются в момент вызова функции, и там это ни у кого проблем не вызывает. Бонусом это даёт возможность ссылаться в значении по умолчанию на предыдущие аргументы. Наоборот, предостережения видел только в руководствах по Python и по Common Lisp. Причём в Lisp это относится только к литералам типа '(1 2), т.к. они по стандарту могут быть вычислены однократно при определении функции, вместо этого рекомендуется значения по умолчанию просто задавать как (list 1 2), тогда они гарантированно вычисляются в момент вызова.


          1. Tishka17
            19.01.2026 08:34

            я не знаю julia, можете показать как будут выгдятеть аналоги этого кода?

            def new_generator(default: int):
               def number_generator(arg: int=default):
                  return arg
               return number_generator
            
            def baz(generator=new_generator(1)):
               pass



            1. Pand5461
              19.01.2026 08:34

              Примерно так же, как и в Python

              function make_generator(default::Integer)
                  function generator(arg::Integer=default)
                      return arg
                  end
                  return generator
              end
              
              function baz(generator=make_generator(1))
                  return nothing
              end
              

              Но это, в общем, неинтересно. Интереснее то, что с мутабельными аргументами будет всё то же самое, без танцев с None.

              function make_collector(init::Vector=[])
                  function collector(args...)
                      return append!(init, args...)
                  end
                  return collector
              end
              
              julia> c1 = make_collector();
              
              julia> c2 = make_collector();
              
              julia> c1(3, 4, 5)
              3-element Vector{Any}:
               3
               4
               5
              
              julia> c2(5, 12, 13)
              3-element Vector{Any}:
                5
               12
               13
              
              julia> c1(5, 12, 13)
              6-element Vector{Any}:
                3
                4
                5
                5
               12
               13
              


          1. bbc_69
            19.01.2026 08:34

            Подозреваю, что дело в банальной производительности. Как я понял Julia - язык компилируемый и там это не так критично. А вот в скриптовом могут быть проблемы. Замедлять код на 50-10-20-50% из фичи, которая используется в 3% случаев, если не реже, так себе решение.

            Для меня подобное поведение не новость, потому что я сам язык хорошо знаю. А одно из первых правил питона гласит: будь аккуратен с мутабельными объектами.


            1. Pand5461
              19.01.2026 08:34

              Для Python 1/2 я ещё мог бы согласиться с предположением о производительности. Для Python 3 - сомнительно. Учитывая, что этот пример с мутабельным аргументом по умолчанию упоминается в каждом первом руководстве для новичков как типичный ногострел - ну уж можно было бы подумать, как его убрать. Опять же, с учётом поломки обратной совместимости в любом случае. Думаю, можно это было сделать без потерь производительности, Лисп же как-то справляется, в конце концов. Скорее, никто из core developer'ов не счёл такое поведение проблемным.
              С нормальными областями видимости, такое ощущение, та же история - не было в core developer'ах ярого функциональщика, которого бы поведение из 4-го примера прямо бесило. А так плоская область видимости внутри функции особо не мешает, пока не начинаешь активно пользоваться замыканиями.
              Думаю, если питон основной язык - то к особенностям привыкаешь и перестаёшь замечать, но вот если надо постоянно переключаться с одной семантики на другую - это раздражает.


              1. Fr0sT-Brutal
                19.01.2026 08:34

                Ага, если тысячи юзеров постоянно жмут не ту кнопку - надо менять кнопку, а не писать сотни мануалов о том, какую кнопку жать. Тем более что смысла делать именно такое поведение особо не видно. Это какая-то обрезанная разновидность статических переменных, но только для контейнеров? Могли бы просто запретить ее на уровне языка и все.


                1. bbc_69
                  19.01.2026 08:34

                  1. Это не обычные юзеры, а программисты, которые себя считают умными и должны бы в состоянии документацию прочитать. Ну и на счёт сотни мануалов Вы преувеличиваете.

                  2. Я считаю, что в любом ЯП есть тёмные области, поведение в которых выглядит странным. Про такие области надо знать и не ходить туда. Я думаю, что в принципе невозможно создать полноценный ЯП без "забавных особенностей".

                  3. Вы предлагаете проверять мутабельность объекта? Здорово. Как кастомные объекты проверять будем? Обозначать их мутабельность руками? А если он обозначен как немутабельный, а на самом деле что-то изменяет? Вот на ровном месте проблемы создать можно.


                  1. Fr0sT-Brutal
                    19.01.2026 08:34

                    1. У программистов и так полно чего нужно держать в контексте. Забивать при этом голову еще и картой минных полей конкретного ЯП - излишнее усложнение. Насчет сотни не преувеличиваю. Только за последние месяцы этот злополучный param: list=[] встретился в разных каналах с десяток раз

                    2. Но стремиться к их сокращению - стоит. В конце концов, бездушную машину приятнее нагружать, чем себя. К счастью, есть линтеры, которые ловят то, что не хочет ловить сам интерпретатор

                    3. Есть же понятие хешируемости для применения в качестве ключей. Либо можно в принципе запретить любое значение дефолта, кроме элементарных базовых.


                    1. bbc_69
                      19.01.2026 08:34

                      1. Ну так то каналы. А в реальном коде? Там, где я встречал, можно было и без этого обойтись.

                      2. Вот Вы сами решение и нашли. По мне так, это издержки того, что пишешь на многих языках. Ты знаешь либо один хорошо, либо много так себе. Электронные помощники спешат на помощь!

                      3. Написать хеш метод - минутное дело. И это вообще ничего не говорит об мутабельности объекта. А на счёт базовых, в том то и дело, что они зашиты внутри, переменные выглядят для него одинаковыми. Да и если запретить, то мне что, нельзя использовать мною придуманый, особо точный Decimal(5/2)?


                      1. Fr0sT-Brutal
                        19.01.2026 08:34

                        Там, где я встречал, можно было и без этого обойтись

                        Написать хеш метод - минутное дело. И это вообще ничего не говорит об мутабельности объекта

                        Всегда можно без этого, да, но речь не об этом. В моей картине мира подобные вещи - как открытый канализационный люк. Он должен быть всячески огорожен знаками, предупреждениями и заборами, чтобы исключить ненамеренное попадание (в контексте ЯП - не давать попасть туда, используя простые и типичные конструкции). Но если ты идешь туда целенаправленно, игнорируя знаки - предполагается, что ты знаешь, что делаешь, и готов к последствиям (в контексте ЯП - если ты задаешь дефолтное значение мутабельного объекта с переопределенным хешем, это явно не типичная штука, и тут уже сам несешь ответственность).


                      1. bbc_69
                        19.01.2026 08:34

                        О, мы пришли к разному пониманию ЯП. По мне, так это инструмент: есть крутой, с отдельной камерой, автоматами аварийной защиты, двойной изоляцией и прочее. А есть простой, как скальпель, где пораниться самому проще, чем сделать что-то полезное. Ну и кучу всего между ними. И перед тем, как брать в руки инструмент, надо пройти ТБ: знать, на что он способен и как работать так, чтобы без рук не остаться.


                      1. Fr0sT-Brutal
                        19.01.2026 08:34

                        ЯП все-таки скорее огромный и сложный инструмент, нежели скальпель)


                      1. bbc_69
                        19.01.2026 08:34

                        Такую аналогию я в своё время читал про C. Мощный инструмент, но в неумелых руках больше проблем принесёт.


              1. bbc_69
                19.01.2026 08:34

                Думаю, если питон основной язык - то к особенностям привыкаешь и перестаёшь замечать,

                Вот тут полностью соглашусь.

                Касаемо примера. Может я уже адаптировался к написанию на Питоне, но как по мне, этот пример несколько синтетический. Я не очень представляю себе ситуацию, когда нам было бы без разницы, как обрабатывать данные, со списком или нет. Ну т.е. либо ты это хочешь вставить в конкретный контейнер, либо он тебе совсем не нужен. Единственное, что могу припомнить, так это рекурсивные функции. Но даже там лучше убрать дефолтное значение и передавать пустой список при граничных условиях. А в приведённом примере функция выполняет два действия: обработку значения и вставку его в контейнер. А такое надо разбивать на два. Если же нам нужны данные из контейнера (по типу последовательности Фибоначчи), так тут лучше свёртку использовать (reduce).

                В общем, если подытожить, то тут должна быть скорее лекция о том, почему в принципе списки не должны выступать дефолтными значениями.

                Касаемо самого поведения. Могу предложить ещё одну причину. Предложенное Вами изменение нарушит простое правило Питона:

                Данные передаются по ссылке. Всегда.

                И тогда у нас может возникнуть вопрос, а почему тогда:

                x = [1,2,3,4,5]
                y=x
                y.append(6)
                print(x)

                Оба этих явления одного порядка.

                Скрытый текст

                Равно как и поведение в третьем примере.

                А это уже совсем другая ситуация.

                И ведь что характерно, изменения на этом могут не остановиться, а начать менять всё это и в других местах. Тогда мы вместо простого и лаконичного правила получим кучу условий здесь делаем так, тут этак, а тут сяк. Я за консистентное решение.


                1. Fr0sT-Brutal
                  19.01.2026 08:34

                  >Я не очень представляю себе ситуацию, когда нам было бы без разницы, как обрабатывать данные, со списком или нет

                  Например, если внутри там for item in lst: ... Нет списка в аргументах - подставляется пустой - ничего не делается, без каких-либо доп условий. А так пришлось бы писать lst or [] или if lst: for item in lst или еще что-то, а если таких мест много, то lst = lst or [].


                  1. bbc_69
                    19.01.2026 08:34

                    А не заменяется ли это на

                    if not list:
                      return []

                    Если нечего обрабатывать, то там и думать нечего как правило.

                    Опять же, что может заставить вас использовать такую функцию без аргументов? Оно ж тогда вообще теряет смысл. Ну т.е. если мы обрабатываем список, да ещё и в нескольких местах, то наверное мы его очень хотим получить.


                    1. Fr0sT-Brutal
                      19.01.2026 08:34

                      Вы придираетесь к упрощенным примерам, которые по определению могут не иметь смысла. Ну ок, как насчет опционального списка-фильтра-стоп-листа для некой выборки. Он вполне мб пустым. И предположим, что он пробрасывается еще в десяток мест внутри, в т.ч. в какой-нибудь си через не очень тщательно продуманное АПИ, которое может не ожидать null вместо массива.


                      1. bbc_69
                        19.01.2026 08:34

                        Я придираюсь потому, что в реальности не помню таких случаев. Хорошо, пусть будет чёрный список. Не тривиальна ли тогда обработка случая, когда его нет или он пустой? А если он может потом дополниться, то не лучше ли сначала его полностью собрать, а уже потом с ним что-то делать. Я к тому, что подобная запись свидетельствует о проблемах с кодом.

                        И я опять прихожу к тому, что это может быть какое-нибудь легаси, которое трогать страшно. Но легаси на то и легаси, что мы смотрим на него и умиляемся.


                      1. Fr0sT-Brutal
                        19.01.2026 08:34

                        Да тривиальна конечно, но она требует доп строчек, тогда как пустой список - нет.

                        К тому же не могу представить, зачем вообще может понадобиться конструкция lst: list=[] в текущем виде. Т.е. имеем не просто открытый люк, а люк, в который никто не лазает, только случайно падает. Ну и на фиг он нужен?


                      1. bbc_69
                        19.01.2026 08:34

                        Чтобы было что новичкам рассказывать :).

                        "А вот я в твои годы мутабельные параметры дебажил!"


                1. Pand5461
                  19.01.2026 08:34

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

                  В численных задачах часто опционально передается буфер для промежуточных вычислений или выходных данных. Тут, конечно, несколько другая проблема возникает - что размер буфера будет зависеть от входных данных - но при вычислении выражения для дефолтного значения при вызове функции это можно реализовать, а в текущем подходе уж точно никак. В Лиспе, опять же, дефолтное значение может ссылаться на предыдущие аргументы, т.е. на момент создания питона такое поведение не было совсем уж новаторством.

                  Данные передаются по ссылке. Всегда.

                  Ну тут можно и дальше зайти. Почему запись мутабельного объекта в аргументах функции будет передачей данных, а запись в теле функции нет тогда? Для меня, наоборот, более консистентной моделью было бы, что в записи функции всё передаётся by-expression, включая значения аргументов, а вот при вызове эти выражения вычисляются и создают необходимые объекты.


                  1. bbc_69
                    19.01.2026 08:34

                    В численных задачах часто опционально передается буфер для промежуточных вычислений или выходных данных.

                    Благодарю. Тут - да, без этого никак.

                    в записи функции всё передаётся by-expression, включая значения аргументов, а вот при вызове эти выражения вычисляются и создают необходимые объекты.

                    Вы говорите про другой ЯП. Как верно было отмечено в статье, def - это команда. С весьма простым поведением.


  1. Pand5461
    19.01.2026 08:34

    Ещё замечание: except Exception: таки считается более культурным вариантом, чем просто except:, потому что системные исключения наследуются не от Exception, и программу как раз-таки можно завершить без kill -9.


  1. Ktulhy
    19.01.2026 08:34

    Шо? Опять? Стабильно раз в месяц появляется подобная статья


  1. bbc_69
    19.01.2026 08:34

    # Создаем список функций: lambda x: x 0, lambda x: x 1 ...multipliers = [lambda x: x i for i in range(5)]# Проверяем. Ожидаем: 2 0 = 0, 2 * 1 = 2 ...results = [m(2) for m in multipliers]

    # Передаем i как аргумент по умолчанию

    multipliers = [lambda x, i=i: x * i for i in range(5)]

    За такой код я бы канделябром, чесслово.

    Если вам нужно последовательно применить изменения к элементам списка, надо использовать map:

    >>> list(map(lambda x: x*2, range(5)))
    [0, 2, 4, 6, 8]

    Из смешного, генерация функций тоже работает:

    >>> a = list(map(lambda i:(lambda x: x*i), range(5)))
    >>> [m(2) for m in a]
    [0, 2, 4, 6, 8]

    На крайняк, если нужно сначала создать именно список функций, сделать сначала декоратор:

    >>> def create_handler(i):
    ...   
    ...   def wrapper(x):
    ...     return x*i
    ...   return wrapper
    ...  
    >>> a = [create_handler(i) for i in range(5)]
    >>> [m(2) for m in a]
    [0, 2, 4, 6, 8]

    Чуть более многословно, зато более читаемо. У нас вроде нет лимитов на использование строчек.

    А то научатся гланды чесать через задницу, а потом такие: "Надо быть аккуратным, чтобы руки в говне не измазать."


    1. Fr0sT-Brutal
      19.01.2026 08:34

      Ну или хотя бы разделить аргумент и счетчик цикла

      multipliers = [lambda x, loc_i=i: x * loc_i for i in range(5)]

      А вообще зря создатели в лямбдах не стали отделять параметры скобками, теряется читаемость


      1. bbc_69
        19.01.2026 08:34

        Я может неясно выразился, но мой поинт был в другом: не надо писать сложный код, когда есть код простой. Нафига мне помнить о том, как работает замыкание, если этого можно избежать. Проблема предложенного автором "правильного" кода ровно в том же, в чём и код изначальный: его действие неочевидно. Увы, но и Ваш код страдает тем же.

        Правильный вариант: писать через декоратор и не мучать людям мозг. В том числе и себе в будущем.


        1. Fr0sT-Brutal
          19.01.2026 08:34

          Согласен насчет не мучать мозг. Код не "мой", а просто вариант чуть улучшить нечитаемое. Я бы тоже сделал как выше, через обертку (create_handler(i))