Всем привет! Меня зовут Денис Аникин, я тимлид в команде Chat в Райффайзенбанке. А также представитель внутреннего Python-сообщества, так называемый «community lead» (об этом как-нибудь в другой раз). В этой статье я хотел поговорить про отношение к Python среди разработчиков и обсудить все основные претензии, которые очень давно следуют за языком по пятам.
В начале я хочу коротко рассказать про причины этой статьи — она не претендует на статус серьезного исследования, статья, скорее, немного ироничная. И возникла она как ответ на бесконечные камни, летящие в сторону моего любимого языка.
Я часто принимал участие в больших дискуссиях, действительно ли Python можно назвать серьезным языком программирования. Именно это, во многом, и стало причиной для написания этой статьи. Здесь я хочу попытаться донести идею, что как минимум для меня Python — действительно серьезный, без всяких скидок, язык программирования. Заодно я хочу воодушевить тех, кто уже пишет на Python, но начал немного стесняться этого, начитавшись интернета.
Также я вспоминаю нашу внутреннюю шутку, которая сформулирована моим коллегой, Даниилом: «Денис начинает свой понедельник с очередного доклада на тему, почему Python — серьезный язык». Я долго думал над этой шуткой (извините, я не гений), а потом решил: почему бы и не начать с этого статью? :)
Поэтому я хочу поговорить про проблемы Python и обсудить накопившиеся к нему основные претензии.
Динамическая типизация
За динамическую типизацию Python ругали практически все — это одна из крупнейших претензий к нему в среде разработчиков. Но на самом ли деле динамическая типизация — такое уж зло? Как минимум, разработка на языках динамической типизации проще, быстрее и зачастую приятнее — конечно, это субъективный тезис, но он имеет право на существование.
Какой-нибудь JSON-парсер на языке со статической типизацией будет гораздо более многословным и тяжелым в написании, чем на языке с динамической типизацией (ох уж эта элегантная связка orjson и pydantic). Где-то, где мы в Python отобьемся манипуляциями в рантайме, придется подтаскивать большие объёмы кодогенерации.
При этом у динамической типизации есть минусы. Например, нам приходится писать больше тестов, у нас может быть больше ошибок в рантайме при некоторых условиях. У нас динамическая типизация, поэтому до запуска понять, что с типами проблема, мы не сможем. И вроде бы статическая типизация здесь выигрывает.
При этом Uncle Bob говорил, что тесты на бизнес-логику не отменяются статической типизацией, да и в Python теперь типы можно проверять статически с помощью аннотаций и MyPy. Конечно, мы пишем больше тестов и тщательно проверяем бизнес-логику, но, на мой взгляд, это идет на пользу сервисам. Всё очень неоднозначно.
Да, у нас есть gradual типизация. MyPy, typing, аннотации типов — это позволяет нам значительно улучшить качество кода, уберечься от массы ошибок, получить экстрафункциональность языка. Например, в Python нет констант, но с помощью связки typing и MyPy можно получить их аналог. Аннотации в Python можно анализировать как статически, так и динамически: в typing есть интроспекция, появляются проекты вроде Beartype.
На базе аннотаций типов построены современные фреймворки, такие как Pydantic (строгие схемы валидации в рантайме) или FastAPI. Я считаю вот так: Python в связке с аннотациями типов являет нам gradual типизацию и тем самым предоставляет здоровый компромисс между преимуществами динамической и статической типизаций. Он не порождает хтоническое чудовище, каким его часто объявляют в интернете. Напротив, нам дают способ писать и быстро и надежно, здесь и сейчас.
Где-то тут к нам подкрадывается вопрос о типобезопасности. Полагаю, что аннотации типов проверку на типобезопасность не пройдут, хотя вопрос это довольно сложный, не для моего уровня понимания. На текущий момент ошибок типов с MyPy и аннотациями типов я не встречал, но уверен, что где-то при определенном стечении обстоятельств это возможно. Единственное, что видно у нас — увеличение надежности нашего кода (пока мы пишем наши бэкенды, аннотации часто помогают отлавливать ошибки), умение гибридно использовать аннотации и почти никаких type error в коде.
Также стоит вспомнить, что люди, предпочитающие статическую типизацию, часто говорят, что для маленьких проектов динамическая типизация «не страшна», а на действительно больших мы начинаем чувствовать её недостатки.
Честно говоря, пункт уже раздулся, поэтому я просто набросаю свои мысли списком:
-
Статический анализ хорош везде;
-
Как мы уже сказали выше, в Python есть статистический анализ, пускай и немного уступающий «настоящему» выводу типов — «настоящей» типобезопасности. Мы используем его в полный рост, и с ним можно писать действительно большие проекты
-
Мы живем в эпоху, когда микросервисная архитектура очень популярна, поэтому проблема проектов в сотни тысяч и миллионы строк кода уже не так сильна. И накал претензий к динамической типизации, соответственно, куда ниже. Даже если мы их принимаем.
Скорость
Этот аспект языка часто подвергается критике очень многими разработчиками. И если динамическая типизация — сложная и неоднозначная тема для обсуждения, то скорость — то, о чем говорят вообще все.
Если посмотреть на синтетические бенчмарки, то Python часто можно обнаружить в самому низу турнирных таблиц. Многие разработчики считают это Ахиллесовой пятой Python, что не может не сказываться на восприятии языка
Когда обсуждают скорость Python, обычно вспоминают про C++, ведь на нем все бенчмарки выглядят в сто раз лучше, чем на Python. Но если мы начнём разбираться в вопросе, то станет ясно, что скорость Python на данном этапе — не такая уж проблема. Конечно, Python действительно медленный в CPU-bound задачах. И тут у вас в голове возникает мысль — если он такой медленный, то это точно проблема!
На мой взгляд, дело обстоит так. В современной web-разработке полно i/o-bound нагрузки: часто мы принимаем запросы по сети, отправляем их в сеть, читаем из БД, пишем в БД, оперируем с файлами и так далее.
Для этого типа задач у Python есть прекрасный ответ — асинхронный подсет языка, нативный async/await, event loop из коробки и uvloop, для тех, кто хочет ещё быстрее. С помощью этой части Python мы можем эффективно утилизировать ресурсы CPU. А для исключительно CPU-bound в мире Python тоже много «припарок»: multiprocessing, subprocess, Pypy, Cython, Numba и так далее. Поэтому асинхронный Python работает очень даже быстро.
Подведу субъективный итог: многое, если не всё, можно смело писать на Python, а CPU-bound, при необходимости (если стандартная библиотека не тянет), переписывать на более быстрых языках и ставить эти микросервисы-воркеры за очередями сообщений. Разве это не идеальное сочетание?
Язык для новичков
Довольно часто Python считают языком для новичков. Ему учат на каждом шагу, в мире миллион курсов по Python. Топовые блоггеры обещают обучить ему чуть ли не за один вечер, говорят, что все сразу же получают серьезную зарплату, уютный офис и счастливое будущее. На этом слишком радужном фоне за Python прочно закрепился имидж языка для новичков, где достаточно написать пару for и def, объявить несколько переменных — и вот ты уже профессиональный Python-разработчик уровня middle. А потом «перерастаешь» и идешь писать на «серьезном языке».
Это все какой-то интернет-морок, потому что Python, как и любой другой язык, требует освоения, изучения, времени.
В самом языке у нас целая куча всяких знаний, нюансов, сложностей и декларативных частей. Судите сами:
-
У нас есть протокол итерации. Конечно, можно сказать, что итератор — не совсем часть функционального программирования, что итераторы есть и в других языках. Но я хочу подчеркнуть, что итераторы — это не совсем просто, особенно вначале, особенно когда нам нужно реализовывать свои.
-
Декораторы. Здесь тебе и функции высшего порядка, замыкания, три уровня вложенности для параметризированных декораторов — слишком много всего для такого «простого» языка. Я сам по началу долгое время не понимал паттерн «декоратор», с трудом писал их. И я знаю, что у многих с этим есть проблемы, это видно как минимум на собеседованиях.
-
Метаклассы. Классы, создающие классы? Функция type, она же класс? А тип type — тоже type? Все классы создаются type? Популярный сценарий применения — ORM? Можно, пожалуйста, мне другой «простой» язык?
-
Библиотека functools в полном составе :)
-
Генераторы. Можно спрашивать на собеседованиях: «А для чего вы используете генераторы?» — и услышать очень много разных ответов. Некоторые вообще не понимают, зачем они нужны. А ведь генератор поддерживает протокол итерации, в него можно слать значения, yield from — ну, сами все понимаете.
-
Контроль зависимостей. Venv, virtualenv, pipenv, poetry, pip tools, pdm, pipx, pyproject и так далее.
-
Инфраструктура вокруг Python. Крайне полезно для работы было бы знать, что такое pypa, pypi, psf — а это ещё отдельный пласт знаний.
-
PEP. Иногда спрашиваю людей: «А что такое PEP?», — и самый частый ответ: «Стандарт кода». Даже опытные разработчики помнят PEP8 в лучшем случае.
-
Около сотни магических методов. Хорошо было бы в них просто ориентироваться.
-
Могущественная интроспекция и «мета-язычные» вещи. Runpy, importlib, trace, traceback, gc, inspect, sys, typing.get_type_hints, typing.get_origin, typing.get_args.
-
Встроенные классы ошибок, методов и типов. Можете выполнить и получите 156:
import builtins; len(dir(builtins))
. Это количество встроенных классов ошибок, методов, типов. Желательно помнить многое из этого. -
Новые вещи. Оператор «морж», pattern matching.
-
Конкурентное выполнение. Threading с кучей нюансов, multiprocessing, subprocess, свежий communicating sequential processes «паттерн» (PEP 554).
-
GIL. Комментарии не нужны, но они будут (я не умею лаконично, помогите).
-
Особые языковые возможности. Dict, list, set comprehensions, generator expressions, например.
-
Асинхронность. Это отдельный «подсет» языка, где уже целая своя вселенная с кучей пакетов и даже отдельными (не встроенными в язык) концепциями, вроде structured concurrency.
-
Аннотации типов. Или аннотированный Python — тоже, считай, свой мини-«подсет» языка, вводящий новый понятийный аппарат, и новый вид типизации — структурную саб-типизацию, ближайшим аналогом которой из мира динамической типизации можно было бы назвать утиную.
-
Динамическая типизация. Строгая (местами «обходимая» полимформизмом, или неявным приведением int к float), утиная типизация, typing.Protocol (про него выше), gradual типизация. Скажите мне — это правда так просто?
И я бы мог ещё продолжать какое-то время. Этот список я составил не для того, чтобы отпугнуть от языка, но мне хочется продемонстрировать, что Python — не такой простой язык программирования. Действительно, на нём легко начать, у него низкий порог первоначального входа. Но это вовсе не язык только лишь для новичков, такое отношение к нему порождает безответственный подход к разработке. Язык взял много хорошего от других, он не простой, со своими плюсами и минусами, очень красивыми местами и не самыми лучшими. И всё это требует освоения.
GIL
Можем сказать, что мы снова про скорость. Когда заходит разговор о тредах и Python, о скорости и Python, сразу на сцену выползает наш любимый и обожаемый GIL.
Ограничения GIL известны всем — в Python при попытке распаралеллить любую CPU-bound нагрузку с помощью тредов, разработчики неизбежно сталкиваются с тем, что в один момент у всегда будет работать только один поток. Тема абсолютно выдающаяся и имеющая за собой такой поток реминисценций, что про это даже немного неловко говорить, — но поговорить нам, всё-таки, в рамках статьи нужно.
Издалека проблема GIL — и сложная, и, как кажется, практически нерешаемая (мы все помним, что на этот счет сказали Гвидо, Дэвид Бизли; UPD: тут недавно вышла статья, говорят о nogil… но давайте не будем об этом).
В современных бэкендах и Python накал страстей вокруг GIL сильно стих — веб-сервисы очень часто по своей природе могут быть асинхронными, а для этого у нас есть множество чудесных фреймворков, вроде FastAPI, поэтому ограничения GIL нас не так сильно касаются. При этом горизонтально мы масштабируемся процессами, как в синхронных бэкендах, так и в асинхронных. У многих есть кубер — и там мы масштабируемся тоже процессами, только над ними стоит абстракция уровнем выше — pod.
Некоторые разработчики спрашивают, зачем нам тогда треды?
В современном Python их используют для одной интересной штуки — на них можно делать wannabe-асинхронный код, при этом не делая асинхронный код, как ни странно. То есть можно написать на тредах что-то, что занимается интенсивной i/o-нагрузкой. И тогда внезапно все станет асинхронным за счет того, что GIL отпускается на i/o-операциях.
Это важное примечание, так как это сложный и неочевидный нюанс работы GIL — он описан в документации, но кто же её читает — шутка :). Но благодаря ему мы можем получить преимущество даже в синхронном коде, а также не блокировать петлю событий, когда у нас возникает потребность вызвать синхронное i/o в петле событий (вспомним про запись файлов, привет экзекьюторам).
Python — «неказистый язык для набрасывания прототипов на Django»
Частое мнение в интернете — Python годен только для быстрого, реактивного набрасывания прототипов на Django, а все остальное удел «серьезных языков» (тм). И это ещe одно, с моей точки зрения, не очень корректное суждение.
Конечно, на Django, на Python быстро набрасывают прототипы — я не буду отрицать очевидного. Но на языке делают не только это, описанное — лишь малая часть возможностей экосистемы Python. Однако быстро прототипирующие люди, использующие язык одним способом, почему-то взяли на себя прерогативу оценивать позиционирование языка по своей сфере. И с этим я не согласен.
В качестве одного из контраргументов можно привести наш выдающийся тулсет для написания тестов, который не мог возникнуть в языке для быстрого набрасывания прототипов за ненадобностью.
Pytest — это прекрасный фреймворк для написания тестов и яркий представитель этого тулсета. Он позволяет выстраивать очень сложные тестовые сценарии с инверсией зависимостей всего с помощью двух инструментов — fixtures и parametrize. С их помощью можно делать очень сложные тесты — подробно изучать производительность и бизнес-логику системы.
В тестировании, помимо Pytest, у нас есть Hypothesis — отличный фреймворк для fuzzy, property тестирования. Фреймворк настолько впечатляет, что недавно, на Python Language Summit 2021, стало известно, что его используют core developer'ы и с помощью него нашли ошибки в PEG парсере самого Python.
Использовать его просто, а результатом являются такие тесты, о которых я раньше только мечтал. В итоге из одного теста у нас получается целая последовательность, которая имеет различные статические проверки, динамические, а также случайно сгенерированные:
from hypothesis import given
from hypothesis.strategies import text
def encode(input_string: str) -> list:
"""Это просто синтетический пример."""
count: int = 1
prev: str = ""
lst: list = []
character: str
for character in input_string:
if character != prev:
if prev:
lst.append((prev, count))
count = 1
prev = character
else:
count += 1
lst.append((character, count))
return lst
def decode(lst: list) -> str:
"""Это просто синтетический пример (2)."""
q: str = ""
character: str
count: int
for character, count in lst:
q += character * count
return q
@given(text())
def test_decode_inverts_encode(s):
"""И здесь мы получаем совершенно невероятный объем тестов. При том,
что повесили 1 декоратор."""
assert decode(encode(s)) == s
Для Python существует библиотека для мутационного тестирования mutmut. Если вы вдруг с ней не сталкивались — посмотрите, это очень интересный инструмент. С его помощью можно проверять ваши тесты через небольшие изменения в коде — я бы назвал это мета-тестированием.
Вообще, в Python есть огромное количество вспомогательных библиотек для тестирования — Faker, Factory boy, Mixer, Seed, куча расширений для Pytest — например, для параллелизации. Поэтому говорить, что Python можно использовать только для быстрого прототипирования на Django — немного некорректно.
Безопасность
Как-то раз нам написали комментарий, что за Java и C# стоят крупные компании, бизнес, а за Python только Гвидо и никакой ответственности нет. Видимо, это означало, что языком пользоваться страшно и от этого он абсолютно несерьезен.
Мне кажется, этот вопрос довольно сложный. Крупные проблемы языковой среды — не такой уж и частый сценарий. Но вряд ли при серьезных проблемах разработчики на Java или C# моментально получат их решение.
А ещё можно вспомнить, что современный софт пишется с использованием огромного количества Open Source библиотек, которые пишутся сторонними разработчиками, лежат в открытом доступе — и баги там случаются часто, а гарантий их исправления нет и быть не может. Тогда как серьезные проблемы Python, в случае их возникновения, точно будут исправлены. К тому же у Python «сжался» релизный график.
Поэтому проблемы у многих языков, как я считаю, общие, — и выделять энтерпрайз-языки отдельно, ставить их выше — не совсем правильно.
Популярность
Популярность языка программирования часто оценивают на результатах рейтингов. Поскольку вокруг них ходит огромное количество споров, я решил проанализировать почти все известные рейтинги по популярности языков.
Причин роста много, но кроме известных, для себя я предпочитаю выделять такую: одна из сильнейших черт Python в том, что он по пути вбирал в себя огромное количество разных парадигм и подходов, зачастую беря из них если не лучшие, то как минимум хорошие куски. Именно поэтому он умудряется быть таким универсальным и при этом довольно дружелюбным. Хм, дружелюбный сосед-питон? (шутка, предоставлена ЗАО «бумер-кринж»).
И о минусах
Что? Секундочку, ведь я только что активно поддерживал Python, а теперь начинаю перечислять минусы? Дело в том, что мне не хочется выглядеть как фанатик в розовых очках. Поэтому я просто назову уже не совсем типовые вещи, но тоже часто встречающиеся:
-
GIL всё ещё нас беспокоит. В итоге I/O-bound с небольшим количеством CPU-bound может привести к тому, что некоторые сигналы будут не доставлены.
-
У нас нет оптимизации хвостовой рекурсии (а нужна ли она?). Это большой топик.
-
Некоторые не любят пробелы… вспомним Whython. Я до сих пор не понимаю, в чем проблема — я везде код форматирую пробелами, а в Python они дают возможность избежать написания «лишних» скобок. Но не признать того, что есть недовольные, нельзя.
-
Lambda — есть разработчики, которые хотели бы видеть их как функции. Тогда как в Python они устроены в качестве выражений.
-
Python 2 и 3. Это неактуальный топик на текущий день, и Python 3 принес столько невероятных вещей, что о второй версии говорить нет желания. Но стоит признать — это было больно, и многие ещё помнят, насколько болезненно дался переход. Некоторых это отвращает от языка: подсознательно ты ждешь Python 3 vs 4.
-
Специфический подход к ООП.
-
Смешение парадигм даётся не всегда просто.
Я не буду сейчас пытаться разбираться ещё и в этих пунктах, но это всегда повод для отдельной дискуссии. Возможно — в комментариях.
Небольшие итоги
Я хочу сказать, что Python, на мой взгляд, надежен для большинства кейсов, с которыми мы сталкиваемся в бэкенд-разработке. С поправкой на хайлоад, конечно.
Python используется большинством авторитетных компаний, таких как Google и Яндекс. При этом популярность языка продолжает расти, а так же Гвидо недавно на Python Language Summit 2021 обещал, что с поддержкой Microsoft он сделает Python быстрее раз в пять, во многом с помощью jit. Может быть из конца списка языков в бенчмарках мы переместимся в середину, и уж там то точно заживем.
В Python фантастические веб-фреймворки. Можем вспомнить Django или FastAPI — это выдающиеся фреймворки, со своими плюсами и минусами. Современный Python можно брать для написания быстрых, хороших и серьезных бэкендов. В Python-среде много крутых разработчиков и приятных людей!
Python жутко популярен и продолжает расти. Потенциал его роста не исчерпан.
Для меня Python — язык для написания бэкенда номер один. Язык, с которого я начинаю любой бекенд в 2021 и только в каких-то исключительных ситуациях беру другие.
А ещё на Python очень приятно писать код — и этого совершенно не стоит стесняться.
Эта статья — расширенная версия доклада с IT-конференции Райффайзенбанка <code/R>. Здесь много новых подробностей, но если хотите услышать часть голосом — смотрите видео.
Автор: Денис