Брендон Роудс ? весьма скромный человек, представляющий себя в твиттере как «Python-программиста, возвращающего долг сообществу в форме докладов или эссе». Число этих «докладов и эссе» впечатляет, равно как и число свободных проектов, контрибьютором которых Брендон являлся или является. А ещё Брэндон опубликовал две книги и пишет третью.
Я очень часто встречаю в комментариях на Хабре принципиальное непонимание или неприятие динамических языков, динамической типизации, обобщённого программирования и других парадигм. Я публикую этот авторизованный (сокращённый) перевод (стенограмму) одного из докладов Брендона в надежде, что он поможет программистам, существующим в парадигмах статических языков, лучше понять динамические языки, в частности, Python.
Как у нас принято, прошу сообщать в личку о допущенных мной ошибках и опечатках.
Что означает словосочетание «предельный случай» в названии моего доклада? Предельный случай возникает, когда вы перебираете последовательность опций, пока не дойдёте до крайнего значения. Например, n-сторонний многоугольник. Если n=3, то это треугольник, n=4 ? четырёхугольник, n=5 ? пятиугольник, и т. д. По мере приближения n к бесконечности стороны становятся всё меньше и всё многочисленнее, и очертание многоугольника становится похоже на окружность. Таким образом, окружность является предельным случаем для правильных многоугольников. Вот что происходит, когда некая идея доводится до предела.
Я хочу поговорить о Python как о предельном случае для C++. Если вы возьмёте все хорошие идеи из C++ и очистите их, доведя до логического завершения, я уверен, в результате вы придёте к Python так же естественно, как серия многоугольников приходит к окружности.
«Непрофильные активы»
Я заинтересовался Python в 90-х: это был такой период в моей жизни, когда я избавлялся от «непрофильных активов», как я это называю. Многие вещи начали меня утомлять. Прерывания, например. Помните, когда-то на многих компьютерных платах были такие контакты с перемычками? И вы выставляли эти перемычки по мануалам, чтобы видеокарта получала более приоритетное прерывание, чтобы ваша игра работала быстрее? Так вот, мне надоело распределять и освобождать память с помощью malloc()
и free()
примерно тогда же, когда я перестал настраивать производительность своего компьютера перемычками. Был 1997 год или около того.
Я имею в виду, что, когда мы изучаем какой-то процесс, мы обычно стремимся получить полный контроль над ним, иметь под руками все возможные рычажки и кнопки. Потом некоторые люди так и остаются заворожены этой возможностью контроля. Но мой характер таков, что я, как только освоюсь с управлением и пойму, что к чему, сразу начинаю искать возможность сложить с себя часть полномочий, передать рычажки и кнопки какой-нибудь машине, чтобы она назначала прерывания за меня.
Поэтому в конце 90-х я искал такой язык программирования, который позволит мне заняться предметной областью и моделированием задачи, а не беспокойством о том, в какой области памяти компьютера хранятся мои данные. Как мы можем упростить C++, не повторяя грехи известных скриптовых языков?
Например, я так и не смог использовать Perl, и знаете, почему? Этот знак доллара! Он сразу давал понять, что создатель Perl не понимал, как работают языки программирования. Вы используете доллар в Bash, чтобы отделить имена переменных от остального содержимого строки, потому что программа на Bash состоит из буквально воспринимаемых команд и их параметров. Но после того, как вы познакомитесь с настоящими языками программирования, в которых строки размещаются между парами маленьких символов, называемых кавычками, а не по всему тексту программы, вы начинаете воспринимать $
как визуальный мусор. Знак долллара бесполезен, он уродлив, он должен уйти! Если вы хотите спроектировать язык для серьёзного программирования, вы не должны использовать специальные символы для обозначения переменных.
Синтаксис
Как же быть с синтаксисом? Возьмём за основу C! Это неплохо работает. Пусть присваивание обозначается знаком равенства. Такое обозначение принято не во всех языках, но, так или иначе, многие к нему привыкли. Но давайте не будем делать присваивание выражением. Пользователями нашего языка будут не только профессиональные программисты, но и школьники, учёные или дата-саентисты (если вы не в курсе, какая из этих категорий пользователей пишет худший код, то я намекну ? это не школьники). Не дадим пользователям возможность изменять состояние переменных в неожиданных местах, и сделаем присваивание оператором.
Что же тогда использовать для обозначения равенства, если знак равенства уже использован для присваивания? Конечно же, двойное присваивание, как это сделано в C! Многие уже привыкли к этому. Также мы позаимствуем из C обозначения всех арифметических и побитовых операций, потому что эти обозначения работают, и многие ими вполне довольны.
Разумеется, кое-что мы можем и улучшить. О чём вы думаете, когда видите в тексте программы знак процента? О строковой интерполяции, конечно! Хотя %
? это прежде всего оператор взятия модуля, просто для строк он оставался не определён. А раз так, то почему бы не переиспользовать его?
Численные и строковые литералы, управляющие последовательности с обратными слэшами ? всё это будет выглядеть, как в C.
Управление потоком исполнения? Те же if
, else
, while
, break
и continue
. Конечно, мы добавим немного фана, кооптировав старый добрый for
для итерирования по структурам данных и диапазонам значений. Позже это будет предложено в C++11, но в Python оператор for
изначально инкапсулировал все операции по вычислению размеров, обходу ссылок, инкрементированию счётчика и т. д., иначе говоря, делал всё, что нужно, чтобы предоставить пользователю элемент структуры данных. Структуры какого типа? Это неважно, просто передайте её for
, он разберётся.
Мы также позаимствуем у C++ исключения, но сделаем их настолько дешёвыми в плане потребления ресурсов, что их можно будет использовать не только для обработки ошибок, но и для управления потоком исполнения. Мы сделаем индексирование интереснее, добавив слайсинг ? возможность индексировать не только отдельные элементы последовательных структур данных, но и их диапазоны.
Ах, да! Мы исправим изначальный недостаток дизайна C ? добавим висящую запятую!
Эта история началась с Pascal ? ужасного языка, в котором точка с запятой используется в качестве разделителя выражений. Это означает, что пользователь должен ставить точку с запятой в конце каждого выражения в блоке, кроме последнего. Поэтому каждый раз, когда вы меняете порядок выражений в программе на Pascal, вы рискуете получить синтаксическую ошибку, если не проследите за тем, чтобы убрать точку с запятой с последней строки, и добавить её в конец той строки, которая раньше была последней.
If (n = 0) then begin
writeln('N is now zero');
func := 1
end
Керниган и Ритчи поступили правильнее, когда определили точку с запятой в C как терминатор выражения, а не разделитель, создав ту замечательную симметрию, когда каждая строка в программе, включая последнюю, заканчивается одинаково, и их можно свободно менять местами. К сожалению, в дальнейшем чувство гармонии им изменило, и они сделали запятую разделителем в статических инициализаторах. Это выглядит нормально, когда выражение умещается в одной строке:
int a[] = {4, 5, 6};
но когда ваш инициализатор становится длиннее, и вы компонуете его вертикально, вы получаете ту же неудобную асимметрию, что и в Pascal:
int a[] = {
4,
5,
6
};
Python на раннем этапе своего развития сделал висящую запятую в структурах данных полностью опциональной, независимо от того, как располагаются элементы этой структуры: горизонтально или вертикально. Кстати, это очень удобно для автогенерации кода: вам не нужно обрабатывать последний элемент как особый случай.
Позже стандарты С99 и С++11 так же исправили первоначальное недоразумение, позволив добавлять запятую после последнего литерала в инициализаторе.
Пространства имён
Ещё мы должны реализовать в своём языке программирования такую вещь, как пространства имён или неймспейсы (namespaces). Это критичная часть языка, которая должна избавить нас от ошибок вроде конфликтов имён. Мы поступим проще, чем C++: вместо того, чтобы дать пользователю возможность произвольно именовать неймспейсы, мы создадим по одному неймспейсу на модуль (файл) и обозначим их именами файлов. Например, если вы создадите модуль foo.py
, ему будет присвоен неймспейс foo
.
Для работы с такой упрощённой моделью пространств имён пользователю достаточно лишь одного оператора.
Создадим каталог my_package
, поместим туда файл my_module.py
, а в файле объявим класс:
class C(object):
READ = 1
WRITE = 2
тогда доступ к атрибутам класса будет осуществляться так:
import my_package.my_module
my_package.my_module.C.READ
Не беспокойтесь, мы не заставим пользователя каждый раз печатать полное имя. Мы дадим ему возможность использовать несколько версий оператора import
, чтобы варьировать степень «близости» неймспейсов:
import my_package.my_module
my_package.my_module.C.READ
from my_package import my_module
my_module.C.READ
from my_package.my_module import C
C.READ
Таким образом, одинаковые имена, заданные в разных пакетах, никогда не будут конфликтовать:
import json
j = json.load(file)
import pickle
p = pickle.load(file)
Тот факт, что каждый модуль имеет собственное пространство имён, означает также, что модификатор static
нам не нужен. Мы, однако, вспомним об одной функции, которую выполнял static
? инкапсуляции внутренних переменных. Чтобы показать коллегам, что данное имя (переменная, класс или модуль) не является публичным, мы начнём его с символа подчёркивания, например, _ignore_this
. Это также может являться сигналом для IDE, чтобы не использовать данное имя в автодополнении.
Перегрузка функций
Мы не будем реализовывать в нашем языке перегрузку функций. Механизм перегрузки слишком сложен. Вместо этого, мы применим опциональные аргументы с дефолтными значениями, которые можно не задавать при вызове, а также именованные аргументы, чтобы «перепрыгнуть» через опциональные аргументы с годными дефолтами, и задать только те значения, которые отличаются от дефолтных. Что немаловажно, отсутствие перегрузки избавит нас от необходимости определять, какая функция из набора перегруженных функций была только что вызвана, как сработал диспетчер вызовов: функция всегда одна в данном модуле, её легко найти по имени.
Системные API
Мы дадим пользователю полный доступ ко многим системным API, включая сокеты. Я не понимаю, почему авторы скриптовых языков всегда предлагают собственные хитроумные способы для того, чтобы открыть сокет. При этом они никогда не реализуют возможности Unix Socket API полностью. Они реализуют 5-6 функций, которые они понимают, и выбрасывают всё остальное. Python, в отличие от них, имеет стандартные модули для взаимодействия с ОС, реализующие каждый стандартный системный вызов. Это значит, что вы можете прямо сейчас открыть книгу Стивенса и начать писать код. И все ваши сокеты, процессы и форки будут работать именно так, как там написано. Да, возможно, Гвидо или ранние контрибьюторы Python сделали всё именно так, потому что им было лень писать свою имплементацию системных библиотек, лень заново объяснять пользователям, как работают сокеты. Но в результате они добились замечательного эффекта: вы можете перенести все ваши знания UNIX, полученные в С и C++, в среду Python.
Итак, мы определились с тем, какие фичи мы «займём» у C++ для создания нашего простого скриптового языка. Теперь надо определиться с тем, что мы хотим исправить.
Undefined behavior
Неизвестное поведение, неопределённое поведение, поведение, определяемое реализацией… Это всё ? плохие идеи для языка, которым будут пользоваться школьники, учёные и дата-саентисты. Да и выигрыш в производительности, ради которого допускаются такие вещи, зачастую ничтожен по сравнению с неудобствами. Вместо этого мы объявим, что любая синтаксически верная программа даёт одинаковый результат на любой платформе. Мы будем описывать стандарт языка такими фразами, как «Python вычисляет все выражения слева направо» вместо того, чтобы пытаться переупорядочивать вычисления в зависимости от процессора, ОС или фазы луны. Если пользователь уверен, что порядок вычислений важен, он вправе должным образом переписать код: в конце концов, пользователь здесь главный.
Приоритеты операций
Вы, наверное, сталкивались с подобными ошибками: выражение
oflags & 0x80 == nflags & 0x80
всегда возвращает 0, потому что сравнение в C имеет более высокий приоритет, чем побитовые операции. Иначе говоря, это выражение вычисляется как
oflags & (0x80 == nflags) & 0x80
Ох уж этот C!
Мы уничтожим потенциальную причину подобных ошибок в нашем простом скриптовом языке, поставив приоритет операций сравнения позади арифметики и манипуляций битами, чтобы выражение из нашего примера вычислялось более интуитивно:
(oflags & 0x80) == (nflags & 0x80)
Другие улучшения
Читабельность кода важна для нас. Если арифметические операции языка C знакомы пользователю ещё по школьной арифметике, то путаница между логическими и побитовыми операциями ? явный источник ошибок. Мы заменим двойной амперсанд на слово and
, а двойную вертикальную черту ? на слово or
, чтобы наш язык больше походил на человеческую речь, чем на частокол «компьютерных» символов.
Мы оставим нашим логическим операторам возможность сокращённого вычисления (https://en.wikipedia.org/wiki/Short-circuit_evaluation), но также наделим их способностью возвращать финальное значение любого типа, а не только булевого. Тогда станут возможны выражения наподобие
s = error.message or 'Error'
В этом примере переменной будет присвоено значение error.message
, если оно непустое, в противном случае ? строка 'Error'.
Мы расширим идею C о том, что 0 эквивалентен false, на другие объекты, помимо целых. Например, на пустые строки и контейнеры.
Мы уничтожим целочисленное переполнение. Наш язык будет последовательным в реализации и простым в использовании, так что нашим пользователям не нужно будет запоминать особое значение, подозрительно близкое к двум миллиардам, после которого целое, увеличенное на единицу, внезапно меняет знак. Мы реализуем такие целые числа, которые будут вести себя как целые числа до тех пор, пока не исчерпают всю доступную память.
Строгая vs нестрогая типизация
Ещё один важный вопрос в дизайне скриптового языка: строгость типизации. Многие в аудитории знакомы с JavaScript? Что будет, если число 3 отнять от строки '4'?
js> '4' - 3
1
Отлично! А если к строке '4' прибавить число 3?
js> '4' + 3
"43"
Это называют нестрогой (или слабой) типизацией. Это что-то вроде комплекса неполноценности, когда язык программирования думает, что программист осудит его, если он не сможет вернуть результат любого, даже заведомо бессмысленного, выражения, путём многократного приведения типов. Проблема в том, что приведение типов, которое слабо типизированный язык производит автоматически, очень редко приводит к осмысленному результату. Попробуем чуть более сложные преобразования:
js> [] + []
""
js> [] + {}
"[object Object]"
Мы рассчитываем, что операция сложения коммутативна, но что будет, если поменять слагаемые местами в последнем случае?
js> {} + []
0
JavaScript не одинок в своих проблемах. Perl в аналогичной ситуации также пытается вернуть хоть что-то:
perl> "3" + 1
4
И awk предпримет что-то в этом духе:
$ echo | awk '{print "3" + 1}'
4
Создатели скриптовых языков традиционно считали, что нестрогая типизация удобна. Они заблуждались: нестрогая типизация ужасна! Она нарушает принцип локальности. Если в коде есть ошибка, то язык программирования должен сообщить пользователю о ней, вызвав исключение как можно ближе к проблемному месту в коде. Но во всех этих языках, которые бесконечно приводят типы, пока не получится хоть что-то, управление обычно доходит до конца, и мы получаем результат, судя по которому, в нашей программе где-то что-то не так. И нам приходится отлаживать всю нашу программу, одну строку за другой, чтобы найти эту ошибку.
Нестрогая типизация также ухудшает читабельность кода, потому что, даже если мы правильно используем неявное приведение типов в программе, для другого программиста это происходит неожиданно.
В Python, как и в C++, подобные выражения вернут ошибку.
>>> '4' - 3
TypeError
>>> '4' + 3
TypeError
Потому что приведение типов, если оно действительно необходимо, несложно написать в явном виде:
>>> int('4') + 3
7
>>> '4' + str(3)
'43'
Этот код легко читается и поддерживается, он ясно даёт понять, что именно происходит в программе, что приводит к данному результату. Это потому что Python-программисты полагают, что явное лучше неявного, и ошибка не должна оставаться незамеченной.
Python является строго типизированным языком, и единственное неявное приведение типов в нём происходит при арифметических операциях над целыми числами, результат которых должен быть выражен дробным числом. Возможно, этого тоже не стоит допускать в программе, но в таком случае слишком многим пользователям пришлось бы сходу объяснять разницу между целыми числами и числами с плавающей запятой, что усложнило бы их первые шаги в Python.
Продолжение: «Python как предельный случай C++. Часть 2/2».
Комментарии (21)
mrFieldy
20.08.2019 18:31+1Можете меня минусовать, но это эта статья лютейший высер. Автор совершенно не знает С++ (разве только что его синтаксис) и не понимает, как им пользоваться.
BoogieMan75
20.08.2019 18:36поддерживаю. автор с перемычками тоже нормально гонит.
sshikov
20.08.2019 20:59+2Да он и перл в общем-то похоже ниасилил…
BoogieMan75
20.08.2019 21:30+1Еще хуже, имхо. Он не понял, для кого и зачем Перловку делали.
В целом «с боку надпись кирпичом...»sshikov
20.08.2019 21:32+1Ну где-то да. Человек, который начинает (и заканчивает) обсуждать перл с имен переменных (и придирается к доллару) — нихрена в перле не понял, скорее всего.
hokeroid
20.08.2019 21:02'4' — 3 в c++ ни коим образом не вернет ошибку, а вычтет из номера символа 4 в ASCII тройку. Получится, кстати, символ единицы
KanuTaH
21.08.2019 01:31Ну это в C++ одинарные кавычки обрамляют символ, а двойные — строку. В Питоне одинарные кавычки обрамляют именно строку, а типа аналогичного char ЕМНИП там вообще нет, есть только строки длиной в 1 символ, так что здесь речь идет все же о попытке «вычитания» из строки.
Scrayer
21.08.2019 08:28Я слабо разбираюсь в плюсах и еще меньше в питоне, но что-то мне подсказывает, что синтаксис вещь весьма незначительная, к тому же, при взгляде на питон порой невозможно понять что там происходит.
Считаю, что как раз синтаксис С/С++/С# это то, за что нельзя его ругать, потому что он «классический»/«привычный»/«одинаковый», а питон в этом плане «новатор» (но не первопроходец), код на нем для меня выглядит так же дико, как все эти «begin»/«end»/":="/«комментарии процентом» в паскалях и некоторых других языках.
sami777
21.08.2019 11:48Позабавила статья. Особенно про приоритеты операций.
А еще, что мне даст замена двойного амперсанда на «and» если я все равно не помню, где логическая, а где побитовая операция.
valis
21.08.2019 14:43Хотя сам пишу на Python и он устраивает меня на все 100%, со статьей во многом не соглашусь.
Разводить холливар, придираясь к каждой строке статьи не буду, скажу только такую философскую мысль:
— Многограннику никогда не стать кругом. Даже если он из дали начинает напоминать круг он все равно сложный многогранник.
Более того — не стоит из многогранника делать подобие круга. Хотя это сейчас болезнь многих языков программирования, разве что, кроме неизменной красоты минимализма LUA (во завернул :-)). Все равно делать этого не стоит.Satim
21.08.2019 15:37Строго говоря круга как такого не существует)
Есть многоугольник с бесконечным кол-вом углов.
{зануда_mode=on}
В вашем случае еще и не круг, а шар.
{зануда_mode=off}PiaFraus
21.08.2019 21:41+1> Строго говоря круга как такого не существует
> Есть многоугольник с бесконечным кол-вом углов.
Довольно сильные утверждения. Это почему?
Круг определяется как часть плоскости ограниченной окружностью (множеством точек равноудалённых от центра). Многоугольник же — ломаной. Не важно сколько у этой ломаной вершин, в любом случае по опеределению будут соседние вершины, между которыми ломаная — прямая. На этой прямой только граничные точки будут удоволетворять определению окружности, остальные точки не будут. Значит ломаная не является окружностью. И как следствие многоугольник не является кругом.
Izaron
21.08.2019 22:11Даже больше — множество точек окружности имеет мощность континуум, а множество вершин многоугольника не более чем счётное и при всём желании не будет равномощно первому множеству, так что можно утверждать, что аффтар налажал даже в метафоре.
0xd34df00d
Вроде же ещё не пятница.
Ни разу такой ассоциации не было.
Типа, исключения в питоне быстрее, чем в C++? Или просто сам питон настолько медленный, что относительная скорость исключений не так выделяется?
Ещё можно goto добавить, чтобы рассуждать о потоке исполнения было весело вдвойне.
Конечно сложен, если нет статических типов.
А если они есть, то в чём сложность-то?
Почему с этим нет проблем в C++?
А, ну да, статические типы…
У питона есть стандарт? Или хотя бы language report?
Что значит «непустое»?
""
?None
?В C++ подобные выражения не вернут ошибку (и даже не кинут экзепшон) потому, что они не скомпилируются.
Chaos_Optima
Ну вообще конкретно тот пример что он привёл, скомпилится без всяких ошибок
0xd34df00d
Только у него же там это строки, а не
char
ы. Кstd::string
троечку прибавить уже не получится.Deosis
Пока кто-нибудь не перегрузит оператор сложения.
svr_91
К std::string нельзя, к const char* — можно
Сколько раз об это обжигался…
iroln
А всё же жаль, что в Python не завезли множественную диспетчеризацию (multiple dispatch) на уровне языка.
Izaron
Скорее перед нами очередной фантазер или любитель топить печку канадскими долларами. А давайте еще сделаем LOCK TABLE таким дешевым, что все разговоры о уровнях изоляции в СУБД пойдут лесом. А давайте еще сделаем CPU таким быстрым, чтобы код могла написать баба Зина кактусом по клавиатуре, не задумываясь о различиях между TreeSet и HashSet.