Русский
Русский
English
Статистика
Реклама

Яндекс.практикум

Как проходит собеседование Junior фронтенд-разработчика

16.02.2021 12:20:24 | Автор: admin
Меня зовут Максим Чеченёв, я фронтенд-разработчик уже почти девять лет. Работаю в компании MessageBird в Амстердаме и наставником на курсе Веб-разработчик в Яндекс.Практикуме. Ещё я веду канал в телеграме Сеньор Разработчик.

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


Из чего состоит собеседование


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

Обычно собеседование можно разбить на три части:

  • знакомство,
  • технические вопросы и/или задание,
  • ваши вопросы.


Знакомство


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

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

Рассказ о себе


Представьте, вы пришли на собеседование в компанию, о которой мечтали. Рекрутер предложил вам чашечку кофе, спросил, как вы добрались, а после попросил рассказать немного о себе. Такой простой вопрос застал вас врасплох: С чего начать, стоит ли рассказывать о своём образовании, хобби или рекрутеру важен только мой опыт?

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

  • Подготовьте короткий рассказ о себе.
    Ответьте так, чтобы это не было длинной запутанной историей, но также и не: Меня зовут Максим, я учился на программиста. Советую заранее потренировать рассказ на друзьях, семье или кошке.
  • Расскажите, чему вы обучались.
    Если обучение было непрофильное, поясните, как пришли в разработку.
  • Уделите время опыту в разработке.
    Если вы совсем начинающий разработчик не страшно. В этом случае можно рассказать об учебных проектах.
  • Затроньте тему профильного хобби.
    Опишите проект, над которым вы работаете в свободное время. Даже если вам кажется, что проект простой, про него стоит рассказать. Это покажет вашу заинтересованность в разработке. Простой проект, который не решает грандиозных задач, куда лучше, чем пустой профиль на гитхабе.

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

Почему вы хотите работать у нас?


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

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

Будьте честны, но не наглейте. Недавно я собеседовал кандидата из Бразилии, который на вопрос: Почему ты хочешь у нас работать? ответил: В Европе платят больше, чем в Бразилии. Несмотря на то, что это максимально честный ответ, это показывает, что ему всё равно, где работать и чем заниматься.

Почему вам интересен фронтенд?


Я лично люблю задавать такой вопрос: Почему вам интересен фронтенд? Почему не бекенд, например? он отлично показывает заинтересованность и страсть кандидата.

Как не стоит отвечать: Да я просто попробовал, вроде интересно.

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

Сработаемся ли мы вместе?


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

  • Чем вы увлекаетесь?
  • Чего вы ждёте от работы у нас?
  • Что хотелось бы прокачать в себе?
  • Какими достижениями вы гордитесь?

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



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

Техническое собеседование


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

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

Теоретические вопросы


Блок с теоретическими вопросами можно условно разделить на два: вопросы на знание основ JS, CSS и других инструментов и вопросы о том, как вы привыкли работать с кодом.

Вопросы на знание инструментов

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

  • Как работает наследование?
  • Что такое замыкание и зачем оно нужно?
  • Как искать элементы в DOM-дереве?
  • Как работает Event loop?
  • Что такое this-объект?
  • Что такое типы данных и как их сравнивать? Чем отличается == от ===? Что такое null и undefined?
  • Как работают методы .filter, .map, .reduce и зачем они нужны?
  • Что такое Promise, зачем нужен async/await?

Вопросов про HTML и CSS обычно меньше. Вас могут спросить: Какой селектор сильнее по ID или по классу? или Как выровнять элемент по центру?

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

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

  • Зачем нужен React? Почему нельзя обойтись без него?
    Я ожидаю услышать, какие проблемы решает React, что такое Virtual DOM, об экономии времени и сил при написании приложения.
  • Зачем нужны различные методологии и подходы при работе с CSS?
    Мне не так важно, любит ли кандидат БЭМ, CSS Modules или css-in-js. Мне важно, что разработчик понимает, какие проблемы они решают.
  • На странице отображается больше тысячи позиций, и страница начинает тормозить. Как это лучше исправить?
    Здорово, если кандидат расскажет, например, о lazy loading или пагинации.

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

Вопросы о привычках в работе

Кроме вопросов на знание технологий вас могут спросить о том, как вы работаете, к примеру:

  • Когда код не работает, что вы предпринимаете сначала и как решаете проблему?
  • Как вы называете переменные и функции?
  • Как вы изучаете новые технологии и подходы? Что читаете?

Мой любимый вариант вопросов вам рассказывают про проект или задачу, над которой команда уже работает или будет работать. Затем спрашивают, как бы вы сделали её и с чего бы начали. Здесь важно не просто рассказать, какие технологии вы бы использовали, но и активно задавать уточняющие вопросы: Что уже готово? Какие сроки? Есть ли дизайн?

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

Задание: что выполнит данный код?


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

Например:

let b = {};let c;b.b = 1;c = b;c.b = 2;console.log('b.b =', b.b); // ?console.log('c.b =', c.b); // ?

или

console.log(1)const a = new Promise((resolve, reject) => resolve(console.log(2)))a.then(res => console.log(3))setTimeout(() => {    console.log(4)}, 0)console.log(5)// В каком порядке выведутся числа?

или

<style>.green { color: green; }.blue { color: blue; }</style><div class="green blue">Раз</div><div class="blue green">Два</div>//  Какого цвета оба элемента?

Такие задания проверяют, как вы понимаете основы JS или CSS. Например, как работают переменные и как они друг на друга ссылаются.

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

Задачи


Компании любят давать небольшую задачку (уровень Easy на LeetCode например, вот такую), которую надо решить во время интервью. Чаще всего это небольшой алгоритм. Например, про работу со строками или с массивами.



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

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

Как готовиться к таким задачам?


Посмотрите задачи уровня Easy на leetcode.com, hackerrank.com или codewars.com. Не пытайтесь решить их все разом за один вечер. Делайте это по чуть-чуть по одной-две задачи в день.

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

Ваши вопросы




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

Вас обязательно спросят: Какие вопросы у вас есть к нам?

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

Какой ответ понравится: Над чем я буду работать, если вы меня наймёте? Слушайте ответ и задавайте уточняющие вопросы. Узнайте про команду, как устроен процесс работы, есть ли код-ревью, будет ли кто-то обучать вас. Спросите про планы компании: какие проекты запускают? Какие сложности и вызовы есть сейчас?

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

Общие советы


  • Не опаздывайте. Звучит очевидно, но это может повлиять на всё дальнейшее общение. Даже если это удалённое собеседование, будьте на звонке за пару минут. Если опаздываете, то обязательно предупредите.
  • Внимательно изучите описание вакансии. Посмотрите на продукт компании, если он уже есть. Это поможет подготовиться к возможным вопросам и задачам по проектам компании.
  • Не молчите, даже если не знаете ответ на вопрос. Можно просто смело сказать: Я не знаю, но я предполагаю вот так... Даже если вы ошибётесь, это нормально. Вам нужно показать свой интерес и желание разобраться.
  • Не отвечайте слишком длинными фразами. В их середине можно легко потерять смысл того, что вы хотите сказать.
  • Освежите в памяти теорию.
  • Комментируете свои решения.
  • Задавайте вопросы.
  • Хорошо выспитесь накануне. Отдых куда полезнее выученной за ночь теории.


После собеседования


Вас пригласили на работу

В случае успеха всё просто: вы обсуждаете зарплату, дату начала работы и празднуете.

Вам отказали

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

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


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

Как обойти ограничение браузера и прикрепить сразу два файла и более мультидобавление файлов

03.03.2021 16:23:30 | Автор: admin
Привет, Хабр!

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

Возьмём по умолчанию Chrome v.88. Задача звучит так:

  • Сгенерировать файлы на стороне клиента.
  • Скачать все сгенерированные файлы одним кликом.

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


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

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

var zip = new JSZip();zip.file("Hello.txt", "Hello World\n");var img = zip.folder("images");img.file("smile.gif", imgData, {base64: true}); zip.generateAsync({type:"blob"}).then(function(content) {// see FileSaver.jssaveAs(content, "example.zip"); });

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

Сначала подготовим браузер. Chrome по умолчанию запрещает скачивать мультифайлы. Это сделано в целях безопасности. Поэтому эту функцию надо сначала разблокировать в настройках браузера:

  1. Заходим в настройки сайтов: chrome://settings/content.
  2. Переходим в доступ дополнительных прав (Additional permissions).
  3. Выбираем Automatic downloads.
  4. Добавляем необходимый сайт в категорию Allow.




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

Подход #1 FileReader


Первый подход рассмотрим на примере генерации файлов с помощью FileReader и API для чтения base64. Отмечу сразу, что у FileReader довольно обширное API, поэтому выбирайте то, что больше нравится: text, arrayBuffer или binaryString.

(function () {  const button = document.getElementById("download_with_reader");  const content = ["content-1", "content-2", "content-3"];    const createLink = () => {    let link = document.createElement('a');    link.download = 'hello.txt';    return link;  }    const generateBlob = () => {    for (const [index, value] of content.entries()) {      const blob = new Blob([value], { type: "text/plain" });      download(blob, index);    }  }  const download = (blob, index) => {    const link = createLink();    let reader = new FileReader();    reader.readAsDataURL(blob);    reader.onload = function () {      link.href = reader.result;      link.download = `content-${index+1}.txt`;      link.click();    }  }    button.addEventListener("click", generateBlob);}) ();

[код на Gitlab]

Подход #2 createObjectURL


А ещё можно использовать createObjectURL он позволяет хранить File-объекты или Blob-объекты.

(function () {  const button = document.getElementById("download_with_url_object");  const content = ["content-1", "content-2", "content-3"];    const createLink = () => {    let link = document.createElement('a');    link.download = 'hello.txt';    return link;  }  const generateBlob = () => {    for (const [index, value] of content.entries()) {      const blob = new Blob([value], { type: "text/plain" });      download(blob, index);    }  }  const download = (blob, index) => {    const link = createLink();    link.href = URL.createObjectURL(blob);    link.download = `content-${index+1}.txt`;    link.click();    URL.revokeObjectURL(link.href);  }   button.addEventListener("click", generateBlob);}) ();

[код на Gitlab]

Подход #3 Скачивание по URL


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

(function () {(function () {  const button = document.getElementById("download_with_request");  const urls = ["images/image-1.jpg", "images/image-2.jpg", "images/image-3.jpg"];    const delay = () => new Promise(resolve => setTimeout(resolve, 1000));    const downloadWithRequest = async () => {    for await (const [index, url] of urls.entries()) {      await delay();      const link = document.createElement("a");      link.href = url;      link.download = `image-${index+1}`;      link.click();    }  }    button.addEventListener("click", downloadWithRequest);}) ();

[код на Gitlab]

Итого


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

Публичное техническое собеседование на мидл фронтенд-разработчика 15 июня в 19.00

07.06.2021 18:08:54 | Автор: admin
15 июня в 19:00 (Мск.) Яндекс.Практикум проведёт открытое публичное собеседование на тестовую вакансию мидл фронтенд-разработчика. Будут два собеседующих, кандидат и вакансия, на которую он откликнулся. Участники смогут не только следить за происходящим, но и задать вопросы спикерам.

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



Ведущие


Семён Левенсон Frontend-разработчик в Яндекс.Дзен, в роли первого собеседующего.

Алексей Малинов ex. Frontend-разработчик в Яндекс.Маркет, в роли второго собеседующего.

Владимир Сурыгин фронтенд-разработчик с полуторалетним стажем, в роли собеседуемого, выпускник курса Мидл Фронтенд-разработчик.

Вебинар пройдет 15 июня в 19.00 (Мск). Подробности и регистрация.
Подробнее..

Стандарт C20 обзор новых возможностей C. Часть 1 Модули и краткая история C

29.04.2021 18:18:03 | Автор: admin


25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся на почти 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

  1. Модули и краткая история C++.
  2. Операция космический корабль.
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

Это первая часть, рассказывающая о модулях в современном C++. Если вы предпочитаете снайдеркатам краткие изложения, то добро пожаловать в статью.

Краткая история C++


В самом начале я задал слушателям вебинара вопрос: сколько всего существует стандартов C++?

Результаты голосования:

  • правильных ответов 58 (96.67%)
  • неправильных ответов 2 (3.33%)



Давайте посчитаем. Бьёрн Страуструп занялся разработкой C++ в восьмидесятых годах. К нему пришли люди из ISO [международная комиссия по стандартизации] и предложили стандартизировать язык. Так и появился C++98 первый Стандарт.

Прошло пять лет, и Стандарт исправили. Получился C++03. Это было не что-то революционное, а просто исправление ошибок. Кстати, иногда C++03 не считают отдельным Стандартом. Возможно, C++03 самый популярный Стандарт с точки зрения примеров в интернете и ответов на Stack Overflow, но назвать его современным C++ сейчас невозможно.

Всё изменил следующий Стандарт, который планировалось выпустить до 2010 года. Он носил кодовое название C++0x, которое потом сменилось на C++1x. Решить все проблемы и издать Стандарт смогли только в 2011 году, он получил название C++11. Заметно расширились возможности языка: там появились auto, move-семантика, variadic templates. Когда я учил этот Стандарт, у меня возникло ощущение, что освоить C++11 равносильно изучению нового C++.

Прошло три года. Вышел C++14. Он не стал таким революционным и в основном содержал фиксы ошибок, неизбежных при принятии такого огромного набора документов, как C++11. Но и в 2014 году добавилось новое.

Ещё через три года C++17 добавил больше интересных вещей: дополнительные возможности стандартной библиотеки, распаковку при присваивании и прочее.

Логично ожидать, что за большим Стандартом последует Стандарт с исправлениями ошибок. Но что-то пошло не так. C++20 это практически новый язык. По количеству нововведений он сравним с C++11, а может быть, обгоняет его.



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

Модули




Мотивация


Код на C++ хранится в .cpp, .cxx, .cc файлах. На самом деле этот код программа не на C++, а на языке препроцессора C++. Это другой язык, который не понимает синтаксис C++. И наоборот, C++ не понимает синтаксис препроцессора. Формально он входит в Стандарт, поэтому препроцессор можно относить к C++. Но фактически это два разных языка.

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

До C++20 вместо модулей использовали хедеры отдельные текстовые файлы .h. При подключении хедера программой на C++ он просто копируется в место включения. В связи с этим возникает много проблем.

  • Дублирование. При добавлении определения функции в .cpp-файл, нужно добавить объявление в .h-файл. А дублирование порождает ошибки.
  • Неочевидный побочный эффект включения заголовочных файлов. В зависимости от порядка расположения два скопированных фрагмента могут влиять друг на друга.
  • Нарушение one definition rule
    Правило одного определения. В программе не должно быть конфликтующих определений одной и той же сущностей. Наличие нескольких определений может влечь неопределённое поведение
    Функция или класс могут включаться в разные файлы .cpp, разные единицы трансляции. Если вдруг они включились по-разному например, в этих единицах трансляции определены разные макросы, нарушится one definition rule. Это серьёзная ошибка.
  • Неконсистентность включений. То, что включится из хедера, зависит от макросов, которые определены в момент включения хедера.
  • Медленная компиляция. Когда один и тот же хедер целиком включается в разные единицы трансляции, компилятор вынужден его компилировать каждый раз. Кстати, это же касается стандартных библиотек. Например, iostream это огромный файл, и компилятор вынужден компилировать его со всеми зависимыми единицами трансляции.
  • Мы не можем контролировать, что нужно экспортировать, а что нет. При включении хедера единица трансляции получит всё, что в нём написано, даже если это не предназначено для включения.

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

В итоге использование хедеров:

  • небезопасно;
  • повышает время компиляции;
  • некрасиво: компилятор никак не обрабатывает процедуру включения, а просто вставляет один текст в другой.

У хедеров есть плюсы. Перечислять их я, конечно же, не буду.

Что у других


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



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



В Fortran единицу трансляции можно скомпилировать только в том случае, если все зависимости уже скомпилированы. Из-за этого появилось правило run make until it succeeds, то есть нужно продолжать запускать make, пока наконец не скомпилируется. В первый раз скомпилируются модули, у которых нет зависимостей, во второй раз модули, которые зависели от первых. В какой-то момент вся программа соберётся. Если повезёт, даже раньше, чем вы ожидаете.

Как вы думаете, по какому пути пошёл C++?



Конечно же, по пути Фортрана! Хотя за три десятка лет в Фортране как-то научились обходить проблемы модулей, фортрановские решения для C++ не годятся ситуация сложнее.

Но не всё так плохо.

Пример


Рассмотрим пример из трёх файлов. Заметьте, что два из них имеют расширение .cppm такое расширение для модулей принято в компиляторе Clang. Третий файл обычный .cpp, который импортирует модули.



В модулях есть ключевое слово export. Те декларации, которые мы хотим экспортировать, нужно пометить этим словом. Тогда к ним получат доступ все единицы трансляции, импортирующие этот модуль, cpp-файлы и другие модули.

При компиляции примера нужно вначале собрать модуль foo2.cppm, потому что он ни от чего не зависит. Затем нужно собрать foo.cppm и только потом bar.cpp.

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

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



Для начала нужно предкомпилировать оба файла .cppm. Создастся файл с расширением .pcm бинарный аналог файла .h. То есть h-файл теперь не нужно создавать вручную. Затем собирается вся программа. В данном случае это bar.cpp, который зависит от двух модулей.

В Visual Studio модули реализованы из коробки. Вы добавляете в проект module unit с расширением .ixx, и VS всё соберёт за вас.

Эта концепция полностью ломает некоторые из существующих систем сборки C++ кода. Хотя всё налаживается. К примеру, в CMake добавили экспериментальную поддержку модулей. Такие системы, как Build2, b2, cxx_modules_builder, xmake, Meson, autotools, Tup, Scons, уже поддерживают модули.

Теория


Рассмотрим, какие проблемы модули решают, а какие не решают. Зададим вопросы.

  • Можем ли мы импортировать выбранные имена?
  • Получится ли переназвать имена при импорте, как в Python?
  • Структурируют ли модули имена?

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

Следующий блок вопросов.

  • Импортируются только нужные имена?
  • Ускоряют ли модули процесс сборки?
  • Модули не влияют друга на друга?
  • Не пишем больше отдельно .cpp и .h?
  • Не можем испортить код других модулей макросами при импорте?

Ответы на них да. Это те проблемы, которые решает новый Стандарт.

Последний вопрос.

  • В Python при импорте можно выполнять произвольный код. Есть ли в C++ такое?

В C++ импорт происходит во время compile-time, а не в runtime. Поэтому вопрос не имеет смысла.

Модули нарушают несколько устоявшихся принципов C++:

  1. Принцип независимости сборки. До этого программа на C++ состояла из разных единиц трансляции файлов .cpp. Каждый из них можно было компилировать отдельно: сегодня один, завтра другой, через неделю третий, а потом уже слинковать всё вместе. Теперь порядок не произвольный. Файл нельзя собрать, пока не предкомпилированы модули, от которых он зависит. Поэтому собрать модуль не получится, если в каком-то зависимом модуле ошибка. Процесс сборки сильно усложняется.
  2. Принцип гомогенности кода. Хотя #include обычно пишут в начале, это договорённость, а не правило. Его можно писать в любом месте программы. И так со всем, что есть в C++: никакой глобальной структуры у кода до C++20 не было. Синтаксические конструкции могли идти в любом порядке. Новым Стандартом вводится преамбула. И только в ней могут располагаться импорты модулей. Как только преамбула закончилась, писать import стало нельзя. У файла кода появляется структура. Кроме того, перед преамбулой возможна предпреамбула так называемый Global module fragment. В нём могут располагаться только директивы препроцессора. Но они допускают #include, а значит, по факту всё что угодно. Подробно разбирать Global module fragment не будем.

Я считаю появление структуры хорошим шагом, но это нарушение давно существовавших принципов C++.

Модули добавляют новые понятия. Например, новые типы единиц трансляции они называются module unit и header unit. Появился тип компоновки module linkage.

Module unit бывают двух типов:

  • Module interface unit. Начинается с export module.
  • Module implementation unit. Начинается с module.

Разница у них в том, что module interface unit это интерфейс, предназначенный для тех, кто этот модуль будет импортировать. К нему может прилагаться любое количество module implementation units, в которые по желанию выносятся реализации функций и методов из этого модуля. Главное правило: для каждого модуля ровно один module interface unit и сколько угодно module implementation unit.

В большинстве случаев module implementation unit вообще не понадобится. Он предназначен для больших модулей, код которых сам по себе требуется структурировать. Поэтому чаще всего один модуль один module interface unit.

Посмотрим на допустимый формат импорта и экспорта из модулей.

import M;import "my_header.h";import <version>;

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

В теории, чтобы импортировать .h-файл, его тоже нужно предкомпилировать. При этом заголовок, который раньше был лишь придатком cpp-файла, рассматривается как самостоятельная единица трансляции, а вернее, header unit. Компилятор C++ вынет из него все имена и сделает подобие предкомпилированного модуля. Модуль в старом стиле, почему нет?

Интересно, что при этом импортируются макросы то, от чего нас пытается избавить новый Стандарт. По легенде, когда комитет по стандартизации хотел полностью вычеркнуть импорт макросов, к нему обратились представители Microsoft. Они заявили, что никак не могут обойтись в Windows.h без макросов min, max и некоторых других. Меня сейчас поняли те, кто программирует на C++ под Windows.

В отличие от #include, при импорте нужна точка с запятой.

Я описал, что можно импортировать. Теперь обсудим, что модуль может экспортировать. Ответ прост: декларации, определения, псевдонимы. Всё, что создаёт новое имя. Достаточно написать перед соответствующей конструкцией слово export.

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

Посмотрим на примерах. Из модулей экспортируются:

  • декларации и определения, создающие имя (типы, using-декларации, функции, глобальные переменные, классы, enum). В том числе шаблонные.

export module M;export template<class R>struct Point {    R x, y;};export int f();int f() { return 42; }export int global_variable = 42;

  • Целые namespaceы или декларации внутри namespace'ов.

export namespace {    int prime_number = 13;    class CppCompiler {};}namespace A { // exported   export int f(); // exported   int g(); // not exported}

Тут можно найти ещё одно применение безымянным namespace.

  • Другие модули

export import MyModule;

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

  • Любые имена через using.

struct F {};export using ::F;

Таким образом, имена тоже экспортируются: для этого пишите :: перед именем, потому что using требует указания пространства имён.

  • Имена под другим именем.

export using G = ::F;

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

// TU 1export module A;export import :Foo;export int baz();// TU 2export module A:Foo;import :Internals;export int foo() { return 2 * (bar() + 1); }// TU 3export module A:Internals;int bar();// TU 4module A; int baz() { return 30; }int bar() { return baz() - 10; }

Статус




В Visual Studio у модулей частичная поддержка. Очень здорово, что в VS стандартная библиотека уже реализована на модулях, то есть вы можете написать import std.core;. Импорт h-файлов в VS пока не работает.

В GCC поддержки модулей нет в trunk, но есть в ветке. Эту ветку планируют влить в GCC 11.

В Clang модули присутствуют давно. Вообще даже техническая спецификация модулей, принятая в C++20, далеко не первая. Их давно обсуждали и даже планировали включить в Стандарт C++17, но не успели. Clang поддерживает обе спецификации: новую и старую, но всё равно не полностью.

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

Заключение


Во время трансляции мы провели голосование, крутая это фича или нет. Результаты опроса:

  • Суперфича 16 (23.53%)
  • Так себе фича 6 (8.82%)
  • Пока неясно 46 (67.65%)

Расскажу о своём мнении по этому вопросу. Я считаю, что модули нужны обязательно, потому что так, как было 40 лет назад в C, никуда не годится. Во всех современных языках есть модули, почему в нашем современном языке их нет? Конечно, модули решают далеко не все проблемы: проблемы структурирования имён и распространения пакетов остаются нерешёнными. Но всё-таки они ускоряют сборку, структурируют зависимости, избавляют от дублирования и нарушения ODR. Поэтому вещь очень полезная.

Главный минус: существенно усложняется процесс сборки. С их активным применением я бы пока подождал.

Опрос


Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.
Подробнее..

Стандарт C20 обзор новых возможностей C. Часть 2 Операция Космический Корабль

05.05.2021 12:08:12 | Автор: admin


25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся почти на 2,5 часа. Для вашего удобства мы разбили текст на шесть частей:

  1. Модули и краткая история C++.
  2. Операция космический корабль.
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

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

Это вторая часть, рассказывающая об операции космический корабль в современном C++.

Операция космический корабль


В C++ теперь свой космос!



Мотивация


В C++ шесть операций сравнения:

  1. меньше,
  2. больше,
  3. меньше или равно,
  4. больше или равно,
  5. равно,
  6. не равно.

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

Предположим, вы определили структуру, содержащую одно число:

struct X {    int a;};

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

bool operator== (X l, X r) { return l.a == r.a; }bool operator!= (X l, X r) { return l.a != r.a; }bool operator>= (X l, X r) { return l.a >= r.a; }bool operator<= (X l, X r) { return l.a <= r.a; }bool operator< (X l, X r) { return l.a < r.a; }bool operator> (X l, X r) { return l.a > r.a; }

А теперь представьте, что мы хотим сравнивать элементы этой структуры не только между собой, но также с числами int. Количество операций возрастает с шести до 18:

bool operator== (X l, int r) { return l.a == r; }bool operator!= (X l, int r) { return l.a != r; }bool operator>= (X l, int r) { return l.a >= r; }bool operator<= (X l, int r) { return l.a <= r; }bool operator< (X l, int r) { return l.a < r; }bool operator> (X l, int r) { return l.a > r; }bool operator== (int l, X r) { return l == r.a; }bool operator!= (int l, X r) { return l != r.a; }bool operator>= (int l, X r) { return l >= r.a; }bool operator<= (int l, X r) { return l <= r.a; }bool operator< (int l, X r) { return l < r.a; }bool operator> (int l, X r) { return l > r.a; }

Что делать? Можно позвать штурмовиков. Их много, и они быстро напишут 18 операций.



Или воспользоваться космическим кораблём. Эту новую операцию в C++ называют космический корабль, потому что она на него похожа: <=>. Более формальное название трёхстороннее сравнение фигурирует в документах Стандарта.

Пример


В структуру X я добавил всего одну строчку, определяющую операцию <=>. Заметьте, что я даже не написал, что именно она делает:

#include <iostream>struct X {    auto operator<=>(const X&) const = default; // <-- !    int a;};

И C++ всё сделал за меня. Это сработает и в более сложных случаях, например, когда у X несколько полей и базовых классов. При этом всё, что есть в X, должно поддерживать сравнение. После того, как я написал эту магическую строчку, я могу сравнивать объекты X любым способом:

int main() {    X x1{1}, x42{42};    std::cout << (x1 < x42 ? "x1 < x42" : "not x1 < x42") << std::endl;    std::cout << (x1 > x42 ? "x1 > x42" : "not x1 > x42") << std::endl;    std::cout << (x1 <= x42 ? "x1 <= x42" : "not x1 <= x42") << std::endl;    std::cout << (x1 >= x42 ? "x1 >= x42" : "not x1 >= x42") << std::endl;    std::cout << (x1 == x42 ? "x1 == x42" : "not x1 == x42") << std::endl;    std::cout << (x1 != x42 ? "x1 != x42" : "not x1 != x42") << std::endl;}

Получилась корректная программа. Её можно собрать и запустить. Текстовый вывод выглядит так:

x1 < x42not x1 > x42x1 <= x42not x1 >= x42not x1 == x42x1 != x42

Операция космического корабля сработает и для сравнения элемента структуры X с числом. Но придётся написать реализацию. На этот раз C++ не сможет придумать её за вас. В реализации воспользуемся встроенной операцией <=> для чисел:

#include <iostream>struct X {    auto operator<=>(const X&) const = default;     auto operator<=>(int r) const {   // <-- !        return this->a <=> r;    }    int a;};

Правда, возникает проблема. C++ создаст не все операции. Если вы определили эту операцию не через default, а написали сами, проверка на равенство и неравенство не будет добавлена. Кто знает причины пишите в комменты.

int main() {    X x1{1}, x42{42};    std::cout << (x1 < 42 ? "x1 < 42" : "not x1 < 42") << std::endl;    std::cout << (x1 > 42 ? "x1 > 42" : "not x1 > 42") << std::endl;    std::cout << (x1 <= 42 ? "x1 <= 42" : "not x1 <= 42") << std::endl;    std::cout << (x1 >= 42 ? "x1 >= 42" : "not x1 >= 42") << std::endl;    std::cout << (x1 == 42 ? "x1 == 42" : "not x1 == 42") << std::endl; // <--- ошибка    std::cout << (x1 != 42 ? "x1 != 42" : "not x1 != 42") << std::endl; // <--- ошибка}

Впрочем, никто не запрещает определить эту операцию самостоятельно. Ещё одно нововведение C++20: можно добавить проверку только на равенство, а неравенство добавится автоматически:

#include <iostream>struct X {    auto operator<=>(const X&) const = default;    bool operator==(const X&) const = default;    auto operator<=>(int r) const {        return this->a <=> r;    }    bool operator==(int r) const { // <-- !        return operator<=>(r) == 0;    }    int a;};int main() {    X x1{1}, x42{42};    std::cout << (x1 < 42 ? "x1 < 42" : "not x1 < 42") << std::endl;    std::cout << (x1 > 42 ? "x1 > 42" : "not x1 > 42") << std::endl;    std::cout << (x1 <= 42 ? "x1 <= 42" : "not x1 <= 42") << std::endl;    std::cout << (x1 >= 42 ? "x1 >= 42" : "not x1 >= 42") << std::endl;    std::cout << (x1 == 42 ? "x1 == 42" : "not x1 == 42") << std::endl;    std::cout << (x1 != 42 ? "x1 != 42" : "not x1 != 42") << std::endl;}

Хоть 2 операции и пришлось определить, но это гораздо лучше, чем 18.

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

#include <iostream>struct X {    auto operator<=>(const X&) const = default;    bool operator==(const X&) const = default;    auto operator<=>(int r) const {        return this->a <=> r;    }    bool operator==(int r) const { // <-- !        return operator<=>(r) == 0;    }    int a;};int main() {    X x1{1}, x42{42};    std::cout << (1 < x42 ? "1 < x42" : "not 1 < x42") << std::endl;    std::cout << (1 > x42 ? "1 > x42" : "not 1 > x42") << std::endl;    std::cout << (1 <= x42 ? "1 <= x42" : "not 1 <= x42") << std::endl;    std::cout << (1 >= x42 ? "1 >= x42" : "not 1 >= x42") << std::endl;    std::cout << (1 == x42 ? "1 == x42" : "not 1 == x42") << std::endl;    std::cout << (1 != x42 ? "1 != x42" : "not 1 != x42") << std::endl;}

Теория


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

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

#include <iostream>struct X {    auto operator<=>(const X&) const = default;     int a;};int main() {    X x1{1}, x42{42};    std::cout << (x1.operator<(x42) ? "<" : "!<")    // <--- ошибка              << std::endl; }

Удивительно, как же компилятор выполняет операцию, которой нет. Всё благодаря тому, что поменялись правила поведения компилятора при вычислении операций сравнения. Когда вы пишете x1 < x2, компилятор, как и раньше, проверяет наличие операции <. Но теперь, если он её не нашёл, то обязательно посмотрит операцию космического корабля. В примере она находится, поэтому он её использует. При этом, если типы операндов разные, компилятор посмотрит сравнение в обе стороны: сначала в одну, потом в другую. Поэтому нет необходимости определять третий космический корабль для сравнения int и типа X достаточно определить только вариант, где X слева.

Если вам по какой-то причине вместо x < y нравится писать x.operator<(y), то определите операцию < явно. У меня для вас хорошие новости: реализацию можно не писать. default будет работать для обычных операций сравнения так же, как и для <=>. Напишите его, и C++ определит его за вас. Вообще, C++20 многое делает за вас.

#include <iostream>struct X {    auto operator<=>(const X&) const = default;     bool operator<(const X&) const = default; // <-- !    int a;};int main() {    X x1{1}, x42{42};    std::cout << (x1.operator<(x42) ? "<" : "!<")              << std::endl;}

Заметьте, что у operator< потребовалось указать явный тип возврата bool. А в <=> эту работу предоставляли компилятору, указывая auto. Оно означает, что тип я писать не хочу: компилятор умный, он поймёт сам, что нужно поставить вместо auto. Но какой-то тип там есть функция же должна что-то возвращать.

Оказывается, тут не всё так просто. Это не bool, как для простых операций сравнения. Здесь сразу три варианта. Эти варианты разные виды упорядочивания:

  • std::strong_ordering. Линейный порядок, равные элементы которого неразличимы. Примеры: int, char, string.
  • std::weak_ordering. Линейный порядок, равные могут быть различимы. Примеры: string, сравниваемый без учёта регистра; порядок на точках плоскости, определяемый удалённостью от центра.
  • std::partial_ordering. Частичный порядок. Примеры: float, double, порядок по включению на объектах set.

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

При частичном порядке элементы могут быть несравнимы. Числа с плавающей запятой float и double подпадают под понятие частичного порядка, потому что у них есть специальное значение NaN, не сравнимое ни с каким другим числом.

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

Статус




Космический корабль уже есть везде, и им можно пользоваться:

  • GCC. Хорошо поддерживается с версии 10, хотя и не до конца. Полную поддержку обещают только в GCC 11.
  • Clang. Полная поддержка в версии 10.
  • Visual Studio. Полная поддержка в VS 2019.

Заключение


Во время трансляции мы опросили аудиторию, нравится ли ей эта функция. Результаты опроса:

  • Суперфича 47 (87.04%)
  • Так себе фича 2 (3.70%)
  • Пока неясно 5 (9.26%)

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

Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.
Подробнее..

Стандарт C20 обзор новых возможностей C. Часть 3 Концепты

12.05.2021 14:11:23 | Автор: admin


25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным и растянулся на почти 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

  1. Модули и краткая история C++.
  2. Операция космический корабль.
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

Это третья часть, рассказывающая о концептах и ограничениях в современном C++.

Концепты




Мотивация


Обобщённое программирование ключевое преимущество C++. Я знаю не все языки, но ничего подобного и на таком уровне не видел.

Однако у обобщённого программирования в C++ есть огромный минус: возникающие ошибки это боль. Рассмотрим простую программу, которая сортирует вектор. Взгляните на код и скажите, где в нём ошибка:

#include <vector>#include <algorithm>struct X {    int a;};int main() {    std::vector<X> v = { {10}, {9}, {11} };    // сортируем вектор    std::sort(v.begin(), v.end());}

Я определил структуру X с одним полем int, наполнил вектор объектами этой структуры и пытаюсь его отсортировать.

Надеюсь, вы ознакомились с примером и нашли ошибку. Оглашу ответ: компилятор считает, что ошибка в стандартной библиотеке. Вывод диагностики занимает примерно 60 строк и указывает на ошибку где-то внутри вспомогательного файла xutility. Прочитать и понять диагностику практически невозможно, но программисты C++ делают это ведь пользоваться шаблонами всё равно нужно.



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

  • сложно,
  • не всегда возможно в принципе.

Сформулируем первую проблему обобщённого программирования на C++: ошибки при использовании шаблонов совершенно нечитаемые и диагностируются не там, где сделаны, а в шаблоне.

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

Задачу можно решить хаком SFINAE, написав две функции. Хак использует std::enable_if. Это специальный шаблон в стандартной библиотеке, который содержит ошибку в случае если условие не выполнено. При инстанцировании шаблона компилятор отбрасывает декларации с ошибкой:

#include <type_traits>template <class T>T Abs(T x) {    return x >= 0 ? x : -x;}// вариант для чисел с плавающей точкойtemplate<class T>std::enable_if_t<std::is_floating_point_v<T>, bool>AreClose(T a, T b) {    return Abs(a - b) < static_cast<T>(0.000001);}// вариант для других объектовtemplate<class T>std::enable_if_t<!std::is_floating_point_v<T>, bool> AreClose(T a, T b) {    return a == b;}

В C++17 такую программу можно упростить с помощью if constexpr, хотя это сработает не во всех случаях.

Или ещё пример: я хочу написать функцию Print, которая печатает что угодно. Если ей передали контейнер, она напечатает все элементы, если не контейнер напечатает то, что передали. Мне придётся определить её для всех контейнеров: vector, list, set и других. Это неудобно и неуниверсально.

template<class T>void Print(std::ostream& out, const std::vector<T>& v) {    for (const auto& elem : v) {        out << elem << std::endl;    }}// тут нужно определить функцию для map, set, list, // deque, arraytemplate<class T>void Print(std::ostream& out, const T& v) {    out << v;}

Здесь SFINAE уже не поможет. Вернее, поможет, если постараться, но постараться придётся немало, и код получится монструозный.

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

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

Что у других


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

class Eq a where(==) :: a -> a -> Bool(/=) :: a -> a -> Bool

Это пример класса типов, который требует поддержки операции равно и не равно, выдающих Bool. В C++ то же самое будет реализовано так:

template<typename T>concept Eq =    requires(T a, T b) {        { a == b } -> std::convertible_to<bool>;        { a != b } -> std::convertible_to<bool>;    };

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

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

Пример


Дополним код программы, в которой вы недавно искали ошибку:

#include <vector>#include <algorithm>#include <concepts>template<class T>concept IterToComparable =     requires(T a, T b) {        {*a < *b} -> std::convertible_to<bool>;    };    // обратите внимание на IterToComparable вместо слова classtemplate<IterToComparable InputIt>void SortDefaultComparator(InputIt begin, InputIt end) {    std::sort(begin, end);}struct X {    int a;};int main() {    std::vector<X> v = { {10}, {9}, {11} };    SortDefaultComparator(v.begin(), v.end());}

Здесь мы создали концепт IterToComparable. Он показывает, что тип T это итератор, причём указывающий на значения, которые можно сравнивать. Результат сравнения что-то конвертируемое к bool, к примеру сам bool. Подробное объяснение чуть позже, пока что можно не вникать в этот код.

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

Концепт использовали вместо слова class или typename в конструкции с template. Раньше было template<class InputIt>, а теперь слово class заменили на имя концепта. Значит, параметр InputIt должен удовлетворять ограничению.

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

  • Что случилось? Вызов функции с невыполненным ограничением.
  • Какое ограничение не удовлетворено? IterToComparable<InputIt>
  • Почему? Выражение ((* a) < (* b)) некорректно.


Вывод компилятора читаемый и занимает 16 строк вместо 60.

main.cpp: In function 'int main()':main.cpp:24:45: error: **use of function** 'void SortDefaultComparator(InputIt, InputIt) [with InputIt = __gnu_cxx::__normal_iterator<X*, std::vector<X> >]' **with unsatisfied constraints**   24 |     SortDefaultComparator(v.begin(), v.end());      |                                             ^main.cpp:12:6: note: declared here   12 | void SortDefaultComparator(InputIt begin, InputIt end) {      |      ^~~~~~~~~~~~~~~~~~~~~main.cpp:12:6: note: constraints not satisfiedmain.cpp: In instantiation of 'void SortDefaultComparator(InputIt, InputIt) [with InputIt = __gnu_cxx::__normal_iterator<X*, std::vector<X> >]':main.cpp:24:45:   required from heremain.cpp:6:9:   **required for the satisfaction of 'IterToComparable<InputIt>'** [with InputIt = __gnu_cxx::__normal_iterator<X*, std::vector<X, std::allocator<X> > >]main.cpp:7:5:   in requirements with 'T a', 'T b' [with T = __gnu_cxx::__normal_iterator<X*, std::vector<X, std::allocator<X> > >]main.cpp:8:13: note: the required **expression '((* a) < (* b))' is invalid**, because    8 |         {*a < *b} -> std::convertible_to<bool>;      |          ~~~^~~~main.cpp:8:13: error: no match for 'operator<' (operand types are 'X' and 'X')

Добавим недостающую операцию сравнения в структуру, и программа скомпилируется без ошибок концепт удовлетворён:

struct X {    auto operator<=>(const X&) const = default;    int a;};

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

#include <type_traits>template <class T>T Abs(T x) {    return x >= 0 ? x : -x;}// вариант для чисел с плавающей точкойtemplate<class T>requires(std::is_floating_point_v<T>)bool AreClose(T a, T b) {    return Abs(a - b) < static_cast<T>(0.000001);}// вариант для других объектовtemplate<class T>bool AreClose(T a, T b) {    return a == b;}

Модифицируем и функцию печати. Если вызов a.begin() и a.end() допустим, будем считать a контейнером.

#include <iostream>#include <vector>template<class T>concept HasBeginEnd =     requires(T a) {        a.begin();        a.end();    };template<HasBeginEnd T>void Print(std::ostream& out, const T& v) {    for (const auto& elem : v) {        out << elem << std::endl;    }}template<class T>void Print(std::ostream& out, const T& v) {    out << v;}

Опять же, это неидеальный пример, поскольку контейнер не просто что-то с begin и end, к нему предъявляется ещё масса требований. Но уже неплохо.

Лучше всего использовать готовый концепт, как is_floating_point_v из предыдущего примера. Для аналога контейнеров в стандартной библиотеке тоже есть концепт std::ranges::input_range. Но это уже совсем другая история.

Теория


Пришло время понять, что такое концепт. Ничего сложного тут на самом деле нет:

Концепт это имя для ограничения.

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

Ограничение это шаблонное булево выражение.

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

Самое простое ограничение это true. Ему удовлетворяет любой тип.

template<class T> concept C1 = true;

Для ограничений доступны булевы операции и комбинации других ограничений:

template <class T>concept Integral = std::is_integral<T>::value;template <class T>concept SignedIntegral = Integral<T> &&                         std::is_signed<T>::value;template <class T>concept UnsignedIntegral = Integral<T> &&                           !SignedIntegral<T>;

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

template<typename T>constexpr bool get_value() { return T::value; } template<typename T>    requires (sizeof(T) > 1 && get_value<T>())void f(T); // #1 void f(int); // #2 void g() {    f('A'); // вызывает #2.}

И список возможностей этим не исчерпывается.

Для ограничений есть отличная возможность: проверка корректности выражения того, что оно компилируется без ошибок. Посмотрите на ограничение Addable. В скобках написано a + b. Условия ограничения выполняются тогда, когда значения a и b типа T допускают такую запись, то есть T имеет определённую операцию сложения:

template<class T>concept Addable =requires (T a, T b) {    a + b;};

Более сложный пример вызов функций swap и forward. Ограничение выполнится тогда, когда этот код скомпилируется без ошибок:

template<class T, class U = T>concept Swappable = requires(T&& t, U&& u) {    swap(std::forward<T>(t), std::forward<U>(u));    swap(std::forward<U>(u), std::forward<T>(t));};

Ещё один вид ограничений проверка корректности типа:

template<class T> using Ref = T&;template<class T> concept C =requires {    typename T::inner;     typename S<T>;         typename Ref<T>;   };

Ограничение может требовать не только корректность выражения, но и чтобы тип его значения чему-то соответствовал. Здесь мы записываем:

  • выражение в фигурных скобках,
  • ->,
  • другое ограничение.

template<class T> concept C1 =requires(T x) {    {x + 1} -> std::same_as<int>;};

Ограничение в данном случае same_as<int>
То есть тип выражения x + 1 должен быть в точности int.

Обратите внимание, что после стрелки идёт ограничение, а не сам тип. Посмотрите ещё один пример концепта:

template<class T> concept C2 =requires(T x) {    {*x} -> std::convertible_to<typename T::inner>;    {x * 1} -> std::convertible_to<T>;};

В нём два ограничения. Первое указывает, что:

  • выражение *x корректно;
  • тип T::inner корректен;
  • тип *x конвертируется к T::inner.

В одной строчке целых три требования. Второе указывает, что:

  • выражение x * 1 синтаксически корректно;
  • его результат конвертируется к T.

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

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

Ограничение для функции можно написать в трёх разных местах:

// Вместо слова class или typename в шаблонную декларацию.// Поддерживаются только концепты.template<Incrementable T>void f(T arg);// Использовать ключевое слово requires. В таком случае их можно вставить // в любое из двух мест.// Годится даже неименованное ограничение.template<class T>requires Incrementable<T>void f(T arg);template<class T>void f(T arg) requires Incrementable<T>;

И есть ещё четвёртый способ, который выглядит совсем магически:

void f(Incrementable auto arg);

Тут использован неявный шаблон. До C++20 они были доступны только в лямбдах. Теперь можно использовать auto в сигнатурах любых функций: void f(auto arg). Более того, перед этим auto допустимо имя концепта, как в примере. Кстати, в лямбдах теперь доступны явные шаблоны, но об этом позже.

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

Для класса возможностей меньше всего два способа. Но этого вполне хватает:

template<Incrementable T>class X {};template<class T>requires Incrementable<T>class Y {};

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

template<class T> void ReadAndFill(T& container, int size) {     if constexpr (requires {container.reserve(size); }) {         container.reserve(size);     }    // заполняем контейнер }

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

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

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

template<class T, class U>concept Derived = std::is_base_of<U, T>::value; template<Derived<Other> X>void f(X arg);

У концепта Derived два шаблонных параметра. В декларации f один из них я указал, а второй класс X, который и проверяется. Аудитории был задан вопрос, какой параметр я указал: T или U; получилось Derived<Other, X> или Derived<X, Other>?

Ответ неочевиден: это Derived<X, Other>. Указывая параметр Other, мы указали второй шаблонный параметр. Результаты голосования разошлись:

  • правильных ответов 8 (61.54%);
  • неправильных ответов 5 (38.46%).

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

Итак, я рассказал как определять новые концепты, но это нужно не всегда в стандартной библиотеке их уже предостаточно. На этом слайде приведены концепты, которые находятся в заголовочном файле <concepts>.



Это ещё не всё: концепты для проверки разных типов итераторов есть в <iterator>, <ranges> и других библиотеках.



Статус




Концепты есть везде, но в Visual Studio пока что не полностью:

  • GCC. Хорошо поддерживается с версии 10;
  • Clang. Полная поддержка в версии 10;
  • Visual Studio. Поддерживается VS 2019, но не полностью реализован requires.

Заключение


Во время трансляции мы опросили аудиторию, нравится ли ей эта функция. Результаты опроса:

  • Суперфича 50 (92.59%)
  • Так себе фича 0 (0.00%)
  • Пока неясно 4 (7.41%)

Подавляющее большинство проголосовавших оценило концепты. Я тоже считаю это крутой фичей. Спасибо Комитету!

Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.
Подробнее..

Стандарт C20 обзор новых возможностей C. Часть 4 Ranges

19.05.2021 14:18:09 | Автор: admin


25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас, и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся почти на 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

  1. Модули и краткая история C++.
  2. Операция космический корабль.
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

Это четвёртая часть, рассказывающая о новом модуле стандартной библиотеки, Ranges.

Ranges


До этого рассматривалось ядро языка. Теперь я расскажу про изменение в стандартной библиотеке, которое не добавляет нового в синтаксис, введённый C++20 заголовочный файл <ranges>.



Мотивация


В C++ много стандартных алгоритмов. Но они применимы не всегда. Если действие нестандартное, то для его решения, скорее всего, придётся вводить некрасивые вложенные циклы, флаги. При этом код лавинообразно теряет выразительность.

Слушателям предлагался вопрос. Сколько строк выведет код ниже?

  1. 5
  2. 6
  3. 9
  4. Ни одной.
  5. Будет выводить, пока не остановим.

Подумайте, прежде чем прочитать ответ.

#include <iostream>int main() {    const int days = 3;   // количество дней с играми    const int games = 2;  // количество игр с питомцем в день    for (int i = 0; i < days; i++) {        std::cout << "День " << i << std::endl;        for (int j = 0; j < games; i++) {            std::cout << "  Игра " << j << std::endl;        }    }}

Во время вебинара правильных ответов оказалось больше, чем я ожидал. Это тест на внимательность, потому что в коде есть ошибка: цикл никогда не завершится. Проблема в том, что я перепутал инкрементируемую переменную во внутреннем цикле. Допустить такую ошибку куда проще, чем её заметить. Если вы ни разу так не ошибались, вряд ли вы программируете на C/C++.

Другой пример. Стандартные алгоритмы принимают два итератора: на первый элемент и на последний, фиктивный. Но передавать пары утомительно. Например, алгоритм merge, который смешивает два отсортированных диапазона, требует сразу пять итераторов две пары и ещё один выходной. Приходится дублировать имя контейнера. Вполне возможно, что контейнер назван не одной буквой, а длинным именем или выражением:

std::merge(  collection_holder.get_left_collection().begin(),   collection_holder.get_left_collection().end(),   collection_holder.get_right_collection().begin(),   collection_holder.get_right_collection().end(),  std::back_inserter(    collection_holder.get_destination_collection()  ));

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

Ещё один пример для мотивации. Для стандартных действий есть алгоритм, скажем, copy_if, который копирует элементы, удовлетворяющие условию. Он используется для фильтрации элементов. Алгоритм transform применяет к каждому элементу функцию. Предположим, нужно выполнить обе операции: отфильтровать элементы и к оставшимся применить функцию. Такой алгоритм можно назвать transform_if, но к сожалению, в стандартной библиотеке его нет.

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

std:vector<int> numbers_in;std:vector<int> numbers_out;// задача: поделить на 2 все чётные числа из numbers_in// и записать результаты numbers_outstd:vector<int> intermediate;// скопируем в intermediate только чётные числаstd::copy_if(numbers_in.begin(), numbers_in.end(),               std::back_inserter(intermediate),              [](int x) {                        return x % 2 == 0;             });// поделим их на 2std::transform(intermediate.begin(), intermediate.end(),               std::back_inserter(numbers_out),               [](int x) {                          return x / 2;               })

Тут пришлось завести промежуточное хранилище. Получили неэффективное решение с двумя проходами по элементам.

Что у других


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

Пример с transform_if сокращается до одной строки:

# transform_if в одну строку средствами языка:numbers_out = [x // 2 for x in numbers_in if x % 2 == 0]

Обычные циклы упрощаются в Python. Пример, в котором я допустил ошибку, мог бы выглядеть так:

days = 3   # количество дней с играмиgames = 2  # количество игр с питомцем в деньfor i in range(days):    print("День %d" % i)    for j in range(games):        print("  Игра %d" % j)

В Python не нужно писать i = 0; i < N; ++i. Буква i набирается один раз, и возможности перепутать что-либо у вас нет. Кстати говоря, range даёт не контейнер, который содержит все элементы, а генерирует числа на лету. По производительности это если и будет уступать обычному циклу, то едва-едва.

Приведу преимущества Python списком:

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

Другой пример SQL. Хотя это не язык программирования, мы можем реализовать transform_if даже на нём:

SELECT n / 2 FROM tab WHERE n % 2 == 0;

Код получается вполне выразительным.

Примеры


В C++20 появилась возможность решать проблемы, озвученные в начале статьи, это библиотека Ranges. Например, так выглядят циклы привычные for, которые перебирают все целые числа в некотором интервале:

#include <iostream>#include <ranges>namespace rng = std::ranges;namespace view = rng::views;int main() {    const int days = 3;   // количество дней с играми    const int games = 2;  // количество игр с питомцем в день    for (int i : view::iota(0, days)) {        std::cout << "День " << i << std::endl;        for (int j : view::iota(0, games)) {            std::cout << "  Игра " << j << std::endl;        }    }}

Теперь не придётся три раза писать i, три раза писать j, и нельзя их перепутать. Вместо привычных циклов с инкрементом итерируемся по std::ranges::views::iota. Как и в Python, функция не будет генерировать контейнер, а выдаст числа на лету.

Range упрощает реализацию transform_if:

#include <iostream>#include <ranges>#include <vector>namespace rng = std::ranges;namespace view = rng::views;int main() {    auto even = [](int i) { return i % 2 == 0; };    auto half = [](int i) { return i / 2; };        std::vector<int> numbers_in = {100, 55, 80, 2, -1};    auto numbers_out = numbers_in | view::filter(even) |  // <-- вся магия                 view::transform(half);        for (auto i : numbers_out) {        std::cout << i << std::endl; // 50, 40, 1    }}

Здесь различные преобразования комбинированы операцией |, или как её ещё называют, pipe. filter, отфильтровывающий нечётные числа, комбинирован с transform, делящим на два. Получаем объект-диапазон, по которому можно итерировать. Этот объект ленивый: он не будет ничего вычислять, пока вы не запросите первое значение. Более того, вернув первое значение, диапазон не будет вычислять второе до тех пор, пока оно не будет запрошено. Это позволяет не хранить в памяти все значения единовременно и применять алгоритмы в один проход. Замечательная функция.

Но если всё же вам необходимо вычислить всё сразу, вы можете по старинке сложить все элементы диапазона в какой-нибудь контейнер: numbers_vector = std::vector(number_out.begin(), numbers_out.end()).

На этом слайде приведено сравнение, как это пишется в C++20, а как в Python. Видно, что Python по-прежнему лаконичнее, но всё-таки C++ уже гораздо ближе.



Такие возможности появились в C++20, и это классно. Разумный вопрос: а что с производительностью?

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

В варианте с вызывается функция iota. Каждую итерацию будет что-то вычисляться. Возможно, будет оверхед.

Я сделал бенчмарк для сравнения цикла в старом стиле с itoa. Его результат на слайде:



Различия в производительности нет! Оказывается, что цикл с itoa настолько же эффективен, как и простой с инкрементом переменной.

При добавлении сложного синтаксического сахара производительность часто падает. Но только не в C++.

Ещё один бенчмарк, в котором сравнивается transform_if в старой реализации и в новой на основе <ranges>.



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

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

Ещё примеры


Это было только начало. В Ranges ещё масса интересного.

Вот код, который сортирует элементы вектора и выводит их:

#include <iostream>#include <vector>#include <algorithm>#include <ranges>namespace rng = std::ranges;template <rng::input_range Range>void Print(const Range& range) {    std::cout << "Elements:";    for (const auto& x : range) {        std::cout << ' ' << x;    }    std::cout << std::endl;}int main() {    std::vector v = { 4, 1, 7, 2, 3, 8 };    rng::sort(v);    Print(v); // Elements: 1 2 3 4 7 8    return 0;}

Заметьте, что здесь применяется не обычный алгоритм std::sort, а sort из пространства имён std::ranges. В этот алгоритм можно передать не пару итераторов, а контейнер то, чего и хотелось.

Я написал функцию Print, которая использует возможности концептов. Используется концепт input_range из пространства имён std::ranges. Он нужен в шаблонной функции для того, чтобы она принимала только объекты-диапазоны с точки зрения Ranges.

В C++20 этот код можно упростить:

void Print(const rng::input_range auto& range) {    std::cout << "Elements:";    for (const auto& x : range) {        std::cout << ' ' << x;    }    std::cout << std::endl;}

Слово template убирается и становится неявным. А const auto& это тип параметра, к нему применён концепт input_range.

Ещё одно заметное преимущество новых алгоритмов в библиотеке <ranges> это параметр проекции. Предположим, что вам требуется написать сортировку по некоторому полю объекта:

struct Lecture {    int course;    int local_idx;    int complexity;};std::vector<Lecture> ReadLectures();int main() {    std::vector<Lecture> lectures = ReadLectures();    // как раньше    std::sort(lectures.begin(), lectures.end(),          [](const Lecture& lhs, const Lecture& rhs) {            return lhs.complexity < rhs.complexity;    });    return 0;}

Пришлось разработать свой компаратор и дублировать название поля complexity. В <ranges> добавлены новые реализации старых алгоритмов, которые помимо передачи диапазона допускают параметр-проекцию. Такой алгоритм применит заданную вами функцию и сравнит её результаты вместо сравнения элементов контейнера напрямую:

struct Lecture {    int course;    int local_idx;    int complexity;};std::vector<Lecture> ReadLectures();namespace rng = std::ranges;int main() {    std::vector<Lecture> lectures = ReadLectures();    // как теперь    rng::sort(lectures, std::less<>{}, [](const Lecture& x) {        return x.complexity;    });    return 0;}

В качестве компаратора взят обычный std::less, который сравнивает, применяя операцию <, но благодаря проекции применяется он не к элементам вектора, а к значениям лямбда-функции.

Теория


На этом слайде приведены новые алгоритмы из пространства имён std::ranges. Работа Комитета впечатляет: 85 алгоритмов, каждый из которых тщательно проработан.



Все они могут принимать диапазоны. Там, где используется компаратор, возможны параметры проекции.

Вернёмся к примеру с transform_if.

auto range = numbers_in | view::filter(even) |             view::transform(half);

Мы отфильтровали вектор numbers_in и применили к его элементам функцию. Имена filter и transform примеры адаптеров, то есть таких объектов Ranges, которые меняют диапазон.

В библиотеке есть несколько видов адаптеров. Адаптер drop отбрасывает элементы, а take ограничивает их количество. Предположим, нам нужны элементы с 5-го по 14-й. На SQL это было бы сделано так:

SELECT * FROM tab LIMIT 10 OFFSET 5

На C++ теперь можно сделать похожим образом:

using namespace view = rng::views;for (const auto& x : tab | view::drop(5) | view::take(10)) {    std::cout << x << std::endl;}

Адаптеров в стандартной библиотеке не так много, как алгоритмов, а жаль. Вот список всех: all, filter, transform, take, take_while, drop, drop_while, join, split, counted, common, reverse, elements, keys, values.

Зато можно создавать свои! Этот пример кода я не буду разбирать подробно. Посмотрите, как просто мы создали адаптеры для выделения разных категорий пользователей и комбинировали их:

auto ByGender(Gender gender) {    return view::filter([gender](const Person& person) {        return person.gender == gender;     });}auto ByEmployment(bool is_employed) {    return view::filter([is_employed](const Person& person) {        return person.is_employed == is_employed;     });}template <rng::ForwardRange Range>AgeStats ComputeStats(Range&& persons) {    auto females = ByGender(Gender::FEMALE);    auto males = ByGender(Gender::MALE);      auto employed = ByEmployment(true);    auto unemployed = ByEmployment(false);    return {        ComputeMedianAge(persons),        ComputeMedianAge(persons | females),        ComputeMedianAge(persons | males),            ComputeMedianAge(persons | females | unemployed),        ComputeMedianAge(persons | males | employed)     };}

Статус




Ranges это нововведение стандартной библиотеки. У каждого компилятора есть своя родная реализация.

  • К сожалению, в библиотеке Clang диапазоны пока не реализованы.
  • В библиотеке Visual Studio 2019 присутствует поддержка <ranges>, но частично. Например, нет некоторых стандартных алгоритмов.
  • В библиотеке GCC уже всё хорошо, и <ranges> можно использовать смело.

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

Заключение


Во время вебинара мы спросили у аудитории, нравится ли эта функция. Результаты опроса:

  • Суперфича 21 (80,77%)
  • Так себе фича 2 (7,69%)
  • Пока неясно 3 (11,54%)

Больше всего разочаровывает в Ranges небольшое количество стандартных адаптеров. Например, в Python есть функция enumerate, которая к элементу приписывает его порядковый номер. Там есть возможность итерироваться по декартову произведению парам элементов разных диапазонов. Есть zip, при котором итерирование будет происходить по диагонали декартова произведения. Ничего подобного в C++ я не нашёл. Будем надеяться на новые выпуски Стандарта.
Их добавляют в C++23.
Антон Полухин
Но в целом мне диапазоны нравятся, хотя я понимаю, почему некоторые зрители вебинара не оценили их. Наверное, многим это нововведение кажется сложным. Но мы привыкнем ведь пишем на C++ и разбирались и не с таким.

Одно могу сказать точно: Ranges изменит стиль программирования очень многих вещей. И это здорово, потому что код станет более понятным, а программы более надёжными!

Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.
Подробнее..

Стандарт C20 обзор новых возможностей C. Часть 6 Другие фичи ядра и стандартной библиотеки. Заключение

08.06.2021 16:18:29 | Автор: admin


25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся на почти 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

  1. Модули и краткая история C++.
  2. Операция космический корабль.
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

Это шестая, заключительная часть. Она рассказывает о других нововведениях ядра и стандартной библиотеки, добавленных Стандартом C++20.

Другие фичи ядра


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

Шаблоны


Многое из нововведений Стандарта касается шаблонов. В C++ шаблонные аргументы делятся на два вида: типовые и нетиповые. Типовые названы так неспроста: их значение тип данных, например int, контейнер, ваш класс, указатель. Нетиповые шаблонные аргументы это обычные значения, вычисляемые на этапе компиляции, например число 18 или true. В C++17 возможности нетиповых шаблонных параметров были сильно ограничены. Это могло быть числовое значение, bool, enum-тип или указатель. C++20 расширил список и позволил передавать в качестве шаблонного аргумента объект пользовательского класса или структуры. Правда, объект должен удовлетворять ряду ограничений.

Пример:

struct T {    int x, y;};template<T t>int f() {    return t.x + t.y;}int main() {    return f<{1,2}>();}

Статус:
GCC , CLANG , VS 


Ещё из нового крутые возможности вывода шаблонного типа класса. Не буду комментировать, просто оставлю пример. Кому интересно разберитесь:

template<class T> struct B {    template<class U> using TA = T;    template<class U> B(U, TA<U>);  //#1}; B b{(int*)0, (char*)0}; // OK, выведен B<char*>


Статус:
GCC , CLANG , VS 


Лямбды


Раньше в лямбда-функциях было три вида скобок. Теперь их может быть четыре стало возможным писать явные параметры для лямбда-функций:

  1. квадратные скобки для переменных связывания,
  2. угловые скобки для шаблонных параметров,
  3. круглые скобки для списка аргументов,
  4. фигурные скобки для тела функции.

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

int main() {    auto lambda = []<class T>(T x, T y){                                return x * y - x - y; };    std::cout << lambda(10, 12);}


Статус:
GCC , CLANG , VS 


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

int main() {    using LambdaPlus3 = decltype([](int x) {    return x + 3;    });    LambdaPlus3 l1;    auto l2 = l1;    std::cout << l2(10) << std::endl; // 13}

Статус:
GCC , CLANG , VS 

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

Compile-time


Появился новый вид переменных constinit. Это некоторая замена static-переменным. Локальные static-переменные могут быть проинициализированы во время первого обращения, что иногда нежелательно. А вот constinit инициализируются по-настоящему статически. Если компилятор не смог проинициализировать такую переменную во время компиляции, код не соберётся. По Стандарту constinit-переменная может быть также thread_local.

const char* g() { return "dynamic initialization"; }constexpr const char* f(bool p) { return p ? "constant "      "initializer" : g(); } constinit const char* c = f(true); // OKint main() {    static constinit int x = 3;    x = 5; // OK}

Удивительно, но constinit-переменная не обязана быть константной. Константным и constexpr обязан быть её инициализатор.

Статус:
GCC , CLANG , VS 


И у функций тоже есть новый вид consteval. Это функции, которые вычисляется исключительно во время компиляции. В отличие от constexpr, которые могут вызываться как в run-time, так и в compile-time, эти функции не получится даже вызвать во время выполнения, будет ошибка.

consteval int sqr(int n) {    return n * n;}constexpr int r = sqr(100);  // OK int x = 100;int r2 = sqr(x); // <-- ошибка: результат не константаconsteval int sqrsqr(int n) {    return sqr(sqr(n));}constexpr int dblsqr(int n) { return 2*sqr(n); } // <-- ошибка, constexpr может                                                  // вычисляться в run-time

Многие слушатели вебинара не поняли смысла этой фичи. Зачем нужен ещё один constexpr. Но представьте действие, которое имеет смысл только во время компиляции. Например, вы положили в проект файлы ресурсов и во время компиляции их читаете. Пока что так делать нельзя, поэтому просто представьте. В run-time этих ресурсов уже не будет.

Статус:
GCC , CLANG , VS 

В C++20 очень существенно расширили сам constexpr, внутри него теперь можно вызывать даже виртуальные функции! А ещё, что особенно замечательно, многие контейнеры стали поддерживать constexpr.

Новые синтаксические конструкции


Мы ждали их ещё с 1999 года. Всё это время программисты на чистом C дразнили нас, демонстрируя, как они могут ими пользоваться, а мы нет. Вы уже поняли, о чём речь? Конечно, о designated initializers, или обозначенных инициализаторах!

Теперь, конструируя объект структуры, можно явно написать, какому полю присваивается какое значение. Это очень круто. Ведь для структур с 1015 полями понять, что чему соответствует, практически невозможно. А такие структуры я видел. Кроме того, поля можно пропускать. Но вот порядок менять нельзя. И нельзя ещё несколько вещей, которые можно в C. Они приведены в примере.

struct A { int x; int y; int z; }; A b{ .x = 1, .z = 2 };A a{ .y = 2, .x = 1 }; // <-- ошибка  нарушен порядокstruct A { int x, y; };struct B { struct A a; };int arr[3] = {[1] = 5};     // <-- ошибка  массив   struct B b = {.a.x = 0};    // <-- ошибка  вложенныйstruct A a = {.x = 1, 2};   // <-- ошибка  два вида инициализаторов

Статус:
GCC , CLANG , VS 

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

#include <vector>#include <string>#include <iostream>int main() {    using namespace std::literals;    std::vector v{"a"s, "b"s, "c"s};    for(int i=0; const auto& s: v) {        std::cout << (++i) << " " << s << std::endl;    }}

Статус:
GCC , CLANG , VS 

Новая конструкция using enum. Она делает видимыми без квалификации все константы из enum-а:

enum class fruit { orange, apple };enum class color { red, orange };void f() {    using enum fruit; // OK    using enum color; // <-- ошибка  конфликт}

Статус:
GCC , CLANG , VS 


Другое


  • Операция запятая внутри [] объявлена как deprecated. Намёк на что-то интересное в будущем.
GCC , CLANG , VS 
  • Запрет некоторых действий с volatile-переменными. Эти операции тоже объявлены как deprecated. Переменные volatile сейчас часто используются не по назначению. Видимо, комитет решил с этим бороться.
GCC , CLANG , VS 
  • Агрегатные типы можно инициализировать простыми круглыми скобками ранее были допустимы только фигурные.
GCC , CLANG , VS 
  • Появился уничтожающий оператор delete, который не вызывает деструктор, а просто освобождает память.
GCC , CLANG , VS 
  • Слово typename в некоторых случаях можно опускать. Лично меня нервировала необходимость писать его там, где оно однозначно нужно.
GCC , CLANG , VS 
  • Теперь типы char16_t и char32_t явно обозначают символы в кодировках, соответственно UTF-16 и UTF-32. Ещё добавили новый тип char8_t для UTF-8.
GCC , CLANG , VS 
  • Различные технические нововведения. Если я что-то упустил пишите в комменты.

Другие фичи стандартной библиотеки


Это были нововведения ядра, то есть то, что меняет синтаксис самого C++. Но ядро даже не половина Стандарта. Мощь C++ также в его стандартной библиотеке. И в ней тоже огромное число нововведений.

  • В <chrono> наконец-то добавлены функции работы с календарём и часовыми поясами. Появились типы для месяца, дня, года, новые константы, операции, функции для форматирования, конвертации часовых поясов и много магии. Этот пример из cppreference оставлю без комментариев:

#include <iostream>#include <chrono>using namespace std::chrono; int main() {    std::cout << std::boolalpha;     // standard provides 2021y as option for std::chrono::year(2021)    // standard provides 15d as option for std::chrono::day(15)     constexpr auto ym {year(2021)/8};    std::cout << (ym == year_month(year(2021), August)) << ' ';     constexpr auto md {9/day(15)};    std::cout << (md == month_day(September, day(15))) << ' ';     constexpr auto mdl {October/last};    std::cout << (mdl == month_day_last(month(10))) << ' ';     constexpr auto mw {11/Monday[3]};    std::cout << (mw == month_weekday(November, Monday[3])) << ' ';     constexpr auto mwdl {December/Sunday[last]};    std::cout << (mwdl == month_weekday_last(month(12), weekday_last(Sunday))) << ' ';     constexpr auto ymd {year(2021)/January/day(23)};    std::cout << (ymd == year_month_day(2021y, month(January), 23d)) << '\\n';}// вывод: true true true true true true


Статус:
GCC , CLANG , VS 


  • Целая новая библиотека format. Про неё также можно послушать в докладе у Антона Полухина. Теперь в C++ есть современная функция для форматирования строк с плейсхолдерами. Библиотека позволит делать подобную магию:

auto s1 = format("The answer is {}.", 42); // s1 == "The answer is 42."auto s2 = format("{1} from {0}", "Russia", "Hello"); // s2 == "Hello from Russia"int width = 10;int precision = 3;auto s3 = format("{0:{1}.{2}f}", 12.345678, width, precision);// s3 == "    12.346"

Она обещает быть куда более производительной, чем вывод в поток stringstream, но тем не менее не лишена проблем. Первая проблема: format не проверяет все ошибки, и вообще, на данном этапе не разбирает формат в compile-time. Это очень огорчает.
Сейчас в комитете хотят это поправить с бэкпортированием функционала в C++20.
Антон Полухин
Вторая проблема: пока не реализован ни в одной стандартной библиотеке, проверить в деле нельзя.

За время, прошедшее с вебинара, format успели реализовать в Visual Studio.

Статус:
GCC , CLANG , VS 


  • Потрясающие новости: теперь в Стандарте есть . Помимо него добавлено обратное , число Эйлера, логарифмы некоторых чисел, корни и обратные корни, корень из вместе с обратным, постоянная Эйлера Маскерони и золотое сечение. Все они доступны при подключении <numbers> в пространстве имён std::numbers.

Статус:
GCC , CLANG , VS 

  • Новые алгоритмы: shift_left и shift_right. Они сдвигают элементы диапазона на заданное число позиций. При этом закручивания не происходит: элементы, уходящие на край, не телепортируются в другой конец, а уничтожаются. С другого края возникают пустые элементы из них был сделан move.

Статус:
GCC , CLANG , VS 

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

Статус:
GCC , CLANG , VS 

  • Ещё одна несложная функция in_range. Она позволяет проверить, представимо ли целое число значением другого типа:

#include <utility>#include <iostream> int main() {    std::cout << std::boolalpha;     std::cout << std::in_range<std::size_t>(-1)               << '\\n'; // false, так как отрицательные числа не представимы в size_t    std::cout << std::in_range<std::size_t>(42)               << '\\n'; // true}

Статус:
GCC , CLANG , VS 

  • make_shared стал поддерживать создание массивов.

Статус:
GCC , CLANG , VS 

  • Добавились операции сравнения для неупорядоченных контейнеров unordered_map и unordered_set.

Статус:
GCC , CLANG , VS 

  • Новая функция std::to_array делает array из C-массива или строкового литерала.

Статус:
GCC , CLANG , VS 

  • В заголовочный файл version добавили макросы для проверки наличия фич стандартной библиотеки. Уже были макросы для фич ядра, но теперь это распространяется и на стандартную библиотеку.

Статус:
GCC , CLANG , VS 

  • Отличное нововведение: многие функции и методы стали constexpr. Теперь его поддерживают контейнеры, такие как string, vector.

Все алгоритмы из <algorithm>, не выделяющие память, стали constexpr. Можно сортировать массивы и делать бинарный поиск на этапе компиляции :)
Антон Полухин

Статус:
GCC , CLANG , VS 

  • Появился новый тип span. Он задаёт указатель и число количество элементов, на которые указывает указатель.

istream& read(istream& input, span<char> buffer) {    input.read(buffer.data(), buffer.size());    return input;}ostream& write(ostream& out, std::span<const char> buffer) {    out.write(buffer.data(), buffer.size());    return out;}std::vector<char> buffer(100);read(std::cin, buffer);

span позволяет заменить два параметра функции одним. Он чем-то похож на string_view это тоже лёгкая оболочка, которая может представлять элементы контейнеров. Но набор допустимых контейнеров больше это может быть любой линейный контейнер: вектор, std::array, string или C-массив. Ещё одно отличие от string_view он позволяет модификацию элементов, если они не константного типа. Важно, что модификацию только самих элементов, но не контейнера.

Статус:
GCC , CLANG , VS 


  • Ещё одно замечательное нововведение файл <bit>. Он добавляет большое количество возможностей для манипуляций с беззнаковыми числами на битовом уровне. Теперь функции вида определить количество единиц в бинарном числе или двоичный логарифм доступны из коробки. Эти функции перечислены на слайде.




Также файл определяет новый тип std::endian. Он, например, позволяет определить, какая система записи чисел используется при компиляции: Little endian или Big endian. А вот функций для их конвертации я, к сожалению, не нашёл. Но в целом считаю, что <bit> очень крутое нововведение.

Статус:
GCC , CLANG , VS 


  • Дождётся тот, кто сильно ждёт! Этот цитатой можно описать многое из Стандарта C++20. Поздравляю всех, мы дождались: у string теперь есть методы starts_with и ends_with для проверки суффиксов и постфиксов. А также другие методы контейнеров и функции, с ними связанные:
    • метод contains для ассоциативных контейнеров. Теперь можно писать my_map.contains(x) вместо my_map.count(x) > 0 и всем сразу понятно, что вам нужно проверить наличие ключа;
    • версии функции std::erase и std::erase_if для разных контейнеров;
    • функция std::ssize для получения знакового размера контейнера.

Статус:
GCC , CLANG , VS 

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

void f(int* p) {   int* p1 = std::assume_aligned<256>(p);}

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

Статус:
GCC , CLANG , VS 


  • Добавился в Стандарт и новый вид потоков osyncstream в файле syncstream. В отличие от всех остальных потоков, osyncstream одиночка: у него нет пары на букву i. И это не случайно. Дело в том, что osyncstream всего лишь обёртка. Посмотрите на код:

#include <thread>#include <string_view>#include <iostream>using namespace std::literals;void thread1_proc() {    for (int i = 0; i < 100; ++i) {        std::cout << "John has "sv << i                   << " apples"sv << std::endl;    }}void thread2_proc() {    for (int i = 0; i < 100; ++i) {        std::cout << "Marry has "sv << i * 100                  << " puncakes"sv << std::endl;    }}int main() {    std::thread t1(thread1_proc);    std::thread t2(thread2_proc);    t1.join(); t2.join();}

В его выводе наверняка будет подобная абракадабра:

Marry has John has 24002 applesJohn has 3 applesJohn has 4 applesJohn has 5 applesJohn has 6 applesJohn has 7 apples puncakesJohn has 8 apples

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

...#include <syncstream>...void thread1_proc() {    for (int i = 0; i < 100; ++i) {        std::osyncstream(std::cout) << "John has "sv << i                                     << " apples"sv << std::endl;    }}void thread2_proc() {    for (int i = 0; i < 100; ++i) {        std::osyncstream(std::cout) << "Marry has "sv << i * 100                                     << " puncakes"sv << std::endl;    }}...


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

Статус:
GCC , CLANG , VS 


  • В Стандарт добавился набор из шести новых функций для сравнения целых чисел:
    • cmp_equal,
    • cmp_not_equal,
    • cmp_less,
    • cmp_greater,
    • cmp_less_equal,
    • cmp_greater_equal.

Пользователь Хабра в комментариях к предыдущей части назвал их чуть ли не главной фичей нового Стандарта. Их особенность в корректной работе с любыми типами аргументов, если это целые числа. Обычные операции сравнения могут давать неадекватный результат, особенно если вы сравниваете знаковое с беззнаковым:

-1 > 0u; // true

По правилам в подобном случае знаковый операнд преобразуется к беззнаковому значению: 0xFFFFFFFFu для 32-битного int. Функция cmp_greater позволит обойти эту особенность и выполнить настоящее математическое сравнение:

std::cmp_greater(-1, 0u); // false

Статус:
GCC , CLANG , VS 

  • Ещё одно нововведение source_location. Это класс, который позволит заменить макросы __LINE__, __FILE__, используемые при логировании. Статический метод current этого класса вернёт объект source_location, который содержит строку и название файла, в котором этот метод был вызван. Тут я задал вопрос. Какое число выведет функция со слайда?



Есть два варианта:

  • число 2, что соответствует строке, где source_location написан;
  • число 7, что соответствует строке, где функция log вызвана.

Правильный ответ 7, где вызвана функция, хоть это и кажется неочевидным. Но именно благодаря этому обстоятельству source_location можно использовать как замену макросам __LINE__, __FILE__ для логирования. Потому что, когда мы логируем, нас интересует не где написана функция log, а откуда она вызвана.

Статус:
GCC , CLANG , VS 


  • Закончим обзор на радостной ноте: в C++ существенно упростили многопоточное программирование.
    • Новый класс counting_semaphore ждём, пока определённое количество раз разблокируют семафор.
    • Классы latch и barrier блокируют, пока определённое количество потоков не дойдёт до определённого места.
    • Новый вид потоков: jthread. Он делает join в деструкторе, не роняя вашу программу. Также jthread поддерживает флаг отмены, через который удобно прерывать выполнение треда stop_token. С этим флагом связаны сразу несколько новых классов.
    • Ещё один новый класс atomic_ref специальная ссылка, блокирующая операции других потоков с объектом.
    • Возможности atomic значительно расширены. Он теперь поддерживает числа с плавающей точкой и умные указатели, а также новые методы: wait, notify_one и notify_all.

Статус:
GCC , CLANG , VS 


Заключение


Рассказ о фичах C++ 20 окончен. Мы рассмотрели все основные изменения, хотя на самом деле в Стандарте ещё много разного и интересного. Но это уже технические особенности, которые не обязательно знать каждому профессиональному программисту.

Вполне возможно, я что-то упустил тогда добро пожаловать в комментарии.
C++ на этом не останавливается, Комитет по стандартизации активно работает. Уже было заседание, посвящённое Стандарту 2023 года. То, что в него уже включили, нельзя назвать киллер-фичами. Но ожидания от нового Стандарта большие. Например, в него войдут контракты и полноценная поддержка корутин.

На Хабре круто встретили предыдущие части. В статьях про Модули и Ranges развернулись особо оживлённые дискуссии. Будет здорово, если вы расскажете о своих впечатлениях от C++20 и ожиданиях от новых стандартов. Например, какую фичу C++20 вы считаете самой крутой и важной? Чего больше всего ждёте от будущего языка? Приветствуются также дополнения, уточнения, исправления наверняка что-то важное я упомянуть забыл.

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

Поживем увидим.

Опрос


Читателям Хабра, в отличие от слушателей вебинара, дадим возможность оценить нововведения.
Подробнее..

Конвертеры маршрутов в Django 2.0 (path converters)

05.02.2021 18:14:24 | Автор: admin
Всем привет!

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

Меня зовут Александр Иванов, я наставник в Яндекс.Практикуме на факультете бэкенд-разработки и ведущий разработчик в Лаборатории компьютерного моделирования. В этой статье я расскажу о конвертерах маршрутов в Django и покажу преимущества их использования.



Первое, с чего начну, границы применимости:

  1. версия Django 2.0+;
  2. регистрация маршрутов должна выполняться с помощью django.urls.path.

Итак, когда к Django-серверу прилетает запрос, он сперва проходит через цепочку middleware, а затем в работу включается URLResolver (алгоритм). Задача последнего найти в списке зарегистрированных маршрутов подходящий.

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

users/21/reports/2021-01-31/teams/4/reports/2021-01-31/


Как бы могли выглядеть маршруты в urls.py? Например, так:

path('users/<id>/reports/<date>/', user_report, name='user_report'),path('teams/<id>/reports/<date>/', team_report, name='team_report'),

Каждый элемент в < > является параметром запроса и будет передан в обработчик.
Важно: название параметра при регистрации маршрута и название параметра в обработчике обязаны совпадать.

Тогда в каждом обработчике был бы примерно такой код (обращайте внимание на аннотации типов):

def user_report(request, id: str, date: str):   try:       id = int(id)       date = datetime.strptime(date, '%Y-%m-%d')   except ValueError:       raise Http404()     # ...

Но не царское это дело заниматься копипастом такого блока кода для каждого обработчика. Разумно этот код вынести во вспомогательную функцию:

def validate_params(id: str, date: str) -> (int, datetime):   try:       id = int(id)       date = datetime.strptime(date, '%Y-%m-%d')   except ValueError:       raise Http404('Not found')   return id, date

А в каждом обработчике тогда будет просто вызов этой вспомогательной функции:

def user_report(request, id: str, date: str):   id, date = validate_params(id, date)     # ...

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

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

Стандартные конвертеры


Django предоставляет стандартные конвертеры маршрутов. Это механизм определения, подходит ли часть маршрута или нет самим URLResolver. Приятный бонус: конвертер может менять тип параметра, а значит, в обработчик может прийти сразу нужный нам тип, а не строка.

Конвертеры указываются перед названием параметра в маршруте через двоеточие. На самом деле конвертер есть у всех параметров, если он не указан явно, то по умолчанию используется конвертер str.

Осторожно: некоторые конвертеры выглядят как типы в Python, поэтому может показаться, что это обычные приведения типов, но это не так например, нет стандартных конвертеров float или bool. Позднее я покажу, что из себя представляет конвертер.


После просмотра стандартных конвертеров, становится очевидно, что для id стоит использовать конвертер int:

path('users/<int:id>/reports/<date>/', user_report, name='user_report'),path('teams/<int:id>/reports/<date>/', team_report, name='team_report'),


Но как быть с датой? Стандартного конвертера для неё нет.

Можно, конечно, извернуться и сделать так:

'users/<int:id>/reports/<int:year>-<int:month>-<int:day>/'

Действительно, часть проблем удалось устранить, ведь теперь гарантируется, что дата будет отображаться тремя числами через дефисы. Однако всё ещё придётся обрабатывать проблемные случаи в обработчике, если клиент передаст некорректную дату, например 2021-02-29 или вообще 100-100-100. Значит, этот вариант не подходит.

Создаём свой конвертер


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

Для этого надо сделать два шага:

  1. Описать класс конвертера.
  2. Зарегистрировать конвертер.

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

  1. Должен быть атрибут regex, описывающий регулярное выражение для быстрого поиска требуемой подпоследовательности. Чуть позже покажу, как он используется.
  2. Реализовать метод def to_python(self, value: str) для конвертации из строки (ведь передаваемый маршрут это всегда строка) в объект python, который в итоге будет передаваться в обработчик.
  3. Реализовать метод def to_url(self, value) -> str для обратной конвертации из объекта python в строку (используется, когда вызываем django.urls.reverse или тег url).

Класс для конвертации даты будет выглядеть так:

class DateConverter:   regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'   def to_python(self, value: str) -> datetime:       return datetime.strptime(value, '%Y-%m-%d')   def to_url(self, value: datetime) -> str:       return value.strftime('%Y-%m-%d')

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

class DateConverter:   regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'   format = '%Y-%m-%d'   def to_python(self, value: str) -> datetime:       return datetime.strptime(value, self.format)   def to_url(self, value: datetime) -> str:       return value.strftime(self.format)

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

from django.urls import register_converterregister_converter(DateConverter, 'date')

Вот теперь можно описать маршруты в urls.py (я специально сменил название параметра на dt, чтобы не сбивала запись date:date):

path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),path('teams/<int:id>/reports/<date:dt>/', team_report, name='team_report'),

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

def user_report(request, id: int, dt: datetime):   # больше никакой валидации в обработчиках   # сразу правильные типы и никак иначе

Выглядит потрясающе! И это так, можно проверять.

Под капотом


Если посмотреть внимательно, то возникает интересный вопрос: нигде нет проверки, что дата корректна. Да, есть регулярка, но под неё подходит и некорректная дата, например 2021-01-77, а значит, в to_python должна быть ошибка. Почему же это работает?

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

У Django есть подсистема маршрутизации с возможностью добавления конвертеров, которая берёт на себя обязанности по вызову метода to_python и отлавливания ошибок ValueError.

Привожу код из подсистемы маршрутизации Django без изменений (версия 3.1, файл django/urls/resolvers.py, класс RoutePattern, метод match):

match = self.regex.search(path)if match:   # RoutePattern doesn't allow non-named groups so args are ignored.   kwargs = match.groupdict()   for key, value in kwargs.items():       converter = self.converters[key]       try:           kwargs[key] = converter.to_python(value)       except ValueError:           return None   return path[match.end():], (), kwargsreturn None

Первым делом производится поиск совпадений в переданном от клиента маршруте с помощью регулярного выражения. Тот самый regex, что определен в классе конвертера, участвует в формировании self.regex, а именно подставляется вместо выражения в угловых скобках <> в маршруте.

Например,
users/<int:id>/reports/<date:dt>/
превратится в
^users/(?P<id>[0-9]+)/reports/(?P<dt>[0-9]{4}-[0-9]{2}-[0-9]{2})/$

В конце как раз та самая регулярка из DateConverter.

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

Для каждого параметра имеется свой конвертер, который и используется для вызова метода to_python. И вот здесь самое интересное: вызов to_python обёрнут в try/except, и отлавливаются ошибки типа ValueError. Именно поэтому и работает конвертер даже в случае некорректной даты: валится ошибка ValueError, и это расценивается так, что маршрут не подходит.

Так что в случае с DateConverter, можно сказать, повезло: в случае некорректной даты валится ошибка нужного типа. Если будет ошибка другого типа, то Django вернёт ответ с кодом 500.

Не стоит останавливаться


Кажется, что всё отлично, конвертеры работают, в обработчики сразу приходят нужные типы Или не сразу?

path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),

В обработчике для формирования отчёта наверняка нужен именно User, а не его id (хотя и такое может быть). В моей гипотетической ситуации для создания отчёта нужен как раз именно объект User. Что же тогда получается, опять двадцать пять?

def user_report(request, id: int, dt: datetime):   user = get_object_or_404(User, id=id)     # ...

Снова перекладывание обязанностей на обработчик.

Но теперь понятно, что с этим делать: писать свой конвертер! Он убедится в существовании объекта User и передаст его в обработчик.

class UserConverter:   regex = r'[0-9]+'   def to_python(self, value: str) -> User:       try:           return User.objects.get(id=value)       except Category.DoesNotExist:           raise ValueError('not exists') # именно ValueError   def to_url(self, value: User) -> str:       return str(value.id)

После описания класса регистрирую его:

register_converter(UserConverter, 'user')

И наконец описываю маршрут:

path('users/<user:u>/reports/<date:dt>/', user_report, name='user_report'),

Так-то лучше:

def user_report(request, u: User, dt: datetime):     # ...

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

class ModelConverter:   regex: str = None   queryset: QuerySet = None   model_field: str = None   def __init__(self):       if None in (self.regex, self.queryset, self.model_field):           raise AttributeError('ModelConverter attributes are not set')   def to_python(self, value: str)-> models.Model:       try:           return self.queryset.get(**{self.model_field: value})       except Category.DoesNotExist:           raise ValueError('not exists')   def to_url(self, value) -> str:       return str(getattr(value, self.model_field))

Тогда описание нового конвертера в модель сведётся к декларативному описанию:

class UserConverter(ModelConverter):   regex = r'[0-9]+'   queryset = User.objects.all()   model_field = 'id'

Итоги


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

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

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

P.S. На практике я делал и использовал только конвертер для дат, как раз тот самый, который приведён в статье, поскольку почти всегда использую DRF или GraphQL. Расскажите, пользуетесь ли вы конвертерами маршрутов и, если пользуетесь, то какими?
Подробнее..

Код-ревью в Практикуме как мы делаем его быстрее и эффективнее

12.02.2021 14:21:36 | Автор: admin
Код-ревью полезный инструмент для командной разработки и для прокачки собственных навыков. Код-ревью помогает обнаружить недочёты в коде: как синтаксические или стилистические ошибки, так и неоптимальные или неэффективные подходы. В командной разработке, когда команда делает большой проект, код-ревью также помогает оставаться в курсе изменений в разных частях кода.

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



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

Код-ревью в Практикуме


В Практикуме мы проводим ревью кода на собственной онлайн-платформе, которая называется Ревизор. Туда попадают все сданные студентами работы. Платформа работает по аналогии с интерфейсами в Gitlab/Github/Bitbucket: можно просмотреть список файлов, изменения между версиями, а также оставить комментарии к определённым строкам.

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



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



Во вкладке Код-ревью можно оставлять разные типы комментариев к коду.



Ну а в последней вкладке видно все предыдущие варианты с комментариями.



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

Главное отличие Ревизора от других платформ в том, что у нас комментарии делятся на три типа: Надо исправить, Отлично и Можно лучше.

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

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

Отлично это комментарии, которыми мы хвалим студентов за то, что они верно справились с заданием или использовали нестандартные и интересные подходы.

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

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

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

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

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

Формула идеального комментария


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

Идеальный комментарий содержит:

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

Вот пример комментария:



А вот пример комментария Отлично за выбор эффективного решения:



Или пример необязательной правки:



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

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

Принципы код-ревью


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

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

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

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

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

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

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

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

За счёт всех этих факторов у ревьюеров появляется больше времени на более качественный фидбек студенту.

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

Что почитать


Для тех, кто хочет повысить качество своего кода, я могу посоветовать книги Стива Макконнелла Совершенный код и Антона Спрола Думай как программист.

Как сделать код-ревью быстрее и эффективнее


  1. Используйте мягкие формулировки и чёткие пояснения в комментариях.
  2. Комментарии должны быть подкреплены ссылками на дополнительные материалы и/или документацию.
  3. Не давайте готовое решение, а подталкивайте к нему.
  4. В сложных ситуациях обращайтесь к человеку, который управляет всеми процессами в ревью.
  5. Используйте автотесты и линтеры. Это поможет не допустить до ревью неработающий код и сместить фокус проверки со стилистики на эффективность и оптимальность решения.
  6. В учебных проектах используйте чеклисты возможных ошибок. В реальной работе заранее договоритесь об общих подходах к разработке, задокументируйте их и применяйте на ревью.
Подробнее..

Вебинар Ускорение на простых типах данных и битовые операции

07.04.2021 14:14:13 | Автор: admin
15 апреля Яндекс.Практикум проводит открытый вебинар Ускорение на простых типах данных и битовые операции. Приглашаем разработчиков на Python и C/C++, которые хотят научиться трюкам для ускорения кода, а также программистов на других языках, которым интересны фишки, связанные с типами данных.

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

Вебинар будет состоять из двух частей: 80 минут обзор, 15 минут ответы на вопросы.



В программе вебинара


  1. Ускорение на простых типах данных. Введение.
  2. Что не так с символами и строками:
    Строка это массив символов или как ещё бывает в разных языках.
    Поиск подстроки в строке.
    Замена части строки.
    Что такое длина строки.
  3. Что не так с числами:
    Как хранятся числа.
    Что такое числа с плавающей запятой и частые ошибки.
    Переполнения и арифметика по модулю.
    Сколько места это всё занимает.
  4. Что не так с логическим типом данных:
    Приоритеты операций в разных языках.
    Как упростить сложное логическое выражение.
    Популярные задачи с собеседований.
  5. Ответы на вопросы.

Ведущая


Вебинар проведет Александра Воронцова разработчик в Joom и автор курса Алгоритмы для разработчиков в Яндекс.Практикуме.

Вебинар пройдет 15 апреля в 19.30 (Мск). Программа и регистрация.
Подробнее..

Вебинар от Яндекс.Практикума Конечные автоматы вреальной жизни теория, кодинг и QampA за один вечер

26.04.2021 16:20:03 | Автор: admin
Теория конечных автоматов лежит в основе многих алгоритмов, к которым часто прибегают разработчики. Решение многих проблем уже давно придумано и зачастую с использованием конечных автоматов.

8 мая в 15.00 (Мск) Яндекс.Практикум проводит открытый вебинар Конечные автоматы вреальной жизни, на котором наставник курса Мидл фронтенд-разработчик Захар Овчаров расскажет про применение автоматов, а выпускник курса Антон Субботин попытается понять, зачем они нужны икак их применять.

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



В программе вебинара


  1. Поговорим про абстрактные машины, не существующие в реальности. Выувидите, как на их основе строятся алгоритмы решения широкого круга задач.
  2. Разберём на примерах, как работают конечные автоматы и где находят применение.
  3. Построим небольшой конечный автомат для валидации и допуска строк пользовательского ввода.
  4. Обсудим использование, сложности иограничения конечных автоматов для решения прикладных повседневных задач.

Ведущие


Захар Овчаров front-end team-lead в Respona и Ezil projects, наставник на курсе Мидл фронтенд-разработчик.

Антон Субботин выпускник курса Мидл фронтенд-разработчик. После курса сменил место работы и теперь работает в команде разработки Практикума.

Вебинар пройдет 8 мая в 15.00 (Мск). Подробности и регистрация.
Подробнее..

Вебинар Вычисляем на видеокартах. Технология OpenCL

18.06.2021 12:04:53 | Автор: admin
22 июня в 18.30 (Мск) Яндекс.Практикум проведет открытый вебинар Вычисляем на видеокартах. Технология OpenCL. На вебинаре расскажем, как использовать видеокарту в качестве полноценного вычислительного устройства, мощности которого чаще всего простаивают.

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



Для кого?


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


В программе вебинара


1. История возникновения и особенности видеокарт


Почему устройство, созданное для графики в играх, идеально для прогнозирования погоды, майнинга криптовалюты и deep learning

2. Архитектура видеокарты


О написании кода, если у вас 4000 ядер, правильном использовании видеопамяти, локальных и глобальных размерах, рабочих группах и ворпах

3. Конкретные цифры: обзор технологий


  • CUDA vs OpenCL. Преимущества и недостатки этих технологий
  • Чем отличаются видеокарты компаний NVidia и AMD Radeon


4. Как программировать на OpenCL


  • Пишем первую программу для видеокарты
  • Разберём, что можно, а что нельзя на OpenCL
  • Посмотрим, как использовать OpenCL не только с C++, но и с другими языками программирования


5. Популярные алгоритмы


  • Базовые алгоритмы и принципы программирования через призму видеокарты
  • Разбор типичных проблем


6. Отладка, обфускация, тулинг


  • Что делать, если ваш код BSODит. Отладка на видеокарте: миф или реальность
  • Профилируем OpenCL и делаем его быстрее
  • Как защитить свой код: пара слов об обфускации


7. Q&A-сессия



Ведущий Георгий xjossy Осипов, разработчик в Лаборатории компьютерной графики и мультимедиа ВМК МГУ и автор факультета Разработчик C++ в Яндекс.Практикуме

Вебинар пройдёт 22 июня в 18.30 (Мск). Подробности и регистрация
Подробнее..

АТАТА распутываем задачу про палиндром

13.04.2021 12:15:42 | Автор: admin
Очень часто авторы алгоритмических задач делают ход конём: они берут задачу с простыми формулировками, заменяют их сложными и непонятными эквивалентами и выдают вам сложную задачу. В этом посте мы разберём пример одной такой задачи и обсудим пару полезных для её решения приёмов. Задача будет про палиндром.



Продолжение под катом.

Что такое палиндром


Палиндромом называется строка, которая одинаково читается как слева направо, так и справа налево. Например, слово АТАТА это палиндром, а вот слово АЙАЙАЙ нет.


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

Известный кинематографический палиндром название вышедшего в 2020 году фильма Довод (англ. Tenet). Русская адаптация в каком-то плане уникальна, потому что у нас нашлась подходящая альтернатива слову tenet, которая тоже является палиндромом. На многих других языках (в том числе славянских) название фильма оставили как есть. Например, на украинском это ТЕНЕТ (Википедия).



Постановка задачи


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

Вот несколько примеров нечётных палиндромов: ATATA, KKKKKKKK, ABA, ZO.

Рассмотрим подробнее первую строку АТАТА. Выпишем все её подстроки нечётной длины:

  • A, T, A, T, A однобуквенное слово всегда палиндром
  • ATA, TAT, ATA очевидно, палиндромы
  • ATATA тоже

В слове ZO нет подстрок нечётной длины больше чем в одну букву. И Z, и O палиндромы, поэтому ZO нечётный палиндром.

Пусть нам дана строка ABCDEF, и мы можем заменить не более одного символа (K=1), чтобы сделать из неё нечётный палиндром. Оптимальным решением было бы, например, заменить первую букву на C, тогда мы получили бы CBCDEF, где длина наибольшей подстроки, являющейся нечётным палиндромом, была бы равна трём (это CBC).

С тем же успехом мы могли бы прийти к варианту ABCFEF.

А вот если изначально у нас была строка ZXXXZ, и опять можно изменить не более одного символа, то надо заменить средний, так как ZXX и XXZ не являются палиндромами. В итоге мы получим ZXZXZ.

Структура нечётного палиндрома


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

Посмотрим ещё раз на определение: нечётным палиндромом будем называть такую строку, у которой все подстроки нечётной длины являются палиндромами. Если все подстроки нечётной длины являются палиндромами, то и все подстроки длины 3 являются палиндромами. Отсюда сразу же следует, что на чётных позициях не может быть двух различных букв, то же самое верно для нечётных.


На рисунке выше показано, как получается чередующаяся структура строки. Одинаковым цветом выделены одинаковые символы. Сначала посмотрим на палиндром длины 3, который начинается в самом первом символе исходной строки. Тогда 1 и 3 символ можно пометить зеленым. Про 2-й символ пока ничего непонятно. Сдвинем палиндром на единицу вправо, получим, что 2 и 4 символы можно покрасить в один цвет. Так, сдвигаясь каждый раз на единицу, мы получим, что все символы на нечётных позициях зелёные, а на чётных синие. Более строго можно доказать этот факт с помощью метода математической индукции, например.

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

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

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

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

Наивное решение


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

Если применить описанные действия независимо для всех L от 0 до n 1, где n длина исходной строки, а затем найти наиболее длинную найденную хорошую подстроку, то мы решим задачу. Однако временная сложность данного решения составит O(n^2), так как для каждой позиции L мы сделаем в худшем случае примерно n L шагов при передвижении R.

Улучшаем асимптотику решения


Мы можем ускорить это решение с помощью техники двух указателей. Не будем обнулять счётчики и сбрасывать позицию R после того, как максимально отошли вправо от L. Переиспользуем текущую информацию при переходе от L к L+1. Для этого надо убрать из счётчиков элемент на позиции L и всё. Затем можно продолжать делать проверки и отодвигать R вправо до тех пор, пока не исчерпаются K операций изменения элементов.


На рисунке выше показан ход указателей L и R, K=2. Подчёркнутые символы будут изменены при соответствующих L и R

Оценим сложность новой версии алгоритма. Указатель R суммарно сделает не более n шагов вправо, указатель L тоже. Передвижение указателя сопровождается обновлением счётчиков и проверкой числа изменений для получения хорошей подстроки эти действия выполняются за константное время, O(1). Таким образом мы получаем сложность O(n).

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

Подробнее про метод двух указателей и про другие интересные приёмы мы рассказываем на курсе Алгоритмы и структуры данных. Если вам интересна эта тема, приглашаю на наш курс.
Подробнее..

Цифровая доступность пять ключевых проблем в интерфейсах. Совместный вебинар Яндекс.Практикума и Валерии Курмак

05.05.2021 16:05:29 | Автор: admin
13 мая Яндекс.Практикум вместе с Валерией Курмак проводит открытый вебинар Цифровая доступность: пять ключевых проблем в интерфейсах. Вебинар будет полезен дизайнерам и разработчикам интерфейсов, которые хотят научиться проектировать доступно.



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

На вебинаре слабовидящий Дима Глюз покажет на примерах, какие барьеры он встречает в интерфейсах. А руководитель продуктового дизайна Яндекс.Практикума Сергей Кудинов и руководитель фронтенд-разработки Давид Роганов расскажут, как не создавать такие барьеры при проектировании.

Ведущая


Валерия Курмак член Strategic Leader in Accessibility Initiative в IAAP, автор образовательного курса по цифровой доступности, автор телеграмм-канала об инклюзивном дизайне Не исключение.

Спикеры


Дима Глюз слабовидящий пользователь с диагнозом нейропатия Лебера.

Сергей Кудинов руководитель продуктового дизайна Яндекс.Практикума, www.sergeikudinov.com

Давид Роганов руководитель фронтенд-разработки Яндекс.Практикума.

Вебинар пройдёт 13 мая в 19.30 (Мск). Регистрация.
Подробнее..

Как фотка в портфолио влияет на получение работы и заказов. Обзор исследований

29.01.2021 14:10:10 | Автор: admin
Почти наверняка вам кажется, что реклама или пропаганда действуют на кого угодно, но точно не на вас. Вы всегда действуете рационально и не подвержены подобному влиянию. Но, скорее всего, это не так. Люди подвержены когнитивным искажениям. И это нормально.

Если вы спросите у любого эйчара, уделяет ли он внимание фотке и внешности кандидата, 10 из 10 скажут: Конечно же нет, мы смотрим только на опыт! Ну, ещё на софт-скиллы и вот это всё. Ведь смотреть на внешность и оценивать человека по этому критерию это шаг к дискриминации. Но даже если эйчар (или прямой наниматель) хочет быть предельно беспристрастен, то бессознательно он всё равно воспринимает и учитывает свои ощущения от фото. Даже если не говорит вам об этом. Даже если не говорит об этом самому себе.



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

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

В 2005 году провели исследование (Todorov, Mandisodza, Goren, and Hall, Inferences of competence from faces predict election outcomes) и попросили респондентов оценить компетентность кандидатов в члены сената, один раз глянув на их фотку, не зная образования, политической программы и всего остального.

Забавно, что оценки людей, принимавших решение по единственной фотке, подозрительно совпали с реальными результатами выборов. Скорее всего, эйчары принимают решение об устройстве вас на работу более обдуманно, чем граждане, выбирающие себе депутатов и сенаторов. А вдруг нет?



Улыбаться ли на фотке, если вы норвежский программист?


How to pose for a professional photo: The effect of three facial expressions on perception of competence of a software developer
Petra Filkukov, Magne Jrgensen

Если вы устраиваетесь аниматором или консультантом в магазин, то очевидным кажется ставить в резюме улыбающуюся фотку. Но, может, инженерам, разработчикам или топ-менеджерам лучше делать лицо посерьёзнее? Работа-то серьёзная. Да и нервная.

Высокопоставленные (в статье high-power) люди улыбаются реже, чем их подчинённые. И у них улыбки коррелируют с хорошим расположением духа. Тогда как подчинённые улыбаются независимо от своего настроения и гораздо чаще. Даже у профессиональных бойцов можно предсказать результат боя, посмотрев, кто больше улыбается перед битвой (он, скорее всего, и продует).

Исследование


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



Все модели для фотографий были действующими работниками IT-компаний в Норвегии, 10 мужчин, 10 женщин, все белые, средний возраст 31,4 года. Все модели были сняты в одинаковых условиях, всех попросили одеться нейтрально.

Участники


В исследовании участвовали 238 работников IT-компаний, 218 мужчин, 20 женщин. Средний возраст 29,9 года. 45% нанимают людей время от времени, 10% постоянно, остальные 45% не занимаются наймом.

Методика


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

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

Результаты


Улыбающиеся модели казались более компетентными, чем с нейтральным лицом или задумчивым.


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

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


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

Выводы


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

Улыбаться ли на фотке, если вы бизнес-консультант?


Examining the Relative Impact of Professional Profile Images and Facial Expressions in Small Business-toBusiness Marketing Online
Sanchit Pawar, Asle Fagerstrm, Ifeoma Angelica z. Dimude, Valdimar Sigurdsson and Niklas Eriksson

Исследование


Участникам рассказывали легенду: вы ищете бизнес-консультанта и наткнулись на сайт, который предлагает такие услуги. Чтобы сделать запрос, отправьте консультанту email.



На карточке упоминается образование (бакалавр-магистр-PhD), опыт (джуниор-сеньор) и фотка (весёлая-нейтральная-отсутствует)



Участники


В опросе участвовали 67 респондентов из B2B-бизнеса.
Обзор был проведён международной компанией из Норвегии. Компания дала доступ к своей клиентской базе (бывших, возможных и потенциальных клиентов).

Эти участники были наняты с помощью Линкедин. С клиентами связывались тоже с помощью Линкедина. Все респонденты имели минимум степень бакалавра. 35 участников были норвежцами и ещё 32 иностранцами, 49 мужчин и 18 женщин. Возраст:

  • 1824 3 человека
  • 2534 22 человека
  • 3544 17 человек
  • 4554 11 человек
  • 5564 11 человек
  • 6574 3 человека


Результаты


64,2% участников предпочли улыбчивого PhD-сеньора. 15,3% неулыбчивого PhD-сеньора. На третьем месте улыбчивый джун-бакалавр, остальные совсем отстают.



Выводы


Если вы бизнес-консультант улыбайтесь в резюме.

Как фоткаться, если вы CEO


Displays of Status and Expressiveness in Professional Profile Pictures on LinkedIn and Corporate Websites: A Cross-Cultural Comparison of China and the United States
Peter Cardon, Hongqing Li, Hanjing Shi Mt.

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


Процент улыбающихся фото


Процент фото с полуулыбкой


Процент серьёзных фото

Получилось довольно очевидно. Американцы улыбаются буквально все (87%), особенно женщины (97%). Китайцы, которые работают на западный рынок, улыбаются чуть почаще остальных соотечественников (48% у мужчин, 81% у женщин), хотя улыбка у них более спокойная.

Выводы


Исследование стало бы куда интереснее, если бы они приложили туда капитализацию компаний этих CEO.

Зашла в разделы о руководстве полудюжины российских компаний. У нас, по ощущениям, процент примерно такой, как у китайцев, работающих с Америкой.

Итоги


Если вы хороший специалист, вас, конечно, и так наймут. Но, вписавшись в культурный код, теоретически можно повысить первичную открываемость профиля процентов на 20% (если брать в среднем по статьям).

Подробнее..

Как выражение лица на фотке в резюме влияет наощущение компетентности (в России)

17.05.2021 14:19:21 | Автор: admin
Недавно мы публиковали статью Как фотка в портфолио влияет на получение работы и заказов. Обзор исследований. Там мы рассказали про два эксперимента. В первом тестировали несколько фотографий с разными выражениями лиц у норвежских программистов (задумчивое, серьёзное, улыбающееся) и оценивали, как оно влияет на ощущение компетентности. Во втором оценивали, как зависит ощущение от профиля, но в нём менялось не только выражение лица на фотке (серьёзное и улыбающееся), но и опыт (джуниор-сеньор). Во всех европейских исследованиях фотографии с улыбками вызывали ощущение большей компетентности.

В комментариях rcl задал интересный вопрос: Будут ли отличаться результаты в России?

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




Модели


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



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

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



Опрос


Фотографии с разными выражениями лица были равномерно распределены по трём опросам. В каждом из них было 13 фотографий одних и тех же моделей, но модели были с разными выражениями лица.

Каждый респондент получал рандомную ссылку на опрос, сгенерированную с помощью allocate.monster.

На первый опрос ответило 107 человек, на второй 107, на третий 101.



Вопросы внутри теста показывались в случайном порядке.

Респонденты


Всего было получено 315 ответов. Из них 125 (39,7%) женщин и 190 (60,3%) мужчин.
268 (85%) заняты в IT-сфере.

Постоянно в найме участвуют 72 (22,9%) человека, время от времени 139 (44,1%), никогда не участвовали 104 (33%).



В основном на опрос отвечали люди до 35 лет. Возрастной состав: до 25 лет 29,2%, 2635 58,7%, 3645 11,1%, старше 45 лет 0,9%

Результаты


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

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



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


Восприятие компетентности для улыбающихся, нейтральных и задумчивых фоток (зелёные мужские, оранжевые женские). Данные из исследования How to pose for a professional photo: The effect of three facial expressions on perception of competence of a software developer

Но в исходном исследовании тем не менее эффект был признан статистически значимым.

Распределение


Распределение ответов в среднем похоже для нейтрального выражения лица и широкой улыбки, но видно небольшой пик на 5 для сдержанной улыбки.


Распределение оценок для нейтрального выражения лица (1), сдержанной улыбки (2) и широкой улыбки (3)

Исходное исследование


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



У нас дельта между мужчинами и женщинами больше при любом выражении лица. То есть женщины кажутся нам с любым выражением лица в среднем чуть менее квалифицированными, чем в исходном европейском исследовании.

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


Дельта между компетентностью мужчин и женщин в зависимости от опыта найма

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

Опыт работы




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

Личный выбор


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



Очки


У нас была только одна модель в очках, поэтому тут мы не можем делать никаких выводов. Но тем не менее, если из интереса посчитать, её средняя оценка 5,01, а средняя по всем участникам 4,77. Кто знает, возможно, это именно из-за очков.

Борода


У нас было трое бородатых и двое безбородых участника-мужчины. Средний бородатый балл 5,09, а безбородый 4,74. Это превосходит эффект от изменения выражения лица. Более того, эта разница больше, чем разница между средней оценкой мужских и женских фото.



Выводы


На фотке лучше делать средне-приветливое лицо. Если не умеете делать средне-приветливое и работаете в России, то смело ставьте фотку как на паспорт, которой сами боитесь. Возможно, покажетесь кому-то где-то на 4% более компетентным.



А прикладывают ли фотки вообще?


Мы посмотрели, как оценивают приложенные фотки. Но прикладывают ли эти фотки вообще? hh.ru помог нам это узнать.

Наличие фотки в резюме

Данные из резюме, которые были обновлены или созданы в 2020-21 году.



Я оставила на графике десять первых регионов по наличию фотки в резюме и десять последних. В крупных городах люди чаще прикладывают фото, в национальных республиках чаще нет. Региональная разница очень большая, между Москвой и Ингушетией 54%.

Женщины прикладывают фото в среднем по стране чуть чаще, чем мужчины 38% против 31%.

Молодые специалисты редко прикладывают фото, а чаще всего встречаются фотографии в резюме у людей 2534 лет.



Но даже если фотка есть, влияет ли она на приглашение на работу?



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

Соотношение нанятых кандидатов с фотографиями к кандидатам без фотографий

В Сахе и Чукотском АО, видимо, слишком мало данных, поэтому они дают такие выбросы.

Выводы


У нас, к сожалению, нет разбивки по отраслям. Вообще, если вы синьор-разработчик, вас и так, конечно, наймут, хоть что ставь на фотку. Но если вы живёте в Дагестане, Адыгее, Карачаево-Черкесии или Чечне и не синьор-разработчик фотку лучше приложить.

Возражения


Почему вы не посчитали критерий Фишера? Где стандартное отклонение?

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

А вот я никогда не смотрю на фотку, мне важны только профессионализм и опыт

ОЧЕНЬ много людей специально написали мне в личку, что никогда не смотрят на фото, оно вообще никак на них не влияет.

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

С этим правильно бороться, но неправильно это отрицать.

Но ведь дизайн эксперимента отличается!

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

Там же мой знакомый!

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

Я модель и хочу узнать свои результаты

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

Благодарности

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

Спасибо Надежде Зотовой за помощь в обработке данных в экселе.

Спасибо аналитикам hh.ru за предоставленные данные.
Подробнее..

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

31.05.2021 16:13:25 | Автор: admin
В большинстве случаев люди приходят учиться в Яндекс.Практикум, чтобы уйти со старой работы в новую или смежную область. Чтобы студент после окончания обучения действительно нашёл новую работу, мы создали специальный карьерный трек, который помогает правильно ставить цели во время поиска работы, откликаться на вакансии и проходить собеседования. Благодаря карьерному треку мы трудоустроили 1300 выпускников Практикума в разные компании, включая Яндекс.

Меня зовут Таня Гудимова, я работаю в Карьерном центре Практикума и отвечаю за карьерный трек. В этом посте я расскажу, как в Практикуме устроен процесс трудоустройства после окончания обучения.



71,1% выпускников Практикума выходят на новую работу


Целевую аудиторию выпускников Практикума можно разделить на две группы:

  • те, кто пришёл учиться, чтобы сменить сферу профессиональной области и найти новую работу. Их больше половины;
  • те, кто пришёл, чтобы прокачать свои навыки для роста на текущей работе, чтобы работать на фрилансе или запустить свой бизнес.

20 мая институт образования НИУ ВШЭ опубликовал исследование о том, сколько студентов Практикума находят новую работу после окончания обучения. Исследовали выпускников первой половины 2020 года. Результаты такие: среди тех студентов, которые пришли с целью сменить профессию и сохранили своё намерение, трудоустроились 71,1%.

Часть студентов устраиваются на работу по новой специальности ещё во время обучения. Большинство выпускников находит работу в пределах четырёх месяцев:



Как у нас получилось трудоустроить 71,1% выпускников


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

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

Карьерный трек состоит из пяти частей. Расскажу про каждую из них.

Часть 1. Целеполагание


Карьерный трек начинается с целеполагания. На этом этапе мы объясняем важность формулировки чёткой профессиональной цели и помогаем её сформулировать с помощью опорных вопросов.

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

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

Часть 2. Подготовительная программа и резюме


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

Наша задача сделать так, чтобы студент Практикума понимал, что просто прислать работодателю ссылку на своё резюме на hh.ru недостаточно. Нужно искать работу не только через специализированные сайты, но и, например, через соцсети или сайты компаний.

Помимо этого мы помогаем студенту написать хорошее резюме на базе наших методических материалов. По рекомендациям из методички выпускник оформляет резюме. Затем оно уходит на ревью к нашим эйчарам, которые дают подробную обратную связь. Обычно требуется несколько итераций, чтобы наши HR-специалисты сказали: Да, теперь резюме в порядке.
Мы заметили, что выпускники не умеют описывать свой опыт и проекты как правило, они рассказывают об этом слишком коротко и недостаточно точно. К примеру, если мне как эйчару кто-то пишет, что он аналитик, мне приходится сидеть и гадать, какой именно: продуктовый, маркетинг-аналитик или аналитик данных.

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

Часто специалисты указывают неполный список ключевых навыков, потому что не понимают, для чего они нужны. Например, вместо расширенного списка HTML5, CSS3, JavaScript, MySQL, Git, API, ООП, БЭМ, Webpack указывают только HTML, CSS и JavaScript. Но на HeadHunter, например, они используются как теги и повышают ваши шансы, что потенциальный работодатель увидит вас в поисковой выдаче.

Почти никто не умеет заполнять раздел О себе там либо дублируют всё резюме, либо пишут что-то вообще не по теме.

Рита Головко, менеджер по трудоустройству на бэкенд-факультете


Резюме выпускника до и после корректировки (кликабельно). Обратите внимание, насколько изменилось описание опыта работы после наших рекомендаций

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

Часть 3. Сопроводительное письмо, портфолио и тестовое собеседование


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

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

Рита Головко, менеджер по трудоустройству на бэкенд-факультете
Следующий шаг портфолио. Здесь мы рассказываем, какие проекты можно включить в портфолио, как его оформить и где разместить. Для разных факультетов это будут разные рекомендации.
Выпускникам курса Веб-разработчик мы объясняем, что рекрутеры и тимлиды смотрят на портфолио по-разному. Рекрутерам важно, чтобы проекты из портфолио соответствовали навыкам вакансии, а тимлиды смотрят на качество кода и оформление проектов, как минимум Read.me. И мы этому тоже учим.

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

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


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

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

Часть 4. Программа акселерации


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

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


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


Менеджеры по трудоустройству проводят регулярные и внеплановые встречи, чтобы ответить на наболевшие вопросы

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

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

На этом этапе мы помогаем не только с поиском работы, но и отрабатываем проблемы выпускников: синдром самозванца, неуверенность, демотивацию, страх большого количества откликов. Проходить через эти барьеры помогают ребята из нашего комьюнити выпускников, чтобы на своём примере показать, что не всё так страшно. В сообществе выпускников ребята также обмениваются вакансиями, ищут коллег среди одногруппников. По результатам исследования 11% выпускников находят работу через это комьюнити.

Часть 5. Программа сопровождения


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

Жизненный цикл выпускника в карьерном треке может быть долгим. Например, кто-то через пару недель после выхода на работу может написать нам: Ребята, жуть, работать тут не могу, спасайте. Иногда так бывает, когда человек не сразу понял, что стартап это не только модно и красиво, но ещё и ОЧЕНЬ быстро.

Кто-то может проработать в компании какое-то время, затем уволиться и снова прийти к нам за помощью.

В каких компаниях работают выпускники Практикума


Важно понимать, что на старте Яндекс.Практикума мы не задумывали это как проект, призванный пополнить штат Яндекса. Для этого существуют стажировки и Школы Яндекса. Мы хотели учить людей профессиям, с которыми они смогут пойти и устроиться в ту компанию, в которую захотят. Сейчас наши выпускники работают в десятках компаний, среди которых Сбербанк, ivi, Ozon, Барс Групп, Авито, Спортмастер.

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

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

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

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

Тесты должна писать разработка (?)

19.02.2021 16:11:01 | Автор: admin
Привет! Есть старый холивар на тему, кто же должен писать тесты: разработчики или тестировщики. Вроде как если в команде есть тестировщики, то логично, что тесты пишут они, правда? С другой стороны, ребята из разработки (помимо самой разработки) точно знают, как работает их код и как будет вести себя в тех или иных ситуациях. Как минимум предполагают.


Дисклеймер: меня зовут Эрик Бурыгин, я давно работаю тестировщиком, веду студентов на курсе Инженер по тестированию, поэтому может показаться, что тестировщик просто хочет перекинуть кусок работы на разработчиков. На самом деле у описываемого подхода есть как плюсы, так и минусы, поэтому статья носит в том числе и дискуссионный характер. Буду рад увидеть в комментах мнения как разработчиков, так и тестировщиков.

Если тесты пишет разработка, можно решить сразу несколько проблем, например:

  • Ощутимо ускорить релизный цикл.
  • Снять нагрузку с тестирования.

В большинстве команд процесс выглядит примерно так:

  1. Разработчик создаёт новые фичи и допиливает существующие.
  2. Тестировщик всё это тестирует и пишет различные тест-кейсы.
  3. Автоматизатор, оправдывая название должности, автоматизирует всё по написанным тест-кейсам из п.2.

Вроде бы всё выглядит просто.

Но в этой парадигме есть слабые места.

Допустим, разработчик доделал свою фичу и благополучно отдал её в тестирование. Но фича получилась не medium rare, а откровенно сырая. Это приведёт к переоткрытию задачи и дополнительным фиксам, причём итераций может быть от одной до N, в зависимости от размера этой фичи, её сложности, влияния на смежные процессы, добросовестности самого разработчика. А ещё от того, как у вас в принципе устроены процессы внутри разработки, насколько тщательно смотрятся пул-реквесты, запускается ли приложение перед отправкой на тестирование.

В общем, переменных хватает.

После того как задача будет протестирована и готова к релизу, тестированию нужно написать на весь функционал тест-кейсы. Затем сделать регресс/смоук и наконец зарелизить.

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

Просто нужно больше разработчиков


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

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

На восемь разработчиков обычно приходится два тестировщика и один автоматизатор. Автоматизация при этом не участвует в релизном цикле напрямую скорее находится поблизости. И возникает вопрос: как сделать описанные процессы более эффективными, да ещё и не потерять в качестве?

Давайте попробуем сдвинуть этап автоматизации с третьего места на первое, на этап разработки.

Что получится


Получится сразу неплохой набор плюсов, смотрите:

  • разработчики пишут тесты одновременно с написанием самой фичи, что существенно улучшает её качество;
  • падает нагрузка на тестирование: тестировщикам теперь надо посмотреть результаты тестов и оценить, в достаточной ли степени задача покрыта тестами;
  • ручного регресса в схеме больше нет за ненадобностью.

А что тестировщики?

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

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

Так вот, к новой парадигме. Круто же? Да хоть прямо сейчас внедрять. Если получится сделать две вещи.

  1. Продать эту идею разработке. Потому что далеко не каждый разработчик сразу захочет писать тесты, ведь это может быть скучно, или он просто не хочет: у вас, вообще-то, тестировщики вон там для чего сидят?
  2. Продать эту идею менеджерам. Потому что при прочих плюсах у вас увеличивается время разработки каждой фичи.

Какие минусы тут могут вас поджидать.

  1. Большая часть разработчиков просто не умеет тестировать, потому что они разработчики, а не тестировщики. И это нормально. Тут вы можете или научить их тестировать, что будет не самой тривиальной задачей, или просто писать тест-кейсы для них. Что де-факто ломает сам процесс.
  2. На старте автоматизация будет занимать больше времени, ведь не будет кодовой базы тестов, инфраструктуры и привычных подходов задача-то новая.
  3. Будут нужны понятные отчёты для тестирования. Но имейте в виду: даже самый понятный отчёт не всегда можно сразу научиться правильно читать.
  4. Не каждую задачу вы сможете легко и быстро покрыть тестами. В ряде случаев вам придётся на тесты тратить больше времени, чем на саму реализацию фичи.
  5. Масштабные задачи будет сложно отдавать одновременно с тестами, это занимает довольно много времени.
  6. Для этих же масштабных и сложных задач надо будет всё равно закладывать время на то, чтобы просто в них вникнуть, потому что нет другого способа проверить правильность тестов, которые писали разработчики.

Что же делать?




В принципе у каждой из проблем есть решение.

  1. Разработчики не умеют тестировать.
    Можно консультировать их на первых этапах, чтобы помочь разобраться.
  2. Нет кодовой базы, инфраструктуры и подходов.
    Всё решается только временем. На размеченные функции писать тексты проще.
  3. Понятные отчёты.
    В отчёты надо включать все шаги, а название каждого теста должно сразу отражать, что именно им проверяется.
  4. Приходится тратить много времени на ряд задач.
    Спасает здравый смысл: не всё стоит покрывать здесь и сейчас.
  5. Сложно отдавать задачи, когда нет подходов и инструментов.
    Тоже решается постепенно, главное время для анализа и внедрения того или иного инструмента. И это должно быть отдельной задачей.
  6. Проблема масштабных задач.
    Их можно сразу отдавать без тестов либо частично покрытыми тестами. Но это в любом случае не отменяет погружения в контекст.

Вывод


Подход со всеми своими плюсами и минусами имеет право на жизнь. А если ещё и правильно настроить процессы, то это поможет вам и релизный цикл ускорить, и штат не раздувать (:
Подробнее..

Django Rest Framework для начинающих создаём API для чтения данных (часть 2)

15.06.2021 12:07:46 | Автор: admin

В прошлой части мы в общих чертах рассмотрели, как устроен REST API на DRF при работе на чтение. Едва ли не самый сложный для понимания этап сериализация. Вооружившись исходным кодом, полностью разберем этот этап от приема набора записей из модели до их преобразования в список словарей.


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


Код учебного проекта, который используется в этой статье, доступен в репозитории на Гитхабе.



Как создаётся сериалайзер, работающий на чтение


Создание экземпляра сериалайзера мы описывали следующим образом:


# capitals/views.py        serializer_for_queryset = CapitalSerializer(            instance=queryset,  # Передаём набор записей            many=True # Указываем, что на вход подаётся набор записей        )

Благодаря many=True запускается метод many_init класса BaseSerializer.


class BaseSerializer(Field):           def __new__(cls, *args, **kwargs):        if kwargs.pop('many', False):            return cls.many_init(*args, **kwargs)        return super().__new__(cls, *args, **kwargs)

Подробнее о методе many_init:


  • При создании экземпляра сериалайзера он меняет родительский класс. Теперь родителем выступает не CapitalSerializer, а класс DRF для обработки наборов записей restframework.serializers.ListSerializer.
  • Созданный экземпляр сериалайзера наделяется атрибутом child. В него включается дочерний сериалайзер экземпляр класса CapitalSerializer.

 @classmethod    def many_init(cls, *args, **kwargs):        ...        child_serializer = cls(*args, **kwargs)        list_kwargs = {            'child': child_serializer,        }      ...        meta = getattr(cls, 'Meta', None)        list_serializer_class = getattr(meta, 'list_serializer_class',                           ListSerializer)        return list_serializer_class(*args, **list_kwargs)

Экземпляр сериалайзера Описание К какому классу относится
serializer_for_queryset Обрабатывает набор табличных записей ListSerializer класс из модуля restframework.serializers
serializer_for_queryset.child Обрабатывает каждую отдельную запись в наборе CapitalSerializer наш собственный класс, наследует от класса Serializer модуля restframework.serializers

Помимо many=True мы передали значение для атрибута instance (инстанс). В нём набор записей из модели.


Важное замечание: чтобы не запутаться и понимать, когда речь идёт о сериалайзере в целом, а когда о дочернем сериалайзере, далее по тексту мы будем говорить основной сериалайзер (в коде контроллера это serializer_for_queryset) и дочерний сериалайзер (атрибут child основного сериалайзера).


После создания основного сериалайзера мы обращаемся к его атрибуту data:


return Response(serializer_for_queryset.data)

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


Что под капотом атрибута data основного сериалайзера


Важное замечание: атрибут data есть и у основного, и у дочернего сериалайзеров. Поэтому, чтобы найти подходящий исходный код, нужно помнить: экземпляр основного (serializer_for_queryset) относится к классу ListSerializer.


Исходный код атрибута data:


class ListSerializer(BaseSerializer):    ...    @property    def data(self):        ret = super().data        return ReturnList(ret, serializer=self)

Задействован атрибут data родительского BaseSerializer. Исходный код:


class BaseSerializer(Field):            @property    def data(self):    ...        if not hasattr(self, '_data'):            if self.instance is not None and not getattr(self, '_errors', None):                self._data = self.to_representation(self.instance)            ...        return self._data

Поскольку никакие данные ещё не сгенерированы (нет атрибута _data), ничего не валидируется (нет _errors), но есть инстанс (набор записей для сериализации), запускается метод to_representation, который и обрабатывает набор записей из модели.


Как работает метод to_represantation основного сериалайзера


Возвращаемся в класс ListSerializer.


class ListSerializer(BaseSerializer):           def to_representation(self, data):        """        List of object instances -> List of dicts of primitive datatypes.        """        iterable = data.all() if isinstance(data, models.Manager) else data        return [            self.child.to_representation(item) for item in iterable        ]

Исходный код


Код нехитрый:


  • набор записей из модели (его передавали при создании сериалайзера в аргументе instance) помещается в цикл в качестве единственного аргумента data;
  • в ходе работы цикла каждая запись из набора обрабатывается методом to_representation дочернего сериалайзера (self.child.to_representation(item)). Теперь понятно, зачем нужна конструкция основной дочерний сериалайзер.

Сделаем небольшую остановку:


  • Чтобы обрабатывать не одну запись из БД, а набор, при создании сериалайзера нужно указать many=True.
  • В этом случае мы получим матрёшку основной сериалайзер с дочерним внутри.
  • Задача основного сериалайзера (он относится к классу ListSerializer) запустить цикл, в ходе которого дочерний обработает каждую запись и превратит ее в словарь.

Как работает метод to_representation дочернего сериалайзера


Дочерний сериалайзер экземпляр класса CapitalSerializer наследует от restframework.serializers.Serializer.


class Serializer(BaseSerializer, metaclass=SerializerMetaclass):    def to_representation(self, instance):        """        Object instance -> Dict of primitive datatypes.        """        ret = OrderedDict()        fields = self._readable_fields        for field in fields:            try:                attribute = field.get_attribute(instance)            except SkipField:                continue            check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject)      else attribute            if check_for_none is None:                ret[field.field_name] = None            else:                ret[field.field_name] = field.to_representation(attribute)        return ret

Исходный код


Пойдём по порядку: сначала создаётся пустой OrderedDict, далее идёт обращение к атрибуту _readable_fields.


Откуда берётся _readable_fields? Смотрим исходный код:


class Serializer(BaseSerializer, metaclass=SerializerMetaclass):           @property    def _readable_fields(self):        for field in self.fields.values():            if not field.write_only:                yield field

То есть _readable_fields это генератор, включающий поля дочернего сериалайзера, у которых нет атрибутa write_only со значением True. По умолчанию он False. Если объявить True, поле будет работать только на создание или обновление записи, но будет игнорироваться при её представлении.


В дочернем сериалайзере все поля могут работать на чтение (представление) ограничений write only не установлено. Это значит, что генератор _readable_fields будет включать три поля capital_city, capital_population, author.


Читаем код to_representation далее: генератор _readable_fields помещается в цикл, и у каждого поля вызывается метод get_attribute.


Если посмотреть код to_representation дальше, видно, что у поля вызывается и другой метод to_representation. Это не опечатка: метод to_representation под одним и тем же названием, но с разной логикой:


  • есть у основного сериалайзера в классе ListSerializer;
  • у дочернего сериалайзера в классе Serializer;
  • у каждого поля дочернего сериалайзера в классе соответствующего поля.

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


Как запись из модели обрабатывается методами полей сериалайзера


Метод get_attribute работает с инстансом (instance). Важно не путать этот инстанс с инстансом основного сериалайзера. Инстанс основного сериалайзера это набор записей из модели. Инстанс дочернего сериалайзера каждая конкретная запись.


Вспомним строку из кода to_representation основного сериалайзера:


[self.child.to_representation(item) for item in iterable]

Этот item (отдельная запись из набора) и есть инстанс, с которым работает метод get_attribute конкретного поля.


class Field:       ...    def get_attribute(self, instance):        try:            return get_attribute(instance, self.source_attrs)      ...

Исходный код


Вызывается функция get_attribute, описанная на уровне всего модуля rest_framework.fields. Функция получает на вход запись из модели и значение атрибута поля source_attrs. Это список, который возникает в результате применения метода split (разделитель точка) к строке, которая передавалась в аргументе source при создании поля. Если такой аргумент не передавали, то в качестве source будет взято имя поля.


Если вспомнить, как работает строковый метод split, станет понятно, что если при указании source не применялась точечная нотация, то список всегда будет из одного элемента.


У нас есть такие поля:


class CapitalSerializer(serializers.Serializer):    capital_city = serializers.CharField(max_length=200)    capital_population = serializers.IntegerField()    author = serializers.CharField(source='author.username', max_length=200)

Получается следующая картина:


Поле сериалайзера Значение атрибута source поля Значение source_attrs
capital_city 'capital_city' ['capital_city']
capital_population 'capital_population' ['capital_population']
author 'author.username' ['author', 'username']

Как мы уже указывали, список source_attrs в качестве аргумента attrs передаётся в метод get_attribute rest_framework.fields:


def get_attribute(instance, attrs):    for attr in attrs:        try:            if isinstance(instance, Mapping):                instance = instance[attr]            else:                instance = getattr(instance, attr)        ...    return instance

Для полей capital_city и capital_population цикл for attr in attrs отработает однократно и выполнит инструкцию instance = getattr(instance, attr). Встроенная Python-функция getattr извлекает из объекта записи (instance) значение, присвоенное конкретному атрибуту (attr) этого объекта.
При обработке записей из нашей таблицы рассматриваемую строку исходного кода можно представить примерно так:


instance = getattr(запись_о_конкретной_столице, 'capital_city')

С author.username ситуация интереснее. До значения атрибута username DRF будет добираться так:


  • На первой итерации инстанс это объект записи из модели Capital. Из source_attrs берётся первый элемент author, и значение одноимённого атрибута становится новым инстансом. author объект из модели User, с которой Capital связана через внешний ключ.
  • На следующей итерации из source_attrs берётся второй элемент username. Значение атрибута username будет взято уже от нового инстанса объекта author. Так мы и получаем имя автора.

Извлечённые из объекта табличной записи данные помещаются в упорядоченный словарь ret, но перед этим с ними работает метод to_representation поля сериалайзера:


ret[field.field_name] = field.to_representation(attribute)

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


В нашем случае применение to_representation по сути ничего не даст. Например, из поля табличной записи capital_city будет извлечена строка. Метод to_representation поля CharField к извлечённой строке применит метод str. Очевидно, что строка останется строкой, то есть какого-то реального преобразования не произойдёт. Но если бы из поля табличной записи IntegerField извлекались целые числа и передавались полю класса CharField, то в итоге они превращались бы в строки.


При необходимости создать собственный класс поля сериалайзера, описать специфичную логику и для метода get_attribute, и для метода to_representation, чтобы как угодно преобразовывать поступившие на сериализацию данные. Примеры есть в документации кастомные классы ColorField и ClassNameField.


Суммируем всё, что узнали


Преобразованный набор записей из Django-модели доступен в атрибуте data основного сериалайзера. При обращении к этому атрибуту задействуются следующие методы и атрибуты из-под капота DRF (разумеется, эти методы можно переопределить):


Метод, атрибут, функция Класс, модуль Действие
data serializes.BaseSerializer Запускает метод to_representation основного сериалайзера.
to_representation serializers.ListSerializer Запускает цикл, в ходе которого к каждой записи из набора применяется метод to_representation дочернего сериалайзера.
to_representation serializers.Serializer Сначала создаётся экземпляр упорядоченного словаря, пока он пустой. Далее запускается цикл по всем полям сериалайзера, у которых не выставлено write_only=True.
get_attribute fields (вызывается методом get_attribute класса fields.Field) Функция стыкует поле сериалайзера с полем записи из БД. По умолчанию идет поиск поля, чьё название совпадает с названием поля сериалайзера. Если передавался аргумент source, сопоставление будет идти со значением этого аргумента. Из найденного поля табличной записи извлекается значение текст, числа и т.д.
to_representation fields.КлассПоляКонкретногоТипа Извлечённое значение преобразуется согласно логике рассматриваемого метода. У каждого поля restframework она своя. Можно создать собственный класс поля и наделить его метод to_representation любой нужной логикой.

В словарь заносится пара ключ-значение:


  • ключ название поля сериалайзера;
  • значение данные, возвращённые методом to_representation поля сериалайзера.

Итог: список из OrderedDict в количестве, равном числу переданных и сериализованных записей из модели.




Надеюсь, статья оказалась полезной и позволила дать картину того, как под капотом DRF происходит сериализация данных из БД. Если у вас остались вопросы, задавайте их в комментариях разберёмся вместе.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru