База по языкам программирования: Синтаксический сахар или история развития языков
Продолжаю выкладывать выдержки из вводного курса нашей компании по промышленному программированию.
Часть третья: Синтаксический сахар или история развития языков
В данной части расказывается история развития языков программирования, а так же доступно объясняется что такое ООП и функциональное программирование. Другие части можно найти тут.
Синтаксический сахар (syntactic sugar) — общее обозначение дополнений к синтаксису ЯП, которые делают использование языка более удобным, но не добавляют ему новых возможностей.
Вся история развития ЯП — это история повышения сладости синтаксического сахара.
Машинные языки
Всё началось с машинно-зависимых языков — языков, учитывающий структуру и характеристики определённых компьютерных платформ. Те, кто программировал на калькуляторах помнят, как составлялись на них программы.
Десяток регистров, куда записывались результаты вычислений (где они, эти гигабайты оперативки?), пара регистров смещения (вспомните машину Тьюринга, да-да, регистры обозначали из какого регистра данных брать следующую команду!), и регистр команды, куда нужно было записать очередную операцию (прочитать значение, записать значение, сложить значение двух регистров памяти и т.д.).
Архитектура фон Неймана
Даже архитектура этих устройств не всегда соответствовала архитектуре фон Неймана — стандартной для современных компьютеров.
Собственно, архитектура фон Неймана подразумевает отделение памяти от процессора и хранение в памяти изменяемых программ. Калькуляторы же обычно являлись устройствами с фиксированным набором выполняемых программ.
Собственно, переход к архитектуре фон Неймана породил возможность задавать автоматически выполняющиеся программы из внешнего источника — поначалу из перфолент и перфокарт.
Люди так и программировали, пробивая отверстие в карточке, соответствующее определённому регистру, таким образом, побитово задавая значения в них. Много историй связано с тем, как программа, набитая на сотнях перфокарт в буквальном смысле этого слова рассыпалась, когда неуклюжие техники роняли эти стопки картона на пол.
Ассемблер
На машинных кодах программировать было не очень удобно, по этому при первой же возможности появился ассемблер — язык, повторяющий машинные операции, но с человекопонятными командами и возможностью ручкой по бумаге описать алгоритм не как набор битов, а как какой-то более осмысленный текст.
Ассемблер так же привязан к архитектуре машины (поскольку его команды повторяют команды процессора), но шаг в пропасть был уже сделан и языки начинали всё больше и больше обрастать кристаллами сахара.
Стековые языки
Первой ласточкой стало использование стеков данных. Стек появился для решения задачи временного хранения произвольных данных. Конечно, данные можно сохранять и в регистре, однако в этом случае нужно помнить имя каждого регистра, данные из которого хочется получить.
Характерностью стека является особый порядок получения из него данных: в любой момент времени в стеке доступен только верхний элемент, т.е. элемент, загруженный в стек последним. Выгрузка из стека верхнего элемента делает доступным следующий элемент, по аналогии с автоматным рожком — первый засунутый туда патрон достать можно только последним.
Сейчас это может казаться дико неудобным, но это позволило создавать подпрограммы.
Перед вызовом подпрограммы, мы заполняем специально именованный стек данными. Подпрограмма, зная, в каком порядке помещены в стек параметры, может забрать их оттуда и использовать при своем выполнении, а по выполнении поместить результаты своего труда в тот же или в другой стек. Кроме того, основная программа имеет возможность сохранить свои данные в стеке до передачи управления подпрограмме. После возврата контроля программа просто восстанавливает свои значения из стека и не обращает внимание на то, что данные в регистрах процессора могли подпрограммой перетираться.
Макроассемблер
Следующим шагом был макроассемблер. Макроассемблер — это программа для макропроцессора, который в свою очередь являлся транслятором с языка более высокого уровня (макроассемблера) в машинный код. Стало возможным создавать свои команды для, например, использования стека.
Рождаются команды работы со стеком (push, pop), команды копирования стеков данных.
Макроассемблер порождает языки более высокого уровня, за командами которых стоят десятки, а то и сотни команд процессора. FORT, ALGOL, BASIC начинают свой путь…
Модульные языки
Вкусив запретного плода расширенного синтаксиса, программисты не остановились и возжелали модульности: ведь это так удобно — вызывать отдельно написанный модуль программы и не вникать в его алгоритм. Главное — это знать как он принимает на вход данные и как возвращает результат.
Ассемблер пополняется командами, облегчающими именование и подключение модулей, передачу и возврат управления при вызове различных подпрограмм. Развиваются интерфейсы обмена данными. Возникает понятие структуры данных.
Процедурные языки
Логичным добавлением к модульнуму языку послужило понятие процедуры или подпрограммы. Подпрограмма обладает двумя важными особенностями:
1. она именованна, т.е. мы можем вызвать подпрограмму по имени
2. вызвав подпрограмму, мы точно знаем, что она вернёт управление в то же место, откуда была вызвана
К примеру, в BASIC подпрограмма вызывалась как GOSUB :Label:.
Функция
Не хватало только одного: хотелось, чтобы переменные материнской программы (из которой вызывалась подпрограмма) не портились. А то ведь как оно было? Все переменные в глобальном пространстве, начнёшь их же использовать в подпрограмме — она их и затирает.
Так было изобретено понятие функции и локальных переменных: мы вызываем именованную подпрограмму и передаём туда какие-то значения. Подпрограмма воспринимает переданные значения, как локальные именованные переменные.
С развитием функция обросла возможностью возвращать результат: до этого, ведь, как было — возвращаемое значение записывали в одну из глобальных переменных.
Функция обладает следующими особенностями:
1. она именованна
2. туда передаются параметры
3. переданные параметры доступны как именованные параметры только внутри функции, вне функции они не видны
4. функция может использовать свои локальные именованные параметры, не видные вне этой функции
5. функция может возвращать результат работы
Введение в синтаксис функции гармонично дополняет процедурные языки программирования.
Функциональные языки
Естественным желанием было дополнить функции возможностью наблюдать при вызове локальные переменные функции-родителя в вызываемой функции.
Для решения этого гении сумеречного разума порождают понятие контекста исполнения: это область именованных переменных, доступных функции во время выполнения. Эта область данных делается наследовательно-расширяемой: при при вызове дочерней функции она создаёт свой контекст, пополняемый переменными, объявляемыми внутри функции-дочки. При этом вне функции-дочки эти переменные не видны. Зато будут доступны при вызове функции-внучки, функции-правнучки и так далее.
Возможность наследовать контекст исполнения называют замыканием (closure).
Возможность полноценной работы с контекстом исполнения порождает функциональные языки программирования.
Окончательно их оформляет добавление возможности передавать функцию как параметр для вызова другой функции, а так же возвращать функцию в качестве результата выполнения подпрограммы.
Типы данных
В то же самое время мысль программистов не стояла на месте. Программисты изобретали типы данных.
Первоначально, ведь как было, данные были доступны исключительно в бинарном виде — нолики, да единички.
Людям же, для решения практических задач, удобнее оперировать абстракциями более высоких уровней. Так появляются целочисленные типы данных без возможности указать являются ли они отрицательным или нет (byte, unsigned integer, unsigned long integer и т.д.).
Потом, как развитие их — типы данных с возможностью записи отрицательного числа (кодировавшегося первым битом, в связи с чем возникали забавные казусы неравенства +0 и -0). В довершении для более удобной работы с плавающей точкой возникли типы float и double float (как нетрудно догадаться, double float — это тот же float, но с возможностью записать больше знаков как до, так и после запятой).
Интересно байтовое представление типа float — в принципе, для того, чтобы передать число нам необходим тот же integer с возможностью указать отрицательное число и или нет и указанием, через сколько разрядов от начала числа необходимо поставить точку.
Для логических операций, в принципе, вполне хватало того же нуля и единички, но для большей кузявости их обернули в тип boolean с двумя значениями true и false (за которыми, в прочем, стояли те же единичка и нолик).
Следующим типом остро потребовавшийся программистам стал массив. Массив данных кардинально отличался от стека возможностью свободного доступа не только к последнему засунутому элементу, а вообще к любому по номеру. Массив представлялся программистам как склеенные ячейки, внутри которых лежат данные, по этому изначально массив задавался сразу определённого размера и размер этот изменить было невозможно.
Но, ведь, ячейки не обязательно бывают заполненными? Так потребовалось обозначение пустой ячейки и возникает тип null. По факту, вначале он представлял из себя символ с кодом 0x0, что приводило к весёлым казусам, когда в эту самую ячейку требовалось записать нулевое значение, а потом прочитать его и интерпретировать именно как null, а не как unsigned integer со значением 0.
Для объявления массива резервировался фрагмент памяти (буфер) с указанием, сколько ячеек будет расположено в этом фрагменте, а так же какие элементы будут в нём размещаться. И не дай Ричи тебе записать в массив типа int элемент вида long! В лучшем случае повреждались последующие за ним элементы, в худшем — возникало переполнение буфера и могли повредиться другие, не относящиеся к массиву данные, расположенные сразу же за выделенным буфером памяти.
Строки, кстати, вначале появились именно как массивы символов (пришлось ввести ещё один типа данных — char, по сути соответствовавший byte). Из-за этого длину строк приходилось объявлять заранее.
Дабы справиться со строками переменной длины, придумали помечать null-маркером конец строки. Т.е., как и ранее строка представляла из себя массив, но длина этого массива задавалась сразу достаточно большой, чтобы вместить любую строку (640кб памяти хватит любым программам, ага). Строка начиналась с начала массива, а конец её помечался как null-байт, то что шло после null строкой не считалось.
Хорошая на бумаге идея помечать конец строки null-маркером при ближайшем рассмотрении оказалось ужасной: ничто не мешало добавить в середину строки null и поиметь с этого кучу лулзов. Так началась эра C-strings.
Ссылки
Организация работы с данными, как с буфером памяти породило интересную возможность при вызове функции передавать туда не сами данные, а ссылку на них.
Ранее ведь как было? В функции передавались значения переменных, значения эти копировались в именованные переменные функции дабы избежать порчи оригинальных данных.
Но ведь внутрь функции можно передать просто значение адреса выделенного фрагмента памяти и далее зачитать из него переменную любого доступного типа данных! Так появился ещё один тип данных — ссылка.
Ссылка представляет из себя ярлычок (link) на какую-то переменную, под которую выделен блочок памяти. В ЯП появляются методы работы с переменными как по значению (напрямую с этим блоком памяти), так и по ссылке (считываем из переменной указатель, потом идём по нему и уже там меняем значение в памяти).
Структуры данных
Казалось бы, за что боролись на то и напоролись: изолировали-изолировали переменные внутри функций, чтобы их не портить, а теперь даём в руки ружьё из которого ещё и в ногу можно выстрелить!
Но не тут то было: передача переменных по ссылке дало уникальную возможность конструировать из простых типов данных целые конструкции — структуры данных!
К примеру, стало возможно организовать ссылку на массив из ссылок на массивы из… Так это же целое дерево построить можно!
Естественно, сам по себе такой массив никакой практической ценности не несёт, поскольку всё это можно организовать и с помощью простых типов данных, но если добавить в программу функции типа addNode, removeNode, работающие с деревом и передавать в эти функции ссылку на структуру данных, то получается рабочая и весьма соблазнительная конструкция.
Структурные языки
Так это же получается, что программист сам может создавать свои типа данных, удобных для его программы — достаточно только создать структуру данных и описать функции работы с ними!
Так появляются структурные языки программирования. В них немедленно добавляют возможность описания нового типа данных, возможность как-то именовать этот тип и задавать для него какие-то операции.
К примеру, строку можно представить уже не просто массивом, а двусвязанным списком с функциями конкатенации через оператор + и доступом к произвольному символу через оператор [].
Начинается немедленный бурный рост структурных языков (Pascal, C), обладающих следующими особенностями:
1. в них есть формальный язык описания структур данных (*.h файлы в C)
2. в них есть возможность дать описанной структуре название (BTree)
3. в них есть возможность обозначить операции работы с этой структурой данных
Объект
Возможность создавать свои собственные типы данных возбуждает в программистах страстное желание внутри этого типа данных иметь функции для работы с ним.
Как ответ на чаяния этих светлых умов рождается концепция Объекта. Объект — это уже не просто тип данных, не просто ссылка, по которой хранится структуированная информация, но ещё и функции по обработке этой информации, доступные по этой же ссылке.
Под это подводится вселенская философия:
“Объект — некоторая сущность в виртуальном пространстве, обладающая определённым состоянием и поведением, имеющая заданные значения свойств (атрибутов) и операций над ними (методов).”
Инкапсуляция
Глубокие философские исследования позволяют осознать, что объект обладает таким свойством, как инкапсуляция, которую определяют как свойство объекта объединять в себе данные и методы работы с этими данными. Философы вообще любят рекурсивные определения.
Суть инкапсуляции же проста: объект — не объект, если состояние его (т.е. данные, которые он содержит) можно поменять не применяя методов объекта. При этом считается, что публичные переменные объекта, доступные всем и вся для изменения — это как бы тоже методы по изменению внутреннего состояния этого самого объекта.
Собственно, $object->property = 12345; считается эквивалентным методу $object->setProperty(12345);, ведь без указания в операции имени объекта $object доступа к переменной $property напрямую не получить.
Наследование
Ещё до философов Объекта программисты при работе со структурами данных очень сильно захотели и придумали, как расширять структуры данных, наследовать схему структуры-родителя в структуре-детёныше.
Создание же Объекта, объединяющего данные с функциями породило интересную инженерную задачу, как бы так извернуться: и структуру унаследовать, и функции унаследовать, да ещё бы и новые возможности в наследника добавить.
А всё дело в том, что есть у тебя в Объекте-родителе функция, есть в Объекте-наследнике функция, делают они разное, а вот чтоб имена у них — опа — одинаковые. Решение этой задачи назвали полиморфизмом.
Полиморфизм
Философы и тут подсуетились, дав определение: “Полиморфизм — возможность объектов с одинаковой спецификацией иметь различную реализацию.”. Под спецификацией тут понимается названия-сигнатуры методов работы с объектом (включая публичные переменные).
Реализаций полиморфизма множество, вот некоторые из них:
— чистый полиморфизм (полиморфизм по сигнатуре)
— параметрический полиморфизм (полиморфизм по имени метода)
— переопределение (абстрагирование, абстрактные классы)
— перегрузка (неполное замещение метода предка методом потомка)
Абстрагирование
Философская мысль тоже не стояла на месте. Изучив свойства наследования, философы поняли, что его можно заменить абстрагированием.
Абстрагирование это такая вещь… Как бы объяснить? Вот есть у вас Объект — отлично, это что-то материальное. А ещё есть представление о том, каким этот объект может быть: какие методы он должен выставлять, что эти методы должны делать, но без конкретики, так, абстрактно (напоминает заказчиков, не правда ли?). Собственно мы только что описали интерфейс объекта или абстрактного предка заветы которого можно отлить в реальность кода.
Собственно, ООП — Объектно Ориентированное Программирование. Это умение некоторых программистов работать с объектами в рамках всех трёх концепций: инкапсуляции, наследования и полиморфизма. Ну или инкапсуляции, абстрагирования и полиморфизма.
Никакого отношения к модели MVC парадигма OOP не имеет (в отличие от мнения некоторых PHP программистов). ООП — это просто работа с данными и методами обработки данных, как с наследуемыми объектами.
В отличие от парадигмы процедурного и структурного программирования, где если и есть объекты, то они не наследуемы. Ну или и объектов-то нет, все данные передаются в массивах, структурах, выделенных буферах памяти.
Класс-ориентированное программирование
Объектное программирование требует создания множества объектов (как ни странно). Соответственно требуются как-то организовать иерархию объектов, как-то скучковать их.
В ответ на эти чаяния была разработана концепция класс-инстанс. Что такое класс? Класс — это набор методов и функций без данных. Сам по себе класс — это нечто нерабочее, для работы нужны данные. Собственно, для того чтобы получить рабочий объект необходимо инстанциировать класс — сказать “создай мне объект с теми функциями, что описанные в этом классе и данными, которые я тебе сейчас скажу”.
Фактически класс — это такой синтаксический кусище сахара, который позволяет не просто описать API объекта (как это делает интерфейс), но и задать функции по обработке данных.
Система классов позволяет формально описать свойства объекта, правила наследования свойств объектов, правила доступа к данным объекта. Использование классов задаёт парадигму класс-ориентированного программирования.
Класс — прикольная штука, но не необходимая для ООП, поскольку бывают объектно-ориентированные языки прекрасно обходящиеся и без классов.
Прототипное программирование
Другим способом задавать наследование является прототип. При прототипном программировании нет инстансов объектов, объект существует в уникальном виде. Но для каждого объекта можно задать прототип или прототипы — список объектов, свойства и методы которых он будет наследовать.
Исторически модель наследования через прототипы, которую разделяют такой язык, как JavaScript, является более старой, чем через описание классов. Но класс-ориентированное программирование оказалось более удобным для описания API и фреймворков (а как известно, каждый половозрелый Java программист обязан написать свой фреймворк, так же как половозрелость PHP программиста определяется по самописной CMS), по этому стало более распространённым.
Что такое синтаксический сахар
Иногда на форумах и в комментариях опытных коллег-программистов можно услышать что-то вроде «Это просто синтаксический сахар, не обращай внимания». Давайте разберёмся, что это такое, зачем оно нужно и откуда такое название.
Что такое синтаксический сахар
Синтаксический сахар — это способ написания кода, чтобы сделать его более понятным для программиста. Иногда сахар нужен для того, чтобы сделать код короче, оставив ту же самую логику. При этом на работу программы такое оформление вообще не влияет — при запуске компьютер упрощает код, выбрасывает сахар и исполняет суть программы.
Можно сделать код короче
Проще всего синтаксический сахар показать на примерах. Допустим, у нас значение одной переменной зависит от другой:
Этот же самый фрагмент можно записать короче, используя синтаксический сахар — тернарный оператор, который обрабатывает сразу три параметра:
hasName = name ? ‘Y’ : ‘N’;
Этот код делает всё то же самое:
- Проверяет, в name — истина или ложь.
- Если истина — присваивает переменной hasName значение ‘Y’.
- Иначе присваивает ей значение ‘N’.
Логика работы и действия остались точно такими же, но второй код получился компактнее, чем второй, хотя для новичка он выглядит гораздо сложнее.
Сделать код проще
Есть сахар, который, наоборот, делает код проще. Например, вот классический способ организовать цикл, чтобы вывести все его элементы на экран:
А вот то же самое, но с синтаксическим сахаром:
Здесь сразу понятно, что мы перебираем все значения массива massiv, кладём их в переменную element и выводим её на экран.
Ещё примеры синтаксического сахара
В большинстве случаев мы даже не задумываемся над тем, что используем синтаксический сахар в своём коде. Но часто с ним удобнее, чем без него:
Например, вот классический способ сделать объект в JavaScript:
var obj = new Object();
А вот более короткий вариант с сахаром:
Первая строчка — классический способ завести пустой массив, вторая — более привычный сахарный способ:
var arr = new Array();
var arr = [];
Если в JavaScript нужно проверить что-то с помощью регулярных выражений, переменную с этим выражением можно задать двумя способами: традиционным и с сахаром. Сделают они одно и то же, просто вторая будет короче:
var regex = new RegExp(‘something’);
var regex = /something/;
А вот пример чистого сахара. Мы объявляем анонимную функцию, и тут же её выполняем:
В каких языках есть синтаксический сахар
Почти во всех языках программирования есть сахар, причём чем высокоуровневее язык, тем больше сахара можно в нём встретить. Меньше всего сахара в Ассемблерах и в странных языках типа Brainfuck.
Обязательно ли использовать сахар в коде
Использовать синтаксический сахар необязательно, на то он и сахар. Программа с ним и без него будет работать одинаково. Другое дело — как потом эту программу будут поддерживать и обновлять, но и тут есть нюанс. Программа с сахаром может выглядеть читаемее, а может, и менее читаемо — зависит от того, какие именно конструкции вы используете.
Когда код пишут начинающие разработчики, они чаще используют код без сахара, чтобы дисциплинировать себя и сделать код максимально читаемым для себя же. Со временем, начитавшись StackOverflow и набравшись опыта, они нахватаются разных оформительских и структурных привычек и будут использовать тот сахар, который им будет казаться самым полезным. А следующие за ними новички будут смотреть на их код и ничего не понимать.
Синтаксический сахар C#
Синтаксический сахар – довольно полезный инструмент для программиста, несмотря на столь пренебрежительно-гламурное название. Для начала, что вообще такое «синтаксический сахар»? Стандартная википедия дает такое определение: «Синтаксический сахар – синтаксические возможности, применение которых не влияет на поведение программы, но делает использование языка более удобным для человека». Вообще, насчет применения синтаксического сахара существует множество противоположных мнений. Кто-то скажет, что это очень мощная штука, облегчающая жизнь, а кто-то возразит, что это излишество, к тому же, мешающее думать.
На самом же деле синтаксический сахар это способ представить длинный код в боле компактном, лаконичном и читабельном виде. Рассмотрим ряд примеров.
Тернарный оператор. Допустим, у нас есть некоторое условие, в зависимости от которого мы присваиваем значение переменной:
Благодаря тернарному оператору, весь код можно записать короче:
Операции +=,-=,*=,/*. На самом деле и этот код можно сократить:
Краткая проверка на null. В данном примере на null переменная проверяется условным оператором:
Замечу, что в старых версиях C#, например в той версии, которая в Visual Studio 2010 данный синтаксис не работает. В версии Visual Studio 2015 Он уже есть. В промежуточных – не знаю, не проверял.
Кто-то, возможно, спросит, для чего делать проверку на null? А для того, чтобы программа не вылетела с исключением, так как у не инициализированного указателя мы не можем вызвать метод – объекта то еще не существует.
Псевдонимы. Предположим, что у нас сесть тип со множеством вложенных нэймспейсов, например вот такой Domen.Submomen.SubSubDomen.MyClass, см пример:
Можно сделать код более лаконичным, введя псевдоним:
Переменное число аргументов. Очень удобно, когда в функции можно указать любое число аргументов, например, для вывода их на печать (как команда print в старом добром бэйсике), или, например, просуммировать эти аргументы. C# позволяет подобные выкрутасы. Правда, все аргументы типизированы, но это можно обойти, елси тип сделать object. Давайте рассмотрим парочку примером. Сначала суммирование:
Функция суммирует аргументы типа int. Пример использования:
А вот функция с любым типом аргументов (выводит на печать):
Теперь, если нам надо закодировать, например, красный цвет, мы можем обратиться к Colors . Red :
А потом проверить его, например, в операторе условия или в свитче:
Но мало кто знает, что можно сделать перечисления флагов. Например, вот так:
Синтаксический сахар¶
— это синтаксис, который позволяет разработчикам писать код проще, «сладким» способом. Следовательно, такой способ даёт роскошь не знать, как работает система под капотом. Синтаксический сахар связан не только с Python, подробнее по ссылке.
Раннее уже были изучены некоторые способы использования синтаксического сахара в Python, такие как:
Важно понимать и другие варианты применения такого функционала изучаемого языка.
Магические методы (Magic Methods)¶
(иногда их ещё называют специальными методами) — это методы с предопределенными именами, характеризующимися двойным подчеркиванием в начале и в конце — __init__ как пример. Они являются «магическими», потому что
- эти методы вызываются косвенно
- не нужно вызывать напрямую
- все делается за кулисами
Например, когда создаётся экземпляр класса x = MyClass() , Python выполнит необходимые вызовы __new__ и __init__ .
Пример¶
Как видно из примера, метод __repr__() тесно связан с известной функцией print() . Вот еще несколько примеров:
В этом разделе не будут подробно рассматриваться магические методы, если есть желание узнать больше о них — можно заглянуть в документацию (как пример для Python версии 3.11).
Являются ли магические методы синтаксическим сахаром?
Генератор словарей (dict-comprehension)¶
Операторы присваивания¶
Операторы сравнения¶
Тернарный оператор¶
Большие числа¶
Для удобства чтения большие числа можно записывать с использованием символа нижнего подчёркивания.
Многоточие (Ellipsis)¶
Python имеет специальный встроенный одноэлементный объект под названием Ellipsis . Если вывести три точки или слово Ellipsis в интерактивной оболочке Python, результаты будут следующими:
Этот простой предмет кажется незаметным, но если правильно его использовать, он может облегчить нашу жизнь. Рассмотрим три общих сценария, в которых можно использовать Ellipsis .
Ellipsis — это заполнитель для ненаписанного кода¶
При разработке нового модуля обычно определяются некоторые функции или классы, но не реализуются сразу. Потому что хочется только определить, что нужно написать в будущем, и не слишком заботимся о деталях реализации на раннем этапе. В таком сценарии Ellipsis — наш лучший друг:
Можно просто использовать Ellipsis в качестве заполнителя для функции или класса использую тождественный . pass .
Вообще говоря, хорошей практикой программирования является то, что сначала проектируются необходимые функции или классы, а потом реализуются. Поэтому такой способ может помочь нашему уму ясно видеть всю структуру и не зацикливаться сразу на деталях.
Использование Ellipsis в NumPy , чтобы опустить размеры¶
NumPy — важная библиотека Python для Data Science. Ellipsis полезно при работе с многомерными массивами в NumPy .
Например, если есть трёхмерная матрица и необходимо разделить её, то есть как минимум три способа как сделать это:
Использование Ellipsis (трёх точек) является наиболее экономичным способом разделения многомерной матрицы. Потому что это требует наименьшего набора текста. Время программистов — деньги, не так ли?
Использование Ellipsis для подсказки типа¶
Подсказки типов были новым функционалом Python версии 3.5. На основе PEP 484 Ellipsis имеет особое значение для этого функционала.
С одной стороны однородные кортежи произвольной длины могут быть выражены с помощью одного типа и Ellipsis , например, Tuple[int, . ] .
С другой стороны можно объявить возвращаемый тип вызываемого объекта без указания сигнатуры вызова, заменив Ellipsis (три точки) на список аргументов:
Ellipsis — интересный синтаксический сахар в Python. Его удобно использовать в некоторых сценариях.
Морж-оператор (walrus-operator)¶
Каждая новая версия Python добавляет в язык новые функции. Для Python 3.8 самым большим изменением было добавление выражений присваивания. В частности, оператор := дает новый синтаксис для присваивания переменных в середине выражений. Такой оператор в просторечии известен как морж-оператор.
Уже было упоминали ранее о существовании оператора := , полезно погрузиться в дополнительные возможности такого оператора.
Поговорим о том, как:
- определить морж-оператора и понять его значение
- понимать варианты использования морж-оператора
- избегать повторяющегося кода с помощью морж-оператора
- конвертировать код, использующий морж-оператора, в код, использующий другие методы присваивания и наоборот
- понимать влияния на обратную совместимость при использовании морж-оператора
- использовать соответствующий стиль в выражениях присваивания
Для корректной работы морж-оператора требуется Python 3.8 или более поздней версии.
Основы морж-оператора¶
Оператор := официально известен как оператор выражения присваивания. Во время ранних дискуссий его назвали оператором моржа, потому что синтаксис := напоминает глаза и бивни лежащего на боку моржа. Также иногда можно увидеть, что оператор := называют «оператор двоеточие равно» (colon equals operator). Еще одним термином, используемым для выражений присваивания, являются именованные выражения.
Чтобы получить первое представление о том, что такое выражения присваивания, можно поэкспериментировать со следующим кодом:
В примере выше показан традиционный оператор присваивания, в котором переменной walrus присваивается значение False .
Затем, используется выражение присваивания, чтобы присвоить значение True для walrus . В обоих случаях можно ссылаться на присвоенные значения, используя имя переменной walrus .
Между двумя типами присваиваний, рассмотренными выше с переменной walrus , есть тонкая, но важная разница. Выражение присваивания возвращает значение, а традиционное присваивание — нет.
В этих примерах можно увидеть еще один важный аспект операторов моржа. Оператор := не делает ничего такого, что было бы невозможно без него. С помощью оператора моржа можно лишь делать определенные конструкции более удобными и иногда можно более четко передать цель кода.
Общее представление о том, что такое оператор := и что он может делать получено. Это оператор, используемый в выражениях присваивания, который может возвращать присваиваемое значение, в отличие от традиционных операторов присваивания.
Один из принципов разработки, лежащий в основе оператора walrus, заключается в том, что не существует идентичных контекстов кода, в которых были бы допустимы как оператор присваивания, использующий оператор = , так и выражение присваивания, использующее оператор := . Например, невозможно выполнить простое присваивание с помощью оператора walrus:
В большинстве случаев можно добавить круглые скобки () вокруг выражения присваивания, чтобы сделать код валидным:
Однако такая конструкция не имеет большого смысла, т.к. удобнее воспользоваться оператором = . При этом запись традиционного оператора присваивания = внутри таких скобок не допускается.
Варианты использования морж-оператора¶
Рассмотрим несколько примеров, где морж-оператора может упростить код. Общая тема всех этих примеров заключается в том, чтобы избегать различных видов повторения:
- повторные вызовы функций могут сделать код медленнее
- повторяющиеся операторы могут затруднить сопровождение кода
- повторяющиеся вызовы итераторов могут сделать код чрезмерно сложным
- увидим, как морж-оператор может помочь в каждой из этих ситуаций
Отладка¶
Возможно, один из лучших вариантов использования морж-оператора — отладка сложных выражений. Допустим, стоит задача найти расстояние между двумя точками на земной поверхности. Один из способов сделать это — использовать формулу гаверсинуса :
где \(\varphi\) представляет широту, а \(\lambda\) — долготу каждого местоположения.
Чтобы продемонстрировать эту формулу, можно рассчитать расстояние между Москвой (55,45° с. ш., 37,36° в. д.) и Ванкувером (51,3° с. ш., 0,7° з. д.) следующим образом:
Как можно увидеть, расстояние от Москвы до Лондона составляет чуть более 2500 километров.
Теперь предположим, что нужно перепроверить реализацию и посмотреть, насколько члены гаверсинуса влияют на конечный результат. Можно скопировать и вставить часть из основного кода, чтобы оценить его отдельно. Однако также можно использовать оператор := , чтобы дать имя интересующему подвыражению:
Преимущество использования морж-оператора здесь заключается в том, что одновременно можно вычислять значение итогового значения и отслеживаете значение ϕ_hav . Это позволяет подтвердить, что не было допущено никаких ошибок во время отладки.
Списки и словари¶
Списки — это мощные структуры данных в Python, которые часто представляют собой ряд связанных атрибутов. Точно так же словари используются во всем Python и отлично подходят для структурирования информации.
Иногда при настройке таких структур данных приходится выполнять одну и ту же операцию несколько раз. В качестве первого примера вычислим некоторую базовую описательную статистику списка чисел и сохраним их в словаре:
Важно обратить внимание, что и сумма, и длина списка чисел вычисляются дважды. В этом простом примере последствия не так уж плохи, но если бы список был больше или вычисления были бы более сложными, вероятно захотелось бы оптимизировать такой код. Для этого можно сначала переместить вызовы функций из определения словаря:
Переменные num_length и num_sum используются только для оптимизации вычислений внутри словаря. Используя морж-оператор, эту роль можно сделать более понятной:
num_length и num_sum теперь определены внутри определения описания. Это явный намёк на то, что эти переменные используются только для оптимизации этих вычислений и больше не используются позже.
Область видимости переменных num_length и num_sum одинакова в примере с морж-оператором и в примере без него. Это означает, что в обоих примерах переменные доступны после определения описания.
Несмотря на то, что оба примера функционально очень похожи, преимущество использования выражений присваивания заключается в том, что оператор := сообщает о назначении этих переменных как одноразовые оптимизации.
f-строки (f-strings)¶
О f-строках говорили ранее, полезно погрузиться в дополнительные возможности этого удобного и в последнее время наиболее часто используемого функционала для форматирования строк.
В большинстве случаев синтаксис аналогичен старому % -форматированию с добавлением <> и : вместо % . Например, %03.2f можно перевести как <:03.2f>.
Спецификаторы формата¶
Спецификаторы в f-строках можно использовать таким образом:
Спецификаторы формата могут содержать оцениваемые выражения. Это позволяет использовать следующий код:
После оценки выражений в спецификаторе формата (при необходимости) спецификаторы формата не интерпретируются оценщиком f-строки. Как и в str.format(), они просто передаются в метод format() форматируемого объекта.
Переменные со значениями¶
Версия Python 3.8 не обошла стороной и f-строки, дополнив их возможностями по выводу имён переменных вместе со значениями.