Тесты для гугл форм (1). Python является Варианты ответов
Чувствителен ли PYTHON к регистру (большая или маленькая буквы):
- Да
- Нет
Какие существуют типы переменных (выбрать несколько выриантов):
- float
- list
- num
- int
- bool
- integer
- вещественная переменная
- символьная строка
- логическая переменная
- целая переменная
- символьная строка
- логическая переменная
- целая переменная
- целая переменная
- вещественная переменная
- логическая переменная
Каков будет результат выполнения int(«88»):
- «88»
- 88
- 88.00
Каков будет результат выполнения str(88):
- «88»
- 88
- 88.00
Имена переменных не могут включать:
- Русские буквы
- Латинские буквы
- Пробелы
- Скобки, знаки + = ! ? b др.
Какие имена являются правильными в PYTHON (выбрать несколько):
- N
- ABC
- sum
- 41And
- A+B
- _mam
Что будет в результате выполнения команды:
- 25
- 2500
- 25000
- 1000
Что будет в результате следующего действия print(2**20)
- 104576
- 1048576
- 964
- 2
Что будет в результате выполнения следующего действия print(23 % 2)
- 11
- 1
- 0
Результатом вычисления print(24 // 3) будет число:
- 4
- 8
- 12
Что будет результатом выполнения алгоритма:
a = int(input())
b = int(input())
s = a + b
print(s)
- 57
- 12
- 35
Что будет результатом выполнения алгоритма:
a = input()
b = input()
s = a + b
print(s)
- 12
- 57
- 35
Что будет в результате выполнения следующего алгоритма:
Входные данные: -57
x = int(input())
if x > 0:
print(x)
else:
print(-x)
Arduino.ru

Коллеги, сегодня мне захотелось поговорить о типе float и его использовании.
Как многие, наверное, знают, аппаратно восьмиразрядные AVR контроллеры плавающую арифметику не поддерживают, а потому, всё, что с нею связано, реализовано программно. Ну, а раз программно, то работает это не так быстро как хотелось бы, да ещё и отжирает приличную память в области программ на библиотеку с программной реализацией всех функций (как только используете float – порядка двух кило памяти «вынь, да положь»).
Поэтому, первый совет по использованию типа float – избегать такового использования без крайней нужды. Ну, вот, как пример, вот нафига библиотека DallasTemperature возвращает результат измерений именно как float (причём больше там float ни для чего не используется)? Руки бы поотрывал таким «писателям». Неужели датчику с паспортной погрешностью в 0,5 градуса нужна точность до семи знаков? Да, ну нафиг – вполне хватило бы возвращать целое число в десятых долях градуса. Так нет же … и два кило программной памяти псу под хвост!
Но иногда без float действительно не обойтись, так что давайте посмотрим, какие грабли там вокруг него разбросаны. Все приводимые примеры, исполнялись на Nano, а компилировались IDE 1.8.1 с опциями «из коробки».
1. Общие сведения о числах с плавающей запятой
В языке С++ определены три типа для работы с такими числами (стандарт ISO/IEC 14882:2014, п. 3.9.1.8):
float — тип с плавающей точкой одинарной точности;
double — тип с плавающей точкой двойной точности;
long double — тип с плавающей точкой повышенной точности.
В нашем случае, все три типа идентичны – все представляют собой 32-битное число одинарной тонности и разницы между ними нет никакой.
Первое важное замечание: в памяти плавающие числа представлены как отдельно мантисса, отдельно порядок и отдельно знак.
Прямым следствием из первого важного замечания является тот факт, что в плавающей арифметике существуют два нуля – положительный и отрицательный. В принципе, ничего страшного, операция == выдаёт для них равенство, но иногда это важно при некоторых видах умножения (см. ниже).
Точность представления чисел приблизительно семь знаков мантиссы, а наибольшее и наименьшее возможные значения приблизительно ±3,4х10 ±38 .
Из последнего абзаца прямо следует второе важное замечание: числа с плавающей запятой точно представлены быть не могут – почти всегда они представляются с некоторой погрешностью. Например, невозможно точно записать число 0,1 (или 0,2). То, что хранится в памяти, обязательно будет отличаться от точного значения в каком-нибудь далёком разряде.
Также существуют три специальных значения: NaN – «не число» и две бесконечности, положительная и отрицательная. Деление на 0 даёт бесконечность со знаком делимого. Деление на бесконечность даёт ноль с соответствующим знаком.
Вот, собственно всё, что нам важно знать для дальнейшего. Те, кого интересуют подробности, могут почитать стандарт представления плавающих чисел IEEE 754-2008 и/или многочисленные статьи и эссе на эту тему, коими забит Интернет.
Мы же перейдём непосредственно к граблям и вкусняшкам.
2. Неточность представления значений
Давайте запустим вот такую программу:
и полюбуемся на её результат:
Ну, что, вроде бы a и b честно равны 0,2 (строки 2 и 3 результата), но при этом почему-то a не равно b (строки 4 и 5 результата)!
Как такое может быть? Правильно — числа представленные в памяти на самом деле не равны друг друг другу, а строки 2 и 3 вводят нас в заблуждение исключительно из-за округления.
Давайте напечатаем наши a и b с большим количеством знаков после запятой.
Вот теперь всё стало на свои места. a > b, что мы и наблюдаем.
Таким образом, мы выяснили, что выражения «1.2 — 1.0» и «0.2» в плавающей арифметике вовсе не равны друг другу. И это не косяк конкретного компилятора и даже не следствие китайского происхождения Вашей ардуины – это факт, прямо следующий из особенностей реализации плавающей арифметики. Бороться с этим невозможно, остаётся только приспособиться. Вот давайте и начнём приспосабливаться.
Первый шаг в нашем приспособленчестве – это научиться таки сравнивать плавающие числа, чтобы «1.2 — 1.0» и «0.2» всё же получались равными друг другу, а не выносили нам мозг своей экзистенциальной сущностью.
3. Сравнение плавающих чисел
Зайти на форум по теоретическому программированию и сказать, что Вы придумали универсальный алгоритм сравнения плавающих чисел – это высшая форма троллинга! Всё равно, как на форуме зоозащитников заявить о необходимости санитарного отстрела бродячих животных. Многостраничный, плавно переходящий в срач, холивар, сопровождаемый невосполнимыми потерями форумного населения от банов и прочих ограничений области видимости – это меньшее на что Вы можете рассчитывать.
Но это касается «универсального», применимого везде и всегда метода, т.е. «сферического коня в вакууме». Мы же с Вами всегда решаем конкретную задачу, и исходя их это задачи, примерно представляем какая точность для нас приемлема и желательна, и потому всегда можем определить некую величину eps, о которой можем сказать, что если два числа различаются на величину меньшую, чем eps, то мы будем считать их равными. Тогда сравнения будут выглядеть так:
Функция fabs, использованная в строке 4, возвращает свой аргумент, взятый «по модулю» и нужна здесь для того, чтобы результат сравнение не зависел то того, что больше (на величину меньшую, чем eps) a или b.
Ещё раз — eps Вы выбираете сами, исходя из сущности задачи. Например, если a и b — температуры. полученные с далласовского датчика, погрешность которого, как известно ±0,5 градуса, то eps можно взять 1 или, скажем, 0.75.
На самом деле, такой подход к сравнению, с точки зрения теоретиков, не выдерживает никакой критики, и может быть разнесён в хвост и в гриву. Однако, очень многие программисты его используют поскольку, если есть понимание задачи и величина eps выбирается разумно, то он вполне прилично работает, хотя, с точки зрения теоретиков, и не должен бы.
Нужно только помнить, что если то, что Вы считаете — не окончательный результат, а только данные для дальнейших рассчётов, то лучше считать точнее, т.к. погрешности накапливаются. Подробнее об этом я планирую написать в «Приложении для любопытного читателя» чуть позже в этом топике.
4. Как не накапливать погрешность
Как мы уже видели на примере вычитания единицы из 1,2, любая операция с плавающей точкой выполняется с некоторой погрешностью. Она небольшая, но проблема в том, что если операции выполнять много раз, то она имеет очень неприятную привычку накапливаться. А ведь многие программы для микроконтроллеров предназначены для работы в безостановочном режиме сутками, неделями и месяцами – за такое время можно такую погрешность накопить, что будь здоров.
Для примера, предположим, что нам нужно что-то делать с величинами 0.000002, 0.000004, 0.000006, 0.000008 и т.д. до 2.0 – т.е. в общей сложности с миллионом значений. Как получать эти значения? Можно завести переменную, равную 0, к ней прибавить 0.000002, сделать с результатом что нужно, прибавить ещё 0.000002, опять сделать с результатом что нужно и так миллион раз. Запишем это:
Что мы здесь имеем. Переменна fi получается последовательным прибавлением шага 0.000002 к старому значению. Значит, чем больше раз мы это сделаем, тем большую погрешность накопим. Окончательное (самое последнее) значение fi будет получено путём миллиона операций сложения!
Давайте попробуем другой подход. Теперь мы не будем прибавлять шаг миллион раз, а вместо этого, на каждом шаге будем брать величину, полученную умножением 0.000002 на номер текущего шага. Запишем это:
Здесь на любом шаге, наше значение fi всегда получается в результате одной операции умножения. Потому, никакая погрешность накапливаться не будет и погрешность значения fi на любом шаге буде равна погрешности одной операции умножения – независимо от количества шагов!
Давайте сравним погрешности в конечном значении переменной fi для первого и второго подходов.
Как видите, при первом подходе (где fi получалась накапливающим сложением) погрешность за миллион шагов составила почти 2%. А при втором подходе, погрешность оказалось пренебрежимо малой, такой, что при нашей печати и вовсе обратилась в 0.
Вывод: по возможности, старайтесь никогда не вычислять значение величины итеративным путём, т.к. при этом Вы накапливаете погрешность. Везде, где возможно, старайтесь свести количество операций для получения значения к минимуму.
Возможно, кому-то проблема накопления погрешности покажется искусственной и надуманной. Напрасно! Именно с этой ошибкой связаны наиболее эпичные техногенные катастрофы, вызванные ошибками в программном обеспечении. Например, именно накопление погрешности в системе управления американским ракетным комплексом Patriot стало причиной того, что древняя как Фортран советская ракета 8К14, выпущенная иракскими военными, сумела долететь до американской военной базы, охраняемой Patriot’ами и угробить почти 30 военнослужащих, прихватив ещё под сотню ранеными.
5. Знаковые нули, бесконечности и не числа
Как уже говорилось, ноль здесь может быть положительным и отрицательным. Само по себе это не страшно, но знак может повлиять на дальнейшие вычисления. Также, есть три специальных значения: положительная бесконечность, отрицательная бесконечность и «не число» — NaN.
Бесконечности получаются либо когда результат операции выходит за границы возможно, либо при делении на 0. С ними можно продолжать операции, например, число, разделённое на бесконечность, даст 0, сложение бесконечности с числом даст ту же бесконечность. Операции с двумя бесконечностями (например, деление их друг на друга) недопустимы, результат – NaN.
Вообще значение NaN получается в результате любой ошибки вычислений. С самим NaN также можно проделывать операции (сложение, умножение и т.п.), результат — всегда NaN. Забавным свойством значения NaN является то, что оно одновременно не больше и не меньше любого числа, а также NaN не равно ничему, включая самого себя.
Обычно появление в расчётах значений типа NaN и бесконечностей, является следствием ошибок. Для контроля таких ситуаций. Стандарт предоставляет специальные функции, позволяющий проверить является ли число бесконечность или NaN. Функции эти: isnan(f) и isfinite(f). Первая возвращает истину, если аргумент является NaN, а вторая возвращает истину, если аргумент не является ни бесконечностью, ни NaN. Для использования этих функций требуется включить #include <math.h>
Если у Вас возникла проблема и Вам нужно искать ошибку, то для печати значений типа float в монитор порта лучше не использовать напрямую Serial.print, а делать это через dtostrf (как в моём примере ниже), т.к. Serial.print плохо печатает как раз такие специальные значения, например, он никогда не печатает знак перед нулём, даже, если ноль отрицательный.
Теперь посмотрите пример работы с этим хитрыми значениями. Пример достаточно самодокументирован, потому пояснять его отдельно я не буду.
Преобразуем в строку. Часть 2. Числа с фиксированной и плавающей точкой.

Продолжаем преобразовывать всё, что можно в строки.
В предыдущей статье были целые числа, теперь очередь чисел с фиксированной и плавающей точкой.
Все рассмотренные примеры с фиксированной точкой используют формат с 16-ю битами для дробной части и 16-ю битами для целой части, так называемый формат Q16, однако легко могут быть адаптированы для других форматов.
В качестве чисел с плавающей точкой использован 32-х разрядный float.
Числа с фиксированной точкой.
Стандартная библиотека языка Си не предоставляет никаких средств для вывода чисел с фиксированной точкой. Зато есть вывод чисел с плавающей точкой — функции *printf. Можно преобразовать число с фиксированной в число с плавающей точкой и вывести его с помощью, например, sprintf. Первый наивный способ преобразовать фиксированную точку в плавающую, состоит в том, что целочисленное представление числа с фиксированной точкой непосредственно приводится к типу с плавающей точкой, после чего делится на целочисленное представление на единицы в формате с фиксированной точкой:
Тип double, а не float, в качестве типа с плавающей точкой использован потому, что sprintf принимает именно его. Для AVR это не имеет значения, так как float и double там одно и тоже.
Этот метод очень прост и быстр в реализации, к тому-же достаточно надёжен: меньше кода — меньше ошибок. На этом его преимущества заканчиваются и остаются недостатки:
— медленный: AVR 4200 тактов, STM32 — 4234 тактов в среднем;
— использует sprintf и тянет за собой всё её обвязку;
— использует целых две операции с плавающей точкой.
Первый недостаток усугубляется третьим. Попробуем избавится от последнего недостатка, вручную преобразовав число с фиксированной точкой во float. Для этого надо найти в числе с фиксированной точкой самый старший единичный бит и сдвинуть его на место неявного старшего единичного бита числа с плавающей точкой — 23-его бита. После чего этот бит надо обнулить, так как там хранится младший бит экспоненты. Таким образом получится мантисса искомого float-а. Экспонента вычисляется по формуле:
exponent = 127 + «число бит в дробной части» — 9 + «насколько бит сдвинули исходное число»
— 9 здесь нужно потому, что целевой бит, куда нужно сдвинуть старший единичный бит — девятый с конца.
«насколько бит сдвинули исходное число» — положительное, если сдвигали вправо, отрицательное, если влево.
При сдвиге вправо могут потеряться до 8 бит младших дробной части.
Число с фиксированной точкой в этом примере без знаковое, поэтому знак не трогаем.
В результате на AVR такой метод работает за 2298 тактов в среднем — почти в два раза быстрее. На STM32 всё по прежнему «плохо» — 4194 такта — операции с плавающей точкой там не являются узким местом — это sprintf так медленно работает.
Следующий вариант — это преобразовывать целую часть отдельно по одному из методов из предыдущей статьи, а дробную — методом умножения на 10. Для преобразования целой части целесообразно взять самый быстрый метод — деление на 10 сдвигами и сложениями.
Получилось неплохо никаких внешних зависимостей и кода не много. На AVR этот метод работает в среднем за 490 тактов, на STM32 — за 148. Результат отличный — быстрее, чем 32-х битное целое преобразуется по методу быстрого деления.
Также для чисел с фиксированной точкой применим метод умножения на 10. Он почти ничем не отличается от аналогичного для целых чисел. Только на этот раз для умножения исходного числа на AVR-ках я применил не тупой цикл со сдвигами и сложениями, честное 64-битное умножение по алгоритму Д. Кнута. Дело в том, что встроенное 64-битное умножение в avr-gcc не оптимизировано и работает неприлично долго порядка 800 тактов. Реализация, приведённая ниже работает за чуть более 100 тактов. Функция mul32hu возвращает старшие 32 бита от произведения, которые получаются в переменной u32[1]. Если нужно полное 64-х битное произведение, то можно вытащить и младшую часть — она находится в переменной u32[0].
На STM32 имеется инструкция умножения 32×32=>64, по этому используем встроенное умножение.
Кстати, если из метода с умножением на 10 из предыдущей статьи (utoa_fract) выкинуть цикл умножения и заменить его на mul32hu, то он будет выполняться на AVR в среднем за 648 тактов, что делает его быстрее, чем метод вычитания степеней 10 (utoa_cycle_sub).
В самом методе, как и для целых чисел, сначала умножаем исходное число на множитель, который представляет из себя степень двойки, соответствующую той битовой позиции, где будут извлекаться цифры, делённую на степень 10, такую, чтоб не было переполнения. Затем в цикле извлекаются десятичные цифры с определённой битовой позиции, в данном случае с 29 бита, и число умножается на 10. После извлечения всех цифр производится коррекция коррекция старшей цифры, так как она может оказаться больше 10. Для этой коррекции в строковом буфере предусмотрен 1 символ, который инициализируется в ‘0’. После удаляются ведущие и завершающие нули.
На AVR этот метод работает в среднем за 604 такта, на STM32 — 236. Не плохо, но всё-же хуже чем у предыдущего метода. Однако, если дробная часть будет не 16 разрядов, как в примере, а занимать все 32 бита, то умножение будет не нужно и этот метод станет проще и быстрее предыдущего.
Внимательный читатель заметит эту конструкцию.
Почему нельзя оставить только закомментированный фрагмент?
Потому, что в avr-gcc сдвиг 32-х разрядного числа плохо оптимизирован. Вместо того, чтоб взять старший байт и сдвинуть его на 4 разряда, компилятор генерирует цикл, в котором честно сдвигает все 4 байта на 28 разрядов. Это у него занимает 28*(4+1) + 2 = 142 цикла вместо 3 (mov, swap, andi), которые должны быть. А ведь это выполняется в цикле 9 раз, итого 1278 такта — дофига для такой мелочи. Приходится компилятору немного помогать.
Плавающая точка.
Для преобразования чисел с плавающей точкой в строку тоже можно придумать много вариантов, но я этого делать не буду. Приведу только один вариант и сравню его с sprintf.
Этот вариант будет, естественно, умножение на 10, потому, что чтоб вытащить цифры из мантиссы, её всё-равно придётся умножать. В методах умножения на 10 для целых и чисел с фиксированной точкой, начальное 64 битное умножение занимало заметную долю времени. Поэтому они оказывались несколько медленнее, чем некоторые другие, несмотря на то, что умножение на 10 само по себе быстрее, чем деление. В случае с плавающей точкой он должен оказаться безоговорочным лидером.
Для начала посмотрим на что способна sprintf, при выводе float-ов.
Для AVR sprintf работает в среднем за 2000 такта, на STM32 — 3950. Не стоит ругать sprintf на STM32 — помним, что там преобразуется double, а не float.
При этом sprintf с ключиком %g округляет результат до заданной точности, удаляет незначащие нули и выводит результат в наиболее подходящем виде: в обычном 123.45, или научном 1.2345e+2.
Задача состоит в том, чтоб получить вывод идентичный sprintf, но гораздо быстрее.
Разделим задачу на две части:
1 — извлечение из float-а значащих цифр и десятичной экспоненты;
2 — форматирование результата в нужном виде.
Для первой части нужно:
— распаковать float, вытащить из него мантиссу, двоичную экспоненту и знак;
— обработать особые случаи: ±0, ±inf, nan.
— получить десятичную экспоненту;
— получить множитель 2 в степени экспоненты, делённая на степень 10, такую, чтоб не было переполнения;
— умножить мантиссу на этот множитель;
— извлечь требуемое количество значащих цифр + одну для округления;
— округлить полученные цифры;
— удалить незначащие нули.
Извлечение цифр и десятичной экспоненты выделим в отдельную функцию:
presc — это необходимое количество значащих цифр.
Теперь распакуем float:
Финт ушами со сгвигим экспоненты на 16 бит, а потом еще на 7, опять-же чтоб задобрить компилятор.
Теперь извлечём знак:
Первый символ в выходном буфере — знак + или -.
Обрабатываем ±0, ±inf, nan:
Денормализованные числа считаем равными нулю. Это не совсем правильно, но мне не хотелось с ними возиться.
Резервируем один символ в буфере для округления:
Вычислить требуемый множитель за приемлемое время не получится, можно конечно, сдвигать влево-вправо, умножать-делить на 10 константу в цикле в зависимости от экспоненты, но это очень медленно. Поэтому множитель нужно брать из таблицы. Но 256 32-х битный чисел — этож целый килобайт, слишком жирно. Соседние элементы этой таблицы будут отличаться на степень 2, следовательно результат умножения для соседних элементов можно получить сдвигая его в нужную сторону на количество бит, равное расстоянию между этими элементами. Поскольку мантисса 24-х разрядная, то сдвигать её можно на 8 разрядов без потери точности. Это значит, что в таблице можно оставить только каждый 8 элемент, 32 элемента — уже приемлемо. Чтоб сохранить точность, сначала будем сдвигать мантиссу на 8 бит влево, потом умножать, потом сдвигать влево на требуемое количество бит. Так удаётся сохранить все 24 бита точности не выходя за 32-х битную арифметику (кроме умножения, конечно).
Десятичную экспоненту несложно вычислить из двоичной, они связаны линейной зависимостью. Сложность только в том, чтоб подобрать масштабирующие и сдвигающие коэффициенты, при которых будет верный результат во всём диапазоне входных значений, и не вылезти при этом за пределы 16-ти битной арифметики.
Теперь удалим ведущие нули:
Удаление завершающих нулей.
В итоге в буфере знак и значащие цифры, десятичная экспонента в возвращаемом из функции значении.
Теперь дело за форматированием:
Получилось достаточно многословно, но результат того стоил. На AVR в среднем 759 тактов, на STM32 — 425. Для сравнения в avr-libc есть функция dtostre, которая использует для преобразования тот-же движок, что и sprintf, но выводит всегда в экспоненциальном формате и не удаляет незначащие нули. Её среднее время в этом тесте — 1215 тактов. И это при том, что преобразование там реализовано на ассемблере.
Результат тестов с фиксированной точкой для AVR:
Результат тестов с фиксированной точкой для STM32:
Результат тестов с плавающей точкой для AVR:
Результат тестов с плавающей точкой для STM32: 
Все тесты проведены на компиляторах avr-gcc и arm-gcc соответственно, с оптимизацией -O3.