Python - один из самых популярных языков программирования. Как мы писали, в январе 2022 года он во второй раз за свою историю стал лидером ежемесячного рейтинга языков программирования Tiobe. Рост популярности Python за год составил 1,86%.
Популярность языка обусловлена его относительной простотой - работать с ним может быстро начать даже новичок. Конечно, никто не говорит, что этот новичок сможет сразу же писать высоконагруженные проекты, нет. Но решать задачи базового уровня - вполне. Но все же есть проблемы даже здесь, и о них поговорим в статье. Разбираем подводные камни Python вместе с Алексеем Некрасовым, лидером направления Python в МТС, программным директором направления Python и спикером профессии “Python-разработчик” в Skillbox.
Немного о достоинствах языка
На Хабре очень много статей о достоинствах Python, поэтому повторяться не будем. Скажем только, что язык действительно знаменит своей простотой. Причем с течением времени в нём появилось много “синтаксического сахара”, который позволяет быть ему немногословным и понятным языком, похожим на псевдокод. А всё сложное убрано “под капот”, чтобы не отвлекать разработчика. Простота позволяет быстро создавать прототипы и проверять гипотезы. Благодаря этому новичкам проще всего начать изучать программирование с Python.
Что такое синтаксический сахар? Это набор синтаксических конструкций, применение которых не влияет на поведение программы, но делает использование языка более удобным для человека. Например, паттерн “декоратор”:
def decorator_log(func):
def wrapper(*args, **kwargs):
print("вызов функции с параметрами", args, kwargs)
res = func(*args, **kwargs)
return res
return wrapper
def func_1():
print('Тестовая функция')
func_1 = decorator_log(func_1)
# Вместо func_1 = decorator_log(func_1) можно теперь написать:
@decorator_log
def func_1()
print('Тестовая функция')
Таких конструкций в языке достаточно много, они упрощают чтение кода и работу с ним.
Интерпретатор Python берёт на себя всю скучную и сложную работу по управлению памятью, потоками и т.д. Но есть и проблема. Дело в том, что из-за всех этих приятных плюсов Python теряет в скорости по сравнению с другими языками программирования.
Подробно о проблемах
Кроме потерь в скорости есть и другие подводные камни, о которых далеко не всегда знают начинающие разработчики. Среди них особенно выделяются две крупные проблемы, это динамическая типизация и так называемый "новый сахар".
Динамическая типизация
Python - динамически типизированный язык программирования. Это позволяет быстро разрабатывать прототипы и писать код. Динамическая типизация, если объяснять “на пальцах”, значит, что одна и та же переменная в разное время может ссылаться на данные разного типа. Например:
data = input() or '123456' # сейчас в переменной data лежит строка
data = int(data) # теперь в переменной data лежит целое число
И это будет работать.
Статистические языки программирования, например C, такого сделать не позволяют. Но в динамической типизации есть подвох. Если в простых проектах все ок и проблем не возникает, то, чем масштабнее проект на Python, тем больше появляется ошибок, связанных с типом переменных. Так, где-то далеко в коде введена функция, которая на вход принимает переменную data, введенную пользователем. В функции проверили, что введенное число является двузначным:
if len(data) != 2: print("Нужно ввести двухзначное число")
При выполнении этого кода мы внезапно получаем ошибку
if len(data) != 2: TypeError: object of type 'int' has no len()
Это означает, что в каком-то участке нашего кода строка уже переведена в число, и добавлена в переменную data. Подобные ошибки - далеко не редкость. Чаще всего они возникают на проектах, где работает от двух человек. Для того, чтобы ошибок не было, необходимо писать тесты, которые проверяют, нужный ли формат данных у переменных. У статических языков программирования таких проблем нет.
Еще один вариант решения - использование псевдостатической типизации и ее проверки при помощи статического анализатора mypy. Если проблемы с типами данных не нужны, то потребуется писать код следующим образом:
data: str = input() or '123456'
number: int = int(data)
if len(number) != 2:
print("Нужно ввести двухзначное число")
После написания кода приходится запускать для проверки mypy. Система проверяет, что int как строка нигде не используется. И если запустить mypy, то появится ошибка о передаче неверной переменной в функцию len:
error: Argument 1 to "len" has incompatible type "int"; expected "Sized"
Почему здесь употребляется термин "псевдостатическая типизация"? Дело в том, что при запуске скрипта Python проверка не будет проводиться сразу же, так что разработчик не будет знать, корректно ли выполняется передача значения в функцию len или нет. Ошибка появится лишь тогда, когда будет запущена программа с передачей в нее входного значения.
Новый сахар и усложнения в языке
Python постоянно развивается, в каждой новой версии языка появляется что-то новое, что упрощает разработчикам жизнь, позволяя решить задачу кодом с меньшим количеством строк, чем раньше. Но здесь и кроется проблема - программист может увлечься созданием конструкций, которые занимают меньше строк кода, но при этом и значительно уменьшают его читаемость.
Пример - задача по очистке входных данных с приведением их к целым числам. Раньше код программы выглядел бы следующим образом:
from typing import Optional
def to_int(string: str) -> Optional[int]:
try:
return int(string)
except ValueError:
return None
data = ['sadf', '12', '1', 'a1']
filter_data = []
for i_str in data:
i_number = to_int(i_str)
if i_number is not None:
filter_data.append(i_number)
print(filter_data)
Но начиная с версии 3.8 блок с for вполне можно записать следующим образом:
filter_data = [y for x in data if (y := to_int(x)) is not None]
Опытный разработчик в этом разберется без проблем, но для новичка чтение кода будет сложной задачей.
Какие еще есть сложности?
В целом, их не так мало. Обучение Python можно разделить на несколько этапов, и на каждом из них будут встречаться проблемы. Что касается этапов, то вот они:
Изучение простого синтаксиса, типов данных, функций, классов и т.д. На этом этапе нужно научиться понимать простые типы и структуры данных, а также “набить руку” на решении множества простых задач.
Изучение паттернов, которые реализованы уже в самом языке: декораторы, итераторы, генераторы, контекст менеджеры и т.д. На этом этапе идет осознание паттернов проектирования и как они реализованы в самом языке. Тут новичку из другой сферы будет непросто, но часто в осознании этих тем помогают наставники или хорошо подобранные статьи.
Изучение различных прикладных фреймворков: web (flask, FastApi, Django и др.), работа с БД (SQLAlchemy, sqlite3, Tortoise ORM и др.), работа с данными (numpy, pandas, marshmallow, pydantic и др.) и т.д. Здесь сталкиваемся со сложностью в определении направления своего развития и выстраивания своего маршрута в изучении доп. инструментов. Новичкам я бы посоветовал для этого проконсультироваться с опытным разработчиком или наставником.
Углублённое изучение языка: метаклассы, дескрипторы и т.д. В обычной работе разработчик с этим практически не встречается, так как 90% всех задач можно решить без этих знаний. Но если вы хотите стать первоклассным разработчиком, то этих тем вам не обойти.
В целом, обойти проблемы помогает постоянное обучение, а также более опытные коллеги и наставники. Не бойтесь обращаться к ним за помощью, как говориться: “Не тот глуп, кто не знает, но тот, кто знать не хочет.” К слову, если у вас возникли проблемы на каком-то из этих этапов, можете писать мне, постараюсь помочь.
А что насчет перспектив языка?
Если коротко, то с уверенностью могу сказать - они есть, изучать Python стоит, не сомневайтесь.
Если же расписать подробнее, то скажу, что с каждым годом требования для новичков со стороны работодателей возрастают. Все дело в том, что появляются новые фреймворки, более сложные версии языка программирования, новые инструменты и т.д.
Пять лет назад, чтобы устроиться junior python web-разработчиком нужно было уметь решать алгоритмические задачи и знать основы языка программирования. А сейчас добавилось следующее:
знание одного из фреймворка Flask/Django (желательно ещё и асинхронного фреймворка, например, FastAPI);
знание SQL и работа с PostgreSQL;
знание тестовых фреймворков pytest, unittest;
знание принципов контейнеризации и работы с docker;
знание основ Linux.
В ближайшее три-пять лет спрос будет расти на middle и senior разработчиков, так как именно они выполняют основную часть задач в IT-компаниях. Если смотреть на спрос стажёров/junior, то тут всё сложнее по следующим причинам:
Junior разработчик приносит только убыток компании, так ему нужен наставник (уровня middle/senior). В итоге на решение определенной задачи с учеником тратится больше времени, чем если бы задачу делал сам наставник.
Часто через полгода-год стажер/junior уходит из компании, в которой обучался, в другую с увеличением зарплаты в среднем в 2 раза. Соответственно, терпит убытки компания, которая первой наняла молодого специалиста на работу.
Завышенные ожидания у стажёров/junior. Многие переходят из других сфер, где они уже привыкли к своему доходу и не готовы переходить, к примеру, со 100 тыс. руб в месяц на 50 тыс. руб.
Через три-четыре года у стажёров/junior начнёт появляться конкурент в лице ИИ. Недавно компания DeepMind (дочка Alphabet), выполнила на платформе Codeforces 10 тестов и попала в 54% лучших участников. Вполне вероятно, что в скором будущем ИИ сможет решать простые, шаблонные задачи, которые в обычной ситуации дают стажерам/junior-разработчикам.
В качестве вывода
Подводя итог, мы видим, что если вы хотите перейти в сферу IT как python-разработчик, то вам нужно:
Максимально сконцентрировать свои силы на обучении, учиться предстоит многому, это будет занимать время.
Быть готовым тому, что в течение полугода-года ваша зарплата не превысит 100 тыс. руб. в месяц.
Постараться устроиться на первую работу и начать учиться у более опытных коллег.
Не стоит пугаться вышесказанного, изучение Python часто позволяет внедрить автоматизацию в ту сферу, в которой вы уже работаете. Это повышает вашу ценность на текущей работе как специалиста, а также позволяет автоматизировать часть рутинных задач. Плюс ко всему, вы получаете коммерческий опыт разработки.
Комментарии (27)
splatt
28.02.2022 21:28-1Работаю с Python уже почти 10 лет, и на мой взгляд по количеству неочевидных подводных камней, он может легко сравниться с низкоуровневыми языками.
Классический пример, дефолтные параметры функций:
from time import time def foo(timestamp=time()): print(timestamp)
Все что связано с модулями и импортами это вообще отдельная история.
unsignedchar
01.03.2022 00:19Классический пример, дефолтные параметры функций:
А можно разъяснить, что не так с дефолтными параметрами? КМК достаточно очевидно, зачем оно.
Soarex16
01.03.2022 02:30+2Дефолтные аргументы создаются один раз при первом обращении к функции. Например, если бы по-умолчанию подставлялся список, то он был бы всегда один и тот же между вызовами:
def foo(bar=[]): return bar x = foo() x.append(4) y = foo()
В результате x и y ссылаются на один и тот же список.
CheeHaveDreams
01.03.2022 11:46Только не
при первом обращении к функции
, а при создании интерпретатором объекта функции.
kx13
01.03.2022 12:40Про дефолтные значения в FAQ еще написано
https://docs.python.org/3/faq/programming.html#id13
Я вообще всем новичкам рекомендую FAQ читать, а потом еще перечитывать. А его к сожалению мало читают, а там как раз много полезного, чтобы поменьше на грабли наступать.splatt
02.03.2022 03:49Да, на любую критику языка можно сказать "RTFM". Но это так не работает.
Мне нужен инструмент для быстрого и эффективного решения бизнес-задач, в котором код легко читаем, а результат его выполнения очевиден. А не ночные сессии починки продакшна, потому что 3 сеньора которые аппрувили пулл реквест, не перечитывали в очередной раз FAQ и пропустили вроде как "простую" ошибку.
kx13
02.03.2022 09:29Как Вы хотите, к сожалению, тоже не работает.
Я тоже так хочу, как вы говорите: чтобы было было все просто и очевидно.
Но любая сложная система (к которой относится любой развитый язык программирования) полна различных "особенностей". Вопрос только в том, насколько их много и насколько они хитрые.
Надо быть реалистами и ожидать подвоха даже от самых "простых", "удобных" и "дружелюбных" инструментов. Поэтому, чем лучше R этот самый TFM, тем меньше сюрпризов будет.
Bedal
01.03.2022 10:33+1Статистические языки программирования
языки со статической типизацией точно называются статистическими?
amarao
На мой взгляд, самый вопиющий речекряк питона - это существование
StopIteration
BasicWolf
А по-моему это гениально. Или как в других языках, будем иметь два метода вместо одного, типа
Iterator.hasNext() -> bool
иIterator.next()
.amarao
После того, как я перешёл на Rust... ладно, не перешёл, но плотно изучил, меня каждый раз от этого StopIteration передёргивает.
В Rust (для сравнения) next возвращает Option<Value>, который либо Some(Value), либо None (None - это не питоновский None, это из enum'а None|Some). В рамках синтаксического сахара
for
вычитывает из итератора пока не получит None. Но любой желающий может взять next() сам сколько нужно раз и посмотреть содержимое.И правильно обработать None. В питоне "правильно обработать" можно только так:
Сравните это с rust'овой версией:
Передёргивает меня тут не от числа строк, а от того, что exception используется не для исключительной ситуации.
UDP задумался, есть ли дефолт у next. Есть, спасибо. Можно
foo=next(bar, "something else")
, хотя моя претензия по поводу эксепшена в неисключительной ситуации всё равно остаётся.sswwssww
Какая претензия может еще оставаться? Какое должно быть поведение по умолчанию если нет следующего элемента, а ты вызываешь next()? Почему именно ваше поведение по умолчанию лучше возбуждения StopIteration? В крайнем случае, реализуйте __iter__ и __next__ у класса объекта bar задав свое поведение при случае когда последовательность закончилась.
Что будет делать Rust если убрать .unwrap_or("something else")? Чем подход bar.next().unwrap_or("something else") лучше подхода next(bar, "something else")?
amarao
Тем, что вместо unwrap я могу написать так:
Как вы такое на Python напишете?
sundmoon
Это преимущества не Rust, а функциональной парадигмы.
Pattern Matching до сих пор нет даже в Kotlin: ждут появления его в Java (оно задерживается). Бреслав где-то говорил, что "патмат" требует примерно столько же строк в реализации с нуля, сколько весь остальной язык.
BasicWolf
Да пожалуйста, если не хотите
StopIteration
:Только это не нормальный Питоний код же. Вы часто видели, чтобы
next()
вызывался явным образом, а не в контекстеfor
? Или вам часто приходится кидатьStopIteration
вручную?Мне кажется, что вы не до конца рассмотрели идею со
StopIteration
.Это исключение появилось как часть протокола итераторов и генераторов. А генераторы можно соединять, даже не имея
yield from
.А как в цепочке генераторов проще всего сигнализировать на самый верх, что глубинный генератор истощён? Бросаем исключение, которое просто всплывает наверх.
И да, этот механизм - противоречивый и далеко не самый эффективный. Мне лично больше импонирует функциональный подход Раста. А с другой стороны - этот механизм чертовски простой и проверенный временем :)
amarao
Рассказываю простой пример, где надо next вызывать вручную.
У нас грок-подобный парсер потока строк. Мы хотим найти match1, после чего мы почти уверены, что будет match2 и match3. "Почти", потому что может оказаться, что нет, и тогда match1 тоже не валидный, и строки под ним тоже, потому что по спецификации, если match1, то match2 и match3 идут строго после.
А теперь сделайте так, пожалуйcта, чтобы не надо было это обтыкивать try/except и покажите пример красивой реализации
break
.BasicWolf
У вас соль в том, что парсер и токенайзер жестко связаны друг с другом. EOF должен возвращаться токенайзером. Парсер же читает и обрабатывает поток токенов. Но даже если их по какой-либо причине нельзя разоединить, тут есть над чем поработать, например:
amarao
Пардон, понял.
Но мне всё равно неприятно видеть штатную ситуацию обрабатываемую как исключительную. Исключения - для ошибок или неожиданных ситуаций.
mayorovp
Конкретно в вашем случае, насколько я понял, окончание входящего потока автоматически означает что вы не найдёте какой-то из своих матчей, что означает отсутствие данных для обработки.
А значит, тут можно обойтись ОДНИМ try/except вокруг всего цикла. Что внезапно оказалось даже лучше паттерн-матчинга, которых потребовалось бы аж три.
amarao
Данные для обработки есть (предыдущие накоплены). И, главное, я ожидаю окончания, то есть ситуация не исключительная, но почему-то я должен ждать исключения.
AnthonyMikh
Код не скомпилируется, ибо в
foo
будет лежать значение типаOption<SomeType>
, а ниже по коду будет ожидатьсяfoo
типаSomeType
.