Метка: уроки программирования

Подчеркивание в 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 👈 

bin, oct, hex – системы исчисления

Вспомним основы информатики и поговорим о системах исчисления. В жизни мы привыкли к десятичной системе (base-10 или decimal), железо компьютеров оперирует в системе двоичной (base-2 или binary) – нулями и единицами. Часто приходится иметь дело с системой шестнадцатиричной (base-16 или hexadecimal), она позволяет записывать данные в 8 раз короче, чем двоичная. Реже встречается система восьмеричная (base-8 или octal). Система исчисления – это всего лишь средство представления числа, т.е. то, как мы его в строчку запишем или считаем, само число остается самим собой, независимо от системы.

Чтобы получить целое число из строки, записанной в какой-то системе исчисления, используем функцию int (второй параметр – база системы):

>>> int('42')  # по умолчанию 10 
42
>>> int('42', 10)  # тоже самое
42
>>> int('101010', 2)
42
>>> int('BEEF', 16)
48879
>>> int('7654', 8)
4012

Наоборот сделать из числа строку в какой-то системе – встроенные функции bin, oct и hex:

>>> bin(42)
'0b101010'
>>> hex(48879)
'0xbeef'
>>> oct(4012)
'0o7654'

В строке появились префиксы 0b, 0x и 0o. Как и в Си, в Python можно использовать эти префиксы для записи чисел в коде помимо обычного десятичного варианта, если это требуется:

>>> 0b11011, 0xDEAD, 0o777
(27, 57005, 511)

Однако, часто приходится иметь дело с чистыми HEX-данными без префиксов. Многие (да и я) делали вот так:

>>> hex(12345)[2:]
'3039'

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

>>> hex(-12345)
'-0x3039'
>>> hex(-12345)[2:]
'x3039'

К счастью, есть встроенная функция format(value, format_spec) (не путать с str.format), которая форматирует значение, согласно спецификатору форматирования:

>>> format(3039, 'x'), format(3039, 'X'), format(3039, '#x')
('bdf', 'BDF', '0xbdf')
>>> format(120, 'b')
'1111000'
>>> format(79, 'o')
'117'
  • x — 16-ричное без префикса, маленькие букв
  • X — 16-ричное без префикса, заглавные буквы
  • #x — 16-ричное с префиксом, маленькие буквы
  • b — двоичное число без префикса и т.д.

format(value, format_spec) эквивалентная вызову type(value).__format__(value, format_spec).

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

Quine на Python

Программисты тоже умеют развлекаться, так что давайте сегодня развлечемся и напишем quine (квайн). Квайн – это такая программа, которая выводит на экран свой же код, ни больше, ни меньше. Сразу договоримся, что пустая программа на Python, которая ничего не выводит, не считается квайном; это не интересно.

В Python у нас есть чудо-переменная, которая хранит путь к текущему интерпретируемому файлу, поэтому можно сделать так:

print(open(__file__).read())

Эта программа открывает свой же файл, читает и печатает его целиком. Но это жульничество, потому что в квайнах не принято читать файлы. Хорошо, а что если назвать файл print(__file__), записать в него print(__file__) и выполнить python "print(__file__)". Будет работать, но можешь вот без этих трюков, чисто кодом? Да без проблем!

Нам нужно что-то печатать, значит берем print:

>>> print('?')
?

Программа начинается с print…, значит и печатать будем тоже самое:

>>> print('print()')
print()

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

>>> s='print()';print(s)
print()

Но код теперь начинается с s=, исправим:

>>> s='s=?;print(s)';print(s)
s=?;print(s)

Смотрите, уже похоже, осталось только на место знака вопроса воткнуть содержимое строки s из оригинального кода. Это самый важный момент. Используем format, а точнее s.format(s), который в определенном месте строки s вставит саму же строку s, таким образом, мы «разрываем рекурсию»:

>>> s='s={};print(s)';print(s.format(s))
s=s={};print(s);print(s)

Отлично! Но тут два недостатка: во-первых, не забыть добавить s.format(s) в саму строку s:

>>> s='s={};print(s.format(s))';print(s.format(s))
s=s={};print(s.format(s));print(s.format(s))

Во-вторых, нужно вернуть на место кавычки. Не зря я недавно рассказывал о флагах преобразования строк. Используем флаг {!r} в формате, чтобы вывести repr(s), который для строк содержит одинарные кавычки:

>>> s='s={!r};print(s.format(s))';print(s.format(s))
s='s={!r};print(s.format(s))';print(s.format(s))

Ура! Квайн готов и работает!

Вы можете сделать квайн короче, используя другой стиль форматирования строк через процент: {!r} заменяется на %r, s.format(s) на s%s, плюс экранируется процент внутри самой строки s%%s (%% понимается как сам знак процента, а не как место для подстановки):

>>> s='s=%r;print(s%%s)';print(s%s)
s='s=%r;print(s%%s)';print(s%s)

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