21 May 2019

Знакомство с Python для камрадов, переросших «язык A vs. язык B» и другие предрассудки

PythonProgrammingHistory of IT

Для всех хабравчан, у которых возникло ощущение дежавю: Написать этот пост меня побудили статья "Введение в 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: Перед тем как читать дальше, пожалуйста, загляните в уголок Дзена Питона. Язык зиждется на этих постулатах и наше общение будет намного приятнее, если и вы будете с ними знакомы.


Какой умник додумался до отступов?


Первым шоком для тех, кто никогда не видел код на Питоне, является обозначение тела инструкций отступами:


def main():
    ins = input('Please say something')

    for w in ins.split(' '):
        if w == 'hello':
            print('world!')

Вспоминаю вечера в общаге Питерского Политеха, когда мой сосед, 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, эта дискуссия существует почти столько же, сколько существует само понятие "программирование"! Динамическая типизация не плоха и не хороша. Динамическая типизация — это тоже наш инструмент. В Питоне динамическая типизация даёт огромную свободу действий. А там, где большая свобода действий — больше вероятность выстрелить себе в ногу.


Стоит уточнить, что типизация в Питоне строгая и сложить число со строкой не получится:


1 + '1'
>>> TypeError: unsupported operand type(s) for +: 'int' and 'str'

Питон также проверяет сигнатуру функции при вызове и выбросит исключение, если сигнатура вызова не верна:


def sum(x, y):
    return x + y

sum(10, 20, 30)
>>> TypeError: sum() takes 2 positional arguments but 3 were given

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


def sum(x, y):
    return x + y

sum(10, '10')
>>> TypeError: can only concatenate str (not "int") to str

Тем сильнее вызов программисту, особенно при написании больших проектов. Современный Питон ответил на этот вызов механизмом аннотаций и библиотекой типов, а сообщество разработало программы, выполняющие статическую проверку типов. В результате программист узнаёт о подобных ошибках до выполнения программы:


# main.py:
def sum(x: int, y: int) -> int:
    return x + y

sum(10, '10')

$ mypy main.py
tmp.py:5: error: Argument 2 to "sum" has incompatible type "str"; expected "int"

Питон не придаёт никакого значения аннотациям, хотя и сохраняет их в атрибуте __annotations__. Единственное условие — аннотации должны быть валидными значениями с точки зрения языка. С момента их появления в версии 3.0 (что было более десяти лет назад!), именно усилиями сообщества, аннотации стали использовать для типизированной маркировки переменных и аргументов.


Ещё один пример, посложнее.
# Для тех кто очень в теме, напоминаю: это пример :)

from typing import TypeVar, Iterable

Num = TypeVar('Num', int, float)

def sum(items: Iterable[Num]) -> Num:
    accum = 0
    for item in items:
        accum += item
    return accum

sum([1, 2, 3])
>>> 6

Совет 2: На практике больше всего динамическая типизация вызывает проблемы при чтении и отладке кода. Особенно если этот код писался без аннотаций и вам приходится тратить много времени на выяснение типов переменных. Вам не обязательно указывать и документировать типы всего и вся, но время, потраченное на детальное описание публичных интерфейсов и наиболее критических участков кода, воздастся сторицей!


Кря! Утиная типизация


Порой знатоки Питона напускают на себя таинственный вид и говорят об "Утиной типизации".
Утиная типизация (Duck typing) — это применение "утиного теста" в программировании:


Если объект крякает как утка, летает как утка и ходит как утка, то скорее всего это утка.

Рассмотрим пример:


class RpgCharacter:
    def __init__(self, weapon)
        self.weapon = weapon

    def battle(self):
        self.weapon.attack()

Тут — классическое внедрение зависимости (dependency injection). Класс RpgCharacter получает объект weapon в конструкторе и позже, в методе battle() вызывает weapon.attack(). Но RpgCharacter не зависит от конкретной имплементации weapon. Это может быть меч, BFG 9000, или кит с цветочным горшком, готовые приземлиться неприятелю на голову в любой момент. Важно, чтобы у объекта был метод attack(), всё остальное Питон не интересует.



Строго говоря, утиная типизация не является чем-то уникальным. Она присутствует во всех (знакомых мне) динамических языках, реализующих ООП.


Это ещё один пример того, как внимательно приходится программировать в мире динамической типизации. Плохо назвали метод? Неоднозначно нарекли переменную? Ваш коллега, или вы сами, спустя эдак пол-годика, будете счастливы разгребать подобный код :)


Что было бы, используй мы условную Java?
interface IWeapon {
    void attack();
}

public class Sword implements IWeapon {
    public void attack() {
        //...
    }
}

public class RpgCharacter {
    IWeapon weapon;

    public RpgCharacter(IWeapon weapon) {
       this.weapon = weapon;
    }

    public void battle() {
        weapon.attack();
    }
}

А была бы классическая статическая типизация, с проверкой соответствия типам на стадии компиляции. Цена — невозможность использовать объект, имеющий метод attack(), но при этом не реализующий интерфейс IWeapon явным образом.


Совет 3: При желании вы можете описать интерфейс, построив собственный абстрактный класс с методами и свойствами. А ещё лучше будет потратить время на тщательное тестирование и написание документации для себя и пользователей вашего кода.


Процедурный подход и __специальные_методы__()


Питон — объектно-ориентированный язык и в корне иерархии наследования стоит класс object:


isinstance('abc', object)
>>> True

isinstance(10, object)
>>> True

Но там где в Java и C# используется obj.ToString(), в Питоне будет вызов функции str(obj). Или например вместо myList.length, в Питоне будет len(my_list). Создатель языка, Гвидо Ван Россум (Guido van Rossum), объяснил это следующим образом:


Когда я читаю код в котором говорится len(x), то знаю, что запрашивается длина чего-то. Это сразу говорит мне о том, что результатом будет целое число, а аргументом — какой-то контейнер. И наоборот, читая x.len(), мне необходимо знать, что x — это какой-то контейнер, имплементирующий определённый интерфейс или наследующий от класса, в котором имеется метод len(). [Источник].

Тем не менее внутри себя функции len(), str() и некоторые другие будут вызывать определённые методы объекта:


class User:
    def __init__(self, name, last_name):
        self.name = name
        self.last_name = last_name

    def __str__(self):
        return f"Honourable {self.name} {self.last_name}"

u = User('Alex', 'Black')
label = str(u)
print(label)
>>> Honourable Alex Black

Специальные методы также используются операторами языка, как математическими и булевыми, так и операторами цикла for ... in ..., оператором контекста with, оператором индекса [] и т.д.
Например, протокол итератора состоит из двух методов: __iter__() и __next__():


# Никаких Iterable, IEnumerable, std::iterator и т.д.
class InfinitePositiveIntegers:
    def __init__(self):
        self.counter = 0

    def __iter__(self):
        """Возвращает объект по которому будет проводиться итерация.

        Вызывается встроенной фунцкией iter().
        """
        return self

    def __next__(self):
        """Возвращает элементы итерации.

        Вызывается встроенной фунцкией next().
        """
        self.counter += 1
        return self.counter

for i in InfinitePositiveIntegers():
    print(i)
>>> 1
>>> 2
>>> ...
# чтобы остановить, нажмите Ctrl + C

Хорошо, допустим специальные методы. Но почему они выглядят так вырвиглазно? Гвидо объяснил это тем, что имей они обычные имена без подчёркиваний, программисты, сами того не хотя, рано или поздно переопределяли бы их. Т.е. __метод__() это своебразная защита от дурака. Как показало время — защита эффективная :)


Совет 4: Внимательно ознакомьтесь со встроенными функциями и специальными методами объектов. Они являются неотъемлимой частью языка, без которой невозможно полноценно на нём разговаривать.


Где инкапсуляция? Где мой private?! Где моя сказочка?!!


В Питоне нет модификаторов доступа к атрибутам класса. Внутренности объектов открыты для доступа без каких-либо ограничений. Однако существует конвенция, по которой атрибуты с префиксом _ считаются приватными, например:


import os

class MyFile:
    # Поле считается приватным
    _os_handle = None

    def __init__(self, path: str):
        self._open(path)

    # Метод считается приватным
    def _open(self, path):
        # os.open() - *низкоуровневая* функция для открытия файлов.
        # На практике используется встроенная функция open().
        # Нам же os.open() отлично подойдёт для примера.
        self._os_handle = os.open(path, os.O_RDWR | os.O_CREAT)

    # А этот метод считается публичным
    def close(self):
        if self._os_handle is not None:
            os.close(self._os_handle)

f = MyFile('/tmp/file.txt')
print(f._os_handle) # с доступом к "приватному" полю нет никаких проблем!
f.close()

Почему?


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

В конце концов мы все здесь взрослые люди.

— Karl Fast [Источник].

А как же избежать коллизии имён при наследовании?

В Питоне есть специальный механизм искажения (mangling) имени атрибутов, начинающихся с двойного подчёркивания и не заканчивающихся на двойное подчёркивание (__my_attr)! Сделано это для избежания коллизий имён при наследовании. Для вызова вне тела методов класса, Питон добавляет префикс _ИмяКласса__атрибут. Но для внутреннего доступа ничего не меняется:


class C:
    def __init__(self):
        self.__x = 10

    def get_x(self):
        return self.__x

c = C()
c.__x
>>> 'C' object has no attribute '__x'

print(c.get_x())
>>> 10

print(c._C__x)
>>> 10

Давайте рассмотрим практическое применение. Например, классу File, который читает файлы из локальной файловой системы, мы хотим добавить способности кеширования. Наш коллега успел написать для этих целей класс-миксин. Но чтобы отгородить методы и атрибуты от потенциальных коллизий, коллега добавил к их именам префикс __:


class BaseFile:
    def __init__(self, path):
        self.path = path

class LocalMixin:
    def read_from_local(self):
        with open(self.path) as f:
            return f.read()

class CachedMixin:
    class CacheMissError(Exception):
        pass

    def __init__(self):
        # Tepeрь, даже если в соседнем классе в цепочке наследования
        # будет атрибут __cache, или метод __from_cache(),
        # коллизии, а точнее переопределения не произойдёт!
        self.__cache = {}

    def __from_cache(self):
        return self.__cache[self.path]

    def read_from_cache(self):
        try:
            return self.__from_cache()
        except KeyError as e:
            raise self.CacheMissError() from e

    def store_to_cache(self, data):
        self.__cache[self.path] = data

class File(CachedMixin, LocalMixin, BaseFile):
    def __init__(self, path):
        CachedMixin.__init__(self)
        BaseFile.__init__(self, path)

    def read(self):
        try:
            return self.read_from_cache()
        except CachedMixin.CacheMissError:
            data = self.read_from_local()
            self.store_to_cache(data)
            return data

Если вам интересно взглянуть на имплементацию этого механизма в CPython, прошу в Python/compile.c


Наконец, благодаря наличию свойств (properties) в языке, теряется смысл писать геттеры и сеттеры в стиле Java: getX(), setX(). Например, в изначально написанном классе Coordinates,


class Coordinates:
    def __init__(self, x, y):
        self.x = x
        self.y = y

c = Coordinates(10, 10)
print(c.x, c.y)
>>> (10, 10)

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


class Coordinates:
    _x = 0

    def __init__(self, x, y):
        self.x = x
        self.y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, val):
        if val > 10:
            self._x = val
        else:
            raise ValueError('x should be greater than 10')

c = Coordinates(20, 10)
c.x = 5
>>> ValueError: x should be greater than 10

Совет 5: Как и многое в Питоне, понятие о приватных полях и методах класса опирается на устоявшуюся конвенцию. Не обижайтесь на авторов библиотек, если "всё перестало работать" по той причите, что вы активно пользовались приватными полями их классов. В конце концов, мы все здесь взрослые люди :).


Немного об исключениях


В культуре Питона своеобразный подход к исключениям. Кроме привычного перехвата и обработки а-ля C++ / Java, вам придётся столкнуться с использованием исключений в контексте


"Проще попросить прощения, чем спрашивать разрешение" (Easier to ask for forgiveness, than permission — EAFP).

Перефразируя — не пиши лишнего if, если в большинстве случаев исполнение пойдёт по данной ветке. Вместо этого оберни логику в try..except.


Пример: представим обработчик POST-запросов, создающей пользователя в условной базе данных. На входе функции — словарь (dictionary) типа ключ-значение:


def create_user_handler(data: Dict[str, str]):
    try:
        database.user.persist(
            username=data['username'],
            password=data['password']
        )
    except KeyError:
        print('There was a missing field in data passed for user creation')

Мы не стали загрязнять код проверками "содержится ли username или password в data". Мы ожидаем, что скорее всего они там будут. Мы не просим "разрешения" пользоваться этими полями, но "просим прощения" когда очередной кулхацкер запостит форму с отсутствующими данными.


Только не доводите это до абсурда!

Например, вам хочется проверить, присутствуют ли фамилия пользователя в данных и при отсутствии установить её в пустое значение. if здесь будет куда уместнее:


def create_user_handler(data):
    if 'last_name' not in data:
        data['last_name'] = ''

    try:
        database.user.persist(
            username=data['username'],
            password=data['password'],
            last_name=data['last_name']
        )
    except KeyError:
        print('There was a missing field in data passed for user creation')

Errors should never pass silently. — не замалчивайте исключения! У современного Питона есть замечательная консткрукция raise from, позволяющая сохранить контекст цепочки исключений. Например:


class MyProductError(Exception):
    def __init__(self):
        super().__init__('There has been a terrible product error')

def calculate(x):
    try:
        return 10 / x
    except ZeroDivisionError as e:
        raise MyProductError() from e

Без raise from e цепочка исключений обрывается на MyProductError, и мы не сможем узнать, что именно было причиной этой ошибки. С raise from X, причина (т.е. X) выбрасываемого исключения сохраняется в атрибуте __cause__:


try:
    calculate(0)
except MyProductError as e:
    print(e.__cause__)

>>> division by zero

Но есть маленький нюанс в случае с итерацией: StopIteration

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


class PositiveIntegers:
    def __init__(self, limit):
        self.counter = 0
        self.limit = limit

    def __iter__(self):
        return self

    def __next__(self):
        self.counter += 1

        if self.counter == self.limit:
            # никаких hasNext() или moveNext(),
            # только исключения, только хардкор
            raise StopIteration()

        return self.counter

for i in PositiveIntegers(5):
    print(i)
> 1
> 2
> 3
> 4

Совет 6: Мы платим за обработку исключения, лишь в исключительных ситуациях. Не пренебрегайте ими!


There should be one-- and preferably only one --obvious way to do it.


switch или pattern matching? — используйте if и словари. do-циклы? — для этого есть while и for. goto? Думаю вы и сами догадались. Это же относится и к некоторым техникам и шаблонам проектирования, которые кажутся сами собой разумеющимися в других языках. Самое удивительное, что нет никаких технических ограничений на их реализацию, просто "у нас так не принято".


Например, в Питоне не часто встретишь паттерн "Builder". Вместо него используется возможность передавать и явным образом запрашивать именные аргументы функции. Вместо


human = HumanBuilder.withName("Alex").withLastName("Black").ofAge(20).withHobbies(['tennis', 'programming']).build()

будет


human = Human(
    name="Alex"
    last_name="Black"
    age=20
    hobbies=['tennis', 'programming']
)

В стандартной библиотеке не используются цепочки методов для работы с коллекциями. Помню, как коллега, пришедший из мира Kotlin, показывал мне код следующего толка (взято из официальной документации по Котлину):


val shortGreetings = people
    .filter { it.name.length < 10 }
    .map { "Hello, ${it.name}!" }

В Питоне map(), filter() и многие другие — функции, а не методы коллекций. Переписав этот код один в один получится:


short_greetings = map(lambda h: f"Hello, {h.name}", filter(lambda h: len(h.name) < 10, people))

По-моему выглядит ужасно. Поэтому для длинных связок вроде .takewhile().filter().map().reduce() лучше использовать т.н. включения (comprehensions), или старые добрые циклы. Кстати, этот же пример на Котлине, приводится в виде соответствующего list comprehension. А на Питоне это выглядит так:


short_greetings = [
    f"Hello {h.name}"
    for h in people
    if len(h.name) < 10
]

Для тех же, кто скучает по цепочкам

Есть библиотеки, такие как Pipe или py_linq!


Цепочки методов используются там, где они эффективнee стандартных средств. Например в web-фреймворке Django, цепочки используются для построение объекта-запроса к БД:


query = User.objects \
    .filter(last_visited__gte='2019-05-01') \
    .order_by('username') \
    .values('username', 'last_visited') \
    [:5]

Совет 7: Перед тем, как сделать что-то очень знакомое из прошлого опыта, но не знакомое в Питоне, спросите себя, какое бы решение принял опытный питонист?


Питон медленный


Да.


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


Но вы, похоже, желаете развёрнутого ответа?


Референсная имплементация Питона (CPython) — далеко не самая его эффективная имплементация. Одна из важных причин — желание разработчиков не усложнять её. И логика вполне понятна — не слишком заумный код означает меньше ошибок, лучшую возможность внесения изменений и в конце концов, большее число людей, которые этот код захотят прочесть, понять и дополнить.


Jake VanderPlas в своём блоге разбирает, что происходит у CPython под капотом при сложении двух переменных, содержащих целочисленные значения:


a = 1
b = 2
c = a + b

Даже если не углубляться в дебри CPython, можно сказать, что для хранения переменных a, b и c, интерпретатору придётся создать три объекта в куче (heap), в которых будут храниться тип и (указатели на) значения; повторно выяснять тип и значения при операции сложения, чтобы вызвать что-то вроде binary_add<int, int>(a->val, b->val); записать результат в c.
Это чудовищно неэффективно по сравнению с аналогичной программой на C.


Другая беда CPython — это т.н. Global Interpreter Lock (GIL). Этот механизм, по сути — булевое значение, огороженное мьютексом, используется для синхронизации выполнения байткода. GIL упрощает разработку кода, работающего в многопоточной среде: CPython не надо думать о синхронизации доступа к переменным или о взаимных блокировках (deadlocks). За это приходится платить тем, что лишь один поток получает доступ и выполняет байткод в данный момент времени:



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


Если вам интересно, какие попытки предпринимаются для искоренения GIL

Рекомендую прочесть статью Anthony Shaw "Has the Python GIL been slain?".


Каковы выходы из ситуации?


  1. Питон отлично взаимодействует с нативными библиотеками. В простейшем варианте (CFFI) нужно описать источник и сигнатуру функции в Питоне и вызывать её из динамической библиотеки. Для полноценной же работы с интерпретатором и окружением Питон предоставляет API для написания расширений (extensions) на C/C++. А порывшись в Гугле, можно найти реализацию расширений на Rust, Go и даже Kotlin Native!
  2. Использовать альтернативную реализацию Питона, например:
    • PyPy, со встроенным JIT-компилятором. Прирост скорости будет меньше, чем при использовании нативного расширения, но может в конкретном случае большего и не будет нужно?
    • Cython — транспайлер и компилятор надмножества языка Python в код на C.
    • IronPython — имплементация, работающая поверх .NET framework.

Совет 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 — список директорий, по которым пойдёт поиск модулей при их импорте.


Что же дальше?


Вы уже прочли официальный туториал? Тогда не поленитесь взглянуть и на туториал по вышеописанным инструментам. И как в небезызвестной копипасте:


Завтра ищешь в интернете книжку Dive into python...

Уверен, что у вас скопилась гора идей и чешутся руки взяться за новый проект на Питоне. Ведь редкий день проходит на Хабре без появления статьи о применении Питона там где, казалось, ему совсем не место :)


Дерзайте, камрады!

Tags:python
Hubs: Python Programming History of IT
+78
38.2k 334
Comments 145
Popular right now
Python для работы с данными
December 7, 202031,500 ₽Нетология
Python QA Engineer
December 21, 202060,000 ₽OTUS
IT-Recruiter
December 22, 202040,000 ₽OTUS
Product Manager IT-проектов
January 17, 202160,000 ₽OTUS