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

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


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

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


  1. HeaTTheatR
    21.05.2019 19:15
    +1

    Отличная статья!


    1. BasicWolf Автор
      21.05.2019 19:17

      Спасибо! А ещё большое спасибо berez за помощь с орфографией, пунктуацией и стилистикой :)


  1. andreymal
    21.05.2019 20:33
    +1

    Вот, отличный пост.


    sum([1, 2, 3)

    Опечаточка


    вам не надо думать о синхронизации доступа к переменным или о взаимных блокировках (deadlocks).

    На самом деле надо. Освобождение GIL в общем случае случается в непредсказуемые моменты, и словить гонку при, например, манипуляции с одним и тем же объектом из нескольких потоков всё равно можно. Пример ниже, разумеется, говнокод, но суть проблемы всё равно демонстрирует, падая в KeyError:


    from threading import Thread
    
    d = {}
    
    def thread1():
        while True:
            if "foo" not in d:
                d["foo"] = 0
            d["foo"] += 1
    
    def thread2():
        while True:
            if "foo" in d:
                del d["foo"]
    
    Thread(target=thread1).start()
    Thread(target=thread2).start()

    Вот несколько вариантов: использовать Docker, Windows Subsystem for Linux, Cygwin

    Ну точно так же и про линукс можно сказать, что есть вариант установить питон в Wine :)


    Через пару дней вы займётесь machine learning-ом

    Спойлер


    1. zvulon
      21.05.2019 20:45

      это верно, но я думаю имеется ввиду что из-за GIL выполняется всегда одна строка кода,
      и прогнозировать лок в мультитреадед которые исполняют один и тот же код — проще,
      атомарность на уровне строки


      1. andreymal
        21.05.2019 21:08
        +2

        из-за GIL выполняется всегда одна строка кода

        В общем случае это тоже не всегда, в i+=1 тоже может поселиться GIL, из-за чего пример ниже не хочет печатать 1000000:


        from threading import Thread
        
        i = 0
        
        def func():
            global i
            for _ in range(100000):
                i += 1
        
        threads = []
        for _ in range(10):
            threads.append(Thread(target=func))
            threads[-1].start()
        
        for t in threads:
            t.join()
        
        print(i)
        


    1. BasicWolf Автор
      22.05.2019 09:18

      Спасибо за комментарий. Действительно написал двумысленно: хотел сказать, что разработчикам CPython и собственно самому CPython проще с гарантией выполнения байткода одним потоком. Подправил в тексте.


  1. sbnur
    21.05.2019 20:34

    Хороший стиль изложения


  1. ni-co
    21.05.2019 20:35

    " По возможности пользуйтесь свежей версией Питона." Много пакетов, на которых этот совет не прокатывает. Например, TensorFlow.


    1. Siemargl
      22.05.2019 07:55

      Не то слово. Например WebRTC (Chromium) build system — gclient тоже работает только на 2.8

      Причем это нигде не написано и при попытке запустить на 3.6 она просто не работает _без_ адекватной диагностики.

      Впрочем на 2.8 билд тоже пожужжал часов несколько и повис.

      Такой вот прекрасный язык для крупных проектов =)


      1. worldmind
        22.05.2019 09:26

        А причём тут язык? Может дело в каких-то рукожопах написавших что-то криво?


    1. worldmind
      22.05.2019 09:36

      Мне кажется только гугл держится за вторую версию мёртвой хваткой, у них даже в облаке до сих пор нет третьего питона в продакшене, такая вот «технологичная» компания.
      Хотя в данном случае всё должно быть ок судя по pypi.org/project/tensorflow всё поддерживается до 3.6


      1. ni-co
        22.05.2019 10:08

        Собственно, о третьей версии была и речь. 3.7 и далее пока не поддерживают.


        1. nad_oby
          22.05.2019 14:37

          Странно.
          Вроде 3.7 запустили больше месяца как.
          Python 3.7 is now supported officially in TensorFlow 1.13.1
          Обновился давно.


      1. Barafu_Albino_Cheetah
        22.05.2019 18:02

        Ещё программа Calibre. Ведущий разработчик заявлял, что не перейдёт на Питон 3 даже после окончания поддержки второго. То, что он индус по национальности, не имеет никакого отношения ни к этому, ни к потрясающему количеству детских ошибок при работе с ДБ.


      1. Stas911
        23.05.2019 22:09

        Ну, справедливости ради, в aws тоже есть отдельные места, где до сих пор p27 только.


  1. zvulon
    21.05.2019 20:41

    я хочу заметить что атрибуты начинающиеся с _ protected а с __ private.
    и это не просто конвенция, у прайватс происходит name hashing, так что у вас не получитсья переопределить прайват метод.


  1. Pand5461
    21.05.2019 21:10
    +2

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

    А почему? А потому что


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

    А так есть ещё


    Явное лучше, чем неявное.

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


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


    Отсюда понятно, почему рекомендуется пользоваться try-except, когда нужно достать значение, но явным if, если нужно добавить элемент при отсутствии — это ни черта не EAFP, а особенность работы хэш-таблиц. Ясен пень, если нужно вписать что-то, если его нет, — надо сначала убедиться, что его реально нет, и время на эту проверку никак не сэкономить. А вот если надо достать по ключу — то проверку действительно можно сэкономить, но нужно уметь как-то просигнализировать о неудачном поиске ключа. Можно было бы и без исключения обойтись, а возвращать, как в Лиспе, кортежем.


    1. worldmind
      22.05.2019 09:27

      Со словарями, конечно, отдельный случай. Там действительно есть проблема проверки наличия ключа — потому что занимает столько же времени, сколько и поиск элемента по ключу.

      т.е. очень быстро, а значит проблемы нет, тем более что есть dict.get() который позволяет задать дефолтное значение, о чём месье видимо не знает


      1. Pand5461
        22.05.2019 10:06

        т.е. очень быстро, а значит проблемы нет

        Достаточно медленно, чтобы уже рекомендовать не делать одну и ту же операцию дважды с нарушением принципа "явное лучше неявного". Это же анбоксинг, вычисление хеша, проход по таблице, разрешение коллизий — и всё это два раза, если сначала проверять наличие ключа, а потом брать значение по нему. Ну да, макс. 2000 инструкций против макс. 1000 — это всё равно O(1), если чисто по асимптотике считать.


        тем более что есть dict.get() который позволяет задать дефолтное значение

        Я же об этом написал. Пожалуйста, пусть возвращает. Как отличить — это дефолтное значение, которое по странному стечению обстоятельств было по искомому ключу положено или отсутствие ключа в таблице?


        Ясно, что может быть алгоритм, по которому None в словарь точно не положится, тогда его можно использовать как сигнальное значение. А что, блин, если нет?


        1. worldmind
          22.05.2019 10:11

          Вот так понятнее, хотя ещё надо придумать реальные ситуации когда None нужен как значение.


          1. Pand5461
            23.05.2019 11:09

            Это да, но разработчик стандартной библиотеки понимает же, какой вой поднимется, сделай он "при отсутствии ключа в словаре возвращается None безо всяких там исключений".


            1. kt97679
              23.05.2019 18:07

              Именно это происходит в ruby.


              1. Pand5461
                23.05.2019 20:20

                Я Руби не знаю. Доки пишут, что там при создании словаря определяется, что произойдёт при отсутствии ключа.


                1. worldmind
                  24.05.2019 08:39
                  +1

                  Ну в питоне есть defaultdict


        1. BasicWolf Автор
          22.05.2019 10:35
          +1

          Камрады, давайте разберём:


          class NotFound:
              pass
          
          val = dict.get(key, NotFound)
          if val is NotFound:
              ...

          • без проблем.

          Теперь разберём ситуацию, в которой


          # допустим, что вероятность key not in dict ~ 10%
          val = dict.get(key, NotFound)  

          В синтетическом тесте, try..except может оказаться быстрее, например:


          Много кода
          N = 10000
          P_IN = 0.90
          
          d = dict(enumerate(range(int(N * P_IN))))
          
          def sum_dict_in():
              s = 0
          
              for i in range(N):
                  s += d.get(i, 0)
              return s
          
          def sum_dict_try():
              s = 0
          
              for i in range(N):
                  try:
                      s += d[i]
                  except KeyError:
                      s += 0
              return s
          
          if __name__ == '__main__':
              import timeit
              print(f'Testing with {N} elements and probability of element in dictionary {P_IN}')
              print(timeit.timeit("sum_dict_in()", setup="from __main__ import sum_dict_in", number=1000))
              print(timeit.timeit("sum_dict_try()", setup="from __main__ import sum_dict_try", number=1000))


          1. worldmind
            22.05.2019 10:44

            не стоит погребать логику под if valid, если not valid — это исключительная ситуация.

            Именно, если поля в словаре межет не быть в нормальной ситуации и это лишь повод для ветвления логики, то при чём тут исключения?
            Более того, надо понимать, что исключения надо генерировать когда из внешнего мира пришли кривые данные или что-то во внешнем мире пошло не так, с чем программа не может справится, а это скажем так не так много мест.
            А если мы валидируем входные данные для функции предполагая что своими кривыми руками что-то не так сделали, то это уже assert'ы, которые в продашене можно выключить и убрать оверхед.


            1. BasicWolf Автор
              22.05.2019 10:58

              Давайте немного отойдём от словаря. Пусть это сокет, в который отправляются данные. Опять два варианта:


              if socket.is_alive:
                  socket.send(data)

              try:
                  socket.send(data)
              except NetworkError:
                  ...

              Пример из Oracle-овского драйвера в Django:


              
                  def close(self):
                      try:
                          self.cursor.close()
                      except Database.InterfaceError:
                          # already closed
                          pass

              Здесь нет проверок вроде if self.is_closed. Django не думает, что кому-то вздумается вызывать .close() до покраснения :)


              1. worldmind
                22.05.2019 11:05

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


          1. andreymal
            22.05.2019 10:46
            +1

            Можно я немного позанудствую? dict["foo"] = NotFound :)


            1. worldmind
              22.05.2019 10:48
              +1

              Если кто-то хочет навредить, то его ничем не остановить, но питон считает что всё тут взрослые люди.


          1. Pand5461
            22.05.2019 11:04

            Пример для демонстрации принципа не очень хороший, имхо.


            Там известно, что и где может пойти не так и известно, что в этом случае делать. А если всё это известно — как раз лучше явную проверку сделать. Именно со словарями лучше делать try-except по историческим причинам, в Лиспе вот отсутствие ключа не является исключительным случаем.


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


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


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


          1. Taus
            22.05.2019 23:23
            +1

            Если так хочется, то лучше использовать шаблон Sentinel Object в этом случае.


            SENTINEL = object()
            val = dict.get(key, SENTINEL)

            Тогда даже в случае, когда в dict[key] лежит объект object(), то (val is SENTINEL) == False. Ответ и на комментарий от andreymal выше.


        1. assembled
          22.05.2019 16:09

          dict.get не для проверки наличия ключа, значение по-умолчанию позволяет в некоторых случаях делать меньше проверок, например:

          print(key, myDict.get(key, "N/A"), sep = ': ')


    1. germn
      22.05.2019 09:56

      «Проще попросить прощения, чем спрашивать разрешение» (Easier to ask for forgiveness, than permission, EAFP).
      А почему? А потому что
      Питон медленный

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


      1. Pand5461
        23.05.2019 11:58

        Дело в том, что лет 8-10 назад была статья, кажется про PHP, где автор предлагал избавляться от условных выражений вообще и переходить на исключения. Ветвление — дорогая операция, мол, и т.д. ЧСХ, примеры показывали, что действительно — на исключениях быстрее.


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


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


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


        1. worldmind
          23.05.2019 14:01

          Скорее всего эконмия на спичках, и в любом случае никто от условий отказываться не предлагает (кроме маргиналов всяких), у всего своё место в соответствии с семантикой.


  1. Fen1kz
    21.05.2019 23:02

    Хорошая статья, а есть где почитать про экосистему? Откуда брать библиотеки, как совладать с пипом и почему для каких-то либ надо ставить отдельные пакеты через sudo apt install?

    Потому что после npm это кажется каким-то адом. Типа все зависимости всех проектов в кучу, что? Я уже молчу (хотя хочется кричать от ужаса) что питона 2 версии и все говорят «используй третий», но сами используют второй (-_- )


    1. andreymal
      22.05.2019 00:03
      +3

      Откуда брать библиотеки

      pypi.org


      как совладать с пипом

      В каком смысле? pip install обычно просто работает. Единственное, что нужно помнить, это что pip по умолчанию пытается ставить всё глобально, и обычно нужно создать virtualenv (в npm соответственно наоборот, аналог virtualenv он создаёт автоматически в виде node_modules)


      почему для каких-то либ надо ставить отдельные пакеты через sudo apt install?

      Так в npm то же самое: перед запуском какого-нибудь npm install opencv нужно не забыть сделать sudo apt-get install build-essential libopencv-dev. Причина и у pip, и у npm одна и та же: чтобы собрать биндинги к нативным библиотекам.


      все зависимости всех проектов в кучу

      Нет, см. virtualenv


      но сами используют второй

      Это называется легаси :(


    1. hardtop
      22.05.2019 00:32

      Экосистема сильно зависит от сферы деятельности\интересов. А так стаковерфлоу дай много ответов про Питон.

      Sudo — лишь для того, что лезет в систему. Типа, библиотека Pillow использует libjpeg. А там компиляция нужна, если нет в системе.

      Зависимости и кучи. Используйте virtualenv для каждого проекта — и все пакеты будут локально, в нужной песочнице. Удобно.

      Да, сам сижу на 2-м. А потому что есть часть старого кода. Хотя один проект решил начать на 3-м питоне.

      Но сейчас с нуля учить только 3-й питон. Ну, чтоб без вот этого u«это юникодная строка»


    1. worldmind
      22.05.2019 09:34

      Почитать про экосистему можно во всяких awesome python списках, навроде такого.
      С пипом проблем нет, но лучше не ставить через него ничего с sudo, а делать
      pip3 install --user needed_lib
      тогда всё будет ставиться в хомдиру в .local/ (только стоит проверть что .local/bin прописал в PATH)
      если проектов много с разным набором либ, то чтобы не они конфликтовали стоит использовать virtualenv, это позвоялет юзать разный набор либ, но с одной версией питона, если в разных проектах нужны разные версии питона, то есть pyenv со своей приблудой для venv.


      1. DonAgosto
        23.05.2019 23:02

        если в разных проектах нужны разные версии питона, то есть pyenv со своей приблудой для venv.

        мне кажется, что здесь уже точно стоит подключить Docker


        1. andreymal
          23.05.2019 23:52

          Мне кажется, нет никаких проблем установить несколько версий питона в систему без конфликтов (и даже без pyenv). Имею 2.7, 3.3, 3.4, 3.5, 3.6 и 3.7, поставленные штатным пакетым менеджером арчлинукса — отлично работают


          1. DonAgosto
            24.05.2019 09:19

            я не спорю, что можно, но мы же здесь про best practice все-таки.
            такой зоопарк версий (и питона и всех бинарных пакетов со всеми зависимостями) однозначно ведь добавляет проблем при сопровождении, обновлениях системы, переездах/миграциях.
            ну если конечно скрипты простые/одноразовые, мало зависимостей, продакшена не будет, то тут естественно пофиг (хотя тогда не понятно зачем такой зоопарк)


            1. andreymal
              24.05.2019 12:14

              best practice — код, не приколоченный намертво к одному-единственному конкретному окружению :) Тестирую свои проекты в окружениях от 2.7 до 3.7 (или от 3.5, если решил дропнуть второй питон), по возможности и на PyPy, от фряхи до винды и иногда даже Termux на Android — отлично работают. (Впрочем, на серьёзный продакшен я действительно не претендую). А там уже хоть pyenv, хоть docker, хоть vagga+lithos


        1. worldmind
          24.05.2019 08:37

          Всё зависит от проекто/желания, конечно если проект будет деплоится в докер, то в нём его и надо тестить, а если что-то на попробовть, поэкспериментировать, то как по мне проще venv.


    1. BasicWolf Автор
      22.05.2019 09:41

      Если бы библиотеки Питона писались только на самом Питоне, то тогда бы подобных проблем было бы меньше. А теперь представьте, что вам нужна нативное расширение, например PIL/Pillow для обработки изображений? Есть два варианта — либо вы скачиваете кем-то скомпилированное и упаковонное расширение под вашу платформу и вашу версию Питона, либо скачиваете исходники и компилируете его сами! pip может сделать и то и другое, но для компиляции ему нужен собственно компилятор, заголовочные файлы Питона, исходники или бинарники зависимостей и т.д.


      npm пришёл из экосистемы, где всё пишется на JS. А в Питоне, помимо пакетов с "чистым" Питоном есть расширения, которые могут быть написаны на C/C++ и т.д. Отсюда и разница.


      1. staticlab
        22.05.2019 12:15

        А в Питоне, помимо пакетов с "чистым" Питоном есть расширения, которые могут быть написаны на C/C++ и т.д.

        В npm тоже достаточно таких пакетов.


    1. Siemargl
      22.05.2019 09:52

      все говорят «используй третий», но сами используют второй
      Потому что библиотеки переписывать долго, муторно, и главное — зачем?

      А питон весь в зависимостях. В папку пипа смотреть страшно.


    1. germn
      22.05.2019 10:00

      Потому что после npm это кажется каким-то адом.

      Один раз установите глобально и используйте poetry (ну или pipenv) — это `npm` для Питона.

      и все говорят «используй третий», но сами используют второй

      Уже давно всё не так страшно. Не помню, когда последний раз сталкивался с отсутствием поддержки тройки.


  1. Vitaly83vvp
    22.05.2019 08:02

    Моё знакомство с Python было в 2008. Когда мне дали задание написать генератор PDF отчётов на основе данных из PostgreSQL, я даже и не слышал об этом языке. Но он оказался, на удивление, прост и реализация задания не составила особого труда. Благо, уже на тот момент были справочные материалы и примеры кода. Идея выравнивания кода с одной стороны хороша, но, как уже упоминал автор, при большом объёме кода становится сложнее читать. И, да, каждый язык это всего лишь инструмент. Удобство использования определяется привычками и опытом.


    1. worldmind
      22.05.2019 09:35

      Идея выравнивания кода с одной стороны хороша, но, как уже упоминал автор, при большом объёме кода становится сложнее читать.

      атор и говорит, что это должно стимулировать не писать нечитаемый код


      1. Vitaly83vvp
        22.05.2019 09:42

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


      1. adictive_max
        22.05.2019 09:43

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


        1. worldmind
          22.05.2019 09:50

          Ну вот в этом и работа программиста, найти лучшее решение


    1. germn
      22.05.2019 10:04
      +1

      при большом объёме кода становится сложнее читать

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


  1. alkresin
    22.05.2019 11:39

    А что мне надо установить у пользователя моей программы?
    Питон, пип и все пакеты, использованные при разработке?


    1. BasicWolf Автор
      22.05.2019 11:43

      Питон, пакеты и библиотеки которые требуются для работы программы. Есть также бандлеры, которые с переменным успехом собирают Питон и зависимости в единый исполняемый файл.


    1. Barafu_Albino_Cheetah
      22.05.2019 18:32

      В Windows есть возможность упаковать всё в один EXE. В Linux качать зависимости — это норма, никто не возмутится. (Но можно и упаковать, опять же). Вот в Android придётся заставлять юзера пошаманить руками, отчего Питон под Android толком и не взлетел.


  1. amarao
    22.05.2019 11:57

    Когда я познакомился с Python, мне обещали утиную типизацию. Если оно выглядит как строка, крякаяет как строка и плавает как строка, то это и есть строка...


    class X:
      def __str__(self):
        return "I'm X"
    
    arr = [X()]
    ",".join(arr)
    
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: sequence item 0: expected str instance, X found

    Oh, rly?


    Потом я попробовал поймать несколько исключений:


    expected = set([IOError, MemoryError, FileNotFoundError])
    try:
       with open("/none"):
         pass
    except expected as e:
        print("Good exception", e)
    
    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
    TypeError: catching classes that do not inherit from BaseException is not allowed

    WUT?


    Если написать expected = (IOError, MemoryError, FileNotFoundError), то код сработает. Причины? Утиная типизация — если оно выглядит как тапл, ведёт себя как тапл, крякает как тапл, то оно нифига не тапл. Питон ВСЁ ВИДИТ И НЕ ПРОЩАЕТ.


    Но у нас же есть статическая типизация… Которая совершенно опциональна и не вызывает ошибок ни на каком этапе.


    Ах, да, потом мне рассказали как пакетировать питон. С помощью wheel, setuptools distutils easy_setup через pycentral в pip. Если ничего не пропустил.


    А ещё окончание итерации — это exception StopIteration. Отличный метод использовать exception'ы. Может, сделаем это стандартом?


    with open("file") as f:
         raise Result(f.read())

    Вот потеха будет-то...


    Чуден язык, да и только.


    1. worldmind
      22.05.2019 12:01

      Например наличие метода __str__ никоим образом не означает что это строка, это не апи строки, это апи приведения к строковому виду.


      1. amarao
        22.05.2019 12:08

        А что надо сделать, чтобы данный класс начал утино типизироваться к строке?


        1. fireSparrow
          22.05.2019 12:45
          +1

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

          Но если код ожидает объект определённого класса, то ему нужно дать объект определённого класса. Никакой другой класс не подойдёт, какие бы вы методы в него не напихали.

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


          1. amarao
            22.05.2019 12:55

            Я не путаю тёплое с мягким. Я говорю, что когда "код ожитает объект определённого класса" — это нарушение утиной типизации. Зачем он ожидает объект определённого класса? Чтобы посмотреть некоторые его атрибуты или вызывать его методы. Если я сделал те же атрибуты и дал те же методы, зачем кто-то проверяет на "определённый класс"? Как только язык начинает требовать наследоваться от str, чтобы насладиться str.join, то у нас начинается какая-то java с virtual/final и ООП как "основы всего", а не питон.


            1. fireSparrow
              22.05.2019 13:00

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


            1. BasicWolf Автор
              22.05.2019 13:02

              amarao, давайте не будет переливать из пустого в порожнее. Как было сказано — "except принимает tuple и ничего больше, потому что у нас так принято" :) С вами с удовольствием обсудят это в Python mailing list и может даже дело дойдёт до PEP-a. Питон не закрытый язык, если у вас есть идея, как его улучшить или исправить — дело за малым :)


              1. amarao
                22.05.2019 13:05

                Окей. Я просто показал, насколько питон не такой, каким он кажется. Глубоко в недрах его стандарта много веселья и костылей уровня языка.


                Исправлять их никто не будет, потому что "used in production".


                1. geisha
                  22.05.2019 14:25
                  +1

                  Исправлять их никто не будет, потому что "used in production".

                  Там нечего исправлять. В первом случае дак-тайпинг вполне работает, просто вы его не поняли (ожидается Iterable и определять нужно __iter__, а не всякие __str__ и __repr__):


                  >>> x = 'a', 'b', 'c'
                  >>> ''.join(x)
                  'abc'

                  С вашим видением ситуации можно докатиться и до абсурдного


                  >>> ('a', 'b', 'c') == 'abc'
                  True
                  >>> object() == "<object object at 0x7fb21a5e90c0>"
                  True

                  Во втором вашем случае вполне оправданное ограничение, иначе придётся выслушивать на тупых собеседованиях тупые вопросы, вроде "что сделает этот код"


                  class A(BaseException):
                      def __iter__(self):
                          yield ValueError
                          raise StopIteration
                  
                  try:
                      raise A
                  except A:
                      print "A"
                  except ValueError:
                      print "B"
                  except:
                      print "C"

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


                  1. amarao
                    22.05.2019 14:50

                    Окей, вы меня убедили. В питоне всё потрясающе! Надо больше exception'ов для нормального рабочего процесса, наподобие StopIterator, побольше внезапных isinstance. Что там там ещё хорошего питон обещает? (*args, **kwargs) как вершину динамической типизации?


                    1. geisha
                      22.05.2019 15:34

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


                      1. Siemargl
                        23.05.2019 01:11

                        «мнимый продакшн»…

                        независимо от темы, это высказывание стоит скрижали для высечения!


              1. BasicWolf Автор
                22.05.2019 13:06

                P.S. https://hg.python.org/cpython/file/tip/Python/ceval.c#l5159 — причина по которой tuple логично использовать в CPython — это представления tuple в виде обычного массива с указателями. Работать с ним намного проще и быстрее чем с любой другой коллекцией.


        1. worldmind
          22.05.2019 15:20

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


    1. worldmind
      22.05.2019 12:03

      Про исключения не очень понял что вы хотели сказать, что не так? Конечно если положить в множество ссылку на массив, а не сами значения, то оно не будет работать как множество значений.


      1. amarao
        22.05.2019 12:10

        Э… Что за ссылка на массив в моём питоне?


        Возможно пример был слишком сложный. Объясняю: если вы хотите поймать несколько exception'ов в одном except, вам надо положить их в tuple. И только в tuple. Что угодно другое, каким бы итерируемым, индексируемым и иммутабельным оно не было, не прокатит. Причём ошибка, которую выдаст питон, будет феерически запутанной. Это пример №2 нарушения утиной типизации в самом языке.


        1. BasicWolf Автор
          22.05.2019 12:41

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


          ...For an except clause with an expression, that expression is evaluated, and the clause matches the exception if the resulting object is “compatible” with the exception. An object is compatible with an exception if it is the class or a base class of the exception object or a tuple containing an item compatible with the exception.

          tuple containing an item — ключевой момент. Спецификация не допускает каких либо других контейнеров в этом месте.


          1. amarao
            22.05.2019 12:44

            Так я про это и говорю.


            Одной рукой пишем "утиная типизация", а другой говорим "только объекты класса tuple".


            Я утверждаю, что это место в спецификации — это нарушение собственных принципов и яркий пример неконсистентности. Пример с str.join ровно о том же.


            … Причём в каком-нибудь суперфашистском языке уровня Rust, где без правильных типов даже чихнуть нельзя, и то большая гибкость за счёт трейтов. А тут — динамически типизированный язык с утиной типизацией, который проверяет аргументы через isinstance. Позор, да и только.


            1. fireSparrow
              22.05.2019 12:46
              +1

              То, о чём вы пишете, не имеет отношения к утиной типизации. Посмотрите вот этот мой коммент — habr.com/ru/post/450724/#comment_20184610


    1. BasicWolf Автор
      22.05.2019 12:46

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


    1. Barafu_Albino_Cheetah
      22.05.2019 19:05

      Иногда для скорости приходится жертвовать чистотой кода. Класс BaseException и tuple содержит элементы, написанные на С, и их невозможно имитировать средствами Питона. Поэтому есть жёсткое требования наследования от конкретного класса, чтобы получить эти элементы. Как вам такое?


       1
       2 class Point(tuple):
       3     def __new__(self, x, y):
       4         return tuple.__new__(Point, (x, y))
       5
       6 expected = Point(IOError, FileNotFoundError)
       7 try:
       8    with open("/none"):
       9      pass
      10 except expected as e:
      11     print("Good exception", e)
      #Работает!

      Вообще, хорошая часть стандартной библиотеки Питона написана не на Питоне, что ограничивает число возможных выкрутасов с ней.
      В первом примере со строкой вам нужно имитировать не класс str, а интерфейс iterable, про что уже написали.


      1. amarao
        23.05.2019 09:35

        В первом примере у меня был arr=[X()], и мне, в силу моего скудного знания питона, кажется, что list вполне интерфейс iterable реализует.


  1. fireSparrow
    22.05.2019 11:59
    +3

    Всё-таки перенос строки с помощью слэша — не очень хорошо.
    Во-первых, если после слэша вкрадётся пробел, то это всё сломает, и причина ошибки может быть довольно неочевидна.
    Во-вторых, это, конечно, дело вкуса, но код со слэшами-переносами лично мне читать сложнее.

    Вместо такого:

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


    я всегда пишу такое:

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


    1. BasicWolf Автор
      22.05.2019 12:50

      +1, обычно и сам так пишу. Использовал слеши, чтобы не пугать впервые видящих подобное :)


      query = (
          User.objects
          .filter(...
          ...
          .values('username', 'last_visited')
          [:5]
      )```


  1. alex_zzzz
    22.05.2019 15:38
    -1

    "Ты же форматируешь код отступами?", спросил VlK. Конечно же я форматировал его. Точнее, за меня это делала спираченная Visual Studio. Она справлялась с этим чертовски хорошо. Я никогда не задумывался о форматировании и отступах — они появлялись в коде сами по себе и казались чем-то обыденным и привычным. Но крыть было нечем — код был всегда отформатирован отступами.

    Вот и покрыл. В то время как космические корабли бороздят просторы вселенной, а код на условном X# можно писать практически в одну строку и он сам форматируется, в Питоне приходится всё форматировать руками, как деды форматировали.


    1. worldmind
      22.05.2019 15:42

      на условном X# надо руками скобочки расставлять как деды делали


      1. alex_zzzz
        22.05.2019 16:01

        Скобочки отлично работают. Закрыл скобку-две-три ? и всё, что было внутри них, автоматом само перенеслось и само выровнялось как надо, согласно логике. Обрамил блок скобочками, он бац ? сдвинулся вправо. Убрал скобочки ? сдвинулся влево. Вырезал кусок из одной функции, вставил в другую, он на новом месте сам выровнялся как надо. А если форматирование вдруг перекосило, то наличие ошибки в коде понятно сразу, а после того как будешь исправлять форматирование руками и пасьянс не сойдётся.


        Когда Питон придумывали, такого почти полного автоматизма не было. А теперь Питон придумали и уже поздно.


        1. worldmind
          22.05.2019 16:14

          При копипасте могут быть неудобства, хотя редко, нормальные инструменты справляются с типовыми случаями, а форматирование перекосило это выдуманная проблема.


          1. alex_zzzz
            22.05.2019 16:43

            Под "перекосило форматирование" я имел ввиду X#: когда вставляешь из буфера или удаляешь кусок и код вокруг разъехался. Если разъехалось, значит точно есть косяк. Не туда вставил, не всё скопировал, лишнего скопировал, не всё удалил, удалил лишнее. Автоформат — это как дополнительный контроль ошибок, который выполняет IDE, а не ты руками и глазами.


            1. StriganovSergey
              22.05.2019 18:18
              +1

              Вот, кстати, можно написать редактор или плагин к ребактору, который в коде питона отображает скобочки, и позволяет их вводить, а в файл сбрасывает так любимые питоном отступы :)


              1. Siemargl
                23.05.2019 01:13

                за такие мысли епитимью на тебя надо наложить!


              1. assembled
                23.05.2019 09:34
                +2

                Есть же

                from __future__ import braces
                )))



    1. assembled
      22.05.2019 15:57

      Попробуйте перейти с ed (или что у вас там?) на какую-нибудь современную иде. Они делают отступы автоматически. Вам понравится, я гарантирую!


      1. alex_zzzz
        22.05.2019 16:31

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


        Элементарное


        if x < 0:
            x = 0
            n = n + 1
            if n > 100:
                w = 1

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


        Да даже если пишем этот код с нуля и просто после w = 1 переходим на новую строку, нажать } ничем не сложнее, чем нажать Enter и Backspace для отмены одного уровня отступа.


    1. worldmind
      23.05.2019 10:38

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


  1. IGR2014
    22.05.2019 16:55

    пример из статьи

    @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.x = 5
    

    геттер/сеттер на С++
    int SomeClass::getX() const {
        return this->x;
    }
    
    void SomeClass::setX(const int val) {
        if (val > 10)
            this->x = val;
        else
            std::cout << "x should be greater than 10/n";
    }
    
    ...
    
    c.setX(5);
    

    Объём работы по написанию кода примерно одинаков.
    Хотя из преимуществ вижу «подкапотную» проверку, которая выполняется для x в Питоне.
    Спорное удобство лично для меня т.к. привычней когда = является = без лишних проверок. В сеттере-же я буду ожидать с 50% вероятностью проверку значения на попадание в некий интервал


    1. worldmind
      22.05.2019 17:08

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


      1. IGR2014
        23.05.2019 10:11

        Логично, в статье указано что все члены класса открыты. Но без дополнительной логики аналогичное поведение у полей struct в C/C++ — их поля по умолчанию открыты. Тоже не нужно писать ни строчки шаблонного кода.


        1. andreymal
          23.05.2019 11:30

          А если однажды понадобится переделать поле в проперти с геттером-сеттером?


          1. IGR2014
            23.05.2019 17:40

            С такой точки зрения да, это удобно программисту который работает над проектом сейчас. Но если на его место придёт другой, который продолжит поддержку — его явно не обрадует сеттер с условиями, скрывающийся за обычным c.x = 5. Поэтому я и пытаюсь сказать что словосочетание «правильный подход» в данном случае довольно субъективно


            1. andreymal
              23.05.2019 17:53
              +1

              А по-моему это как раз правильно, это больше походит на «каноничное» ООП с типа отправкой сообщения вида «уважаемый объект, пусть x будет 5, пожалуйста?». Я любое такое присваивание воспринимаю как синтаксический сахар для setX — независимо от того, есть ли сеттер на самом деле или нет.


              (Я бы сказал, что то, что в C++ так нельзя, является недостатком C++, но он всё равно не позволит себе так делать из-за возникающих накладных расходов и/или несовместимостей ABI) (Или можно, а я не в курсе?) (Гугл предлагает перегружать operator=, но это слишком странно)


          1. MacIn
            23.05.2019 22:24

            Здесь удобен подход Delphi, пусть закидают меня помидорами.
            Мы определяем property как

              fX: integer;
            ..
            public
            .. 
              property X: integer read fX write fX;
            

            И если нам нужно перекинуть все в сеттер, мы делаем:
              fX: integer;
              procedure SetX(AX: integer);
            public
            ..
              property X: integer read fX write SetX;
            


            IGR2014:
            скрывающийся за обычным c.x = 5. Поэтому я и пытаюсь сказать что словосочетание «правильный подход» в данном случае довольно субъективно

            Да нет, напротив. Внутренние данные объекта, его внутреннее состояние — это его личное, интимное дело. Если бы это была структура — понятно, мы просто работаем с полем. Если это объект, то у него могут быть свои взгляды на то, как реагировать на внешнее изменение. На то он и объект, на то и инкапсуляция. Никаких предположений на тему «мы закинули 5 в х, там и должно быть 5» быть не может.


            1. Pand5461
              24.05.2019 00:43

              Никаких предположений на тему «мы закинули 5 в х, там и должно быть 5» быть не может.

              Вот такое ООП точно не нужно. Нет уж, если мы пытаемся закинуть 5 в x — пускай там будет или 5, или объяснение, почему именно 5 там быть не может.


              1. MacIn
                24.05.2019 01:29

                Нет уж, если мы пытаемся закинуть 5 в x — пускай там будет или 5, или объяснение, почему именно 5 там быть не может.

                Потому что это не поле данных и не простой native тип, для которых определен контракт «что положили, то и есть». Это интерфейс взаимодействия с объектом, и он сам определяет, как реагировать. Допустим, внутри него вообще нет никакого X — что тогда?
                Допустим, есть максимальное допустимое значение, которое меньше 5 или более оптимальное значение Х, которое известно самому объекту (например, Х — это количество элементов, которое может хранить объект, а ему выгоднее выделять память степенями двойки).


                1. DonAgosto
                  24.05.2019 08:40

                  мне кажется в большинстве случаев логичнее и понятнее все-же строить такой интерфейс по типу «уставка» в терминах автоматики — а она должна оставаться такой же, какой ее первый раз сделали. вот что объект будет с ней внутри делать — это его «интимное» дело, да
                  и даже если с Х нужно что-то сделать и вернуть его-же, но в измененном виде, все равно стоит делать явные и раздельные «вход» и «выход»


                1. Pand5461
                  24.05.2019 09:18

                  Потому что это не поле данных и не простой native тип, для которых определен контракт «что положили, то и есть». Это интерфейс взаимодействия с объектом, и он сам определяет, как реагировать.

                  Да, но если этот интерфейс выглядит так же, как команда простого присваивания для "простого native типа", то уж пусть он и работает примерно так же. Если надо что-то именно сложное и странное — то использовать явный сеттер, чтоб было видно, а неявный через = пускай вообще бросает исключение всегда.


                  Этак можно дойти до "руль и педали — это вообще-то интерфейс взаимодействия с автомобилем, решили мы сделать ускорение / торможение поворотом руля, а повороты нажатием педалей — всё ОК, читайте документацию".


                  Допустим, внутри него вообще нет никакого X — что тогда?

                  В Питоне — оно там появится (не то чтобы мне это нравилось).


                  Допустим, есть… более оптимальное значение Х, которое известно самому объекту (например, Х — это количество элементов, которое может хранить объект, а ему выгоднее выделять память степенями двойки).

                  Тогда количество элементов должно называться Y, а сеттер для X менять именно Y на степень двойки, а X — на то, что сказали.


  1. gatoazul
    22.05.2019 17:22

    Прежде, чем учить язык, я всегда спрашиваю себя: зачем его придумали? Какие задачи он должен был легко решать?

    Для Питона ответ такой:

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


    1. worldmind
      22.05.2019 17:47

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


      1. gatoazul
        23.05.2019 10:24

        Как это ни при чем? Перл первым занял нишу массовых динамических языков. Чем Питон лучше Перла? Только синтаксисом.


  1. StriganovSergey
    22.05.2019 17:22
    +1

    Лично для меня, сложность перехода на Питон, заключается в отсутствии необходимости. Бритва Оккама. Если я уже умею решать любые задачи, более чем на шести языках программирования, на любой случай имею кое-какие готовые наработки, то трудно доказать необходимость решать задачи не так, как знаешь и умеешь.


    1. worldmind
      22.05.2019 17:44

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


      1. melesik
        22.05.2019 18:47

        С какого языка перешли? Я вот всё не могу Перл бросить. У меня так быстро получается делать что угодно на нём, и это всё кушает мало памяти и cpu, что никак не могу понять, что с этим Питоном не так.


        1. worldmind
          22.05.2019 18:49

          С перла и перешёл, понял что не воспринимаю его как эффективный инструмент.


        1. assembled
          23.05.2019 09:46

          Раз вы так любите спецсимволы и смыслоёмкие конструкции, ни в коем случае не учите J, а то без работы останетесь ;)


        1. gatoazul
          23.05.2019 10:25

          Плюс куча готовых библиотек на все случаи жизни


          1. worldmind
            23.05.2019 10:28

            Это когда-то было так, а потом я стал наталкиваться на ситуации когда для питона есть, а для перла нет и даже юзал Inline::Python


  1. Megadeth77
    22.05.2019 18:48

    Дзен про явное лучше неявного крайне своеобразный в Питоне. Типа self надо таскать везде, но при этом к магической комбинации подчеркиваний само собой имя класса прицепится и узнаешь ты об этом (если конечно не вызубрил все пепы) случайно из статьи на хабре.


  1. asdfgh
    22.05.2019 20:34

    А знаете, какой побочный эффект у всего этого безобразия? Программист старается избегать длинных конструкций.Как только размер функции выходит за вертикальные границы экрана, становится сложнее различать, к какой же конструкции относится данный блок кода

    Отступы используются во всех языках, а если не видно начало/конце функции (и скобок) — в чем отличие от других языков?


    1. BasicWolf Автор
      23.05.2019 08:25
      +1

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


      1. assembled
        23.05.2019 10:00

        Так не пишите простыни, со скобками тоже не сразу понятно в каком блоке находишся, например:

                    }
                  }
                }
                doSomething();
              }
            }
          }
          return x;
        }

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


        1. alex_zzzz
          23.05.2019 13:15

          И ещё два варианта:


          1. Отступы не в два пробела, а 4 минимум. Я два пробела почти не воспринимаю, что они есть, что их нет.
          2. Block Structure Guides ? вертикальные полоски, которые связывают начало и конец блока (https://dailydotnettips.com/turning-onoff-structure-guide-lines-in-visual-studio-2017/)


  1. mithron
    23.05.2019 10:09

    Привет! Это ностальгия? ))
    По теме: в ходе разговора тут понял, что еще одна не очень очевидная особенность Python — чтобы нормально писать python-код, надо его отлаживать. На Си достаточно просто написать достаточно сложный код, ни разу его не запустив, только компилируя и проверяя ошибки. На python проще всего несколько раз написать ошибочный код, запустить его, посмотреть что там внутри происходит и переписать. Грубо говоря, для нормальной быстрой разработки на python нужен рабочий runtime. Из-за динамической утиной типизации.
    Или надо больше доков читать и исходников библиотек.


    1. BasicWolf Автор
      23.05.2019 10:35

      TDD :)


      1. mithron
        23.05.2019 11:09

        И это тоже. Но еще и необходимость на рабочем месте запускать весь env/иметь доступ к нему. Поэтому и необходимо наличие тестовой инфраструктуры и еще и ограничитель на сложность env — большие проекты сложно на одном рабочем месте развернуть.


    1. andreymal
      23.05.2019 11:35

      На Си достаточно просто написать достаточно сложный код, ни разу его не запустив

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


      1. mithron
        23.05.2019 11:44

        IDE помогают )) Сильно. Особенно в крупном проекте. В Python даже в крупном проекте часто IDE не могут подсказать, что именно в этом объекте — утиная динамическая типизация.


        1. andreymal
          23.05.2019 11:49

          Верю, что помогают, конечно, но блог PVS-Studio демонстрирует, что IDE и даже cppcheck помогают всё же не всегда. Слыш купи


          В Python даже в крупном проекте часто IDE не могут подсказать, что именно в этом объекте

          Поэтому я сейчас стал прописывать аннотации типов абсолютно везде: и PyCharm подсказывать может, и mypy позволяет выловить ошибки без запуска. Не все, конечно (утиную типизацию даже с аннотациями никто не отменял, да и легаси тоже), но тем не менее «помогают )) Сильно.»


          1. mithron
            23.05.2019 11:54

            Согласен. Собственно разница как раз в том, что в языках со статической типизацией есть принуждение ко всему этому, а в Python принуждения нет. Это помогает легче стартовать, но потом в большом проекте становится сложнее.
            P.S. Пришел в голову язык с обязательными тестами.


            1. worldmind
              23.05.2019 11:55

              Это же гибкость, пока тебе не нужна типизация никто не принуждает, а когда проект дозрел — вот пожалуйста.


              1. andreymal
                23.05.2019 11:59

                А когда проект дозрел — уже поздно.) Взять и расставить типы в произвольном коде в общем случае довольно трудно, если он тщательно обмазан всякими там декораторами, ленивостью, генерируемыми на лету классами, getattr'ами и прочим метапрограммированием, так что mypy выпадает в осадок


                1. worldmind
                  23.05.2019 14:02

                  Я слышал про вполне успешные кейсы, хотя проблемы есть, но в основном из-за внешних библиотек без тайпхинтов.


  1. Stas911
    23.05.2019 22:14

    Выше много писали про форматирование — я просто запускаю flake8 и black (vscode поддерживает оба, кстати). То же самое можно и в CI прикрутить.