Парадигмы 1MIT, Стандартные замечания — различия между версиями

Материал из SEWiki
Перейти к: навигация, поиск
(Иногда стоит использовать map вместо list comprehension)
(Иногда стоит использовать map вместо list comprehension)
Строка 82: Строка 82:
 
  xs = [int(x) for x in line.split()]
 
  xs = [int(x) for x in line.split()]
 
  print(", ".join(str(x) for x in xs))
 
  print(", ".join(str(x) for x in xs))
 +
ys[2:4] = [int(x) for x in line.split()]
  
 
'''Do''':
 
'''Do''':
 
  xs = list(map(int, line.split())
 
  xs = list(map(int, line.split())
 
  print(", ".join(map(str, xs)))
 
  print(", ".join(map(str, xs)))
 +
ys[2:4] = map(int, line.split())
  
 
'''Причины''':
 
'''Причины''':
Строка 99: Строка 101:
 
# <code>map</code> возвращает не список, а ''генератор''. Условно, это такой список, который можно прочитать только один раз. Это обычно подходит, если результат <code>map</code> сразу идёт в какую-то функцию, но не подходит, если вам нужен именно список значений. В таком случае сконструируйте список, явно вызвав <code>list(...)</code>.
 
# <code>map</code> возвращает не список, а ''генератор''. Условно, это такой список, который можно прочитать только один раз. Это обычно подходит, если результат <code>map</code> сразу идёт в какую-то функцию, но не подходит, если вам нужен именно список значений. В таком случае сконструируйте список, явно вызвав <code>list(...)</code>.
 
# Тем не менее, <code>np.array(list(map(...)))</code> --- это тавтология. Тут сначала результат <code>map</code> превращается в обычный питоновский список, а потом в список NumPy, можно просто <code>np.array(map(...))</code>.
 
# Тем не менее, <code>np.array(list(map(...)))</code> --- это тавтология. Тут сначала результат <code>map</code> превращается в обычный питоновский список, а потом в список NumPy, можно просто <code>np.array(map(...))</code>.
 +
# Аналогично, если вы присваиваете в срез уже существующего списка или матрицы, то не требуется превращать результат <code>map</code> в отдельно живущий список. Rule of thumb: если из результата работы <code>map</code> будет произведено копирование, причём ровно одно, его в список можно не превращать.
  
 
=== Не указывайте <code>end='\n'</code> в <code>print</code> ===
 
=== Не указывайте <code>end='\n'</code> в <code>print</code> ===

Версия 08:18, 24 сентября 2017

Общее

Не передавайте отдельным параметром то, что можно вычислить из остальных

Don't:

def strassen(a, b, size):
    ...

Do:

def strassen(a, b):
    size = ...

Причины:

  1. Дополнительный параметр --- дополнительное место, где можно налажать при вычислении/передаче; появляются новые инварианты, которые по-хорошему надо проверять.
  2. Сложнее отлаживать --- для тестового запуска функции требуется не просто передать массив, а ещё и его размер.
  3. Обычно такая потребность возникает при передаче кусочков матриц/массивов. Для этого обычно имеются удобные библиотечные обёртки.

Мифы:

  1. Вычислять размер каждый раз --- это медленно. На самом деле основные тормоза идут из-за сложных операций, которых много. Например, перемножение чисел или рекурсивные вызовы. Несколько обращений к памяти в не самом вложенном месте -- это нестрашно. И вообще заоптимизировать вы всегда успеете.
  2. Если не передавать размер отдельно, то нельзя работать с кусочками матриц, не копируя их. Неверно, можно просто использовать срезы. В случае с NumPy копирования во фразе mat[0:4, 0:3] не происходит - просто передаётся тот же самый кусок памяти с пометкой "используем только эту подматрицу".

Осторожно:

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


Проверяйте инварианты и корректность входных параметров при помощи assert

Don't:

# as и bs должны быть одной длины
def very_complex_algorithm(as, bs):
   ...
   # x + y тут должно быть равно 4
   ...

Do:

def very_complex_algorithm(as, bs):
    assert len(as) == len(bs)
    ...
    assert x + y == 4

Причины:

  1. assert'ы позволяют вам в процессе отладки быть уверенными в том, что все "очевидные" предположения выполняются. Заодно получается этакая документация.
  2. Комментарии обычно не читают. Как следствие, вашу процедуру кто-нибудь легко может вызвать с неверными параметрами (например, вы сами).
  3. Комментарии устаревают в процессе разработки программы. Если же какой-нибудь assert устареет, то он сразу упадёт и его придётся изменить.

Мифы:

  1. assert не надо использовать, потому что из-за них падает программа. Программа действительно падает, однако только в тех случаях, когда она ведёт себя не так, как вы предсказали. То есть если программа упала по assert --- то либо в ней баг, либо неверно ваше понимание о том, как она работает. В любом случае это стоит исправить.

Осторожно:

  1. Если понаставить слишком много assert'ов, код становится невозможно читать.
  2. Не стоит проверять корректность интерпретатора/компилятора. Например, после строчки a = x[2] строчка assert a == x[2] бессмысленно, равно как и проверка отсортированности массива чисел после вызова sort.
  3. Не стоит писать в assert код с побочными эффектами. Некоторые языки и/или оптимизаторы (например, Java) могут просто вырезать все assert в не-отладочном режиме.
  4. Если вы пишете production-сервис для Яндекс/Google, то падение программы нежелательно и вместо assert следует использовать другие конструкции и восстанавливать работу программы.

Используйте срезы вместо передачи индексов

Don't:

printMatrix(m, 5)
printArray(a, 2, 10)

Do:

printMatrix(m[:5, :5])
printArray(a[2:10])

Причины:

  1. Если вы передаёте больше данных, чем надо обработать, получается два размера данных: "то, что передали" и "то, что надо обработать". Легко запутаться. Например, вместо print(a[start:end]) по привычке написать print(a).
  2. Если требуется обработать все переданные данные, то в вызывающем коде надо явно передавать параметры "обработай всё". Там можно ошибиться.

Мифы:

  1. При передаче будет копирование, что замедлит программу. Неверно, современные языки и библиотеки поддерживают так называемые "срезы" --- по сути это просто обёртка над куском данных и индексами. Например, NumPy и Python поддерживают.

Осторожно:

  1. Важно понимать, происходит ли копирование. Например, в C++/Java нельзя так просто взять срез массива/std::vector --- требуются специальные отдельные классы.
  2. При изменении исходного массива/матрицы значение среза также меняется. Rule of thumb: не сохраняйте срезы в какие-либо поля или долгоживущие переменные. Если требуется --- сделайте явную копию.

Python

Иногда стоит использовать map вместо list comprehension

Don't:

xs = [int(x) for x in line.split()]
print(", ".join(str(x) for x in xs))
ys[2:4] = [int(x) for x in line.split()]

Do:

xs = list(map(int, line.split())
print(", ".join(map(str, xs)))
ys[2:4] = map(int, line.split())

Причины:

  1. При использовании map может получиться меньше кода.
  2. map не создаёт дополнительный список в памяти; он вычисляет значения "лениво", только когда они становятся нужны.

Мифы:

  1. map - это что-то из функционального программирования и в императивных языках не нужно. Неверно, современные языки в основном мультипарадигмальные --- позволяют писать в нескольких парадигмах. В частности, Python. Более того --- map в коде на Python довольно часто встречается, так что его вполне можно использовать и вы не удивите коллег.

Осторожно:

  1. Не стоит комбинировать много map подряд или передавать лямбду в качестве первого параметра, это уже будет довольно сложно читать. В таких случаях лучше использовать list comprehension.
  2. Аналогично, не стоит передавать list comprehension в качестве второго параметра для map.
  3. map возвращает не список, а генератор. Условно, это такой список, который можно прочитать только один раз. Это обычно подходит, если результат map сразу идёт в какую-то функцию, но не подходит, если вам нужен именно список значений. В таком случае сконструируйте список, явно вызвав list(...).
  4. Тем не менее, np.array(list(map(...))) --- это тавтология. Тут сначала результат map превращается в обычный питоновский список, а потом в список NumPy, можно просто np.array(map(...)).
  5. Аналогично, если вы присваиваете в срез уже существующего списка или матрицы, то не требуется превращать результат map в отдельно живущий список. Rule of thumb: если из результата работы map будет произведено копирование, причём ровно одно, его в список можно не превращать.

Не указывайте end='\n' в print

Don't:

print(1, 2, 3, end='\n')

Do:

print(1, 2, 3)

Причины:

  1. end='\n' --- это значение по умолчанию, которое весьма активно используется. Имхо, это является неким "общим знанием". Так что end='\n' просто загромождает код.
  2. Явное указания параметра, когда у него есть значение по умолчанию, заставляет думать, что в данном случае у нас передаётся другое значение. А это неправда. В результате можно повиснуть, пытаясь при чтении понять, чем же переданный '\n' отличается от значения по умолчанию ('\n').

Мифы:

  1. Значение по умолчанию может измениться. Теоретическая возможность, конечно, есть, но это сломает огромное количество программ. Поэтому так делать не будут --- обратная совместимость для языка важна.

Осторожно:

  1. Если у вас имеется много-много print подряд, и у каких-то end имеет необычное значение, а у каких-то --- значение по умолчанию, может иметь смысл для единообразия выставить его явно у всех сразу. Тогда при чтении не будет возникать ощущение "а, тут у всех выставлено в необычное значение".