Рубрика: Программирование

Посты, связанные с разработкой ПО.

​​Анимация Jupyter Notebook

Сегодня мы будем анимировать график прямо внутри Jupyter Notebook. Сперва сделаем плавную отрисовку графика. Переключим режим отображения графиков в notebook:

%matplotlib notebook

Импортируем все, что нужно:

import matplotlib.pyplot as plt
from matplotlib import animation
import numpy as np

Сгенерируем наши данные:

# время (200 точек)
t = np.linspace(0, 2 * np.pi, 200)
x = np.sin(t)  # синусоида

Создадим пустой график:

fig, ax = plt.subplots()
# пределы отображения
ax.axis([0, 2 * np.pi, -2, 2])
l, = ax.plot([], [])

Функция animate будет вызываться при отрисовка каждого кадра, аргумент i – номер кадра:

def animate(i):
    # рисуем данные только от 0 до i
    # на первом кадре будет 0 точек, 
    # а на последнем - все
    l.set_data(t[:i], x[:i])

Запускаем анимацию:

fps = 30  # карды в сек
# frames - число кадров анимации
ani = animation.FuncAnimation(fig, animate, frames=len(t), interval=1000.0 / fps)

Если мы хотим анимировать сами данные, например, заставить синусоиду «плясать», то на каждом шаге перегенерируем данные заново, используя переменную i:

def animate(i):
    x = np.sin(t - i / len(t) * np.pi * 2) * np.sin(t * 15)
    l.set_data(t, x)

Можно сохранить в GIF:

ani.save('myAnimation.gif', writer='imagemagick', fps=30)

Сам ноутбук я загрузил на GitHub, но поиграться онлайн с ним не получится, надо скачать себе и запустить локально. Анимированные графики отрисовываются в реальном времени, поэтому требуют достаточно много ресурсов. Пример 3D анимации:

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

Временные файлы и директории

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

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

import tempfile

with tempfile.NamedTemporaryFile() as fp:
    print(fp.name)  # путь к файлу
    fp.write(b'Hello world!')
    fp.seek(0)
    print(fp.read())

fp – файло-подобный объект, вроде того, что идет из open. С ним работают также, как с обычным файлом. Он будет удален в момент закрытия.

Есть еще TemporaryFile. Отличие NamedTemporaryFile от TemporaryFile в том, что NamedTemporaryFile будет гарантированно виден в файловой системе и иметь атрибут name, тогда как второй может быть и не виден в ФС. NamedTemporaryFile можно создать с ключем delete=False, чтобы он не был удален. А TemporaryFile всегда будет удален при закрытии.

Режим открытия временного файла по умолчанию "w+b", т.е. можно писать и читать бинарный данные. Можно изменить передав аргумент mode:

tempfile.NamedTemporaryFile(mode='w')

TemporaryDirectory – создает временную директорию и возвращает строку – путь к ней. Мы можем создавать в директории любые файлы в любом количестве. После закрытия контекстного менеджера директория и все файлы в ней будут автоматически удалены. Очень удобно! Можно не запоминать названия или ссылки на файлы. Пример:

with tempfile.TemporaryDirectory() as temp:
    with open(os.path.join(temp, '1.txt'), 'w') as f:
        f.write('hello')

Если надо вручную очистить (можно только 1 раз, после она будет удалена):

tmp = tempfile.TemporaryDirectory()
with open(os.path.join(tmp.name, '1.txt'), 'w') as f:
    f.write('hello')
tmp.cleanup()  # очистка

Узнать где хранятся временные файлы:

>>> tempfile.gettempdir()
'/var/folders/m8/1_wxy73215q9n2vrjetnw0xjc0000gn/T'

Эта директория берется из переменных окружения TMPDIR, TEMP, TEMP или это директория C:\TEMP, C:\TMP, \TEMP и \TMP (для Windows) или /tmp, /var/tmp и /usr/tmp для остальных систем. 

Как поменять место хранения временных данных процесса?

  1. Изменить переменную окружения: TMPDIR="/home/me/temp" python my_program.my
  2. Передать в функции создания временных файлов аргумент dir с нужным путем: tempfile.NamedTemporaryFile(dir='/home/me')

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

Абстрактный класс ABC

Просто лого для статьи

Абстрактный класс – класс, содержащий один и более абстрактных методов.

Абстрактный метод – метод, который объявлен, но не реализован.

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

В Python нет синтаксической поддержки абстрактных классов, но есть встроенный модуль abc (расшифровка – abstract base classes), который помогает проектировать абстрактные сущности.

Абстрактный класс наследуют от ABC (Python 3.4+) или указывают метакласс ABCMeta (для Python 3.0+):

from abc import ABC, ABCMeta
class Hero(ABC):
    ...

# или:
class Hero(metaclass=ABCMeta):
    ...

Любой из вариантов работает, первый современнее и короче. На данном этапе мы можем создавать объекты этих классов, потому что в них пока не абстрактных методов. Добавим:

from abc import ABC, abstractmethod
class Hero(ABC):
    @abstractmethod
    def attack(self):
        pass

Hero() – выдаст ошибку "TypeError: Can't instantiate abstract class Hero with abstract methods attack", которая говорит, что в классе Hero есть абстрактный метод attack. Мы вставили в него заглушку pass, но вообще там может быть какая-то реализация. Отнаследуем от героя Hero – конкретный подкласс лучника Archer:

class Archer(Hero):
    def attack(self):
        print('выстрел из лука')
Archer().attack()

Вот объект Archer мы можем уже создать и использовать реализацию метода attack

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

class C(ABC):
   @classmethod
   @abstractmethod
   def my_abstract_classmethod(cls):
       ...

   @staticmethod
   @abstractmethod
   def my_abstract_staticmethod():
       ...

   @property
   @abstractmethod
   def my_abstract_property(self):
       ...

   @my_abstract_property.setter
   @abstractmethod
   def my_abstract_property(self, val):
       ...

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

Формально говоря, абстрактные классы для Python не являются чем-то необходимым в силу динамичности языка. Если мы выкинем все упоминания абстрактности классов и методов из рабочего кода, он продолжит работать, как и ранее. Абстрактные классы нужны на этапе проектирования или расширения кода, чтобы обеспечивать «правильные» взаимодействия новых классов, защищая от создания экземпляров абстрактных классов. Важно помнить, что эта защита срабатывает на этапе выполнения программы, а не компиляции, как в языках Java, C++ или C#!

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

Decimal числа. Отличия от float

После рассказа про float меня просили рассказать про Decimal. Узнаем же, что это за зверь, как он устроен внутри и как с ним работать. Итак, Decimal – это класс из стандартного модуля decimal. Он представляет собой число с плавающей точкой, как и float. Да, именно с плавающей, потому что некоторые, я слышал, думают, что это число с фиксированной точкой.

Однако, Decimal имеет ряд существенных отличий от float.

Цель

Тип Decimal создан, чтобы операции над рациональными числами в компьютере выполнялись также, как они выполняются людьми, как их преподают в школе. Иными словами, чтобы все-таки 0.1 + 0.1 + 0.1 == 0.3. Из-за ошибок представления, float приводит к утере точности, и такие простые на первый взгляд равенства не выполняются. А это может быть критично в высокоточных научных вычислениях, и главное в сфере бизнеса и финансов!

Внутреннее устройство

float – реализован по стандарту IEEE-754 как число с плавающей запятой двойной точности (64 бита) на основании 2. Реализация таких чисел заложена прямо в железо почти любого современного процессора. Поэтому float в Python работает примерно также, как и double в С, С++, Java и прочих языках. И имеет такие же ограничения и «странности». Так как поддержка float имеет аппаратный характер, то его быстродействие сравнительно велико.

Decimal – число с плавающей точкой с основанием экспоненты – 10, отсюда и название (decima лат. – десятая часть, десятина).

Он реализован по стандарту IBM: General Decimal Arithmetic Specification Version 1.70 – 7 Apr 2009, который в свою очередь основан на стандартах IEEE. По поводу реализации, в исходниках CPython я нашел два варианта: на чистом Python и с помощью Си-библиотеки libmpdec. Обе реализации есть в кодовой базе, хотя в последних версиях Python 3 используется именно Си-версия, очевидно, она в разы быстрее! Видите букву Си?

Python 3.7.5 (default, Nov 13 2019, 14:05:23)
[Clang 11.0.0 (clang-1100.0.33.12)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import decimal
>>> help(decimal)

Help on module decimal:

NAME
    decimal - C decimal arithmetic module
...

Поэтому первый важный вывод:

Хоть Decimal и написан на Си, он в разы медленнее, чем float, так как реализован программно, а float – аппаратно.

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

Теперь самое главное – основание 10. Оно позволяет записывать десятичные дроби точно, без ошибок представления.

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

Мантисса и экспоненты – целые числа.

Помните, что мы не могли представить 0.1 в float с основанием 2? С основанием 10 – это элементарно:

0.1 = 1 * 10-1, и таким образом, 0.3 = 3 * 10-1 = (1 + 1 + 1) * 10-1 = 0.1 + 0.1 + 0.1

Как мы в школе учили десятичные дроби и знаем, как оперировать ими, так и здесь. Все точно и привычно. Ну почти все. Если мы разделим единицу на тройку, то получим бесконечную периодическую дробь 0.33333333…, либо по другому ее пишут 0.(3) – три в периоде. Естественно, что бесконечных чисел, записанных цифрами в памяти компьютера быть не может, иначе бы потребовалась бесконечная память. Поэтому количество троек в записи может быть большим, но обязано быть конечным.

Decimal оперирует с числами с произвольной (задаваемой пользователем), но конечной точностью.

По умолчанию точность – 28 десятичных знаков.

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

  • Точность выполнения операций в количестве десятичных знаках
  • Режимы округления (их целых 8 штук)
  • Пределы по экспоненте
  • Режимы обработки исключительных ситуаций – настройки сигналов (например, деление на ноль, переполнение и прочее).

Флаги в контексте устанавливаются со стороны модуля decimal, если при последнем вычислении случился какой-то из сигналов. (Это отдельная тема, о ней потом.)

Сам же объект Decimal содержит знак, мантиссу (коэффициент перед экспонентой) и саму экспоненту (степень). Лишние нули в мантиссе на обрезаются, чтобы сохранять значимость числа (1.20 * 2.40 = 2.8800).

Decimal – иммутабельный (неизменяемый) тип. Операции над ним приводят к созданию новых объектов, а старые не меняются.

Поработаем с Decimal

Начинаем с импорта и посмотрим, каков контекст по умолчанию:

>>> from decimal import *
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

Мы видим здесь, что точность 28 знаков, округление к ближайшему четному, пределы по экспоненте ±999999, capitals – это про заглавную Е при печати, clamp не будем трогать пока что, флаги все сброшены, а включенные ловушки – неправильная операция, деление на ноль, переполнение. Если ловушка включена, это значит, что при возникновении соответствующего сигнала будет брошено исключение. Если нет ловушки, то при сигнале будет только втихую установлен флаг. Я оставлю тему ловушек на следующую статью.

Создание Decimal

Создать Decimal можно из обычного целого числа, из float, из строки или кортежа. С обычным числом все просто – int представлены и так точно:

>>> Decimal(1)
Decimal('1')
>>> Decimal(-1)
Decimal('-1')
>>> Decimal(10002332)
Decimal('10002332')

Из float – надо быть очень аккуратным. Потому что, float округляется внутри до ближайшего возможного, а Decimal не знает о ваших первоначальных намерениях, поэтому копирует содержимое float. К примеру, числа 0.1 в представлении float просто не существует. Python считывает 0.1 из кода как строку, потому ищет наиболее близкий к нему возможный float, а из него уже копируется содержимое в Decimal, как есть – уже с ошибкой:

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

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

Логически правильно создавать Decimal сразу из строки, избегая фазу с превращением его в float! Что есть в строке – попадет в Decimal. Может показаться, что это немного криво – хранить числах в строках, но теперь вы знаете о представлении двоичного float, и строки обретают реальный смысл.

>>> Decimal('0.1')
Decimal('0.1')
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal('1.2e+10')
Decimal('1.2E+10')
>>> Decimal('10_000_000_000')  # c версии Python 3.6 можно подчеркивания
Decimal('10000000000')

Можно строкой еще задавать бесконечности и NaN (не число). Примеры:

>>> Decimal('Inf')
Decimal('Infinity')
>>> Decimal('-Inf')
Decimal('-Infinity')
>>> Decimal('nan')
Decimal('NaN')

Если использовать кортеж для конструирования Decimal, то он должен содержать три элемента:

  1. Знак, как число: 0 – это плюс, 1 – это минус.
  2. Кортеж из значащих цифр мантиссы
  3. Число – показатель экспоненты

Вообще кортеж для Decimal использует редко. Но вот вам пример:

>>> Decimal((0, (1, 2, 3, 4, 5), -1))
Decimal('1234.5')
>>> Decimal((1, (7, 7, 7), 3))
Decimal('-7.77E+5')

Если число слишком большое, то будет сигнал – неправильная операция. А так как на этом сигнале ловушка по умолчание – то будет исключение:

>>> Decimal("1e9999999999999999999")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]

Точность представление Decimal задается исключительно длиной задающего числа (или длиной строки). Настройки точности и режимов округления из контекста в ступают в игру только во время совершения математических операций.

>>> с = Context(prec=3)  # точность 3
>>> Decimal('5.643434231', c)  # но число целиком сохраняется
Decimal('5.643434231')

>>> Decimal('5.643434231', c) * 2  # после операции уже применяется округление до нужной точности
Decimal('11.287')

>>> +Decimal('5.643434231', c)  # трюк: унарный плюс применит контекст
Decimal('5.6434')

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

Работайте с Decimal как с обычными числами: складывайте, вычитайте, умножайте, делите и прочее. Можете, миксовать их с целыми числами. Но не рекомендуется миксовать их с float.

>>> x = Decimal('1.2')
>>> y = Decimal('2.3')
>>> x + y
Decimal('3.5')
>>> x - y
Decimal('-1.1')
>>> x * y
Decimal('2.76')
>>> x / y
Decimal('0.52174')

>>> y // x  # деление нацело
Decimal('1')
>>> y % x  # остаток
Decimal('1.1')

>>> Decimal('2.2') * 2
Decimal('4.4')
>>> Decimal('2.2') - 1
Decimal('1.2')

Дополнительно еще доступны некоторые математические функции:

>>> getcontext().prec = 10  # просто точность задали

>>> Decimal(2).sqrt()  # корень квадратный
Decimal('1.414213562')

>>> Decimal(2).ln()  # логарифм натуральный
Decimal('0.6931471806')

>>> Decimal(100).log10()  # логарифм десятичный
Decimal('2')

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

Кстати, Decimal можно передавать как аргументы функций, ожидающих float. Тогда они будут преобразованы во float:

>>> math.sin(Decimal(1))
0.8414709848078965

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

>>> Decimal('10.4266').quantize(Decimal('.01'), rounding=ROUND_DOWN)
Decimal('10.42')
>>> Decimal('10.4266').quantize(Decimal('.01'), rounding=ROUND_UP)
Decimal('10.43')
>>> Decimal('10.4266').quantize(Decimal('1.'), rounding=ROUND_UP)
Decimal('11')

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

>>> x = Decimal('0.1')
>>> x + x + x == Decimal('0.3')
True

Можно сортировать списки Decimal, искать минимум и максимум. А также преобразовывать Decimal обратно в обычные типы int, float, str. Пример из документации:

>>> data = list(map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25'.split()))
>>> max(data)
Decimal('9.25')
>>> min(data)
Decimal('0.03')
>>> sorted(data)
[Decimal('0.03'), Decimal('1.00'), Decimal('1.34'), Decimal('1.87'),
 Decimal('2.35'), Decimal('3.45'), Decimal('9.25')]
>>> sum(data)
Decimal('19.29')
>>> a, b, c = data[:3]
>>> str(a)
'1.34'
>>> float(a)
1.34
>>> round(a, 1)
Decimal('1.3')
>>> int(a)
1

Но! Не все сторонние библиотеки поддерживают Decimal. Например, не получится использовать его для numpy!

Не все операции над Decimal абсолютно точные, если результат неточен, то возникает сигнал Inexact.

>>> c = getcontext()
>>> c.clear_flags()

>>> Decimal(1) / Decimal(3)
Decimal('0.3333333333')

>>> c.flags[Inexact]
True

Выводы

Выбор между Decimal и float – это поиск компромисса с учетом условий конкретной задачи.

Если вам нужно считать очень много (симуляции, физика, химия, графика, игры), то иногда имеет смысл отказаться от точности Decimal в пользу скорости и компактности хранения данных у float. В бизнесе и финансах считать приходится не очень много, но нужно делать это предельно точно, тогда ваш взгляд должен обратиться в сторону Decimal. В таблице вы найдете сравнение этих двух типов данных.

Сравнительная таблица

Примечание: а для целочисленных вычислений может сгодится и простой int, он умеет из коробки длинную математику!

Еще

В этой статье я не осветил полностью вопросы:

  • Сигналы, флаги и ловушки
  • Обзор режимов округления
  • Управление контекстами
  • Контексты и многопоточность

Если сообществу будет интересно, то я продолжу тему. Голосование будет на канале!

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

Поддельный User-Agent

Одна из примитивных защит сайтов от парсинга – проверка HTTP заголовка User-Agent, который содержит наименование веб-браузера или клиента, делающего запрос. Если этого заголовка нет, то сервер может не выполнить запрос, раскусив, что его делает робот, а не человек. Обход защиты – имитация реального User-Agent браузера библиотекой fake_useragent. Установка:

pip install fake_useragent

Использование:

from fake_useragent import UserAgent
ua = UserAgent()
print(ua.random)
# Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36

ua.random – агент случайного браузера (с учетом статистики распространенности браузеров по миру). Также доступны агенты для конкретных браузеров: ua.ie, ua.msie, ua.opera, ua.chrome, ua.google, ua.firefox, ua.ff, ua.safari.

Пример отправки запроса через request:

from fake_useragent import UserAgent
import requests
ua = UserAgent()

# куда шлем (этот URL как раз ответит нам наш UA для проверки)
url = 'https://httpbin.org/user-agent'

# создаем заголовок
headers = {'User-Agent': ua.chrome}

# делаем запрос, передав заголовок
result = requests.get(url, headers=headers)
print(result.content)

Еще в классе есть метод ua.update(), что обновляет базу данных браузеров, если она устарела. Это медленный метод, он делает запрос на сервера. Его не нужно вызывать каждый раз.

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