Хабрахабр

[Перевод] Python как предельный случай C++. Часть 1/2

От переводчика

Число этих «докладов и эссе» впечатляет, равно как и число свободных проектов, контрибьютором которых Брендон являлся или является. Брендон Роудс − весьма скромный человек, представляющий себя в твиттере как «Python-программиста, возвращающего долг сообществу в форме докладов или эссе». А ещё Брэндон опубликовал две книги и пишет третью.

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

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

Предельный случай возникает, когда вы перебираете последовательность опций, пока не дойдёте до крайнего значения. Что означает словосочетание «предельный случай» в названии моего доклада? Если n=3, то это треугольник, n=4 − четырёхугольник, n=5 − пятиугольник, и т. Например, n-сторонний многоугольник. По мере приближения n к бесконечности стороны становятся всё меньше и всё многочисленнее, и очертание многоугольника становится похоже на окружность. д. Вот что происходит, когда некая идея доводится до предела. Таким образом, окружность является предельным случаем для правильных многоугольников.

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

Многие вещи начали меня утомлять. Я заинтересовался Python в 90-х: это был такой период в моей жизни, когда я избавлялся от «непрофильных активов», как я это называю. Помните, когда-то на многих компьютерных платах были такие контакты с перемычками? Прерывания, например. Так вот, мне надоело распределять и освобождать память с помощью malloc() и free() примерно тогда же, когда я перестал настраивать производительность своего компьютера перемычками. И вы выставляли эти перемычки по мануалам, чтобы видеокарта получала более приоритетное прерывание, чтобы ваша игра работала быстрее? Был 1997 год или около того.

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

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

Этот знак доллара! Например, я так и не смог использовать Perl, и знаете, почему? Вы используете доллар в Bash, чтобы отделить имена переменных от остального содержимого строки, потому что программа на Bash состоит из буквально воспринимаемых команд и их параметров. Он сразу давал понять, что создатель Perl не понимал, как работают языки программирования. Знак долллара бесполезен, он уродлив, он должен уйти! Но после того, как вы познакомитесь с настоящими языками программирования, в которых строки размещаются между парами маленьких символов, называемых кавычками, а не по всему тексту программы, вы начинаете воспринимать $ как визуальный мусор. Если вы хотите спроектировать язык для серьёзного программирования, вы не должны использовать специальные символы для обозначения переменных.

Возьмём за основу C! Как же быть с синтаксисом? Пусть присваивание обозначается знаком равенства. Это неплохо работает. Но давайте не будем делать присваивание выражением. Такое обозначение принято не во всех языках, но, так или иначе, многие к нему привыкли. Не дадим пользователям возможность изменять состояние переменных в неожиданных местах, и сделаем присваивание оператором. Пользователями нашего языка будут не только профессиональные программисты, но и школьники, учёные или дата-саентисты (если вы не в курсе, какая из этих категорий пользователей пишет худший код, то я намекну − это не школьники).

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

О чём вы думаете, когда видите в тексте программы знак процента? Разумеется, кое-что мы можем и улучшить. Хотя % − это прежде всего оператор взятия модуля, просто для строк он оставался не определён. О строковой интерполяции, конечно! А раз так, то почему бы не переиспользовать его?

Численные и строковые литералы, управляющие последовательности с обратными слэшами − всё это будет выглядеть, как в C.

Те же if, else, while, break и continue. Управление потоком исполнения? Позже это будет предложено в C++11, но в Python оператор for изначально инкапсулировал все операции по вычислению размеров, обходу ссылок, инкрементированию счётчика и т. Конечно, мы добавим немного фана, кооптировав старый добрый for для итерирования по структурам данных и диапазонам значений. Структуры какого типа? д., иначе говоря, делал всё, что нужно, чтобы предоставить пользователю элемент структуры данных. Это неважно, просто передайте её for, он разберётся.

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

Мы исправим изначальный недостаток дизайна C − добавим висящую запятую! Ах, да!

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

If (n = 0) then begin writeln('N is now zero'); func := 1
end

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

int a[] = ;

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

int a[] = { 4, 5, 6
};

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

Позже стандарты С99 и С++11 так же исправили первоначальное недоразумение, позволив добавлять запятую после последнего литерала в инициализаторе.

Это критичная часть языка, которая должна избавить нас от ошибок вроде конфликтов имён. Ещё мы должны реализовать в своём языке программирования такую вещь, как пространства имён или неймспейсы (namespaces). Например, если вы создадите модуль foo.py, ему будет присвоен неймспейс foo. Мы поступим проще, чем C++: вместо того, чтобы дать пользователю возможность произвольно именовать неймспейсы, мы создадим по одному неймспейсу на модуль (файл) и обозначим их именами файлов.

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

Создадим каталог 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 нам не нужен. Это также может являться сигналом для IDE, чтобы не использовать данное имя в автодополнении. Чтобы показать коллегам, что данное имя (переменная, класс или модуль) не является публичным, мы начнём его с символа подчёркивания, например, _ignore_this.

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

Я не понимаю, почему авторы скриптовых языков всегда предлагают собственные хитроумные способы для того, чтобы открыть сокет. Мы дадим пользователю полный доступ ко многим системным API, включая сокеты. Они реализуют 5-6 функций, которые они понимают, и выбрасывают всё остальное. При этом они никогда не реализуют возможности Unix Socket API полностью. Это значит, что вы можете прямо сейчас открыть книгу Стивенса и начать писать код. Python, в отличие от них, имеет стандартные модули для взаимодействия с ОС, реализующие каждый стандартный системный вызов. Да, возможно, Гвидо или ранние контрибьюторы Python сделали всё именно так, потому что им было лень писать свою имплементацию системных библиотек, лень заново объяснять пользователям, как работают сокеты. И все ваши сокеты, процессы и форки будут работать именно так, как там написано. Но в результате они добились замечательного эффекта: вы можете перенести все ваши знания UNIX, полученные в С и C++, в среду Python.

Теперь надо определиться с тем, что мы хотим исправить. Итак, мы определились с тем, какие фичи мы «займём» у C++ для создания нашего простого скриптового языка.

Да и выигрыш в производительности, ради которого допускаются такие вещи, зачастую ничтожен по сравнению с неудобствами. Неизвестное поведение, неопределённое поведение, поведение, определяемое реализацией… Это всё − плохие идеи для языка, которым будут пользоваться школьники, учёные и дата-саентисты. Мы будем описывать стандарт языка такими фразами, как «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, на другие объекты, помимо целых.

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

Многие в аудитории знакомы с JavaScript? Ещё один важный вопрос в дизайне скриптового языка: строгость типизации. Что будет, если число 3 отнять от строки '4'?

js> '4' - 3
1

А если к строке '4' прибавить число 3? Отлично!

js> '4' + 3 "43"

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

js> [] + [] "" js> [] + {} "[object Object]"

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

js> {} + []
0

Perl в аналогичной ситуации также пытается вернуть хоть что-то: JavaScript не одинок в своих проблемах.

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 является строго типизированным языком, и единственное неявное приведение типов в нём происходит при арифметических операциях над целыми числами, результат которых должен быть выражен дробным числом.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть