Для всех хабравчан, у которых возникло ощущение дежавю: Написать этот пост меня побудили статья "Введение в Python" и комментарии к ней. К сожалению, качество этого "введения" кхм… не будем о грустном. Но ещё грустнее было наблюдать склоки в комментариях, из разряда "C++ быстрее Python", "Rust ещё быстрее C++", "Python не нужен" и т.д. Удивительно, что не вспомнили Ruby! Как сказал Бьярн Страуструп, Добро пожаловать под кат всем, кто хотел бы познакомиться с Python, не опускаясь при этом до грязных ругательств! Утро в горах Восточного Кавказа ознаменовалось воплями. Два молодых человека сидели на большом валуне и что-то рьяно обсуждали, активно жестикулируя. Через минуту они начали толкать друг друга, а потом сцепились и свалились с валуна в (как оказалось) куст крапивы. Видно этот куст рос там неспроста, — он сразу утихомирил драчунов и внёс перемирие в их неугасающий спор. Как вы, наверное догадались, одним из спорщиков был я, другим — мой лучший друг (привет, Quaker_t!), ну а предметом нашей светской беседы — Visual Basic vs. Delphi! Узнаёте себя? Иногда мы превращаем любимые языки программирования в культ и готовы отстаивать его до последнего! Но годы идут и наступает момент, когда "A vs. B" из предмета споров перерастает в "Мне комфортнее работать с А, но при необходимости я научусь работать с B, C, D, E и вообще, с чем угодно". Вот только когда мы сталкиваемся с новыми языками программирования, старые привычки и культура могут нас долго не отпускать. Я хотел бы познакомить вас с Питоном и помочь перенести ваш опыт в новое русло. Как у любой технологии, у него есть свои сильные и слабые стороны. Python, как и C++, Rust, Ruby, JS, и все остальные — это инструмент. К любому инструменту прилагается инструкция и любым инструментом надо научиться пользоваться правильно. "Автор, не пудри мозги, ты собирался нас с Питоном знакомить?". Давайте знакомиться! Python — динамический, высокоуровневый язык программирования общего назначения. Python — зрелый язык программирования с богатой экосистемой и традициями. Хоть язык и увидел свет в 1991-м году, его современный облик начал формироваться в начале 2000-х. Python — заряженный язык, в его стандартной библиотеке есть решения на многие случаи жизни. Python — популярный язык программирования: Dropbox, Reddit, Instagram, Disqus, YouTube, Netflix, чёрт побери, даже Eve Online и многие другие активно используют Python. В чём причина такой популярности? С вашего позволения, изложу собственную версию. Python — простой язык программирования. Динамическая типизация. Сборщик мусора. Функции высшего порядка. Простой синтаксис для работы со словарями, множествами, кортежами и списками (в т.ч. для получения срезов). Питон отлично подходит для новичков: даёт возможность начать с процедурного программирования, потихоньку перейти к ООП и почуствовать вкус программирования функционального. Но эта простота — как верхушка айсберга. Стоит нырнуть в глубину, как натыкаешься на философию Питона — Zen Of Python. Ныряешь ещё дальше — и попадаешь в свод чётких правил по оформлению кода — Style Guide for Python Code. Погружаясь, программист постепенно вникает в понятие "Python way" или "Pythonic". В этот удивительный этап изучения языка, начинаешь понимать, почему хорошие программы на Питоне пишутся именно так, а не иначе. Почему язык эволюционировал именно в этом направлении, а не в другом. Питон не преуспел в скорости выполнения. Но он преуспел в важнейшем аспекте нашей работы — читабельности. "Пишите код для людей, а не для машины" — это основа из основ Питона. Хороший код на Питоне выглядит красиво. А писать красивый код — чем не приятное занятие? Совет 0: Перед тем как читать дальше, пожалуйста, загляните в уголок Дзена Питона. Язык зиждется на этих постулатах и наше общение будет намного приятнее, если и вы будете с ними знакомы. Первым шоком для тех, кто никогда не видел код на Питоне, является обозначение тела инструкций отступами: Вспоминаю вечера в общаге Питерского Политеха, когда мой сосед, VlK, с горящими глазами рассказывал, что ещё нового он откопал в Питоне. "Тело инструкции отступами? Серьёзно?" — была моя реакция. Действительно, для человека прошедшего от Visual Basic (if ... end if) до C# (фигурные скобки), сквозь C, C++ и Java, подобный подход казался, мягко говоря, странным. "Ты же форматируешь код отступами?", спросил VlK. Конечно же я форматировал его. Точнее, за меня это делала спираченная Visual Studio. Она справлялась с этим чертовски хорошо. Я никогда не задумывался о форматировании и отступах — они появлялись в коде сами по себе и казались чем-то обыденным и привычным. Но крыть было нечем — код был всегда отформатирован отступами. "Тогда зачем тебе фигурные скобки, если тело инструкций в любом случае будет сдвинуто вправо?". В тот вечер я засел за Python. Оглядываясь назад, я могут точно сказать, что́ именно помогало быстро усваивать новый материал. Это был редактор кода. Под влиянием того же VlK, незадолго до вышеописанных событий, я перешёл с Windows на Ubuntu и Emacs в качестве редактора (на дворе 2007й год, до PyCharm, Atom, VS Code и прочих — ещё много лет). "Ну вот, сейчас будет пиарить Emacs..." — скажете вы. Совсем чуточку :) Традиционно, клавиша <tab> в Emacs не добавляет символов табуляции, а служит для выравнивания строки по правилам данного режима. Нажал <tab> — и строка кода сдвигается в следующее подходящее положение:  Таким образом вам никогда не приходится задумываться, правильно ли вы выровняли код. Совет 1: При знакомстве с Python используйте редактор, который возьмёт на себя заботу об отступах. А знаете, какой побочный эффект у всего этого безобразия? Программист старается избегать длинных конструкций. Как только размер функции выходит за вертикальные границы экрана, становится сложнее различать, к какой же конструкции относится данный блок кода. И чем больше вложений, тем сложнее. В результате стараешься писать как можно лаконичнее, разбивая длинные тела функций, циклов, условных переходов и т.д. O, эта дискуссия существует почти столько же, сколько существует само понятие "программирование"! Динамическая типизация не плоха и не хороша. Динамическая типизация — это тоже наш инструмент. В Питоне динамическая типизация даёт огромную свободу действий. А там, где большая свобода действий — больше вероятность выстрелить себе в ногу. Стоит уточнить, что типизация в Питоне строгая и сложить число со строкой не получится: Питон также проверяет сигнатуру функции при вызове и выбросит исключение, если сигнатура вызова не верна: Но загружая скрипт, Python не скажет вам, что функция ожидает число а не строку, которую вы в неё передаёте. И узнаете вы об этом лишь во время исполнения: Тем сильнее вызов программисту, особенно при написании больших проектов. Современный Питон ответил на этот вызов механизмом аннотаций и библиотекой типов, а сообщество разработало программы, выполняющие статическую проверку типов. В результате программист узнаёт о подобных ошибках до выполнения программы: Питон не придаёт никакого значения аннотациям, хотя и сохраняет их в атрибуте __annotations__. Единственное условие — аннотации должны быть валидными значениями с точки зрения языка. С момента их появления в версии 3.0 (что было более десяти лет назад!), именно усилиями сообщества, аннотации стали использовать для типизированной маркировки переменных и аргументов. Совет 2: На практике больше всего динамическая типизация вызывает проблемы при чтении и отладке кода. Особенно если этот код писался без аннотаций и вам приходится тратить много времени на выяснение типов переменных. Вам не обязательно указывать и документировать типы всего и вся, но время, потраченное на детальное описание публичных интерфейсов и наиболее критических участков кода, воздастся сторицей! Порой знатоки Питона напускают на себя таинственный вид и говорят об "Утиной типизации".
Утиная типизация (Duck typing) — это применение "утиного теста" в программировании: Рассмотрим пример: Тут — классическое внедрение зависимости (dependency injection). Класс RpgCharacter получает объект weapon в конструкторе и позже, в методе battle() вызывает weapon.attack(). Но RpgCharacter не зависит от конкретной имплементации weapon. Это может быть меч, BFG 9000, или кит с цветочным горшком, готовые приземлиться неприятелю на голову в любой момент. Важно, чтобы у объекта был метод attack(), всё остальное Питон не интересует.   Строго говоря, утиная типизация не является чем-то уникальным. Она присутствует во всех (знакомых мне) динамических языках, реализующих ООП. Это ещё один пример того, как внимательно приходится программировать в мире динамической типизации. Плохо назвали метод? Неоднозначно нарекли переменную? Ваш коллега, или вы сами, спустя эдак пол-годика, будете счастливы разгребать подобный код :) А была бы классическая статическая типизация, с проверкой соответствия типам на стадии компиляции. Цена — невозможность использовать объект, имеющий метод attack(), но при этом не реализующий интерфейс IWeapon явным образом. Совет 3: При желании вы можете описать интерфейс, построив собственный абстрактный класс с методами и свойствами. А ещё лучше будет потратить время на тщательное тестирование и написание документации для себя и пользователей вашего кода. Питон — объектно-ориентированный язык и в корне иерархии наследования стоит класс object: Но там где в Java и C# используется obj.ToString(), в Питоне будет вызов функции str(obj). Или например вместо myList.length, в Питоне будет len(my_list). Создатель языка, Гвидо Ван Россум (Guido van Rossum), объяснил это следующим образом: Тем не менее внутри себя функции len(), str() и некоторые другие будут вызывать определённые методы объекта: Специальные методы также используются операторами языка, как математическими и булевыми, так и операторами цикла for ... in ..., оператором контекста with, оператором индекса  и т.д.
Например, протокол итератора состоит из двух методов: __iter__() и __next__(): Хорошо, допустим специальные методы. Но почему они выглядят так вырвиглазно? Гвидо объяснил это тем, что имей они обычные имена без подчёркиваний, программисты, сами того не хотя, рано или поздно переопределяли бы их. Т.е. __метод__() это своебразная защита от дурака. Как показало время — защита эффективная :) Совет 4: Внимательно ознакомьтесь со встроенными функциями и специальными методами объектов. Они являются неотъемлимой частью языка, без которой невозможно полноценно на нём разговаривать. В Питоне нет модификаторов доступа к атрибутам класса. Внутренности объектов открыты для доступа без каких-либо ограничений. Однако существует конвенция, по которой атрибуты с префиксом _ считаются приватными, например: Почему? В Питоне есть специальный механизм искажения (mangling) имени атрибутов, начинающихся с двойного подчёркивания и не заканчивающихся на двойное подчёркивание (__my_attr)! Сделано это для избежания коллизий имён при наследовании. Для вызова вне тела методов класса, Питон добавляет префикс _ИмяКласса__атрибут. Но для внутреннего доступа ничего не меняется: Давайте рассмотрим практическое применение. Например, классу File, который читает файлы из локальной файловой системы, мы хотим добавить способности кеширования. Наш коллега успел написать для этих целей класс-миксин. Но чтобы отгородить методы и атрибуты от потенциальных коллизий, коллега добавил к их именам префикс __: Если вам интересно взглянуть на имплементацию этого механизма в CPython, прошу в Python/compile.c Наконец, благодаря наличию свойств (properties) в языке, теряется смысл писать геттеры и сеттеры в стиле Java: getX(), setX(). Например, в изначально написанном классе Coordinates, понадобилось управлять доступом к атрибуту x. Правильным подходом будет заменить его на property, тем самым сохраняя контракт с внешним миром. Совет 5: Как и многое в Питоне, понятие о приватных полях и методах класса опирается на устоявшуюся конвенцию. Не обижайтесь на авторов библиотек, если "всё перестало работать" по той причите, что вы активно пользовались приватными полями их классов. В конце концов, мы все здесь взрослые люди :). В культуре Питона своеобразный подход к исключениям. Кроме привычного перехвата и обработки а-ля C++ / Java, вам придётся столкнуться с использованием исключений в контексте Перефразируя — не пиши лишнего if, если в большинстве случаев исполнение пойдёт по данной ветке. Вместо этого оберни логику в try..except. Пример: представим обработчик POST-запросов, создающей пользователя в условной базе данных. На входе функции — словарь (dictionary) типа ключ-значение: Мы не стали загрязнять код проверками "содержится ли username или password в data". Мы ожидаем, что скорее всего они там будут. Мы не просим "разрешения" пользоваться этими полями, но "просим прощения" когда очередной кулхацкер запостит форму с отсутствующими данными. Например, вам хочется проверить, присутствуют ли фамилия пользователя в данных и при отсутствии установить её в пустое значение. if здесь будет куда уместнее: Errors should never pass silently. — не замалчивайте исключения! У современного Питона есть замечательная консткрукция raise from, позволяющая сохранить контекст цепочки исключений. Например: Без raise from e цепочка исключений обрывается на MyProductError, и мы не сможем узнать, что именно было причиной этой ошибки. С raise from X, причина (т.е. X) выбрасываемого исключения сохраняется в атрибуте __cause__: В случае с итерацией, выкидывание исключения StopIteration является официальным способом сигнализировать о завершении итератора. Совет 6: Мы платим за обработку исключения, лишь в исключительных ситуациях. Не пренебрегайте ими! switch или pattern matching? — используйте if и словари. do-циклы? — для этого есть while и for. goto? Думаю вы и сами догадались. Это же относится и к некоторым техникам и шаблонам проектирования, которые кажутся сами собой разумеющимися в других языках. Самое удивительное, что нет никаких технических ограничений на их реализацию, просто "у нас так не принято". Например, в Питоне не часто встретишь паттерн "Builder". Вместо него используется возможность передавать и явным образом запрашивать именные аргументы функции. Вместо будет В стандартной библиотеке не используются цепочки методов для работы с коллекциями. Помню, как коллега, пришедший из мира Kotlin, показывал мне код следующего толка (взято из официальной документации по Котлину): В Питоне map(), filter() и многие другие — функции, а не методы коллекций. Переписав этот код один в один получится: По-моему выглядит ужасно. Поэтому для длинных связок вроде .takewhile().filter().map().reduce() лучше использовать т.н. включения (comprehensions), или старые добрые циклы. Кстати, этот же пример на Котлине, приводится в виде соответствующего list comprehension. А на Питоне это выглядит так: Есть библиотеки, такие как Pipe или py_linq! Цепочки методов используются там, где они эффективнee стандартных средств. Например в web-фреймворке Django, цепочки используются для построение объекта-запроса к БД: Совет 7: Перед тем, как сделать что-то очень знакомое из прошлого опыта, но не знакомое в Питоне, спросите себя, какое бы решение принял опытный питонист? Да. Да, если речь идёт о скорости исполнения по сравнению с статически типизированными и компилируемыми языками. Но вы, похоже, желаете развёрнутого ответа? Референсная имплементация Питона (CPython) — далеко не самая его эффективная имплементация. Одна из важных причин — желание разработчиков не усложнять её. И логика вполне понятна — не слишком заумный код означает меньше ошибок, лучшую возможность внесения изменений и в конце концов, большее число людей, которые этот код захотят прочесть, понять и дополнить. Jake VanderPlas в своём блоге разбирает, что происходит у CPython под капотом при сложении двух переменных, содержащих целочисленные значения: Даже если не углубляться в дебри CPython, можно сказать, что для хранения переменных a, b и c, интерпретатору придётся создать три объекта в куче (heap), в которых будут храниться тип и (указатели на) значения; повторно выяснять тип и значения при операции сложения, чтобы вызвать что-то вроде binary_add<int, int>(a->val, b->val); записать результат в c.
Это чудовищно неэффективно по сравнению с аналогичной программой на C. Другая беда CPython — это т.н. Global Interpreter Lock (GIL). Этот механизм, по сути — булевое значение, огороженное мьютексом, используется для синхронизации выполнения байткода. GIL упрощает разработку кода, работающего в многопоточной среде: CPython не надо думать о синхронизации доступа к переменным или о взаимных блокировках (deadlocks). За это приходится платить тем, что лишь один поток получает доступ и выполняет байткод в данный момент времени: UPD:  Но это не значит, что программа на Питоне волшебным образом заработает в многопоточной среде! Код на Питоне не переносится в байткод один в один и нет никаких гарантий о совместимости байткода между версиями! Поэтому синхронизировать потоки в коде вам всё-таки придётся. К счастью и тут у Питона имеется богатый набор средств, например, позволяющих переключаться между многопоточной и многопроцессной моделью выполнения. Рекомендую прочесть статью Anthony Shaw "Has the Python GIL been slain?". Каковы выходы из ситуации? Совет 8: Если вам априори важна скорость выполнения, эффективнее будет использовать Питон как связку между нативными компонентами и не пытаться впихнуть невпихуемое. Если же вы работаете над приложением, в котором IO (сеть, БД, файловая система) является узким местом, то к тому моменту, когда скорость Питона перестанет вас устраивать, вы точно будете знать, как решить эту проблему :) Как начинаются первые шаги в Питоне? Если у вас под рукой Linux или MacOS, то в 95% случаев Питон будет установлен из коробки. Если вы живёте на острие прогресса, то скорее всего это версия 3.х, а не отживающая свой век версия 2.7. Для товарищей на Windows всё чуточку сложнее. Вот несколько вариантов: использовать Docker, Windows Subsystem for Linux, Cygwin, наконец, официальный инсталлятор Питона для Винды. Совет 9: По возможности пользуйтесь свежей версией Питона. Язык развивается, каждая версия — это работа над ошибками и всегда что-то новое и полезное. Вы уже написали "Hello world" и он работает? Превосходно! Через пару дней вы займётесь machine learning-ом и вам понадобится какая-нибудь библиотека из каталога Python Package Index (PyPI). Чтобы избежать конфликтов версий при установке пакетов (packages), в Питоне используются т.н. виртуальные окружения (virtual environments). Они позволяют частично изолировать среду путём создания директории, в которой будут находиться установленные пакеты. Там же будут лежать шелл-скрипты для управления этой средой. Установщик пакетов pip также идёт в комплекте. При активированной виртуальной среде pip будет устанавливать пакеты именно в неё. А объединяет всё это такие утилиты, как pipenv или poetry — аналоги npm, bundler, cargo и т.п. Совет 0xA: Ваши главные помощники для управления зависимостями — это pip и virtualenv. Всё остальное — это удобные, красивые, высокоуровневые обёртки. Ведь всё, что нужно нам и Питону — это правильный sys.path — список директорий, по которым пойдёт поиск модулей при их импорте. Вы уже прочли официальный туториал? Тогда не поленитесь взглянуть и на туториал по вышеописанным инструментам. И как в небезызвестной копипасте:  Уверен, что у вас скопилась гора идей и чешутся руки взяться за новый проект на Питоне. Ведь редкий день проходит на Хабре без появления статьи о применении Питона там где, казалось, ему совсем не место :) Дерзайте, камрады! Software Engineer