Узнать тип type() и проверить тип isinstance()

В Python у нас утиная динамическая типизация, поэтому бывает что нужно узнать тип переменной. Функция type() возвращает тип аргумента или, учитывая, что в Python – все класс, то класс аргумента. Результат можно сравнить с известным типом:

>>> i, s = 10, "hello"
>>> type(i)
<class 'int'>
>>> type(i) is int
True
>>> type(s) is str
True
>>> class A: pass
...
>>> a = A()
>>> type(a) is A
True

Можно создать экземпляр объекта того же класса, что и переменная:

>>> new_a = type(a)()
>>> type(new_a) == type(a)
True

⚠️ Нужно знать! type() не принимает во внимание наследование. Тип наследника отличается от типа родителя:

>>> class B(A): pass
...
>>> type(A()) is type(B())
False

Лучший способ проверить типы – функция isinstance(x, type) (instance англ. – экземпляр). Она возвращает True, если первый аргумент является экземпляром класса во втором аргументе:

>>> isinstance(i, int)
True
>>> isinstance(s, str)
True
>>> isinstance(a, A)
True

Функция поддерживает наследование:

class A: pass
class B(A): pass
b = B()
>>> isinstance(b, A)
True

И самое крутое: вторым аргументом допускается передать кортеж из типов, и isinstance вернет True, если хоть один из типов в кортеже подходит:

>>> isinstance(i, (int, float))
True
>>> isinstance(a, (A, B))
True

Загадка:

class A: ...
a = A()
class A: ...
print(isinstance(a, A))

Правильный ответ был False!

Объяснение. Динамическая натура Python позволяет переопределить класс во время интерпретации. Помните, как недавно я рассказывал про декораторы класса? Там мы подменяли один класс другим. Вот это из той же оперы. Тут мы подменили один класс, другим классом, отвязав имя А от старого класса и привязав его к новому. Старый класс А остался жив только как класс объекта в переменной a. Старого и нового классов разные адреса (id):

class A: ...
print(id(A))  # 140479777819720

a = A()

class A: ...
print(id(A))  # 140479777809672

isinstance(a, A)  # False

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

О точности float в Python

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

Кто-то, вероятно, слышал о проблеме 0.1 + 0.1 + 0.1 == 0.3. Вкратце, вбейте в интерпретаторе Python:

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Здравый смысл подсказывает нам, что здесь что-то не так, должно же равняться! Новичков это вообще может вбить в ступор. Программисты поопытнее могут объяснить это ошибками округления float-чисел. Давайте же разберемся, что на самом деле там происходит.

Экспоненциальное представление чисел

Стандарт IEEE-754 регулирует, как должны представляться вещественные числа в железе (процессорах, сопроцессорах и так далее) и программном обеспечении. Так много вариантов представлений, но на практике почти везде сейчас используются числа с плавающей точкой одинарной или двойной точности, причем оба варианта с основанием 2, это важно.

Плавающая точка

Почему точка плавающая? Потому что числе представлены внутри компьютера экспоненциальном формате:

Число = ±мантисса * основаниеэкпонента

Меняя экспоненту можно двигать положение точки в любую сторону. Например, если основание было бы 10, то числа 1,2345678; 1 234 567,8; 0,000012345678; 12 345 678 000 000 000 отличались бы только экспонентой.

float в Python

float – встроенные тип в Python (CPython) и представляет собой число с плавающей точкой двойной точности, независимо от системы и версии.

float в Python – это double из C, C++, C# или Java и имеет 64 бита (8 байт) для хранения данных о числе.

Примечание: сторонними библиотеками можно получить и другие типы, а еще есть Decimal.

В эти 64 бита упакованы как 11 бит на экспоненту и 52 бита на мантиссу (+ 1 бит на знак, итого 53). Вот так:

Расположение бит мантиссы и экспоненты в 64 битах числа с плавающей точкой

Думаете, любое реальное число можно представить, используя эти 64 бита? Конечно, нет. Простая комбинаторика скажет, что у нас может быть не более 264 разных чисел (64 позиции по 2 варианта), а на деле их и того меньше. Диапазон чисел, представимых таким форматом составляет: ±1.7*10-308 до 1.7*10+308. То есть от очень малых по модулю чисел, до очень больших. Допустимые числа на числовой прямой распределены неравномерно: гуще в районе нуля и реже в районе огромных чисел.

Распределение чисел в представлении не равномерно. График показывает, как найти 0.6.
Здесь про 0.6, но смысл тот же.

Источник ошибок

Откуда же берутся ошибки?

Дело в том, что числа 0.1 – нет! Действительно, нет способа представить это немудреное число в формате с плавающей точкой с основанием 2!

0.1 – это просто текст, для которого Python должен подобрать максимально близкое представление в памяти компьютера. Можно найти число поменьше или побольше, но точно 0.1 – не получится. Все дело в основании 2 – именно двойка фигурирует под степенью. Надо подобрать такие J и N, чтобы получить число максимально близкое к 0.1:

0.1 = 1 / 10 ≈ J / (2**N)

или

J ≈ 2**N / 10

При этом в J должно быть ровно 53 бита. Наиболее подходящие N для такого случая равняется 56.

>>> 2**52 <= 2**56 // 10 < 2**53
True

>>> divmod(2**56, 10)
(7205759403792793, 6)

Остаток от деления – 6 чуть больше половины делителя (10), поэтому наилучшее приближение будет, если мы округлим частное вверх, то есть добавим к 7205759403792793 + 1 = 7205759403792794. Таким образом, это будет ближайшее к 0.1 число, возможное в представлении float. Доказательство проверкой:

>>> 7205759403792794 / 2 ** 56 == 0.1
True

Оно чуть больше, чем реальное 0.1. Если бы мы не прибавили единицу, то получилось бы число чуть меньшее, чем 0.1, но никакое сочетание J и N не даст нам ровно 0.1 ни в едином случае!

>>> format(7205759403792793 / 2 ** 56, '.56f')
'0.09999999999999999167332731531132594682276248931884765625'

>>> format(7205759403792794 / 2 ** 56, '.56f')
'0.10000000000000000555111512312578270211815834045410156250'

Это два соседних числа. Между ними не может быть других промежуточных чисел, в том числе и самого 0.1! Множество чисел, представимых числом с плавающей точкой дискретно и конечно, в нем нет всех возможных чисел.

Теперь понятно, что 0.1 и 0.3 аппроксимируются к каким-то ближайшим представлениям в экспоненциальной форме в памяти компьютера. Поэтому и возникает эта разница:

>>> format(0.1 + 0.1 + 0.1 - 0.3, '.56f')
'0.00000000000000005551115123125782702118158340454101562500'

Она мала, но она есть! Именно поэтому никогда не советуют точно сравнивать числа типа float, даже если для вас они равны, их представления могут отличаться, если числа получены разным путем. Могут отличаться, а могут и совпадать! Так что это может сыграть злую шутку.

>>> 0.15 + 0.15 == 0.3
True
>>> 0.1 + 0.15 + 0.05 == 0.1 + 0.1 + 0.1
False
>>> 0.1 + 0.15 + 0.05
0.3
>>> 0.1 + 0.1 + 0.1
0.30000000000000004

Там есть функция, которая делает более аккуратное сложение IEEE-754 чисел, но она тоже работает не идеально. На примере из документации – отлично и проваливается на нашем пресловутом триплете из 0.1:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

# не тут то было!
>>> math.fsum([0.1] * 3) == 0.3
False

Поверьте, это не единственная особенность такого представления чисел. Я обязательно расскажу больше. Будьте на связи!

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Генераторные выражения

Edison dynamo - КДПВ

Мы говорили про map и itertools.starmap, но я тут подумал… Зачем они, если есть замечательные генераторные выражения:

  • Они умеют делать: генераторы, списки list, словари dict и множества set.
  • Поддерживают вложенные циклы для обработки многомерных данных
  • Умеют фильтровать данные, как filter
  • Обладают лаконичным и понятным синтаксисом

По-английски они называются в зависимости от типа данных на выходе: generator expressions и list/dictionary/set comprehensions.

Если нам нужен генератор, то ставим круглые скобки. Если нужен сразу список – квадратные. Если нужен словарь или множество – фигурные. А внутри цикл for/in. Наш «прибавлятор» единицы стал короче и без лямбд:

>>> list(map(lambda x: x + 1, [1, 2, 3, 4]))
[2, 3, 4, 5]

>>> [x + 1 for x in [1, 2, 3, 4]]
[2, 3, 4, 5]

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

>>> from itertools import starmap
>>> list(starmap(pow, [(2, 4), (3, 2), (5, 2)]))
[16, 9, 25]

>>> [pow(base, exp) for base, exp in [(2, 4), (3, 2), (5, 2)]]
[16, 9, 25]

Если нужно множество (коллекция без повторов), то все то же самое, но скобки фигурные. Пример: все уникальные буквы слова:

>>> {r for r in 'BANANA'}
{'N', 'B', 'A'}

Если нужен словарь, то скобки также фигурные, но генерируем парами «ключ: значение». Пример: ключ – строка, значение – строка задом наперед:

>>> {key: key[::-1] for key in ["Mama", "Papa"]}
{'Mama': 'amaM', 'Papa': 'apaP'}

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

>>> g = (x ** 2 for x in [1, 2, 3, 4])
>>> next(g)
1
>>> print(*g)
4 9 16

Если функция принимает ровно 1 аргумент, то передавая в нее генератор можно опустить лишние круглые скобки:

>>> sum(x ** 2 for x in [1, 2, 3, 4])
30

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Flake8 + PyCharm: туториал

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

Я покажу, как настроить Flake8, чтобы он автоматически подсвечивал проблемы прямо в коде

Шаг 1. Установка Flake8

Flake8 – это по сути модуль Python, поэтому его можно установить в одну из виртуальных сред Python. Я обычно использую venv, который хранится в папке проекта, в него и установим. Это легко с помощью терминала прямо в PyCharm.

pip install --upgrade flake8
Терминал установки Flake8

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

pip freeze > requirements.txt

Шаг 2. Плагин File Watcher

У вас должен быть установлен плагин File Watcher, это официальный плагин, но он не всегда по умолчанию загружен – у меня вот не было его. Идем в настройки (на Маке – меню PyCharm – Preferences, в других системах посмотрите в меню File).

Preferences PyCharm на Mac

Там идем в Plugins – Marketplace – вбиваем в поиске File Watchers – Install – Restart IDE.

Как установить File watchers в PyCharm

Шаг 3. Настраиваем Watcher

Нам нужно, чтобы при редактировании файла IDE смотрела за изменениями и прогоняла по файлу Flake8, если что-то изменилось. Для этого надо создать File Watcher – смотрителя, который будет выполнять команду.

Там же в настройках идем в Tools – File Watches – жмем на плюсик.

Как найти File Watcher

Далее настраиваем смотрителя. Имя можно придумать любое. Остальные настройки:

  1. File type – Python
  2. Score – Current File, я выбрал только для текущего файла, чтобы расходовать меньше ресурсов и энергии. Вы можете выбрать на весь проект.
  3. Program – это путь до установленного Flake8 – я использую переменную, которая связана с путем к интерпретатору Python, файл flake8 лежит в ней же: $PyInterpreterDirectory$/flake8
  4. Arguments – аргументы для flake8 – путь к анализируемому файлу: $FileDir$/$FileName$
  5. Show console – Never – мы не будем никогда показывать консоль, а ошибки должны будут подчеркиваться прямо в коде!
  6. Output filters – это формат строки с описанием ошибки, которую выдает flake8, если находит стилистические проблемы. По этому формату IDE поймет, где именно возникла проблема (файл, строка, колонка, сообщение) – скопируйте это: $FILE_PATH$:$LINE$:$COLUMN$: $MESSAGE$
Иллюстрации настройки смотрителя

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

Шаг 4. Отображаем стилистические проблемы в коде

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

В настройках ищем: Editor – Inspections – File Watchers – File Watcher Problems – должна стоять галочка, затем правее выбираем Severity: Error. Жмем ОК и все готово!

Иллюстрация включение инспекции

Поздравляю. Начните редактировать файл и увидите, если возникнут сообщения о стилистических проблемах:

Инспекция в действии! Код подчеркнут!

Git hook!

Создадим хук для системы контроля версий GIT. Вы же пользуетесь ей, я надеюсь. Так вот pre-commit-hook – это действие, выполняемое перед коммитом. Будем запускать flake8 перед каждым коммитом, что плохо оформленный код не попадал в репозиторий. Сделать это очень просто в пару консольных команд:

flake8 --install-hook git
git config --bool flake8.strict true

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

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

starmap – это не звездная карта!

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

>> list(map(lambda x: x + 1, [1, 2, 3, 4]))
[2, 3, 4, 5]

Что делать, если нужно применить функцию, которая принимает большее количество аргументов? Например, возведение в степень pow принимает основание и показатель:

>>> pow(2, 4)
16

Как и требуют, мы даем в map функцию с одним аргументом, но каждый элемент t – кортеж из двух элементов, мы распаковываем его в аргументы pow звездочкой:

>>> list(map(lambda t: pow(*t), [(2, 4), (3, 2), (5, 2)]))
[16, 9, 25]

Если вы не знали: pow(*t) то же самое, что и pow(t[0], t[1]), если в t два элемента.

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

>>> from itertools import starmap
>>> list(starmap(pow, [(2, 4), (3, 2), (5, 2)]))
[16, 9, 25]
Схема работы map и starmap показывает как передаются аргументы

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈