Содержание

Пишем отличный код

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

Стиль кода

Одна из причин, почему код Python прост для понимания, заключается в информативном руководстве по стилю написания кода, оно представлено в двух стандартах по развитию Python (Python Enhancement Proposal – Предложение по улучшению Python) PEP 8 и PEP 20, а также следующие из них простые идиомы.

Различия между PEP 8 и PEP 20 заключаются в том, что первое это большой сборник советов и лучших практик наработанных за долгие годы, а второе список идиом, и афоризмов которые призваны передать принцип мышления и принятия решений при разработке кода.

Конечно следует помнить, что педантичное следование стандартам PEP может снизить читаемость и понятность, и не является догмой. Также помимо этих двух стандартов, существует и множество ответвлений.

PEP 8

PEP 8 представляет собой руководство по стилю написания кода Python помогающее разработчикам создавать более стабильный код. В нем рассматриваются соглашения по именованию, структуре кода, пустым областям и другим аналогичным темам. Сообщество Python старается следовать принципам, изложенным в этом документе.

Некоторые проекты время от времени могут отступать от него, а другие (вроде Requests) – добавлять поправки к рекомендациям.

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

Но важно помнить, что иногда это руководство неприменимо, и были сформулированы две причины, когда следует нарушить данные правила:

  1. Когда применение правила сделает код менее читаемым даже для того, кто привык читать код, который следует правилам.
  2. Чтобы писать в едином стиле с кодом, который уже есть в проекте и который нарушает правила (возможно, в силу исторических причин) – впрочем, это возможность переписать чужой код.

Данный стандарт интегрирован во множестве популярных сред разработки. Также можно установить пакет pip install autopep8 и при запуске вашего файла autopep8 --in-place file.py вы получите переформатированный файл.

Отступы

Используйте 4 пробела на каждый уровень отступа. Как правило среда разработки сама добавляет и контролирует отступы.

# Правильно:
# Выровнено по открывающему разделителю
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# Больше отступов включено для отличия его от остальных
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# Опционально:
# В данном случае нет необходимости в большем количестве отступов
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)
# Неправильно:
# Аргументы на первой линии запрещены, 
# если не используется вертикальное выравнивание
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# Больше отступов требуется, для отличения его от остальных
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

Максимальная длина строки

Ограничьте длину строки максимум 79 символами. Для более длинных блоков текста с меньшими структурными ограничениями (строки документации или комментарии), длину строки следует ограничить 72 символами.

Пустые строки

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

Кавычки в строках

В Python можно использовать как одинарные, так и двойные кавычки. Однако, если в строке используются двойные кавычки, то программист должен выделять строку одинарными и наоборот.

Кодировка исходного файла

Кодировка файлов с расширением .py должна быть UTF-8 (ASCII в Python 2).

Импорты

Каждый импорт должен быть на отдельной строке.

# Правильно:
import os
import sys
# Так тоже можно:
from subprocess import Popen, PIPE
# Неправильно:
import sys, os

Комментарии

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

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

Ставьте два пробела после точки в конце предложения.

Пишите комментарии на английском, если только вы не уверены на 120%, что ваш код никогда не будут читать люди, не знающие вашего родного языка.

Комментарии, противоречащие коду, хуже, чем отсутствие комментариев. Всегда исправляйте комментарии, если меняете код!

Блок комментариев обычно объясняет код (весь, или только некоторую часть), идущий после блока, и должен иметь тот же отступ, что и сам код. Каждая строчка такого блока должна начинаться с символа # и одного пробела после него (если только сам текст комментария не имеет отступа).

Абзацы внутри блока комментариев разделяются строкой, состоящей из одного символа #.

«Строчные» комментарии должны отделяться по крайней мере двумя пробелами от инструкции. Они должны начинаться с символа # и одного пробела. Старайтесь реже использовать подобные комментарии.

x = x + 1  # Компенсация границы - комментарий несет технический смысл
		   # вместо: прибавим единицу

Соглашения о написании хорошей документации (docstrings) зафиксированы в PEP 257. Пишите документацию для всех публичных модулей, функций, классов, методов. Строки документации необязательны для приватных методов, но лучше написать, что делает метод. Комментарий нужно писать после строки с def. Очень важно, чтобы закрывающие кавычки стояли на отдельной строке. А еще лучше, если перед ними будет ещё и пустая строка.

"""Return something useful

Optional plotz says to frobnicate the bizbaz first.

"""

Правила именования

Соглашения:

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

  • b (одиночная маленькая буква)
  • B (одиночная заглавная буква)
  • lowercase (слово в нижнем регистре)
  • lower_case_with_underscores (слова из маленьких букв с подчеркиваниями)
  • UPPERCASE (заглавные буквы)
  • UPPERCASE_WITH_UNDERSCORES (слова из заглавных букв с подчеркиваниями)
  • CapitalizedWords (слова с заглавными буквами, или CapWords, или CamelCase). Замечание: когда вы используете аббревиатуры в таком стиле, пишите все буквы аббревиатуры заглавными — HTTPServerError лучше, чем HttpServerError.
  • mixedCase (отличается от CapitalizedWords тем, что первое слово начинается с маленькой буквы)
  • Capitalized_Words_With_Underscores (слова с заглавными буквами и подчеркиваниями)

В дополнение к этому в Python, используются следующие специальные формы записи имен с добавлением символа подчеркивания в начало или конец имени:

  • _single_leading_underscore: слабый индикатор того, что имя используется для внутренних нужд. Например, from module import * не будет импортировать объекты, чьи имена начинаются с символа подчеркивания.

  • single_trailing_underscore_: используется по соглашению для во избежание конфликтов с ключевыми словами языка Python, например:

    Tkinter.Toplevel(master, class_='ClassName')
    
  • __double_leading_underscore: изменяет имя атрибута класса, то есть в классе FooBar поле __boo становится _FooBar__boo.

  • __double_leading_and_trailing_underscore__ (двойное подчеркивание в начале и в конце имени): методы или атрибуты, которые находятся в пространствах имен, управляемых пользователем. Например, __init__, __import__ или __file__. Не изобретайте такие имена, используйте их только так, как написано в документации.

Общие рекомендации PEP 8 по именованию элементов можно сформулировать так:

Элемент Стиль имени
Функции и переменные lowercase, lower_with_underscores
Глобальные переменные lowercase, lower_with_underscores, __all__
Классы CapitalizedWords
Пакеты и модули lowercase, lower_with_underscores
Константы UPPERCASE, UPPER_WITH_UNDERSCORES

Используйте описательные имена, чтобы было понятно, что означает (хранит в себе) или делает переменная (url, user_list, MAX), класс (User, Cat), функция (get_list, text_filter).

Имена, которые следует избегать

Никогда не используйте символы l (маленькая латинская буква «эл»), O (заглавная латинская буква «о») или I (заглавная латинская буква «ай») как однобуквенные идентификаторы.

В некоторых шрифтах эти символы неотличимы от цифры один и нуля. Если очень нужно l, пишите вместо неё заглавную L.

Циклы

Циклы по спискам. Если нам необходимо в цикле пройти по всем элементам списка, то хорошим тоном (да и более читаемым) будет такой способ:

# Правильно:
colors = ['red', 'green', 'blue', 'yellow']
for color in colors:
    print(color)
    
# Если нужно пройти по списку задом наперед, то лучше всего использовать метод reversed:
colors = ['red', 'green', 'blue', 'yellow']
for color in reversed(colors):
    print(color)
# Неправильно:
colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print(colors[i])
    
# Избыточный код
colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)-1, -1, -1):
    print(colors[i])

Циклы по списку чисел. Если есть необходимость пройти в цикле по ряду чисел, то метод range будет намного приемлемее, как минимум потому, что этот метод потребляет меньше памяти, чем вариант в блоке “Неправильно”.

# Правильно:
for i in range(6):
    print(i**2)
# Неправильно:
for i in [0, 1, 2, 3, 4, 5]:
    print(i**2)

range([start], stop[, step]) – генерирует список чисел.

  • start: число начала последовательности.
  • stop: генерирует число до данного числа, но не включая его.
  • step: разница между каждым числом из последовательности.

Циклы по спискам с индексами. Метод enumerate позволяет получить сразу индекс и значение из списка, что, во-первых, предоставляет множество возможностей для дальнейшего проектирования, а во-вторых, такой код легче читается и воспринимается.

# Правильно:
colors = ['red', 'green', 'blue', 'yellow']
for i, color in enumerate(colors):
    print(i, '-->', color)
# Неправильно:
colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print(i, '-->', colors[i])

enumerate(iterable[, start=0]) – добавляет счетчик в виде индекса для каждого элемента и его значение.

  • iterable: список, dataframe, последовательность, итератор или объекты, поддерживающий итерацию.
  • start: начинает отсчет с этого числа. Если значение start опущено, в качестве начала принимается 0.

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

# Правильно:
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
for name, color in zip(names, colors):
    print(name, '-->', color)
    
result = zip(names, colors)
print(list(result))
# -> [('raymond', 'red'), ('rachel', 'green'), ('matthew', 'blue')]
# Неправильно:
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
n = min(len(names), len(colors))
for i in range(n):
    print(names[i], '-->', colors[i])

zip(*iterables) – принимает итерируемые объекты (от ноля и более) и объединяет их в кортеж.

  • iterables: могут быть как встроенными (например: list, string, dict), так и определяемыми пользователем итерируемые объекты.

lambda-функции

По версии PEP 8, lambda (или как их еще называют анонимные) функции снижают читабельность, если использовать переменную с значением функции как функцию, и лучше использовать def.

Принцип работы:

Разберем принцип работы lambda-функции на примере подсчёта площади треугольника по основанию b и высоте h.

def area(b, h):
    return 0.5 * b * h

area = lambda b, h: 0.5 * b * h

# Вызов одинаков, как для обычной функции, так и для lambda
area(6, 5)

Общий синтаксис: lambda [parametes]: expression

parameters – не обязательная часть. Обычно переменные через запятую.

expression – выражение, тело функции. Нельзя использовать return или yield, а также условные операторы или циклы, но можно условные выражения.

# lambda-функция, которая добавляет 'Not  one!' если аргумент не 1 (окончание множественного числа).
is_one = lambda x: "One!" if x == 1 else "Not one!"

# Использование этой функции.
print("{0} file {1} processed".format(count, is_one(count))

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

# Список кортежей, которые описывают химические элементы (номер группы, порядковый номер, название)
elements = [(2, 12, "Mg"), (1, 11, "Na"), (1, 3, "Li"), (2, 4, "Be")]

# Сортировка по умолчанию.
sorted(elements)
# -> [(1, 3, 'Li'), (1, 11, 'Na'), (2, 4, 'Be'), (2, 12, 'Mg')]

# Если мы не хотим учитывать при сортировке первый элемент кортежа (группу), то можно отдельно написать обычную функцию, которая вернет ключ. 
# Или чтобы не плодить много маленьких функций, использовать lambda-функцию при вызове sorted.
elements.sort(key=lambda e: (e[1], e[2]))   # не забываем про () если возвращаем кортеж
elements.sort(key=lambda e: e[1:2])         # или так
# -> [(1, 3, 'Li'), (2, 4, 'Be'), (1, 11, 'Na'), (2, 12, 'Mg')]

# Отсортировать сначала по названию (без учета регистра), а потом по порядковому номеру.
elements.sort(key=lambda e: (e[2].lower(), e[1]))
# -> [(2, 4, 'Be'), (1, 3, 'Li'), (2, 12, 'Mg'), (1, 11, 'Na')]

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

minus_one_dict = collections.defaultdict(lambda: -1)
point_zero_dict = collections.defaultdict(lambda: (0, 0))
message_dict = collections.defaultdict(lambda: "No message available")
Сравнение:

Сравним def и lambda-функции по быстродействию.

from time import time

# Для измерения быстродействия используем разницу во времени 
# между началом выполнения и концом.
def speed_test(func, n):
    start = time()
 	for i in range(n):
    	func()
 	stop = time()
 	return stop - start

# Оценка скорости создания и скорости выполнения def.
def test_def(n):
	def adding_def_func():
    	#  В PEP 8 допускается записывать функцию в одну строку
    	def test(): return sum((2, 3, 4)) ** 0.5  # создание
 		test()                                    # выполение
	return speed_test(adding_def_func, n)
# Оценка скорости создания и скорости выполнения lambda.
def test_lambda(n):
	def adding_lambda_func():
    	test = lambda: sum((2, 3, 4)) ** 0.5      # создание
    	test()                                    # выполение
	return speed_test(adding_lambda_func, n)

# Вывод времени выполнения.
print(test_def(10000))      # -> 0.00799870491027832
print(test_lambda(10000))   # -> 0.013999700546264648

Если увеличить n до $10000000$, то быстродействие lambda будет более заметно, однако ядро языка Python также усовершенствуется и новые версии языка могут отличаться по результатам.

Вывод, lambda, заточенный под однострочники, быстрее выполняются и создаются.

Для повышения читабельности используйте def, если скорость в приоритете не используйте Python, то стоит качественно описать в комментариях принцип работы и воспользоваться lambda-функциями.

PEP 20

PEP 20 известный как «Дзен Питона» или набор принципов для принятия решений в Python. Несмотря на название, PEP 20 содержит 19 афоризмов, а не 20 (последний не был записан) и в этом заключается философский подтекст – Никакие правила нельзя возводить в абсолют. Здесь каждый может определить для себя свой принцип и будет прав.

Дзен Питона:
  • Красивое лучше, чем уродливое.
  • Явное лучше, чем неявное.
  • Простое лучше, чем сложное.
  • Сложное лучше, чем запутанное.
  • Одноуровневое лучше, чем вложенное.
  • Разреженное лучше, чем плотное.
  • Читаемость имеет значение.
  • Особые случаи не настолько особые, чтобы нарушать правила.
  • При этом практичность важнее безупречности.
  • Ошибки никогда не должны замалчиваться.
  • Если не замалчиваются явно.
  • Встретив двусмысленность, отбрось искушение угадать.
  • Должен существовать один — и желательно только один — очевидный способ сделать это.
  • Хотя он поначалу может быть и не очевиден, если вы не голландец (прим. создатель языка Python Гвидо ван Россум – голландец).
  • Сейчас лучше, чем никогда.
  • Хотя никогда зачастую лучше, чем прямо сейчас.
  • Если реализацию сложно объяснить — идея плоха.
  • Если реализацию легко объяснить — идея, возможно, хороша.
  • Пространства имен — отличная штука! Будем делать их побольше!

Ниже приведены общие советы, которые следуют непосредственно из «Дзена Питона» и здравого смысла.

Явное лучше, чем неявное

В Python предпочтителен наиболее явный способ выражения:

# Хороший код:
def make_dict(x, y):
	return {'x': x, 'y': y}
# Плохой код:
def make_dict(*args):
    x, y = args
    return dict(**locals())

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

Разреженное лучше, чем плотное

В каждой строке размещайте только одно выражение.

# Хороший код:
print('one')
print('two')

if x == 1:
	print('one')
	
cond1 = <complex comparison>
cond2 = <other complex comparison>
if cond1 and cond2:
    # сделать что-нибудь
# Плохой код:
print('one'); print('two')

if x == 1: print('one')

if (<complex comparison> and
    <other complex comparison>):
    # сделать что-нибудь

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

Ошибки никогда не должны замалчиваться, если не замалчиваются явно

Обработка ошибок в Python выполняется с помощью выражения try.

try:
	x = 1 / 0
except ZeroDivisionError as err:
	print(err)
    x = 0
    print(x)
# Хороший код:
def get_division(a, b):
    try:
        x = a / b
    except TypeError:
        print("Значения несовместимых типов.")
    except ZeroDivisionError:
        print("b не может быть нулем!")
        x = -1
        # return x
    except ValueError:
    	print("Ошибка ввода числа")
    except:
        print("Что-то пошло не так...")
    finally:
    	print("Хорошо")
    
    return x

try:
    get_division(4)
except TypeError:
    print("Значения несовместимых типов.")
# Плохой код:
def get_division(a, b):
    return a / b

Исключение, которое вы не увидите при выполнении кода – это BaseException – базовое исключение, от которого берут начало остальные.

В иерархии исключений две основные группы:

  • Системные исключения и ошибки – их обработку лучше не делать.
  • Обыкновенные исключения – обработка выполняется разработчиком.

К системным исключениям можно отнести: SystemExit – исключение, порождаемое функцией sys.exit при выходе из программы; KeyboardInterrupt – возникает при прерывании программы пользователем (обычно сочетанием клавиш Ctrl+C); GeneratorExit – возникает при вызове метода close объекта generator; остальные исключения – это «обыкновенные».

Спектр уже готовых исключений велик. Но даже этих почти 70 исключений не хватает при написании программ на языке Python.

Иерархия исключений:

Иерархия встроенных в Python исключений (также можно создавать собственные исключения):

  • BaseException – базовое исключение, от которого берут начало все остальные.
    • SystemExit – исключение, порождаемое функцией sys.exit при выходе из программы.
    • KeyboardInterrupt – порождается при прерывании программы пользователем (обычно сочетанием клавиш Ctrl+C).
    • GeneratorExit – порождается при вызове метода close объекта generator.
    • Exception – а вот тут уже заканчиваются полностью системные исключения (которые лучше не трогать) и начинаются обыкновенные, с которыми можно работать.
      • StopIteration – порождается встроенной функцией next, если в итераторе больше нет элементов.
      • ArithmeticError – арифметическая ошибка.
        • FloatingPointError – порождается при неудачном выполнении операции с плавающей запятой. На практике встречается нечасто.
        • OverflowError - возникает, когда результат арифметической операции слишком велик для представления. Не появляется при обычной работе с целыми числами (так как Python поддерживает длинные числа), но может возникать в некоторых других случаях.
        • ZeroDivisionError – деление на ноль.
      • AssertionError – выражение в функции assert ложно.
      • AttributeError – объект не имеет данного атрибута (значения или метода).
      • BufferError – операция, связанная с буфером, не может быть выполнена.
      • EOFError – функция наткнулась на конец файла и не смогла прочитать то, что хотела.
      • ImportError – не удалось импортирование модуля или его атрибута.
      • LookupError – некорректный индекс или ключ.
        • IndexError – индекс не входит в диапазон элементов.
        • KeyError – несуществующий ключ (в словаре, множестве или другом объекте).
      • MemoryError – недостаточно памяти.
      • NameError – не найдено переменной с таким именем.
        • UnboundLocalError - сделана ссылка на локальную переменную в функции, но переменная не определена ранее.
      • OSError – ошибка, связанная с системой.
        • BlockingIOError
        • ChildProcessError – неудача при операции с дочерним процессом.
        • ConnectionError – базовый класс для исключений, связанных с подключениями.
          • BrokenPipeError
          • ConnectionAbortedError
          • ConnectionRefusedError
          • ConnectionResetError
        • FileExistsError – попытка создания файла или директории, которая уже существует.
        • FileNotFoundError – файл или директория не существует.
        • InterruptedError – системный вызов прерван входящим сигналом.
        • IsADirectoryError – ожидался файл, но это директория.
        • NotADirectoryError – ожидалась директория, но это файл.
        • PermissionError – не хватает прав доступа.
        • ProcessLookupError – указанного процесса не существует.
        • TimeoutError – закончилось время ожидания.
      • ReferenceError – попытка доступа к атрибуту со слабой ссылкой.
      • RuntimeError – возникает, когда исключение не попадает ни под одну из других категорий.
      • NotImplementedError – возникает, когда абстрактные методы класса требуют переопределения в дочерних классах.
      • SyntaxError – синтаксическая ошибка.
        • IndentationError – неправильные отступы.
          • TabError – смешивание в отступах табуляции и пробелов.
      • SystemError – внутренняя ошибка.
      • TypeError – операция применена к объекту несоответствующего типа.
      • ValueError – функция получает аргумент правильного типа, но некорректного значения.
      • UnicodeError – ошибка, связанная с кодированием / раскодированием unicode в строках.
        • UnicodeEncodeError – исключение, связанное с кодированием unicode.
        • UnicodeDecodeError – исключение, связанное с декодированием unicode.
        • UnicodeTranslateError – исключение, связанное с переводом unicode.
      • Warning – предупреждение.

Аргументы функций должны быть интуитивно понятными

Если реализацию сложно объяснить — идея плоха

Python даёт широкий набор инструментов и позволяет решать практически любую задачу (за что его любят хакеры), в том числе: изменение способа создания объектов, изменения способа импортирования модулей Python, встраивать в Python подпрограммы, написанные на С и многое другое.

Все эти действия имеют недостатки, поэтому всегда лучше выбирать прямолинейный способ достижения цели. Основной минус: при использовании подобных конструкций – снижается читаемость, поэтому то, что вы получаете в результате, должно быть более важным, чем потеря читаемости. Многие инструменты, предназначенные для анализа кода, не смогут работать с таким «волшебным» кодом.

Мы все – ответственные пользователи

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

Так в Python реализуется инкапсуляция. Вместо того чтобы возводить бетонные стены (в таких языках как Java) между своим и чужим кодом, сообщество Python предпочитает полагаться на набор соглашений, которые указывают, к каким элементам нельзя получить доступ напрямую. Философия Python сосредоточена во фразе «Мы все – ответственные пользователи».

Возвращайте значения из одной точки

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

def select_ad(third_party_ads, user_preferences):
	if not third_party_ads:
		return None # Лучше сгенерировать исключение
	if not user_preferences:
		return None # Лучше сгенерировать исключение
	# Сложный код, предназначенный для выбора best_ad
	# Из доступных вариантов на основе индивидуальных предпочтений...
	# Постарайтесь устоять перед искушением вернуть best_ad в случае успеха...
	if not best_ad:
		# Запасной план определения best_ad
	
    return best_ad # Единая точка выхода, которая поможет обслуживать код

Общие соглашения

Проверка на равенство:
# Хороший код:
# Просто проверяем значение
if attr:
	print('attr is truthy!')
# или проверяем на противоположное значение
if not attr:
	print('attr is falsey!')
# если вам нужно только значение 'True'
if attr is True:
	print('attr is True')
# или явно проверяем на значение None
if attr is None:
	print('attr is None!')
# Плохой код:
if attr == True:
	print('True!')

if attr == None:
	print('attr is None!')
Доступ к элементам массива:

Используйте синтаксис x in dict вместо метода dict.has_key() или передавайте аргумент по умолчанию в метод dict.get().

# Хороший код:
d = {'hello': 'world'}

print d.get('hello', 'default_value')
# ->  world

print d.get('howdy', 'default_value')
# -> default_value

# или:
if 'hello' in d:
	print(d['hello'])
# -> world
# Плохой код:
d = {'hello': 'world'}

if d.has_key('hello'):
	print(d['hello'])
else:
	print('default_value')
# -> world
Манипуляции со списками:

Списковые включения – один из способов работы со списками. Функции map() и filter() могут выполнять операции со списками с помощью более выразительного синтаксиса.

# Хороший код - Списковое включение:
# Списковое включение выглядит прозрачнее.
# Отфильтруем все элементы, чье значение превышает 4.
a = [3, 4, 5]
b = [i for i in a if i > 4]
# Или:
b = filter(lambda x: x > 4, a)

# Здесь также прозрачнее.
# Добавим 3 к каждому элементу списка.
a = [3, 4, 5]
a = [i + 3 for i in a]
# Или:
a = map(lambda i: i + 3, a)

# Используйте функцию enumerate(), чтобы определить свою позицию в списке. Этот вариант выглядит более читаемым, чем создание счетчика, и лучше оптимизирован для итераторов:
a = ["icky", "icky", "icky", "p-tang"]
for i, item in enumerate(a):
	print("{i}: {item}".format(i=i, item=item))
# Плохой код - Стандартный цикл:
# Отфильтруем все элементы, чье значение превышает 4.
a = [3, 4, 5]
b = []
for i in a:
	if i > 4:
		b.append(i)

# Добавим 3 к каждому элементу списка.
a = [3, 4, 5]
for i in range(len(a)):
	a[i] += 3
Длинные строки кода:

Если интерпретатор Python встретит незакрытую круглую скобку в одной строке, он будет присоединять к ней следующие строки до тех пор, пока скобка не будет закрыта. То же поведение верно для фигурных и квадратных скобок.

# Хороший код:
french_insult = (
    "Your mother was a hamster, and "
    "your father smelt of elderberries!"
)

from some.deep.module.in.a.module
import (
    a_nice_function,
    another_nice_function,
    yet_another_nice_function
)
# Плохой код:
french_insult = \
"Your mother was a hamster, and \
your father smelt of elderberries!"

from some.deep.module.in.a.module \
    import a_nice_function, \
    	another_nice_function, \
    	yet_another_nice_function

Google Python Style Guide

Корпорация Google разработала собственный стандарт оформления кода и в целом он практически повторяет стандарт PEP 8 с некоторыми уточнениями для конкретных ситуаций (кейсов) и свойств синтаксиса. По этой причине, данный стандарт можно считать приоритетным в использовании.

Для быстрого поиска ответов по оформлению кода, лучше всего гуглить как class docstring google style или function docstring google style.

Исходная документация содержит множество примеров из реальных проектов.

Линтеры – Linters

Следовать всем хорошим практикам и правилам оформления – не самая простая задача. Поэтому появились инструменты автоматизирующие контроль качества кода.

Линтеры (linter, отсылка к программе lint для программирования на языке C) – это программа для проверки исходного кода, на ошибки и качество оформления. Под ошибками подразумеваются базовые правила синтаксиса языка, а под оформлением следование стандартам.

Про линтеры:

Термин «lint» впервые начал использоваться в таком значении в 1979 году. Так называлась программа для статического анализа кода на языке C, которая предупреждала об использовании непортабельных на другие архитектуры языковых конструкций. С тех пор линтерами называют любые статические анализаторы кода, которые помогают находить распространённые ошибки, делать его однообразным и более читаемым.

А названо оно «lint» (от англ. lint – ворс) в честь вот такой штуки:

Один из таких линтеров, был уже упомянут выше – autopep8. В Python их достаточно много, и наиболее популярные из них (которые используются в больших проектах): flake8, mypy, black. Часто на практике могут использовать связку из нескольких последовательных проверок, например black + mypy, таким образом будет полное покрытие кода.

black Установка: pip install black Установка для Jupyter Notebooks: pip install 'black[jupyter]' Запуск: black {source_file_or_directory}


mypy Установка: pip install mypy Запуск: mypy program.py

Плагины для Visual Studio Code, PyCharm быстро устанавливаются и настраиваются, при этом возможны комбинации автоматических запусков разных линтеров после сохранения файла в формате *.py.

Black в VSCode:
  1. Установите black: pip install black
  2. Установите Microsoft’s Python extension в VSCode:
  3. Откройте настройки вашего VSCode: Code -> Preferences -> Settings или Ctrl+Shift+P или F1
  4. Вбейте в поиск “python formatting provider” и выберете “black” из выпадающего меню:
  5. Тут же, в настройках, вбейте “format on save” и включите “Editor: Format on Save":
  6. Теперь Black будет форматировать ваш код каждый раз, когда вы сохраняете *.py файл.

Также существует вариант добиться того же результата для VSCode, при помощи правок в файле настроек settings.json. Для этого после выполнения 1 и 2 пункта из списка выше, выполните:

  1. Файл настроек можно найти:

    • Windows %APPDATA%\Code\User\settings.json
    • macOS $HOME/Library/Application\ Support/Code/User/settings.json
    • Linux $HOME/.config/Code/User/settings.json
    • Или откройте настройки VSCode: Code -> Preferences -> Settings или Ctrl+Shift+P или F1. Напишите “open settings” и выберете Open Settings (JSON)
  2. Добавьте строки:

    "python.formatting.provider": "black",
    "python.formatting.blackPath": "/home/<user>/.local/bin/black",
    "editor.formatOnSave": false,
    "[python]": {
        "editor.formatOnSave": true,
    },
    

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

Про Dataclasses и ООП в Python.

Структура проекта

Под структурой проекта понимается логика и зависимости в коде, структуре файлов и каталогов. Благодаря тому, как в Python устроен процесс импортирования и разбиения на модули, структурировать проект довольно просто. Поэтому перед разработчиком стоит исключительно архитектурная задача – создать различные части проекта и продумать их взаимодействие.

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

  • По какому принципу функции должны размещаться в модулях?
  • Как данные перемещаются по проекту?
  • Какие функции могут быть сгруппированы и изолированы?

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

Рассматриваемые далее модули, декораторы, типизация, объектно-ориентированная парадигма … призваны упростить и сгруппировать логику, обеспечить безопасность и эффективное использование ресурсов.

Модули

С точки зрения архитектуры, модуль – это один из основных уровней абстракции в Python. Уровни абстракции позволяют программисту разбивать код на части, которые содержат связанные данные, функциональность и пространство имен. Фактически модуль - это 1 файл на Python или другом языке (расширение на С, С++, Java и тп).

Стандартная организация программы на языке Python предполагает, что программа состоит из нескольких файлов и сторонних библиотек. Архитектура программы = главный файл (сценарий) + дополнительные файлы (модули, которые подключаются к главному). Обычно файлы модулей ничего не делают, если запустить их отдельно (или выполняют тесты данного модуля), без главного файла, где прописан сценарий.

Например, если один уровень проекта предназначен для взаимодействия с пользователем, а другой обрабатывает данные на низком уровне, наиболее логичным способом разделения этих двух слоев является размещение всей функциональности, связанной со взаимодействием, в одном файле, а все низкоуровневые операции – в другом. Такая группировка разметит их в два разных модуля. Файл для взаимодействия затем импортирует файл для низкоуровневой обработки с помощью выражения import module или from module import attribute для прямого обращения к методу attribute вместо module.attribute .

Модули могут быть встроенными (вроде os и sys), сторонними пакетами установленными в среде (вроде Requests или NumPy), либо внутренними модулями проекта.

Далее показан пример некоторых выражений import (где подтверждается, что импортированный модуль является объектом Python со своим типом данных):

import sys  # built-in module
import matplotlib.pyplot as plt  # сторонний модуль
import mymodule as mod  # внутренний модуль проекта

print(type(sys), type(plt), type(mod))
# <class 'module'> <class 'module'> <class 'module'>

Python позволяет импортировать все имена из модуля в сокращенном виде, используя from module import *, после чего становится возможным использовать напрямую метод из модуля, вместо префикса этих имён с module., однако данный подход может быть использовать только на верхнем уровне.

Разберем пример, пусть есть файл mail.py:

def spam(text):
    print(text, 'spam')

Если мы хотим импортировать этот файл и вызвать функцию spam, то можем:

import вызов что значит
import mail mail.spam(‘a’) mail - имя переменной, которая ссылается на объект модуля после его загрузки.
from mail import spam spam(‘a’) импорт модуля + копирование имени spam в пространство имен.
from mail import * spam(‘a’) импорт модуля + копирование всех имен верхнего уровня (в том числе вне def или class).

Что лучше - import или from?

модуль.атрибут – mail.spam(‘a’)

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

Рецепт (рекомендательный):

  • import – предпочтительнее;
  • from .. import – явно перечисляем имена;
  • from .. import * – только при одном таком импорте.

Упаковка

В Python понятная система упаковки, которая расширяет механизм модулей так, что он начинает работать с каталогами системы. Любой каталог, содержащий файл __init__.py, считается пакетом Python. Каталог высшего уровня, в котором находится файл __init__.py, является корневым пакетом. Разные модули пакетов импортируются аналогично простым модулям, но файл __init__.py при этом будет использован для сбора всех описаний на уровне пакета.

Если файл module.py, находящийся в каталоге pack/, импортируется с помощью выражения import pack.module, то интерпретатор выполнит поиск файла __init__.py в pack/ и запустит все его выражения верхнего уровня. Затем выполнит поиск файла с именем pack/module.py и запустит все его выражения верхнего уровня. После этих операций любая переменная, функция или класс, определенные в файле module.py, будут доступны пространству имен pack.module.

Для импортирования вложенных пакетов доступен синтаксис: import very.deep.module as mod. Это позволяет использовать слово mod в качестве псевдонима на месте избыточной конструкции very.deep.module. Например: import matplotlib.pyplot as plt.

import вызов что значит
import mail as m m.spam(‘a’) m - имя переменной и псевдоним, который ссылается на объект модуля после его загрузки.

Пространство имен

Пространство имен - место, где создаются имена.

Атрибуты модуля - имена, которые находятся в модуле.

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

При поиске переменных, Python использует правило LEGB. Например, если внутри функции выполняется обращение к имени переменной, Python ищет переменную в следующем порядке по областям видимости (до первого совпадения):

  • L (local) – в локальной (внутри функции);
  • E (enclosing) – в локальной области объемлющих функций (это те функции, внутри которых находится наша функция);
  • G (global) – в глобальной (в скрипте);
  • B (built-in) – во встроенной (зарезервированные значения Python).

Наглядный пример:

a = 8

def f(a):
       a = a + 1
       print(a)
    
f(a)
# -> 9

При запуске данной программы, не смотря на то, что название глобальной переменной и атрибута функции совпадает у них разная область видимости и после выполения глобальная переменная a не изменится. Внутри функции f(a) глобальная переменная a невидна и если убрать в определении функции аргумент:

a = 8

def f():
       a = a + 1
       print(a)
    
f(a)
# -> f() takes 0 positional arguments but 1 was given

то данный код выдаст ошибку f() takes 0 positional arguments but 1 was given, означающую, что функция f() принимает 0 аргументов, но при этом внутри функции используется 1 аргумент.

a = 8

def wrap_f():
    	def f(a):
        	a = a + 1
        	print(a)
    	f(a)
    
wrap_f()
# -> 9

Но при этом доступ к переменным в локальной области в функции, недоступны из под внешнего уровня:

a = 8

def wrap_f():
       def f(a):
           a = a + 1
           x = 7
       print(a)
   f(a)
    
wrap_f()
y = x + 10
# -> name 'x' is not defined

Типизация

ООП

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

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

Однако, парадигма ООП не является основной для Python (в отличие от языка Java). Проект, написанный на Python, вполне может быть не объектно-ориентированным, то есть в нем не будут использоваться (или будут, но в небольших количествах) определения классов, наследование классов или другие механизмы, характерные для объектно-ориентированного программирования, т.е. эта функциональность доступна, но необязательна.

Более того, как следует из подраздела «Модули» текущего раздела, способ, с помощью которого Python обрабатывает модули и пространства имен, дает возможность гарантировать инкапсуляцию и разделение между абстрактными уровнями – наиболее распространенную причину использования парадигмы ООП – без наличия классов.

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

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

  • Данные структурируются в виде объектов, каждый из которых имеет определенный тип, то есть принадлежит к какому-либо классу.
  • Классы – результат формализации решаемой задачи, выделения главных ее аспектов.
  • Внутри объекта инкапсулируется (скрывается) логика работы с относящейся к нему информацией.
  • Объекты внутри программы взаимодействуют друг с другом, обмениваются запросами и ответами.
  • При этом объекты одного типа сходным образом отвечают на одни и те же запросы.
  • Объекты могут организовываться в более сложные структуры, например, включать другие объекты или наследовать от одного или нескольких объектов.

Классы

Класс – это шаблон (чертёж) объекта. Для объявления класса Cat мы использовали ключевое слово class. Из классов мы получаем экземпляры, созданные по подобию этого класса.

class Cat:
    pass

pass – оператор-заглушка, равноценный отсутствию операции.

Объект – это экземпляр класса. Объявленный класс – это лишь описание объекта: ему не выделяется память.

Например, экземпляра класса Cat будет выглядеть так:

# obj — экземпляр класса Cat
obj = Cat()

Опишем класс и его объекты.

# Создаем класс и его объекты
class Cat:

    # атрибуты класса
    species = "кошка"

    # атрибуты экземпляра
    def __init__(self, name, age):
        self.name = name
        self.age = age

# создаем экземпляра класса
murzik = Cat("Мурзик", 10)
barsik = Cat("Барсик", 15)

# получаем доступ к атрибутам класса
print("Мурзик — {}".format(murzik.__class__.species))
print("Барсик тоже {}".format(barsik.__class__.species))

# получаем доступ к атрибутам экземпляра
print("{}{}-летняя кошка".format(murzik.name, murzik.age))
print("{}{} летний кот".format(barsik.name, barsik.age))

Выведет:

Мурзик — кошка
Барсик тоже кошка
Мурзик — 10-летняя кошка
Барсик — 15-летний кот

Мы создали класс Cat. После этого мы объявили атрибуты (поля) объекта – характеристики объекта.

Атрибуты объявлены внутри класса, в методе __init__ – это метод-инициализатор, который запускается сразу же после создания объекта.

После этого мы создаем экземпляры класса Cat. murzik и barsikссылки на (значения) наши новые объекты.

Получить доступ к атрибуту класса можно так – __class__.species. Атрибуты класса для всех экземпляров класса одинаковы. Точно так же мы можем получить доступ к атрибутам экземпляра – murzik.name и barsik.age. Но вот атрибуты каждого экземпляра класса уникальны.

Методы – функции, объявленные внутри тела класса. Они определяют поведения объекта.

self – это стандартное имя первого аргумента для методов объекта. Ничто не мешает изменить это имя и использовать любое другое, однако пренебрежение этим устоявшимся соглашением может ввести в заблуждение читателей вашего кода.

В ходе исполнения метода объекта в первом аргументе автоматически окажется экземпляр класса (передавать его специально не требуется).

# Создаем метод
class Cat:
    
    # атрибуты экземпляра
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    # метод экземпляра
    def meow(self, song):
        return "{} мяучет {}".format(self.name, song)

    def play(self):
        return "{} играет".format(self.name)

# создаем экземпляр класса
persik = Cat("Персик", 10)

# вызываем методы экземпляра
print(persik.meow("песенки"))
print(persik.play())

Вывод:

Персик мяучет песенки
Персик играет

В этой программе мы объявили два метода: meow() и play(). Они являются методами экземпляра, потому что они вызываются объектами – например, persik.

Наследование

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

# родительский класс
class Cat:
    
    def __init__(self):
        print("Кошка готова")

    def whoisThis(self):
        print("Кошка")

    def meow(self):
        print("Мяукает")

# дочерний класс - сиамская кошка
class Siamese(Cat):

    def __init__(self):
        # вызов функции super() 
        super().__init__()
        print("Сиамская кошка готова")

    def whoisThis(self):
        print("Сиамская кошка")

    def run(self):
        print("Бежит быстрее")

sima = Siamese()
sima.whoisThis()
sima.meow()
sima.run()

Вывод:

Кошка готова
Сиамская кошка готова
Сиамская кошка
Мяукает
Бежит быстрее

В этой программе мы создаем два класса – Cat (родительский) и Siamese (дочерний). Дочерний класс наследует функции родительского. Это вы можете заметить по методу meow().

Но и дочерний класс изменяет функциональность родительского. Это можно заметить по методу whoisThis(). Более того, мы расширяем функциональность родительского класса – создаем метод run().

Также мы используем функцию super() внутри метода __init__(). Это позволяет запускать метод __init__() родительского класса внутри дочернего. Таким образом у класса Siamese – 3 метода и 1 метод-инициализатор.

Инкапсуляция

В Python можно ограничить доступ к методам и переменным, что предотвратит (непреднамеренную) модификацию данных – это и есть инкапсуляция (от англ. encapsulation и лат. in capsula – сокрытие, капсула). Приватные атрибуты выделяются нижним подчеркиванием: одинарным _ или двойным __.

# Используем инкапсуляцию данных
class Computer:

    def __init__(self):
        self.__maxprice = 900

    def sell(self):
        print("Цена продажи: {}".format(self.__maxprice))

    def setMaxPrice(self, price):
        self.__maxprice = price

c = Computer()
c.sell()

# изменение цены
c.__maxprice = 1000
c.sell()

# используем функцию изменения цены
c.setMaxPrice(1000)
c.sell()

Вывод:

Цена продажи: 900
Цена продажи: 900
Цена продажи: 1000

В данном примере, объявили класс Computer. Затем использовали метод __init__() для хранения максимальной цены компьютера. Затем попытались изменить цену – безуспешно: Python воспринимает __maxprice как приватный атрибут.

Для изменения цены необходимо использовать специальную функцию – setMaxPrice(), которая принимает цену в качестве параметра.

Полиморфизм

Полиморфизм – позволяющая использовать одну функцию для разных форм (типов данных). Допустим, нам нужно закрасить фигуру. Их форма может быть любой: прямоугольник, квадрат, круг. Одним и тем же методом мы можем раскрасить любую фигуру. Такой принцип и называется полиморфизмом.

# Используем полиморфизм
class Parrot:

    def fly(self):
        print("Попугай умеет летать")
    
    def swim(self):
        print("Попугай не умеет плавать")

class Penguin:

    def fly(self):
        print("Пингвин не умеет летать")
    
    def swim(self):
        print("Пингвин умеет плавать")

# общий интерфейс 
def flying_test(bird):
    bird.fly()

# создаем экземпляров класса
kesha = Parrot()
peggy = Penguin()

# передача объектов в качестве аргумента
flying_test(kesha)
flying_test(peggy)

Вывод:

Попугай умеет летать
Пингвин не умеет летать

В этой программе мы объявили два класса: Parrot и Penguin. В каждом из них описан общий метод fly(). Но функции у них разные.

Для использование полиморфизма мы создали общий интерфейс — функцию flying_test(). В качестве аргумента она принимает любой объект, после чего происходит вызов его собственного метода fly().

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

Динамическое изменение

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

class SomeClass(object):
    pass

def squareMethod(self, x):
    return x*x

SomeClass.square = squareMethod
obj = SomeClass()
obj.square(5) # 25

object – также можно использовать как заглушку при определении класса.

Жизненный цикл объекта

В Python также возможно удаление объектов из памяти, при помощи метода-деструктора __del__.

class SomeClass(object):
    def __init__(self, name):
        self.name = name

    def __del__(self):
        print('удаляется объект {} класса SomeClass'.format(self.name))

obj = SomeClass("John");
del obj # удаляется объект John класса SomeClass

На практике деструктор используется редко, в основном для тех ресурсов, которые требуют явного освобождения памяти при удалении объекта.

Объект как функция

Объект класса может имитировать стандартную функцию, то есть при желании его можно вызвать с параметрами. За эту возможность отвечает специальный метод __call__:

class Multiplier:
    def __call__(self, x, y):
        return x*y

multiply = Multiplier()
multiply(19, 19) # 361
# то же самое
multiply.__call__(19, 19) # 361

Резюме

Подведем краткий итог всему вышесказанному и выделим основные особенности реализации ООП на Python:

  • Классы в Python – это тоже объекты.
  • ООП упрощает понимание программы, делает ее эффективнее и структурно понятнее.
  • Все классы являются общими – код можно использовать повторно.
  • Все данные находятся в сохранности и защищены благодаря абстракции.
  • Полиморфизм предоставляет общий интерфейс разным объектам, что делает код эффективнее (или еще говорят: виртуальность всех методов).
  • Допустимо динамическое изменение и добавление атрибутов классов.
  • Жизненным циклом объекта можно управлять.
  • Поддерживается наследование.

Декораторы

Декоратор – это функция или метод класса, которые оборачивают (или декорирует) другую функцию или метод, в результате декорированная функция или метод заменяет оригинал, т.е. возможность изменить поведение функции, не изменяя её код.

В Python всё является объектом, а не только объекты, которые вы создаёте из классов:

  • числа;
  • строки;
  • классы;
  • функции.

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

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

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

Функции высших порядков – это такие функции, которые могут принимать в качестве аргументов и возвращать другие функции. Например дифференциальный оператор $d/dx$ принимает на входе функцию и возвращает другую функцию, производную от исходной. Функции высших порядков в программировании работают точно так же – они либо принимают функцию(и) на входе и/или возвращают функцию(и).

Функция высшего порядка должна возвращать функцию и/или принимать одну функцию в параметр.

Продемонстрируем гибкость функций (высших порядков) в Python:

def hello_world():
	print('Hello world!')


"""
Можно хранить функции в переменных:
"""
hello = hello_world
hello()  
# Hello world!


"""
Определять функции внутри других функций:
"""
def wrapper_function():
	def hello_world():
		print('Hello world!')
	hello_world()
wrapper_function()  
# Hello world!


"""
Передавать функции в качестве аргументов и возвращать их из других функций:
"""
def higher_order(func):
	print('Получена функция {} в качестве аргумента'.format(func))
	func()
	return func
higher_order(hello_world)  
# Получена функция <function hello_world at 0x032C7FA8> в качестве аргумента 
# Hello world!
# <function hello_world at 0x032C7FA8>

На основе принципа работы функций высших порядков построен механизм декораторов.


Поскольку функции являются объектами первого класса в Python, декорирование можно выполнить вручную, но все же более предпочтителен синтаксис @decorator. Рассмотрим пример использования декоратора:

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

Сторонние зависимости

Тестирование и автотестирование кода

Документирование

Документирование проекта и кода облегает использование и скорость обработки кода как для собственного использования, так для тех, кто хочет вносить в проект свой вклад.

Файл README, расположенный в корневом каталоге, призван давать общую информацию как пользователям, так и тем, кто обслуживает проект. В нем должен быть либо простой текст, либо легкая для чтения разметка вроде reStructured Text (сейчас это единственный формат, который понимает PyPI1) или Markdown (https://help.github.com/articles/basic-writing-and-formatting-syntax/). Этот файл должен содержать несколько строк, описывающих предназначение проекта или библиотеки (предполагая, что пользователь ничего не знает о проекте), URL основного исходного кода, инструкцию по запуску и информацию об авторах. Если вы планируете читать код, то в первую очередь должны ознакомиться с этим файлом.

В файле TODO или одноименном разделе файла README должны быть представлены планы по развитию кода.

В файле CHANGELOG или одноименном разделе файла README должны быть приведены изменения, которые произошли с базой кода в последних версиях.

Разделы TODO и CHANGELOG являются необязательными, однако при изучении открытых проектов и репозиториев бывает полезно изучить эти разделы для понимания вектора развития проекта и его хронологии.

Файл LICENSE содержит лицензию, под которой ПО доступно общественности.

Sphinx

Sphinx (http://sphinx.pocoo.org/) – один из инструментов для создания документации для Python, еще их называют генераторами документации. Используйте его: он преобразует язык разметки reStructured Text в огромное множество форматов, включая HTML, LaTeX (для печатаемых версий PDF), страницы руководства и простой текст.

Журналирование – logging

По мере того, как усложняется структура приложения, ведение логов (журнал событий, журналирование) становится всё полезнее для разработчика. Логи не только помогают в процессе отладки, но и помогут обнаружить скрытые проблемы проекта, а также отследить его производительность.

Журналирование бывает двух видов:

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

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

import logging

logging.warning('Осторожно!')  # выведет сообщение в консоль
logging.info('Информация.')  # не будет выведено в консоль из-за приоритета уровня
# -> WARNING:root:Осторожно!


logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logging.debug('Это сообщение должно попасть в файл журнала')
logging.info('Так же как и это')
logging.warning('И это тоже')
logging.error('А также символы, не относящиеся к ASCII')
# -> DEBUG:root:Это сообщение должно попасть в файл журнала
# INFO:root:Так же как и это
# WARNING:root:И это тоже
# ERROR:root:А также символы, не относящиеся к ASCII

Журналирование против функции print.

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

Рассмотрим причины, почему журналирование лучше, чем print:

  • запись в журнале, которая создается при каждом событии (в том числе и во время корректной работы программы), содержит полезную диагностическую информацию вроде имени файла, полного пути, функции и номера строки для события журналирования, logging.info() (или logging.debug() для подробного вывода);
  • предупреждения, относительно определенного события во время выполнения программы,warnings.warn() в модуле, если проблема устранима и необходимо модифицировать код (что бы данное предупреждение исчезло) и logging.warning() но событие все равно должно быть отмечено
  • к событиям, прописанным в подключенных модулях, можно получить доступ автоматически с помощью корневого средства ведения журнала в потоке журналирования для вашей программы;
  • процесс журналирования можно выборочно приостанавливать с помощью метода logging.Logger.setLevel() или отключать путем установки значения атрибута logging.Logger.disabled() равным True.

Уровни журналирования

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

В Python существует шесть уровней лога. Каждому уровню присвоено целое число, указывающее на важность лога: NOTSET=0, DEBUG=10, INFO=20, WARN=30, ERROR=40 и CRITICAL=50.

Все уровни последовательно упорядочены (DEBUG < INFO < WARN), кроме NOTSET, который мы отдельно разберём далее.

Уровень Используется
DEBUG Подробная информация, обычно интересующая только при диагностике проблем.
INFO Подтверждение того, что все работает должным образом.
WARNING Предупреждение, что произошло действие, которое не ждали, при этом программа все равно работает.
ERROR Ошибка, из-за которой программа не работает.
CRITICAL Серьезная ошибка, при которой программа не может продолжить работать.

Все эти уровни можно вызывать, используя соответствующие функции, например, logging.error. Но лучше пользоваться объектом logger (логер):

import logging

logger = logging.getLogger(__name__)

def myfunc():
    # ...
    logger.info("Something relevant happened")
    # ...

Объект logger предоставляет интерфейс для логирования. Также есть объекты handler (обработчик), filter (фильтр), formatter (объект формата вывода). Обработчики отправляют записи логов в соответствующее место назначения, например, в стандартный поток ошибок (stderr) или в файл. Фильтры предоставляют более детальное средство для определения, какие записи логов нужно выводить, а объекты формата вывода каким образом (по какому шаблону) должны отображаться сами логи.

Форматирование журналов

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

Например, при форматировании записи «hello world»:

"%(asctime)s%(name)s%(levelname)s%(funcName)s:%(lineno)d%(message)s"

в логе она отобразится таким образом:

2018-02-07 19:47:41,864 - a.b.c - WARNING - :1 - hello world

Обработчик журналирования

Обработчик журналов – это компонент, который записывает и отображает логи. Например, StreamHandler выводит записи на консоль, FileHandler — в файл, SMTPHandler отправляет на электронную почту.

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

  1. Форматер, добавляющий в лог контекстную информацию.
  2. Уровень лога, который отфильтровывает журналы низшего уровня. Обработчик уровня INFO не будет работать с журналами уровня DEBUG.

Стандартная библиотека содержит обработчики, которых в большинстве случаев должно быть достаточно. Самые распространённые — StreamHandler и FileHandler.

console_handler = logging.StreamHandler()
file_handler = logging.FileHandler("filename")

Лицензии

Лицензии для открытого исходного кода, как правило, делятся на две категории:

  • Разрешительные. Концентрируются на том, чтобы дать пользователю свободу относительно того, что он хочет сделать со своим ПО. Наиболее известные: Apache 2.0, BSD 3-clause, MIT, ISC.
  • Свободные. Концентрируются на том, чтобы гарантировать, что исходный код, включая изменения, которые в него вносятся, будет доступен. Среди таких лицензий наиболее известно семейство GPL. Текущая версия лицензии этого семейства – GPLv3.
Семейство лицензий Ограничения Разрешения Требования
BSD Защитить торговую марку издателя (BSD 3-clause). Дает гарантию (BSD 2-clause и BSD 3-clause).
MIT (X11 или Expat), ISC Защитить торговую марку издателя (ISC и MIT/X11). Разрешает сублицензирование под другой лицензией.
Apache 2.0 Защитить торговую марку издателя. Разрешает сублицензирование, использование в патентах. Необходимо указывать изменения, вносимые в исходный код.
GPL Запрещает сублицензирование под другой лицензией. Дает гарантию и можно (только в GPLv3) использовать в патентах. Необходимо указывать изменения, вносимые в исходный код, и включать исходный код.

В США, если для вашего исходного кода не указана лицензия, пользователи не получат законного права загружать, модифицировать или распространять его. Помимо этого, они не смогут вносить свой вклад в проект, если вы не укажете, по каким правилам играть.

Вопросы и задания

  1. :thinking: Чем различаются стандарты PEP 8, PEP 20 и Google Python Style Guide, и для чего нужны линтеры?
  2. :man_technologist: Напишите функцию подсчета простых чисел в задаваемом диапазоне с учетом стандартов оформления кода и документацию к ней.
  3. :thinking: Что такое модуль, для чего нужен и какими способами можно импортировать все имена из модуля?
  4. :thinking: Для чего нужен каталог высшего уровня и как его задать?
  5. :man_technologist:
  6. :thinking:
  7. :thinking:
  8. :thinking: Что такое журналирование и для чего оно нужно?
  9. :man_technologist: Напишите программу с добавлением и удалением элементов из стека с автоматическим журналированием каждого действия с объектом стека, обработкой исключений и самой программы (запуск и отключение).
  10. :thinking: Какие категории лицензий открытого исходного кода существуют и чем различаются?

Полезные ссылки