Деление с остатком преподнесло сюрприз

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

total_seconds = 119
seconds = total_seconds % 60
minutes = total_seconds // 60
print(f'{minutes}:{seconds}')  # 1:59

Заканчивая тем, что на остатках построена львиная доля криптографии. Нахождения остатка часто называют modulo (или коротко mod). 

При делении a на b неполное частное q и остаток r связаны формулой:

a = b · q + r, где b ≠ 0

В Python 3 частное и остаток вычисляются операторами:

q = a // b
r = a % b

Именно двойной слэш, одинарный слэш – деление без остатка (до конца). Иногда двойной слэш называют целочисленным делением, что не очень справедливо, потому что мы можем без проблем делить числа с запятой. Если оба числа целые (int), то частное будет тоже целым числом (int), иначе float. Посмотрите примеры:

10 / 3 == 3.3333333333333335
10 // 3 == 3
10.0 / 3.0 == 3.3333333333333335
10.0 // 3.0 == 3.0 
10.0 % 3.0 == 1.0
10 % 3 == 1

2.4 // 0.4 == 5.0
2.4 / 0.4 == 5.999999999999999
2.4 % 0.4 == 0.3999999999999998

Последние три примера немного обескураживают из-за особенностей вычислений с плавающей точкой на компьютере, но формула a = b · q + r всегда остается справедлива.

Поговорим об отрицательных числах. Математически остаток не должен быть меньше нуля и больше или равен модулю делителя b: 0 ≤ r < |b|. Однако, Intel в своих процессорах случайно либо намеренно ввела отрицательные остатки в реализации ассемблерных команд деления. Компиляторы языков C и С++, являясь платформо-зависимыми, обычно полагаются на процессорное поведение. Пример на С++. И вообще посмотрите на эту огромную таблицу, каждый язык программирования пляшет, как хочет. Не будем спорить, кто из них прав. Просто узнаем, как у нас в Python:

a, b = [10, -10], [3, -3]
for x in a:
  for y in b:
    print(f'{x} // {y} = {x // y}')
    print(f'{x} % {y} = {x % y}')
    print()

10 // 3 = 3
10 % 3 = 1

10 // -3 = -4
10 % -3 = -2

-10 // 3 = -4
-10 % 3 = 2

-10 // -3 = 3
-10 % -3 = -1

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

def mod_python(a, b):
  return int(a - math.floor(a / b) * b)

# на С++ работает так:
def mod_cpp(a, b):
  return int(a - math.trunc(a / b) * b)

Где floor – ближайшее целое число не превышающее аргумент: floor(-3.3) = -4, а trunc – функция отбрасывания целой части: trunc(-3.3) = -3. Разница проявляется между ними только для отрицательных чисел. Отсюда и разные остатки и частные – все зависит от того, с какой стороны числовой оси мы приближаемся к частному.

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

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

Умножение списка на число

Студент Макс узнал, что в Python умножать можно не только числа, но и другие объекты, например, строку на число:

>>> "Max" * 3
'MaxMaxMax'

«Вау!» — подумал Макс — «А что если умножить список на число?»:

>>> [42, 26] * 3
[42, 26, 42, 26, 42, 26]

Значит можно создать двумерный массив очень кратко и элегантно?

>>> [[]] * 3
[[], [], []]

Заполнить его:

arr = [[]] * 3
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)

Макс ожидал получить:

[[10], [20], [30]]

А вышло:

[[10, 20, 30], [10, 20, 30], [10, 20, 30]]

😯 Как же так?! Дело в том, что умножение списка на число не копирует сам объект, а лишь ссылку на него. Все три элемента arr ссылаются на один и тот же список. Легко проверить, сравнив адреса объектов:

>>> arr[0] is arr[1]
True
>>> id(arr[0]), id(arr[1])
(4400840776, 4400840776)
Диаграмма: все элементы arr указывают на один и тот же список.

Аналогично в случае классов:

class Dummy: ...
arr = [Dummy()] * 2
arr[0].x = 10
arr[1].x = 20
print(arr[0].x, arr[0] is arr[1])  # 20 True

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

arr = [[] for _ in range(3)]  
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)
>>> arr
[[10], [20], [30]]

Менее кратко, но зато работает без сюрпризов: каждую итерацию создается новый пустой список.

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

Подчеркивание в Python

Знак подчеркивания _ или underscore занимает особое место в Python.

Underscore code – код символа подчеркивания

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

  1. змеиный_регистр (snake_case)
  2. имена магических методов и переменных
  3. «приватные» члены класса и коверкание имен (mangling)
  4. игнорирование значения переменной
  5. разделение разрядов в числах
  6. избегание конфликтов с ключевыми словами
  7. хранение последнего результата в интерпретаторе

Поехали от самого известного к необычному!

Змеиный регистр

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

foo_bar = 10
def my_function_to_do_something_special(arg_1, arg_2):
    ...

# не принято писать так:
FooBar = 10
carSpeed = 60
Dont_Do_Like_This()

Магические имена

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

class CrazyNumber:
    __slots__ = ('n',)
    def __init__(self, n):
        self.n = n
    def __add__(self, other):
        return self.n - other
    def __sub__(self, other):
        return self.n + other
    def __str__(self):
        return str(self.n)

Конфликт с ключевым словом

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

Бывает актуально, если вы пишите какой-то биндинг к сторонней библиотеке, где, к несчастью, некоторые понятия имеют такое же имя как и ключевые слова:

Tkinter.Toplevel(master, class_='ClassName')

Но! Если вы пишите классовый метод, принято первый аргумент называть cls, а не class_.

Приватные члены

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

Разделение на приватные и публичные члены – это механизм сокрытия (пожалуйста, не путайте с инкапсуляцией). Я писал про разницу между ними в статье «Сокрытие в Python». Здесь кратко напомню.

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

# приватные переменные в модуле
_internal_variable = 'some secret'
_my_version = '1.6'

# приватная функция модуля
def _private_func():
    ...

# приватный класс модуля
class _Base:
    # приватная переменная класса
    _hidden_multiplier = 1.2
    def __init__(price):
        # приватное поле экземпляра класса
        self._price = price * self._hidden_multiplier

⚠️ Влияние на поведение: from module import * не будет импортировать приватные члены модуля. Но можно импортировать их принудительно: from module import _Base, _my_version

Еще приватнее или name mangling

⚠️ Если мы будем использовать не одно, а целых два подчеркивания перед именем, то это задействует механизм name mangling. На русский это можно перевести как «коверкание имени». Python исковеркает данное имя, чтобы избежать конфликтов имен атрибутов между классами в иерархии наследования. Естественно, внутри класса, где определен атрибут с двойным подчеркиванием спереди, он будет доступен также по своему имени. Но на самом деле к имени добавится префикс _ClassName. Проиллюстрирую правило манглинга на примере. Допустим есть класс Tree, и вы пишите метод __rebalance, то его имя превратится в _Tree__rebalance при доступе из-вне класса. Пример кода:

class Tree:
    def __rebalance(self):
        print('Tree.__rebalance')

    def public_method(self):
        # метод доступен по своему имени
        self.__rebalance()

class BinaryTree(Tree):
    # этот метод не перекроет __rebalance из Tree!
    def __rebalance(self):
        print('BinaryTree.__rebalance')

tree = Tree()
tree._Tree__rebalance()  # Tree.__rebalance

btree = BinaryTree()
btree._Tree__rebalance()  # Tree.__rebalance
btree._BinaryTree__rebalance()  # BinaryTree.__rebalance

Кстати, на слэнге двойное подчеркивание называется dunder. Добавление третьего и четвертого подчеркиваний к дополнительным эффектам не приведет!

Игнорирование

Если вам не нужно значение переменной, назовите его просто подчеркиванием.

# просто повтор 10 раз, а счетчик не нужен
for _ in range(10):
    print('Hello')

Так же при распаковке коллекций в переменные вы можете применить сколько угодно подчеркиваний.

def tup():
    return 1, 2, 3, 4

# третье не нужно 
a, b, _, d = tup()
print(a, b, d)  # 1 2 4

# второе и четвертое не нужны
a, _, c, _ = tup()
print(a, c)  # 1 3

# только первое
a, *_ = tup()
print(a)  # 1

# первое и последние
a, *_, d = tup()
print(a, d)  # 1 4

# нужны только 2 последних
*_, c, d = tup()
print(c, d)  # 3 4

Примечание: использовать значение _ в принципе можно (в нем будет последний присвоенный результат), но зачем?

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

# нужен только x 
def get_only_x(x, _y, _z):
    return x

# так нельзя!
def get_only_x(x, _, _):
    return x

Разделение разрядов в числах

Фишка добавлена в Python 3.6. Можно разделять разряды в длинных числах для облегчения чтения кода.

>>> 1_000_000
1000000

>>> 0b1011_1100_0000_1111
48143

>>> 0x_ee12_3b5f
3994172255

>>> 0o_1_2_3_4_5_6_7  # можно хоть каждый разряд отделить!
342391

>>> 10_20_30_40
10203040

Последний результат в интерпретаторе

⚠️ Лично я не знал, про эту фишку, пока не стал писать эту статью. А между тем, она супер удобна, если вы используете интерпретатор Python как калькулятор:

>>> 10 + 20
30
>>> _ + 3
33
>>> _ * 3 + _ * 2
165

# print возвращает None, но None не затирает _ !
>>> print('hello')
hello
>>> None
>>> _
165

Может, я что-то упустил? Если да, присылайте мне в Телеграм!

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

Юнит-тесты

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

В Python поставляется модуль unittest, который облегчает написание тестов:

  • Обнаружение и автоматическое исполнение тестов
  • Настройка теста и его завершение
  • Группирование тестов
  • Статистика тестирования

Чтобы создать тестовый случай, нужно создать класс, отнаследованный от unittest.TestCase. А внутри этого класса можно добавить несколько методов, начинающихся со слова test. Каждый из этих методов должен тестировать какой-то из аспектов кода. Для примера мы тестируем свойства строк Python: сложение строк (test_sum) и преобразование к нижнему регистру (test_lower):

import unittest

class StringTestCase(unittest.TestCase):
    def test_sum(self):
        self.assertEqual("" + "", "")
        self.assertEqual("foo" + "bar", "foobar")

    def test_lower(self):
        self.assertEqual("FOO".lower(), "foo")
        self.assertTrue("foo".islower())
        self.assertFalse("Bar".islower())

Самые распространенные проверки:

self.assertEqual – непосредственно проверяет, чтобы первый аргумент равнялся второму. Если это будет не так, то тест будет провален, и появится сообщение о том, что и где пошло не так.

self.assertTrue – ожидает, что аргумент будет эквивалентен правде (True), а self.assertFalse – проверяет на ложь (False).

Запуск делается либо непосредственно из самой программы:

if name == '__main__':
    unittest.main()

А можно из консоли:

python -m unittest my_test.py

Модуль unittest сам найдет все тестовые случаи и выполнит в них все тестовые функции.

Скрин результата запуска тестов

Фикстуры

Тестовые фикстуры (test fixtures) – особые условия, которые создаются для выполнения тестов. Сюда могут входить такие вещи:

  • Подготовка тестовых данных
  • Создание подключений к БД, сервисам и т.п.
  • Создание заглушек (mock) для имитации компонентов программы
  • Другие действия по поддержке рабочего окружения для проведения теста

Пример: у вас программа, которая вычисляет вычисляет число π до n-знака, и вам нужно протестировать, как будет выведен на экран миллионнный знак. Вы же не будете в тесте ждать вычисления всех предыдущих 999,999 знаков часами, а просто загрузите какие-то данные в память, чтобы создать условия, как будто мы уже на миллионом знаке.

В unittest фикстуры можно создавать на уровне модуля с тестами, отдельного класса (от unittest.TestCase) и каждого метода в классе теста. 

Метод setUp() вызывается перед каждым вызовом метода test* в классе тестового случая.

Классовый метод setUpClass() вызывается один раз перед запуском тестов в классе тестового случая.

Функция setUpModule() вызывается перед выполнением тестовых случаев в этом модуле.

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

tearDown() – после каждого метода-теста в классе.
tearDownClass() – после всех тестов в классе.
tearDownModule() – после всех классов в модуле.

📎 В примере изучим порядок вызовов этих функций:

import unittest

class StringTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print(' - set up class')

    def setUp(self):
        print(' - - set up method')
        self.foo = "foo"
        self.bar = "bar"

    def test_sum(self):
        self.assertEqual(self.foo + self.bar, "foobar")

    def test_lower(self):
        self.assertTrue(self.foo.islower())

    def tearDown(self):
        print(' - - tear down method')

    @classmethod
    def tearDownClass(cls):
        print(' - tear down class')

def setUpModule():
    print('set up module')

def tearDownModule():
    print('tear down module')

if name == '__main__':
    unittest.main()

Даст такую схему вызовов:

set up module
 - set up class 
 - - set up method
 - - tear down method
 - - set up method
 - - tear down method
 - tear down class
tear down module

Даже если в одной из этих или тестовых функций произошло исключение, то прочие методы tearDown*() будут все равно запущены, чтобы освобождение ресурсов произошло корректно.

Пропуск тестов

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

  • @unittest.skip("причина") – всегда пропускать тест.
  • @unittest.skipIf(условие, "причина") – пропускать тест, если условие сработало (True).
  • @unittest.skipUnless(условие, "причина") – пропускать тест, если условие НЕ сработало (False).
  • self.skipTest("причина") – если нужно остановить выполнение метода, выйти из него и не учитывать его в результатах. Так же может быть вызван в методе setUp(), который вызывается перед каждым тестовым методом.

📎 Пример:

class MyTestCase(unittest.TestCase):
    @unittest.skip("всегда пропустить")
    def test_nothing(self):
        self.fail("не случится")

    @unittest.skipIf(mylib.__version__ < (1, 3),
                     "эта версия библиотеки не поддерживается")
    def test_format(self):
        # этот тест работает только для определенных версий 
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "надо Windows")
    def test_windows_support(self):
        # тест работает только на Windows
        pass

    def test_maybe_skipped(self):
        if not external_resource_available():
            self.skipTest("ресурс недоступен")
        # код дальше будет тестировать, если ресурс доступен
        pass

📎 Пример пропуска класса:

@unittest.skip("как пропустить класс")
class MySkippedTestCase(unittest.TestCase):
    def test_not_run(self):
        pass

Вы можете написать свой декоратор. Например, данный декоратор пропускает тест, если объект obj не имеет атрибут attr:

def skipUnlessHasattr(obj, attr):
    if hasattr(obj, attr):
        return lambda func: func
    return unittest.skip("{!r} не имеет {!r}".format(obj, attr))

class SkipAttrTestCase(unittest.TestCase):
    @skipUnlessHasattr(mylib, "foofunc")
    def test_with_foofunc():
        # у mylib нет атрибута foofunc, тест будет пропущен
        pass

Еще один декоратор @unittest.expectedFailure говорит системе тестирования, что следующий метод должен провалиться (один из self.assert должен не сработать). Таким образом, разработчик говорит, что он осведомлен, что данный тест пока проваливается, и в будущем к этому примут меры.

class ExpectedFailureTestCase(unittest.TestCase):
    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "сломано")

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

OK (skipped=5, expected failures=1)

Код примеров онлайн.

Проверки

В первой части мы обсудили методы проверки assertEqual, assertTrue и assertFalse, так как они самые распространенные на практике. Вообще достаточно одного assertTrue. Действительно, одно и тоже:

assertNotIn(item, list)
assertTrue(item not in list)

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

self.assertEqual(2 + 2, 5, "я не учил математику")

Однако, стоит упомянуть метод self.assertRaises(SomeException), который проверяет, возбуждает ли код нужное исключение. Обычно он применяется как контекст-менеджер (с with). 

📎 Пример: деление на 0 должно бросать исключение ZeroDivisionError:

import unittest

def my_div(a, b):
    return a // b

class MyDivTestCase(unittest.TestCase):
    def test_1(self):
        self.assertEqual(my_div(10, 2), 5)

        # при делении на 0 ждем исключение:
        with self.assertRaises(ZeroDivisionError):
            my_div(7, 0)

        # или так: исключение, ф-ция, аргументы
        self.assertRaises(ZeroDivisionError, my_div, 5, 0)

unittest.main()

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

with self.assertRaises(SomeException) as cm:
    do_something()

self.assertEqual(cm.exception.error_code, 3)
Картинка с таблицей проверок и что они проверяют

PyTest

Ранее мы обсуждали тестирование средствами встроенного модуля unittest. Естественно, есть и сторонние библиотеки для тестирования. Например, библиотека PyTest предоставляет более лаконичный и удобный инструментарий для написания тестов. Однако, ее нужно установить:

pip install pytest

Преимущества PyTest:

  • Краткий и красивый код
  • Только один стандартный assert
  • Подробный отчет
  • Разнообразие фикстур на всех уровнях
  • Плагин и интеграции с другими системами

Сравните этот код с кодом из предыдущих постов про unittest:

import pytest

def setup_module(module):
    #init_something()
    pass

def teardown_module(module):
    #teardown_something()
    pass

def test_upper():
    assert 'foo'.upper() == 'FOO'
    
def test_isupper():
    assert 'FOO'.isupper()
    
def test_failed_upper():
    assert 'foo'.upper() == 'FOo'

Для тестов можно применять и классы (как в unittest), так и отдельные функции.

Запускать тесты тоже просто. В окружении, где установлен pytest, появится команда py.test. Из терминала пишем:

py.test my_test_cases.py  

py.test обнаружит и выполнит тесты из этого файла.

Есть очень хорошая статья на Хабре про PyTest на русском, не вижу смысла дублировать ее сюда, а просто оставлю ссылку.

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

Игра понг ASCII на Python

В продолжение последней темы написал сегодня с утра игру «Понг» для терминала. Обошелся только встроенными модулями. Для графики и ввода использовал модуль curses (обертка над ncurses). Исходный код доступен здесь. Благодаря современным чудо-технологиям в игру можно поиграть прямо в браузере, хоть она и работает не очень стабильно (зависит от вашего интернет соединения). Управление: W — вверх, S — вниз (только английская раскладка).

Скриншот текстовой игры ПОНГ

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