Продолжение. Начало в «Python как предельный случай C++. Часть 1/2».


Переменные и типы данных


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


В С++ у программиста есть выбор: использовать автоматические переменные, размещаемые в стеке, или держать значения в памяти данных программы, помещая в стек только указатели на эти значения. Что, если мы выберем для Python только одну из этих опций?


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


Таким образом, выражение


a = 3

будет означать то, что мы создали в памяти данных программы (так называемой «куче») объект «3» и сделали имя “a” ссылкой на него. А выражение


b = a

в таком случае будет означать, что мы заставили переменную “b” ссылаться на тот же объект в памяти, на который ссылается “a”, иначе говоря ? скопировали указатель.


Если всё является указателем, то сколько списочных типов нам нужно реализовать в нашем языке? Разумеется, только один ? список указателей! Вы можете использовать его для хранения целых, строк, других списков, чего угодно ? ведь всё это указатели.


Сколько типов хэш-таблиц нам нужно реализовать? (В Python этот тип принято называть «словарём» ? dict.) Один! Пусть он связывает указатели на ключи с указателями на значения.


Таким образом, нам не нужно реализовывать в нашем языка огромную часть спецификации C++ ? шаблоны, поскольку все операции мы производим над объектами, а объекты всегда доступны по указателю. Конечно же, программы, написанные на Python, не обязаны ограничиваться работой с указателями: существуют библиотеки вроде NumPy, при помощи которых учёные работают с массивами данных в памяти, как они бы делали это в Fortran. Но основа языка ? выражения вроде “a = 3” ? всегда работают с указателями.


Концепция «всё является указателем» также упрощает до предела композицию типов. Хотите список словарей? Просто создайте список и поместите туда словари! Не нужно спрашивать у Python разрешения, не нужно объявлять дополнительные типы, всё работает «из коробки».


А что, если мы хотим использовать составные объекты в качестве ключей? Ключ в словаре должен иметь неизменяемое значение, иначе как искать значения по нему? Списки могут изменяться, поэтому их нельзя использовать в данном качестве. Для подобных ситуаций в Python есть тип данных, который, аналогично списку, является последовательностью объектов, но, в отличие от списка, последовательность эта не изменяется. Этот тип называется кортеж или tuple (произносится как «тьюпл» или «тапл»).


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


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


def get_address():
    ...
    return host, port

host, port = get_address()

У распаковки есть несколько полезных побочных эффектов, например, обмен переменных значениями можно записать так:


x, y = y, x

Всё является указателем, значит, функции и типы данных могут использоваться как данные. Если вы знакомы с книгой «Паттерны проектирования» за авторством «Банды четырёх», вы должны помнить, какие сложные и запутанные способы она предлагает для того, чтобы параметризовать выбор типа объекта, создаваемого вашей программой во время выполнения. Действительно, во многих языках программирования это сложно сделать! В Python все эти сложности улетучиваются, поскольку мы знаем, что функция может вернуть тип данных, что и функции, и типы данных ? это просто ссылки, а ссылки можно хранить, например, в словарях. Это упрощает задачу до предела.


Дэвид Вилер говорил: «Все проблемы в программировании решаются путём создания дополнительного уровня косвенности». Использование ссылок в Python ? это тот уровень косвенности, который традиционно применяется для решения множества проблем во многих языках, в том числе и в C++. Но если там он используется явно, и это приводит к усложнению программ, то в Python он используется неявно, единоообразно в отношении данных всех типов, и дружественно к пользователю.


Но если всё является ссылками, то на что ссылаются эти ссылки? В языках вроде C++ есть множество типов. Давайте оставим в Python только один тип данных ? объект! Специалисты в области теории типов неодобрительно качают головами, но я считаю, что один исходный тип данных, от которого производятся все остальные типы в языке ? это хорошая идея, обеспечивающая единообразность языка и простоту его использования.


Что касается конкретного содержимого памяти, то различные реализации Python (PyPy, Jython или MicroPython) могут управлять памятью по-разному. Но, чтобы лучше понять, как именно реализуется простота и единообразность Python, сформировать правильную ментальную модель, лучше обратиться к эталонной реализации Python на языке C, называемой CPython, которую мы можем загрузить на сайте python.org.


struct {
    struct _typeobject *ob_type;
    /* followed by object’s data */
}

То, что мы увидим в исходном коде CPython ? это структура, которая состоит из указателя на информацию о типе данной переменной и полезной нагрузки, которая определяет конкретное значение переменной.


Как же устроена информация о типе? Снова углубимся в исходный код CPython.


struct _typeobject {
    /* ... */
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    /* ... */
    newfunc tp_new;
    freefunc tp_free;
    /* ... */
    binaryfunc nb_add;
    binaryfunc nb_subtract;
    /* ... */
    richcmpfunc tp_richcompare;
    /* ... */
}

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


Это радикальным образом отличается от C и C++, в которых информация о типе ассоциируется с именами, а не со значениями переменных. В Python все имена ассоциированы со ссылками. Значение по ссылке, в свою очередь, имеет тип. В этом и заключается суть динамических языков.


Чтобы реализовать все возможности языка, нам достаточно определить две операции над ссылками. Одна из них наиболее очевидна ? это копирование. Когда мы присваиваем значение переменнной, слоту в словаре или атрибуту объекта, мы копируем ссылки. Это простая, быстрая и совершенно безопасная операция: копирование ссылок не изменяет содержимое объекта.


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


У вас может возникнуть вопрос: если все переменные содержат ссылки, то как я могу защитить от изменений значение пременной, передав её функции как параметр?


n = 3
some_function(n)
# Q: I just passed a pointer!
# Could some_function() have changed “3”?

Ответ заключается в том, что простые типы в Python являются неизменяемыми: в них попросту не реализован тот метод, который отвечает за изменение их значения. Неизменяемые (иммутабельные) int, float, tuple или str обеспечивают в языках типа «всё является указателем» тот же семантический эффект, который в C обеспечивают автоматические переменные.


Унифицированные типы и методы максимально упрощают применение обобщённого программирования, или дженериков. Функции min(), max(), sum() и им подобные являются встроенными, нет нужды их импортировать. И они работают с любыми типами данных, в которых реализованы операции сравнения для min() и max(), сложения для sum() и т. д.


Создание объектов


Мы выяснили в общих чертах, как должны вести себя объекты. Теперь определим, как мы будем их создавать. Это ? вопрос синтаксиса языка. C++ поддерживает как минимум три способа создания объекта:


  1. Автоматический, объявлением переменной данного класса:
    my_class c(arg);
  2. С помощью оператора new:
    my_class *c = new my_class(arg);
  3. Фабричный, при помощи вызова произвольной функции, возвращающей указатель:
    my_class *c = my_factory(arg);

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


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


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


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


# Let’s make type objects
# their own type’s factories!
c = MyClass()
i = int('7')
f = float(length)
s = str(bytes)

Все типы являются вызываемыми объектами, и все они возвращают значения своего типа, определяемые аргументами, переданными при вызове.


Таким образом, с использованием только базового синтаксиса языка, могут быть инкапсулированы любые манипуляции при создании объектов, вроде паттернов «Арена» или «Приспособленец», поскольку ещё одна замечательная идея, позаимствованная из C++, заключается в том, что тип сам определяет, как происходит порождение его объектов, как оператор new работает для него.


Как насчёт NULL?


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


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


Вы можете спросить: «Если структура операций над объектами неизменна, как мы видели ранее, то как же пользователи будут создавать собственные классы, с методами и атрибутами, не перечисленными в этой структуре?»


Магия заключена в том, что для пользовательских классов Python имеет очень простую «заготовку» с небольшим числом реализованных методов. Вот самые важные из них:


struct _typeobject {
    getattrfunc tr_getattr;
    setattrfunc tr_setattr;
    /* ... */
    newfunc tp_new;
    /* ... */
}

tp_new() создаёт для пользовательского класса хэш-таблицу, такую же, как для типа dict. tp_getattr() извлекает что-то из этой хэш-таблицы, а tp_setattr(), наоборот, что-то туда кладёт. Таким образом, способность произвольных классов хранить любые методы и атрибуты обеспечивается не на уровне структур языка C, а уровнем выше ? хэш-таблицей. (Разумеется, за исключением некоторых случаев, связанных с оптимизацией производительности.)


Модификаторы доступа


Что же нам делать со всеми теми правилами и концепциями, которые в C++ построены вокруг ключевых слов private и protected? Python, будучи скриптовым языком, не нуждается в них. У нас уже есть «защищённые» части языка ? это данные встроенных типов. Ни при каких условиях Python не позволит программе, например, манипулировать битами числа с плавающей запятой! Этого уровня инкапсуляции вполне достаточно, чтобы поддержать целостность самого языка. Мы, создатели Python, считаем, что целостность языка ? это единственный хороший предлог для сокрытия информации. Все остальные структуры и данные пользовательской программы считаются публичными.


Вы можете написать символ подчёркивания (_) в начале имени атрибута класса, чтобы предупредить коллегу: на этот атрибут не стоит полагаться. Но в остальном Python выучил уроки начала 90-х: тогда многие верили в то, что основной причиной того, что мы пишем раздутые, нечитаемые и забагованные программы, является недостаток приватных переменных. Думаю, следующие 20 лет убедили всех в индустрии программирования: приватные переменные ? это не единственное, и далеко не самое эффективное средство от раздутых и забагованных программ. Поэтому создатели Python решили даже не беспокоиться по поводу приватных переменных, и, как видите, не прогадали.


Управление памятью


Что же происходит с нашими объектами, числами и строками на более низком уровне? Как именно они размещаются в памяти, как CPython обеспечивает совместный доступ к ним, когда и при каких условиях они уничтожаются?


И в этом случае мы выбрали наиболее общий, предсказуемый и производительный способ работы с памятью: со стороны C-программы все наши объекты ? это разделяемые указатели.


С учётом этого знания те структуры данных, которые мы рассмотрели ранее, в части «Переменные и типы данных», должны быть дополнены следующим образом:


struct {
    Py_ssize_t ob_refcnt;
    struct {
       struct _typeobject *ob_type;
        /* followed by object’s data */
    }
}

Итак, каждый объект в Python (мы имеем в виду реализацию CPython, разумеется) имеет свой счётчик ссылок. Как только он становится равным нулю, объект может быть удалён.


Механизм подсчёта ссылок не опирается на дополнительные вычисления или фоновые процессы ? объект может быть уничтожен мгновенно. Кроме того, он обеспечивает высокую локальность данных: зачастую память снова начинает использоваться сразу после освобождения. Только что уничтоженный объект, скорее всего, недавно использовался, а значит, находился в кэше процессора. Поэтому и только что созданный объект останется в кэше. Эти два фактора ? простота и локальность ? делают подсчёт ссылок очень производительным способом сборки мусора.


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


Ошибки разработчиков Python


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


Python 2 из-за инерции мышления, связанной со скриптовыми языками, пытался преобразовывать строковые типы, как делал бы это язык с нестрогой типизацией. Если вы попытаетесь объединить байтовую строку со строкой в Unicode, интерпретатор неявно преобразует байтовую строку в Unicode при помощи той кодовой таблицы, которая имеется в данной системе, и представит результат в Unicode:


>>> 'byte string ' + u'unicode string'
u'byte string unicode string'

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


Эта ошибка проектирования языка была исправлена в Python 3:


>>> b'byte string ' + u'unicode string'
TypeError: can't concat bytes to str

Похожая ошибка в Python 2 была связана с «наивной» сортировкой списков, состоящих из несравнимых элементов:


>>> sorted(['b', 1, 'a', 2])
[1, 2, 'a', 'b']

Python 3 в этом случае даёт пользователю понять, что тот пытается сделать что-то не слишком осмысленное:


>>> sorted(['b', 1, 'a', 2])
TypeError: unorderable types: int() < str()

Злоупотребления


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


class Address(object):
    def __init__(self, host, port):
         self.host = host
         self.port = port

«Но это же неоптимально!» ? говорили некоторые, ? «Что, если порт не отличается от дефолтного значения? Мы всё равно тратим на его хранение целый атрибут класса!» И в результате получалось что-то вроде


class Address(object):
    def __init__(self, host, port=None):
        self.host = host
        if port is not None:  # so terrible
            self.port = port

Так в программе появляются объекты одного типа, с которыми, тем не менее, нельзя работать единообразно, так как одни из них имеют некий атрибут, а другие ? нет! И мы не можем прикоснуться к этому атрибуту, не проверив заранее его наличие:


# code was forced to use introspection
# (terrible!)
if hasattr(addr, 'port'):
    print(addr.port)

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


# today’s best practice:
# every atribute always present
if addr.port is not None:
    print(addr.port)

Так, ранние эксперименты с динамически добавляемыми и удаляемыми атрибутами завершились, и теперь мы рассматриваем классы в Python примерно так же, как и в C++.


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


class Dataframe(object):
    def __init__(self, columns):
        if isinstance(columns, str):
            columns = columns.split(',')
        self.columns = columns

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


Также такой код сложнее поддерживать, отлаживать, и особенно тестировать: в тестах может быть предусмотрена проверка только одного из двух поддерживаемых нами типов, но покрытие всё равно составит 100%, и мы не протестируем другой тип.


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


Необходимость использования eval() в программе считается явным архитектурным просчётом. Скорее всего, вы просто не сообразили, как сделать то же самое нормальным способом. Но в некоторых случаях ? например, если вы пишете программу типа Jupyter notebook или онлайн-песочницу для запуска и тестирования пользовательского кода ? использование eval() вполне оправдано, и в этом типе задач Python проявляет себя великолепно! Действительно, реализовать нечто подобное на C++ было бы намного сложнее.


Как мы уже показали выше, интроспекция (getattr(), hasattr(), isinstance()) не всегда является хорошим средством для выполнения типичных пользовательских задач. Но эти возможности, тем не менее, встроены в язык, и они просто сверкают в ситуациях, когда наш код должен описывать сам себя: логгирование, тестирование, статическая проверка, отладка!


Эра консолидации


В заключение мне хочется отметить следующее: мы живём в такое время, когда лучшие практики разработки на различных языках проявляют тенденцию к консолидации. 20 лет назад я не смог бы даже упомянуть разделяемые указатели в контексте того, что объединяет C++ и Python. А сегодня сообщества, сформировавшиеся вокруг разных языков программирования, свободно обмениваются лучшими практиками. И это изменение произошло в течение девяностых и нулевых.


Чтобы получить количественные измерения в подтверждение моей гипотезы, я мониторил использование shared_ptr в TensorFlow примерно с 2016 по 2018 год.


TensorFlow ? это большой и во многом образцовый C++-проект, но большинство программистов знают его лишь в качестве Python-библиотеки (а C++ ? в качестве сборочной системы TensorFlow, наверное).


image


На диаграмме по вертикали изображено соотношение строк кода TensorFlow, использующих shared_ptr, к общему числу строк кода. В лучших традициях Кремниевой долины, этот график направлен строго вверх.


Так куда же направляется современный C++? В начале мы говорили о предельных случаях. Что происходит на графике, который мы видим? Если время стремится к бесконечности, то всё становится разделёнными указателями, и C++ становится Python!

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


  1. bm13kk
    20.08.2019 16:43
    +1

    Доклад, конечно, интересный. Есть о чем подумать.

    Например, если питон поборол ошибки си — то почему в нем есть два метода — `__copy__` и `__deepcopy__`? Аналогично — дикт и слотс?

    Почему он использует None?


  1. picul
    20.08.2019 17:43
    +1

    Ждем статью «Кухонный нож как предельный случай скальпеля».


  1. 0xd34df00d
    20.08.2019 18:00
    -1

    Вроде же ещё не пятница. Часть 2/2.


    Поэтому мы будем использовать в Python только указатели. Это концептуально упростит язык.

    И, несомненно, поможет сохранить достижимую для C++ скорость исполнения. Она ведь должна сохраниться в предельном случае, да?


    в таком случае будет означать, что мы заставили переменную “b” ссылаться на тот же объект в памяти, на который ссылается “a”, иначе говоря ? скопировали указатель.

    Не понял. Я, конечно, совсем не знаю питон, но что-то здесь не так:


    >>> a = 3
    >>> b = a
    >>> a += 1
    >>> a
    4
    >>> b
    3

    Указатели и ссылки в C++ так себя не ведут.


    Таким образом, нам не нужно реализовывать в нашем языка огромную часть спецификации C++ ? шаблоны, поскольку все операции мы производим над объектами, а объекты всегда доступны по указателю.

    Шаблоны не имеют никакого отношения к доступности по указателю.


    Мне всё интереснее, откуда у автора этого доклада знания по C++.


    Концепция «всё является указателем» также упрощает до предела композицию типов. Хотите список словарей? Просто создайте список и поместите туда словари! Не нужно спрашивать у Python разрешения, не нужно объявлять дополнительные типы, всё работает «из коробки».

    Типы вообще не нужно объявлять (увы). Но не припомню, чтобы в коде на C++ мне приходилось просить разрешения у компилятора.


    Другая возможность, которую дают нам кортежи ? возврат из функции нескольких значений без необходимости объявлять для этого дополнительные типы данных, как это приходится делать в C и C++.

    А std::tuple чем не угодил? А доступная с C++14 конструкция типа


    auto getFoo()
    {
      struct
      {
        std::string name;
        int age;
      } result { "meh", 42 };
      return result;
    }
    
    auto ret = getFoo();
    std::cout << ret.name << std::endl;

    прекрасная для случая, когда надо вернуть что-то с именованными полями?


    У распаковки есть несколько полезных побочных эффектов, например, обмен переменных значениями можно записать так:

    std::tie(a, b) = std::tie(b, a);

    уау.


    Специалисты в области теории типов неодобрительно качают головами, но я считаю

    Действительно, что там эти специалисты с их скучным ресёрчем.


    что один исходный тип данных, от которого производятся все остальные типы в языке ? это хорошая идея

    Это как раз относительно нормально, Top-тип есть почти во всех языках с сабтайпингом. Но проблема в том, что это не имеет никакого отношения к идее «всё есть словарь».


    обеспечивающая единообразность языка и простоту его использования.

    А это уже какие-то ничем не обоснованные утверждения.


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


    Ответ заключается в том, что простые типы в Python являются неизменяемыми: в них попросту не реализован тот метод, который отвечает за изменение их значения. Неизменяемые (иммутабельные) int, float, tuple или str обеспечивают в языках типа «всё является указателем» тот же семантический эффект, который в C обеспечивают автоматические переменные.

    Не понимаю, как это сочетать с примером выше, где


    >>> a = 3
    >>> a += 1
    >>> a
    4

    То есть, я примерно представляю, как надо описать семантику языка, чтобы оно так работало (а именно, a += 1 дешугарится в a = a + 1, и a после этого указывает уже на другой объект), но C++ снова работает не так. Особенно с плюсовыми ссылками.


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


    Что же нам делать со всеми теми правилами и концепциями, которые в C++ построены вокруг ключевых слов private и protected? Python, будучи скриптовым языком, не нуждается в них. У нас уже есть «защищённые» части языка ? это данные встроенных типов. Ни при каких условиях Python не позволит программе, например, манипулировать битами числа с плавающей запятой! Этого уровня инкапсуляции вполне достаточно, чтобы поддержать целостность самого языка. Мы, создатели Python, считаем, что целостность языка ? это единственный хороший предлог для сокрытия информации.

    Ну я даже не знаю, что на это сказать.


    1. BkmzSpb
      20.08.2019 21:10
      +1

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


      a = 1

      вы присваиваете a ссылку на ту самую (одну единственную на весь код) единицу.
      Следуя этой логике, b = a, b += 1 эквивалентно b = a, b = b + 1, где b + 1 возвращает ссылку уже на новую константу. Все сошлось.
      Вот эксперимент:


      >>> x = 4
      >>> y = 4
      >>> hex(id(x))
      '0x7fffe73693a0'
      >>> hex(id(y))
      '0x7fffe73693a0'
      >>> y += 1
      >>> hex(id(5))
      '0x7fffe73693c0'


      1. mkll
        22.08.2019 18:59

        Ага.

        >>> a = 1
        >>> hex(id(a))
        '0x10100ff30'
        >>> hex(id(1))
        '0x10100ff30'
        


      1. Tishka17
        23.08.2019 10:38

        Не все.


        >>> x=-1
        >>> id(x) == id(-1)
        True
        >>> x=123456
        >>> id(x) == id(123456)
        False


    1. AlexSky
      20.08.2019 22:23

      std::tie(a, b) = std::tie(b, a);

      Как по мне, так возможность написать a, b = b, a без загромождения кода и есть плюс. Собственно, автор и пишет, что в Питоне взяли лучшие практики и включили в синтаксис. Также он пишет про обратный перенос лучших практик, что мы и видим в вашем примере — практику из Питона реализовали в Плюсах, насколько это было возможно.


    1. bm13kk
      21.08.2019 10:00

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

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

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

      Никто у Вас не отбирает СИ. Никто не завставляет переходить на питон. Да эта статья реклама. Но все таки она от технаря. И он знает си. И можно получить удовольствие от сравнения одной технологии с другой. Сравнить что им не хватает, куда они движутся. А можно просто накидать говна от двух лагерей и получить по информативности срач на базаре. Лично мне от наличия «умных» слов в сраче — не легче.

      Предвидя буквоедство
      * На глаз. Может и треть, может и четверть.
      ** Почти любым. Но учитывая что виртуалка используется уже и в си (llvm), не знаю как нормальный программист может жать в мире… В худшем случае — не знать что происходит в лагере таких ненавистных врагов.


      1. 0xd34df00d
        21.08.2019 15:16

        Если бы я писал просто чтобы придраться, то мой комментарий был бы раз в 10 длиннее.


        Никто у Вас не отбирает СИ.

        Но я не программист на С.


        И он знает си.

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


        И можно получить удовольствие от сравнения одной технологии с другой. Сравнить что им не хватает, куда они движутся.

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


        Но учитывая что виртуалка используется уже и в си (llvm)

        Лол, нет. LLVM не имеет никакого отношения к виртуалкам в стиле питона или, не знаю, JVM.


        1. bm13kk
          22.08.2019 06:11

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

          мой новый проект с сегодняшнего дня — питон и раст. Хранилище данных HPC. Какие плюсы минусы?

          > Лол, нет.
          > LLVM не… виртуалкам в стиле питона

          Так таки есть? Чем она отличается от виртуалки питона или JVM? Кроме того, что ее нет в скомпилированном коде.


          1. 0xd34df00d
            22.08.2019 17:10

            мой новый проект с сегодняшнего дня — питон и раст. Хранилище данных HPC. Какие плюсы минусы?

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


            mypy хоть гоняете по питону?


            Так таки есть?

            Настолько, насколько IR компилятора является виртуальной машиной. Но тогда почему вы берёте LLVM, почему бы не рассмотреть, например, гццшный GIMPLE? Это тоже виртуалка?


            Чем она отличается от виртуалки питона или JVM? Кроме того, что ее нет в скомпилированном коде.

            А этого недостаточно? Генерируется нативный код под конкретную машину, вот и всё.


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


    1. Tishka17
      21.08.2019 22:09

      Оператор += на самом деле не эквивалентен сложению и последующему присвоению. На самом деле это __iadd__ и присвоение результата. Для изменяемых типов как правило iadd возвращает тот же объект на котором вызван, для неизменяемых — новый. Из-за этого поведения бывают казусы, как например изменение элемента (изменяемого типа) тупла по индексу.


  1. MooNDeaR
    20.08.2019 19:45

    А доступная с C++14 конструкция типа

    Век живи, век учись :) Даже не знал, что так можно и никогда не видел. Хотя откровенно говоря так себе затея так писать, как по мне, но где-то может быть полезно. Хотя уже давно есть structure bindings, так что можно вообще почти как на питоне написать :)


    1. 0xd34df00d
      20.08.2019 19:52

      Там весь расчёт на то, что можно написать auto, и компилятор выведет тип сам (и это будет типом анонимной структуры, объявленной внутри функции). Поэтому и C++14.


      Я этим пользуюсь, когда мне нужно вернуть одноразовый тип из функции, который нигде больше не упоминается (и которому поэтому не нужно имя). Например, insert у всяких std::map — хороший кандидат.


      А structured bindings заставляют вас помнить, на какой позиции какой элемент кортежа.


      1. MooNDeaR
        20.08.2019 19:54

        А подход с анонимным типом — заглядывать внутрь реализации функции, чтобы понять что же там возвращается и какие у этого типы) Так себе идея)


        1. 0xd34df00d
          20.08.2019 19:55

          Мне IDE подскажет, какие там поля, даже заглядывать не нужно.


          Ну или документация.


          1. sshikov
            20.08.2019 21:04

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


            1. 0xd34df00d
              20.08.2019 21:15

              Но там именно что не кортеж, а анонимная структура со вполне себе именованными полями.


              ЧСХ разные структуры из разных функций даже с одинаковыми именами полей — разные.


              1. sshikov
                20.08.2019 21:30

                Сорри, я тогда просто не уловил из переписки, о чем речь шла.


          1. MooNDeaR
            20.08.2019 21:16

            Не все IDE хорошо работают с шаблонами и auto. К сожалению, мне тяжело привести пример, т.к. на мелких примерах IDE работают хорошо, но как только кодовая база переваливает за 100 файлов с 150к+ строк кода, начинается веселье :) Хотя конечно принципиально мне пофигу, любой вариант приемлем.


          1. skymorp
            21.08.2019 08:21

            Смелое утверждение.

            ИМХО: предпочтительней рассчитывать на то, что доки/ide не будет (и вообще, код будет поддерживать маньяк).


            1. 0xd34df00d
              21.08.2019 15:12

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


  1. Alexey_Alive
    21.08.2019 16:26

    Всё это, конечное, хорошо, но python намного медленнее C++, а значит не может претендовать на нишу C++, а значит и предельным случаем быть не может. Сейчас, вроде, один более или менее популярный язык, который пытается и исправить недочёты c/c++, и работать не медленнее: Rust.