Когда я проверяю алгоритмы, написанные другими людьми, я, первым делом, пишу тесты. Так я знаю, что ничего не сломаю + лучше понимаю (попутно документируя) код. Я бы мог просто не показывать тесты, но, имхо, лучше о них знать, чем не знать, поэтому постараюсь объяснить.
На примере кода Ксюши. Допустим, нам скинули на ревью вот такой код, он отвечает за считывание матрицы в двумерный список k.
l = "not_end"
k = []
while True:
l = input()
if l != "end":
k.append([int(i) for i in l.split()])
else:
break
(этот код нужно улучшить. В этом смысл код-ревью. Но улучшать его мы будем во второй части статьи. В этой же части мы будем писать тесты, чтобы точно знать, что наши "улучшения" ничего не сломали)
Пока что, скорее всего, не очень понятно, что он делает. Нужны примеры! Вот примеры входных данных из задания:
9 5 3
0 7 -1
-5 2 9
end
и
1
end
(не спрашивайте меня, почему там "end" в конце, ведь матрицу можно считать и без него. Это -- учебное задание и я беру его "как есть")
Теперь мы оформим примеры в виде тестов. Тесты будут выглядеть, примерно (реальный код -- в конце), так:
assert _input("""
9 5 3
0 7 -1
-5 2 9
end
""") == [
[9, 5, 3],
[0, 7, -1],
[-5, 2, 9]
]
Т.е. на вход мы подаём определённый текст с клавиатуры, а на выходе должны получить двумерный список чисел. Вроде матрицы. На мой взгляд, задача ввода с клавиатуры уже стала выглядеть понятнее.
Итого, что нам нужно:
- Как-то симулировать ввод с клавиатуры. В примере
выше мы просто передаём текстовую строку, но в самом
коде мы используем
input()
для ввода с клавиатуры, а не работаем со строками. - Оформить код в виде функции. Именно функции мы тестируем.
- Нужно знать/понимать точный результат выполнения функции. Тут с этим всё просто, поэтмоу я опущу эту часть.
Пример: человек вводит с клавиатуры матрицу 3х3:
9 5 3
0 7 -1
-5 2 9
end
Мы можем считать её так:
_9_5_3 = input()
_0_7__1 = input()
__5_2__9 = input()
matrix = [_9_5_3, _0_7__1, __5_2__9]
Ну или просто:
matrix = [input(), input(), input()]
Вообще input()
в Питоне (скорее всего) является
сокращением чуть более общей конструкции:
stdin.readline()
. Т.е. мы могли бы написать и вот
так:
from sys import stdin
matrix = [
stdin.readline(),
stdin.readline(),
stdin.readline(),
]
Получается чуть более громоздко, поэтому, поголовно,
при обучении, и в мелких скриптах,
используется input()
. Но эта конструкция более
гибкая, например, вместо stdin
мы могли бы
использовать какой-то другой объект. Например, файл или
поток StringIO, созданный из любой произвольной строки.
Пример:
from io import StringIO
str_in = StringIO("""9 5 3
0 7 -1
-5 2 9
end""")
matrix = [
str_in.readline(),
str_in.readline(),
str_in.readline(),
]
(вас может напрячь отсутствие отступов в строке и адекватных переносов. Однако, если вы попробуете отформатировать "как положено", то код поломается. Не переживайте -- чуть ниже мы разберёмся с этим)
stdin
-- это "поток ввода" с клавиатуры. Вообще,
потоков ввода может быть много: файл,
интернет-соединение, usb-устройство и т.д. Вы можете
создавать собственные потоки ввода, если почитаете доки
к Питону. Когда вы открываете файл, то вам возвращается
именно поток ввода с файла. Довольно стандартная штука
для Питона. Так вот, stdin
-- это именно поток ввода
с клавиатуры. У него, как и у файла, есть стандартные
операции -- readline()
,
read()
и т.д
Мы можем создавать собственные потоки ввода, например,
мы могли бы сделать из строки поток. Так подумали
создатели Питона и уже это сделали, подарив нам
StringIO
в примере выше.
Для красоты, я бы завернул пример выше в функцию, которая принимала бы в себя любой поток, но, по-умолчанию, работала именно во стандартным вводом ( клавиатурой):
from sys import stdin
def _read_mat_3x3(_in=stdin):
return [
_in.readline(),
_in.readline(),
_in.readline(),
]
# читаем с клавиатуры
_read_mat_3x3()
# а вот и версия для тестов:
_test_case_1 = """9 5 3
0 7 -1
-5 2 9
end"""
from io import StringIO
_read_mat_3x3(StringIO(_test_case_1))
# а, вот, и считывание с файла:
with open('mat3x3.txt') as file:
_read_mat_3x3(file)
Итак, со вводом разобрались, теперь нужно оформить код Ксюши в виде тестируемой функции. Было:
l = "not_end"
k = []
while True:
l = input()
if l != "end":
k.append([int(i) for i in l.split()])
else:
break
Стало:
from sys import stdin # 1. импортировали
def _input(_in=stdin): # 2. добавили
l = "not_end"
k = []
while True:
l = _in.readline() # 3. input() => readline()
if l != "end":
k.append([int(i) for i in l.split()])
else:
break
Поменяли всего три строки. Немного усилий. Зато, теперь, возможно написать тесты.
Первый тест. Входные данные беру напрямую из задания. Не спрашивайте меня, почему идёт "end" в конце. Это просто условие задачи и я тупо копирую входные данные:
from io import StringIO
assert _input(StringIO(
"""1
end""")) == [[1]]
Второй тест, берём чуть более сложный пример из условия задачи:
_test_input_2 = """9 5 3
0 7 -1
-5 2 9
end"""
answer2 = [
[9, 5, 3],
[0, 7, -1],
[-5, 2, 9],
]
# проверяем, соответствует ли ожидаемый
# результат и реальному:
assert _input(StringIO(_test_input_2)) == answer2
Если условие не выполняется, то assert
упадёт с
ошибкой. В этом и есть тест.
Тесты готовы! Если вы запустите этот код, то он ничего не покажет и ничего не сделает. Возможно, вы даже не заметите, что он отработал. Зато, если вы поменяете тестовые данные на некорректные, то assert грохнется и покажет ошибку. Пример простого грохающегося теста:
assert _input(StringIO(
"""1
end""")) == [[9]]
И вот текст ошибки:
runfile('/Users/egslava/projs/vova_algos/cs/ksenia/_1.py', wdir='/Users/egslava/projs/vova_algos/cs/ksenia')
Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/IPython/core/interactiveshell.py", line 3457, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-2-46b11f8e76bf>", line 1, in <module>
runfile('/Users/egslava/projs/vova_algos/cs/ksenia/_1.py', wdir='/Users/egslava/projs/vova_algos/cs/ksenia')
File "/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pydev/_pydev_bundle/pydev_umd.py", line 198, in runfile
pydev_imports.execfile(filename, global_vars, local_vars) # execute the script
File "/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "/Users/egslava/projs/vova_algos/cs/ksenia/_1.py", line 103, in <module>
assert _input(StringIO(
AssertionError
Вначале идёт всякий буллшит, зато в последних трёх строчках всё довольно информативно:
- Нам говорят номер строчки с ошибкой (у меня это 103)
- Говорят, что упал assert
А в PyCharm'е, номер строки даже кликабельный, поэтому, кликнув мышкой на 103, мы сразу идём на тест, который валится.
Улучшение читабельности тестов -- постоянная работа. Как мы учимся писать код, так и постоянно учимся писать более читабельные тесты. Поэтому единого рецепта тут нет. Рассмотрим данный конкретный случай:
_test_input_2 = """9 5 3
0 7 -1
-5 2 9
end"""
Попробуем написать красивше:
from textwrap import dedent
_test_input_2 = dedent("""
9 5 3
0 7 -1
-5 2 9
end
""").strip()
- строка больше не "липнет" к левому краю. Добавили
отступы. Однако, если мы просто вставим
пробелы/отступы и т.д., то
readline
их считает. А это нам не нужно. Так что, перед использованием этой строчки, нужно эти отступы удалить. В Python'е, для таких целей уже есть стандартная функцияdedent
. Кроме того, в - Добавили перенос строки после первого
"""
и послеend
. Они убираются при помощиstrip()
Вместо
"""
многострочной
строки
в
Python'е
"""
можно использовать и старый формат, который прижился во
многих языках программирования. Все строки --
однострочные, а где нужно перенести строку, вставляем "
символ переноса строки": \n
. Можете попробовать:
print('1\n2\n3')
выведет:
1
2
3
Это можно использовать в мелких тестах. Было:
assert _input(StringIO("""1
end""")) == [[1]]
Стало:
assert _input(StringIO("1\nend")) == [[1]]
На этом всё. Сама функция осталась нетронутой. В следующей части я не буду объяснять тесты, а буду исправлять только саму функцию и делать её проще.
Вот полная версия кода с падающим и не падающими тестами. Просто скопируйте его в свой ipython3/python3/pycharm и запустите.
from sys import stdin # 1. импортировали
def _input(_in=stdin): # 2. добавили
l = "not_end"
k = []
while True:
l = _in.readline() # 3. input() => readline()
if l != "end":
k.append([int(i) for i in l.split()])
else:
break
from io import StringIO
from textwrap import dedent
# 1. test ok
assert _input(StringIO("""1
end""")) == [[1]]
# 2. test ok
_test_input_2 = dedent("""
9 5 3
0 7 -1
-5 2 9
end
""").strip()
answer2 = [
[9, 5, 3],
[0, 7, -1],
[-5, 2, 9],
]
assert _input(StringIO(_test_input_2)) == answer2
# 3. test fail
assert _input(StringIO("1\nend")) == [[3]]
Very usefull, thanks