Оригинал
1. Точки с запятой
Не заканчивайте строки точкой с запятой и не используйте точку с запятой для размещения двух операторов в одной строке.
2. Длина строки
Максимальная длина строки составляет 80 символов.
Явные исключения из ограничения в 80 символов:
- Длинные операторы импорта.
- URL-адреса, имена путей или длинные флаги в комментариях.
- Длинные строковые константы на уровне модуля, не содержащие пробелов, которые было бы неудобно разбивать на несколько строк, такие как URL-адреса или имена путей.
- Комментарии Pylint disable. (например:
# pylint: disable=invalid-name)
Не используйте обратный слеш (\) для явного продолжения строки.
# Так мы НЕ ДЕЛАЕМ
if 1900 < year < 2100 and 1 <= month <= 12 \
and 1 <= day <= 31 and 0 <= hour < 24 \
and 0 <= minute < 60 and 0 <= second < 60: # Проверяет валидность даты
return 1
if width == 0 and height == 0 and \
color == 'red' and emphasis == 'strong':
bridge_questions.clarification_on \
.average_airspeed_of.unladen_swallow = 'African or European?'
with very_long_first_expression_function() as spam, \
very_long_second_expression_function() as beans, \
third_thing() as eggs:
place_order(eggs, beans, spam, beans)Вместо этого используйте неявные соединения строк внутри скобок, квадратных скобок и фигурных скобок в Python. При необходимости можно добавить дополнительную пару скобок вокруг выражения.
# А вот так можно
month_names = ['Januari', 'Februari', 'Maart', # Это
'April', 'Mei', 'Juni', # голандские
'Juli', 'Augustus', 'September', # месяца
'Oktober', 'November', 'December']
if (
1900 < year < 2100
and 1 <= month <= 12
and 1 <= day <= 31
and 0 <= hour < 24
and 0 <= minute < 60
and 0 <= second < 60
): # Проверяет валидность даты
return 1
foo_bar(self, width, height, color='black', design=None, x='foo',
emphasis=None, highlight=0)
if (width == 0 and height == 0 and
color == 'red' and emphasis == 'strong'):
(bridge_questions.clarification_on
.average_airspeed_of.unladen_swallow) = 'African or European?'
with (
very_long_first_expression_function() as spam,
very_long_second_expression_function() as beans,
third_thing() as eggs,
):
place_order(eggs, beans, spam, beans)
Обратите внимание, что это правило не запрещает использование обратных косых черт для обозначения новых строк внутри строк (см. ниже).
Если буквальная строка не помещается в одной строке, используйте скобки для неявного соединения строк.
x = ('This will build a very long long '
'long long long long long long string')Предпочтительно разбивать строки на максимально возможном синтаксическом уровне. Если необходимо разбить строку дважды, разбивайте ее оба раза на одном и том же синтаксическом уровне.
В комментариях при необходимости размещайте длинные URL-адреса в отдельной строке.
# Так мы делаем
# See details at
#http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html# А так не делаем
# See details at
# http://www.example.com/us/developer/documentation/api/content/\
# v2.0/csv_file_name_extension_full_specification.htmlОбратите внимание на отступы элементов в примерах продолжения строк выше; объяснение см. в разделе отступы.
Строки резюме Docsting должны оставаться в пределах 80 символов.
Во всех других случаях, когда строка превышает 80 символов, а автоформатировщик Black или Pyink не помогает уложиться в ограничение, строка может превышать этот максимум. Авторам рекомендуется вручную разбивать строку в соответствии с приведенными выше примечаниями, когда это целесообразно.
3. Скобки
Используйте скобки с умеренностью. Можно, хотя и не обязательно, использовать скобки вокруг кортежей. Не используйте их в операторах возврата или условных операторах, за исключением случаев, когда скобки используются для подразумеваемого продолжения строки или для обозначения кортежа.
# Так мы делаем
if foo:
bar()
while x:
x = bar()
if x and y:
bar()
if not x:
bar()
# Для кортежа из 1 элемента () более наглядны, чем запятая.
onesie = (foo,)
return foo
return spam, beans
return (spam, beans)
for (x, y) in dict.items(): ...# А так не делаем:
if (x):
bar()
if not(x):
bar()
return (foo)4. Отступы
Отступайте блоки кода на 4 пробела.
Никогда не используйте табуляцию. Подразумеваемое продолжение строки должно выравнивать перенесенные элементы по вертикали (см. примеры длины строки), или используйте отступ в 4 пробела. Закрывающие (круглые, квадратные или фигурные) скобки могут быть размещены в конце выражения или на отдельных строках, но в этом случае они должны быть отступлены так же, как и строка с соответствующей открывающей скобкой.
# Так мы делаем
# Выровнено по открывающему разделителю.
foo = long_function_name(var_one, var_two,
var_three, var_four)
meal = (spam,
beans)
# Выровнено по начальному разделителю в словаре.
foo = {
'long_dictionary_key': value1 +
value2,
...
}
# 4-пробельный отступ; ничего в первой строке.
foo = long_function_name(
var_one, var_two, var_three,
var_four)
meal = (
spam,
beans)
# 4-пробельный отступ; ничего в первой строке,
# закрывающая скобка в новой строке.
foo = long_function_name(
var_one, var_two, var_three,
var_four
)
meal = (
spam,
beans,
)
# 4-пробельный отступ в словаре.
foo = {
'long_dictionary_key':
long_dictionary_value,
...
}# А так мы не делаем!
# Запрещено размещать что-либо в первой строке.
foo = long_function_name(var_one, var_two,
var_three, var_four)
meal = (spam,
beans)
# Запрещены отступы с двумя пробелами.
foo = long_function_name(
var_one, var_two, var_three,
var_four)
# В словаре нет отступов.
foo = {
'long_dictionary_key':
long_dictionary_value,
...
}4.1. Запятые в конце последовательности элементов?
Конечные запятые в последовательностях элементов рекомендуются только в том случае, если закрывающий контейнерный токен ], ) или } не появляется в той же строке, что и последний элемент, а также для кортежей с одним элементом. Наличие конечной запятой также используется в качестве подсказки для нашего автоформатера кода Python Black или Pyink, чтобы он автоматически форматировал контейнер элементов по одному элементу в строке, когда после последнего элемента присутствует ,.
# Так мы делаем
golomb3 = [0, 1, 3]
golomb4 = [
0,
1,
4,
6,
]# Так мы не делаем
golomb4 = [
0,
1,
4,
6,]5. Пустые строки.
Две пустые строки между определениями верхнего уровня, будь то определения функций или классов. Одна пустая строка между определениями методов и между docstring класса class и первым методом. Никаких пустых строк после строки def. Используйте одиночные пустые строки по своему усмотрению внутри функций или методов.
Пустые строки не обязательно должны быть привязаны к определению. Например, связанные комментарии, непосредственно предшествующие определениям функций, классов и методов, могут иметь смысл. Подумайте, не будет ли ваш комментарий более полезным в составе docstring.
6. Пробелы
Следуйте стандартным типографским правилам использования пробелов вокруг знаков препинания. Не используйте пробелы внутри скобок, квадратных скобок или фигурных скобок.
# Так делаем
spam(ham[1], {'eggs': 2}, [])# Так не делаем
spam( ham[ 1 ], { 'eggs': 2 }, [ ] )Не ставьте пробелы перед запятой, точкой с запятой или двоеточием. Ставьте пробелы после запятой, точки с запятой или двоеточия, за исключением случаев, когда они стоят в конце строки.
# Так делаем
if x == 4:
print(x, y)
x, y = y, x# Так не делаем
if x == 4 :
print(x , y)
x , y = y , xПробелы не ставятся перед открывающей скобкой/квадратной скобкой, которая начинает список аргументов, индексацию или нарезку.
# Так делаем
spam(1)
dict['key'] = list[index]# Так не делаем
spam (1)
dict ['key'] = list [index]Пробелы не ставятся в конце строки.
Окружите бинарные операторы одним пробелом с каждой стороны для присваивания (=), сравнений (==, <, >, !=, <>, <=, >=, in, not in, is, is not) и булевых операторов (and, or, not). Используйте свое лучшее суждение для вставки пробелов вокруг арифметических операторов (+, -, *, /, //, %, **, @).
# Так делаем
x == 1# Так не делаем
x<1Не используйте пробелы вокруг = при передаче аргументов по ключевым словам или при определении значения параметра по умолчанию, за одним исключением: если имеются аннотации типа, используйте пробелы вокруг = для значения параметра.
# Так делаем
def complex(real, imag=0.0): return Magic(r=real, i=imag)
def complex(real, imag: float = 0.0): return Magic(r=real, i=imag)# Так не делаем
def complex(real, imag = 0.0): return Magic(r = real, i = imag)
def complex(real, imag: float=0.0): return Magic(r = real, i = imag)Не используйте пробелы для вертикального выравнивания токенов в последовательных строках, так как это усложняет восприятие (относится к :, #, = и т. д.):
# Так делаем
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
'foo': 1,
'long_name': 2,
}# Так не делаем
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
'foo' : 1,
'long_name': 2,
}7. Первая строка
Большинство файлов .py не должны начинаться со строки #!. Начните главный файл программы со строки #!/usr/bin/env python3 (для поддержки virtualenvs) или #!/usr/bin/python3 в соответствии с стандартом PEP-394.
Эта строка используется ядром для поиска интерпретатора Python, но игнорируется Python при импорте модулей. Она необходима только в файлах, предназначенных для непосредственного выполнения.
8. Коментарии и документация
Обязательно правильно форматируйте стиль для модулей, функций, методов docstrings и встроенных комментариев.
8.1. Документация
Python использует docstrings для документирования кода. Docstring — это строка, которая является первым оператором в пакете, модуле, классе или функции. Эти строки могут быть извлечены автоматически через член объекта __doc__ и используются pydoc. (Попробуйте запустить pydoc на вашем модуле, чтобы увидеть, как это выглядит.) Всегда используйте формат с тремя двойными кавычками """ для docstrings (согласно PEP 257). Документационная строка должна быть организована в виде одной строки-резюме (одна физическая строка, не превышающая 80 символов), заканчивающейся точкой, вопросительным знаком или восклицательным знаком. При написании большего количества текста (что приветствуется) за этим должна следовать пустая строка, а затем остальная часть документационной строки, начинающаяся в той же позиции курсора, что и первая кавычка первой строки. Ниже приведены дополнительные рекомендации по форматированию документационных строк.
8.2. Документация модулей
Каждый файл должен содержать стандартную формулировку лицензии. Выберите подходящую формулировку для лицензии, используемой в проекте (например, Apache 2.0, BSD, LGPL, GPL).
Файлы должны начинаться с docstring, описывающего содержание и использование модуля.
"""Суть модуля или программы, 1 строка, заканчивающееся точкой..
Оставьте одну пустую строку. Остальная часть этой строки документации должна содержать
общее описание модуля или программы. По желанию, она также может
содержать краткое описание экспортируемых классов и функций и/или примеры использования.
Пример использования:
foo = ClassFoo()
bar = foo.function_bar()
"""8.2.1. Тест модули
Документация на уровне модуля для тестовых файлов не требуется. Их следует включать только в том случае, если есть дополнительная информация, которую можно предоставить. Примеры включают некоторые особенности того, как следует запускать тест, объяснение необычного шаблона настройки, зависимость от внешней среды и т. д.
"""в этом испытании на возгорание используются подобранные файлы.
Вы можете обновить эти файлы, запустив
`blaze run //foo/bar:foo_test -- --update_golden_files` from the `google3`
directory.
"""Не пишите docstring, которые не содержат новой информации.
"""Tests for foo.bar."""8.3. Функции и методы
В этом разделе «функция» означает метод, функцию, генератор или свойство.
Docstring обязательна для каждой функции, которая имеет одно или несколько из следующих свойств:
- является частью публичного API
- имеет нетривиальный размер
- имеет неочевидную логику
Docstring должна содержать достаточно информации, чтобы можно было написать вызов функции, не читая ее код. Docstring должна описывать синтаксис вызова функции и ее семантику, но, как правило, не детали ее реализации, за исключением случаев, когда эти детали имеют отношение к тому, как функция будет использоваться. Например, функция, которая изменяет один из своих аргументов в качестве побочного эффекта, должна указывать это в своей docstring. В противном случае, тонкие, но важные детали реализации функции, которые не имеют отношения к вызывающему, лучше выражать в виде комментариев рядом с кодом, чем в docstring функции.
Типы docstring
Docstring может быть описательного типа:
("""Извлекает строки из Bigtable.""")
Или императивного типа:
("""Извлечь строки из Bigtable."""),
Но стиль должен быть единообразным в пределах одного файла.
Docstring для дескриптора данных @property должна использовать тот же стиль, что и docstring для атрибута или [[#типы-docstring|аргемунта функции]] ("""Путь Bigtable.""", а не """Возвращает путь Bigtable.""").
Некоторые аспекты функции должны быть задокументированы в специальных разделах, перечисленных ниже. Каждый раздел начинается с заголовка, который заканчивается двоеточием. Все разделы, кроме заголовка, должны иметь отступ в два или четыре пробела (сохраняйте единообразие в пределах файла). Эти разделы могут быть опущены в тех случаях, когда название и сигнатура функции достаточно информативны, чтобы их можно было точно описать с помощью однострочной строки документации.
Args (Аргументы):
Перечислите каждый параметр по имени. Описание должно следовать за именем и отделяться двоеточием, за которым следует пробел или новая строка. Если описание слишком длинное, чтобы поместиться в одной строке длиной 80 символов, используйте отступ на 2 или 4 пробела больше, чем имя параметра (сохраняйте согласованность с остальными строками документации в файле). Описание должно включать требуемые типы, если код не содержит соответствующих аннотаций типов. Если функция принимает *foo (списки аргументов переменной длины) и/или **bar (произвольные ключевые аргументы), они должны быть указаны как *foo и **bar.
Returns/Yields (Возвращаемое)
Опишите семантику возвращаемого значения, включая любую информацию о типе, которая не указана в аннотации типа.
Если функция возвращает только None, этот раздел не требуется.
Его также можно опустить, если docstring начинается со слов «Return», «Returns», «Yield» или «Yields» (например, """Returns row from Bigtable as a tuple of strings.""") и первое предложение достаточно для описания возвращаемого значения.
Не следует копировать старый «стиль NumPy» (пример), в котором возвращаемое значение кортежа часто документировалось так, как если бы это было несколько возвращаемых значений с отдельными именами (ни разу не упоминая кортеж).
Вместо этого опишите такое возвращаемое значение следующим образом:
“Возвращает: кортеж (mat_a, mat_b), где mat_a — это …, а …“.
Вспомогательные имена в docstring не обязательно должны соответствовать каким-либо внутренним именам, используемым в теле функции (поскольку они не являются частью API). Если функция использует yield (является генератором), раздел Yields: должен документировать объект, возвращаемый next(), а не сам объект генератора, который вычисляется при вызове.
Raises (Исключения)
Перечислите все исключения, имеющие отношение к интерфейсу, с последующим описанием. Используйте аналогичное имя исключения + двоеточие + пробел или новую строку и стиль отступа, как описано в Args:. Не следует документировать исключения, которые возникают в случае нарушения API, указанного в docstring (поскольку это парадоксальным образом сделает поведение при нарушении API частью API).
def fetch_smalltable_rows(
table_handle: smalltable.Table,
keys: Sequence[bytes | str],
require_all_keys: bool = False,
) -> Mapping[bytes, tuple[str, ...]]:
"""Извлекает строки из таблицы Smalltable.
Извлекает строки, относящиеся к заданным ключам, из экземпляра Table,
представленного table_handle. Строковые ключи будут закодированы в UTF-8.
Args:
table_handle: экземпляр smalltable.Table.
keys: Последовательность строк, представляющих ключ
каждой строки таблицы, которую необходимо извлечь.
Строковые ключи будут кодированы в UTF-8.
require_all_keys: Если True, будут возвращены только строки
со значениями, установленными для всех ключей.
Returns:
Словарь, сопоставляющий ключи с соответствующими данными
строк таблицы. Каждая строка представлена в виде кортежа строк.
Например:
{b'Serak': ('Rigel VII', 'Preparer'),
b'Zim': ('Irk', 'Invader'),
b'Lrrr': ('Omicron Persei 8', 'Emperor')}
Возвращаемые ключи всегда являются байтами. Если ключ из аргумента
keys из словаря, то эта строка не была найдена в таблице
(и require_all_keys должно быть False).
Raises:
IOError: Произошла ошибка при доступе к smalltable.
"""Аналогично, допускается также вариант Args: с разрывом строки:
def fetch_smalltable_rows(
table_handle: smalltable.Table,
keys: Sequence[bytes | str],
require_all_keys: bool = False,
) -> Mapping[bytes, tuple[str, ...]]:
"""Извлекает строки из таблицы Smalltable.
Извлекает строки, относящиеся к заданным ключам, из экземпляра Table,
представленного table_handle. Строковые ключи будут закодированы в UTF-8.
Args:
table_handle:
Открытый экземпляр smalltable.Table.
keys:
Последовательность строк, представляющих ключ каждой
строки таблицы, которую необходимо извлечь.
Строковые ключи будут кодированы в UTF-8.
require_all_keys:
Если True, будут возвращены только строки
со значениями, установленными для всех ключей.
Returns:
Словарь, сопоставляющий ключи с соответствующими данными
строк таблицы. Каждая строка представлена в виде кортежа строк.
Например:
{b'Serak': ('Rigel VII', 'Preparer'),
b'Zim': ('Irk', 'Invader'),
b'Lrrr': ('Omicron Persei 8', 'Emperor')}
Возвращаемые ключи всегда являются байтами. Если ключ из аргумента
keys из словаря, то эта строка не была найдена в таблице
(и require_all_keys должно быть False).
Raises:
IOError: Произошла ошибка при доступе к smalltable.8.3.1. Переопределение методов
Метод, который переопределяет метод из базового класса, не нуждается в docstring, если он явно украшен @override (из модулей typing_extensions или typing), за исключением случаев, когда поведение переопределяемого метода существенно уточняет контракт базового метода или необходимо предоставить дополнительные сведения (например, документировать дополнительные побочные эффекты). В этом случае для переопределяемого метода требуется docstring, содержащий как минимум эти различия.
from typing_extensions import override
class Parent:
def do_something(self):
"""Parent method, includes docstring."""
# Дочерний класс, метод с аннотацией override.
class Child(Parent):
@override
def do_something(self):
pass# Дочерний класс, но без декоратора @override, требуется docstring.
class Child(Parent):
def do_something(self):
pass
# Docstring является тривиальным, @override достаточно,
# чтобы указать, что документация может быть
# найдена в родительском классе.
class Child(Parent):
@override
def do_something(self):
"""See base class."""8.4. Классы
Классы должны иметь строку документации под определением класса, описывающую класс. Публичные атрибуты, за исключением свойств, должны быть задокументированы здесь в разделе Attributes и иметь такое же форматирование, как и раздел [[#^args|Args функции]]
class SampleClass:
"""Краткое содержание класса здесь.
Более подробная информация о классе...
Более подробная информация о классе...
Attributes:
likes_spam: Булево значение, указывающее, нравится ли нам СПАМ или нет.
eggs: Целочисленное количество яиц.
"""
def __init__(self, likes_spam: bool = False):
"""Инициализирует экземпляр класса на основе настроек спама.
Args:
likes_spam: Булево значение, указывающее, нравится ли нам СПАМ или нет.
"""
self.likes_spam = likes_spam
self.eggs = 0
@property
def butter_sticks(self) -> int:
"""Количество упаковок сливочного масла, которые у нас есть.."""Все описания классов должны начинаться с однострочного резюме, описывающего, что представляет собой экземпляр класса. Это означает, что подклассы Exception также должны описывать, что представляет собой исключение, а не контекст, в котором оно может возникнуть. Строковое описание класса не должно повторять ненужную информацию, например, что класс является классом.
# Так мы делаем:
class CheeseShopAddress:
"""Адрес магазина сыра.
...
"""
class OutOfCheeseError(Exception):
"""Сыр закончился."""# Так мы не делаем:
class CheeseShopAddress:
"""Класс описывающий адрес магазина с сыром
...
"""
class OutOfCheeseError(Exception):
"""Ошибка вызываемая при отсуствии сыра."""8.5. Многострочные и однострочные комментарии
И последнее место, где стоит оставлять комментарии — это запутанные участки кода. Если вы понимаете, что на следующем code review этот фрагмент придется объяснять «на пальцах», лучше прокомментируйте его прямо сейчас. Сложные операции стоит предварять парой строк описания перед их началом. Если же логика просто неочевидна, достаточно короткого комментария в конце строки.
# Мы используем взвешенный поиск по словарю, чтобы найти, где находится i
# в массиве. Мы экстраполируем положение на основе наибольшего числа
# в массиве и размера массива, а затем выполняем двоичный поиск,
# чтобы получить точное число.
if i & (i-1) == 0: # True, если i равно 0 или является степенью двойки.Для лучшей читаемости такие комментарии следует отделять от кода как минимум двумя пробелами. Сам комментарий должен начинаться с символа #, после которого идет хотя бы один пробел перед текстом.
С другой стороны, никогда не описывайте сам код. Исходите из предположения, что человек, читающий ваш код, знает Python лучше вас (пусть он и не знает заранее, что именно вы пытаетесь реализовать).
# ПЛОХОЙ КОММЕНТАРИЙ: Теперь проходим по массиву b и убеждаемся,
# что каждый раз, когда встречается i, следующим элементом идет i+18.6. Пунктуация, орфография и грамматика
Уделяйте внимание пунктуации, орфографии и грамматике: грамотно написанные комментарии читать гораздо легче, чем безграмотные. Комментарии должны быть такими же читабельными, как и обычный повествовательный текст — с правильным использованием заглавных букв и знаков препинания. В большинстве случаев полные предложения воспринимаются лучше, чем обрывочные фразы. Короткие примечания (например, в конце строки кода) могут быть менее формальными, но вам следует придерживаться единого стиля во всем проекте. Хотя замечание от ревьюера о том, что вы использовали запятую вместо точки с запятой, может раздражать, крайне важно, чтобы исходный код оставался максимально ясным и понятным. Правильная пунктуация, грамотность и орфография напрямую способствуют достижению этой цели.
9. Строки.
Используйте f-строки, оператор % или метод format для форматирования строк, даже если все параметры сами являются строками. Выбирайте подходящий вариант форматирования на своё усмотрение. Одиночное объединение через + допустимо, но не используйте + именно для форматирования.
# Так мы делаем:
x = f'имя: {name}; балл: {n}'
x = '%s, %s!' % (imperative, expletive)
x = '{}, {}'.format(first, second)
x = 'имя: %s; балл: %d' % (name, n)
x = 'имя: %(name)s; балл: %(score)d' % {'name': name, 'score': n}
x = 'имя: {}; балл: {}'.format(name, n)
x = a + b# Так мы не делаем:
x = first + ', ' + second
x = 'имя: ' + name + '; балл: ' + str(n)Избегайте использования операторов + и += для накопления строки внутри цикла. При определенных условиях накопление строки через сложение может привести к квадратичному, а не линейному времени выполнения. Хотя в CPython такие операции часто оптимизированы, это лишь особенность реализации. Условия, при которых применяется оптимизация, трудно предсказать, и они могут измениться. Вместо этого добавляйте каждую подстроку в список и применяйте ''.join к списку после завершения цикла либо записывайте каждую подстроку в буфер io.StringIO. Эти методы гарантируют амортизированную линейную сложность времени выполнения.
# Так мы делаем:
items = ['<table>']
for last_name, first_name in employee_list:
items.append('<tr><td>%s, %s</td></tr>' % (last_name, first_name))
items.append('</table>')
employee_table = ''.join(items)# Так мы не делаем:
employee_table = '<table>'
for last_name, first_name in employee_list:
employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
employee_table += '</table>'Придерживайтесь единообразия при выборе кавычек для строк внутри одного файла. Выберите либо ', либо " и используйте их постоянно. Допускается использовать другой тип кавычек, чтобы избежать необходимости экранировать кавычки внутри самой строки с помощью обратного слэша.
# Так мы делаем:
Python('Почему ты прячешь глаза?')
Gollum("Я боюсь ошибок линтера.")
Narrator('"Хорошо!" — подумал счастливый ревьюер Python-кода.')# Так мы не делаем:
Python("Почему ты прячешь глаза?")
Gollum('Этот линтер. Он жжёт. Он жжёт нас.')
Gollum("Всегда этот великий линтер. Следит. Следит.")Для многострочных строк отдавайте предпочтение """, а не '''. В проектах можно использовать ''' для всех многострочных строк (кроме docstrings) только в том случае, если для обычных строк используются одинарные кавычки '. Для строк документации (docstrings) в любом случае должны использоваться """.
Многострочные строки не подстраиваются под отступы остальной программы. Если вам нужно избежать лишних пробелов в строке, используйте либо конкатенацию однострочных строк, либо многострочную строку с использованием textwrap.dedent(), чтобы удалить начальные пробелы в каждой строке:
# Так мы не делаем:
long_string = """Это выглядит довольно некрасиво.
Не делайте так.
"""# Так мы делаем:
long_string = """Это допустимо, если в вашем случае
лишние начальные пробелы не критичны."""
long_string = ("А это подходит, если лишние начальные\n" +
"пробелы недопустимы.")
long_string = ("И это тоже подходит, если нельзя допускать\n"
"лишних начальных пробелов.")
import textwrap
long_string = textwrap.dedent("""\
Это тоже отличный вариант, так как textwrap.dedent()
уберет общие начальные пробелы в каждой строке.""")Обратите внимание, что использование обратного слэша здесь не нарушает запрет на явный перенос строк; в данном случае обратный слэш экранирует символ новой строки внутри строкового литерала.
9.1. Логирование
Для функций логирования, которые ожидают строку-шаблон (с подстановочными знаками %) в качестве первого аргумента: всегда вызывайте их, передавая строковый литерал (не f-строку!) первым аргументом, а параметры шаблона — последующими отдельными аргументами. Некоторые реализации логирования сохраняют неразвернутую строку-шаблон как отдельное поле, по которому можно делать запросы. Кроме того, это избавляет от затрат времени на рендеринг сообщения, если уровень логирования настроен так, что сообщения не выводятся.
# Так мы делаем:
import tensorflow as tf
logger = tf.get_logger()
# Передаем шаблон и аргумент отдельно
logger.info('Версия TensorFlow: %s', tf.__version__)
import os
from absl import logging
logging.info('Текущее значение $PAGER: %s', os.getenv('PAGER', default=''))
homedir = os.getenv('HOME')
if homedir is None or not os.access(homedir, os.W_OK):
# Используем %r для получения представления объекта (repr)
logging.error('Невозможно произвести запись в домашнюю директорию, $HOME=%r', homedir)# Так мы не делаем:
import os
from absl import logging
# Ошибка: логика разбита на два вызова вместо использования шаблона
logging.info('Текущее значение $PAGER:')
logging.info(os.getenv('PAGER', default=''))
homedir = os.getenv('HOME')
if homedir is None or not os.access(homedir, os.W_OK):
# Ошибка: использование f-строки заставляет Python форматировать текст
# даже если уровень логов ERROR отключен
logging.error(f'Невозможно произвести запись в домашнюю директорию, $HOME={homedir!r}')9.2. Сообщения о ошибках
Сообщения об ошибках (такие как строки описания в исключениях вроде ValueError или сообщения, выводимые пользователю) должны соответствовать трем правилам:
- Сообщение должно точно соответствовать фактическому условию ошибки.
- Вставленные значения (интерполяция) должны быть четко идентифицируемы как таковые.
- Сообщения должны позволять легко применять автоматизированную обработку (например, поиск через
grep).
# Так мы делаем:
if not 0 <= p <= 1:
# Использование f-строки с {p=} выводит и имя переменной, и её значение
raise ValueError(f'Это не вероятность: {p=}')
try:
os.rmdir(workdir)
except OSError as error:
# Мы логируем и причину (объект ошибки), и путь.
# %r добавляет кавычки, что помогает выделить путь.
logging.warning('Не удалось удалить директорию (причина: %r): %r',
error, workdir)# Так мы не делаем:
if p < 0 or p > 1: # ПРОБЛЕМА: условие ложно для float('nan')!
raise ValueError(f'Это не вероятность: {p=}')
try:
os.rmdir(workdir)
except OSError:
# ПРОБЛЕМА: Сообщение содержит предположение, которое может быть ложным.
# Удаление могло не сработать по другой причине, что введет в
# заблуждение того, кто будет это отлаживать.
logging.warning('Директория уже была удалена: %s', workdir)
try:
os.rmdir(workdir)
except OSError:
# ПРОБЛЕМА: Такое сообщение сложнее искать через grep, чем следовало бы,
# и оно может звучать запутанно при определенных значениях `workdir`.
# Представьте, что кто-то вызвал функцию с workdir = 'deleted'.
# Предупреждение будет звучать как: "The deleted directory could not be deleted."
logging.warning('Директория %s не может быть удалена.', workdir)10. Файлы, Сокеты и другие ресурсы с состоянием
Всегда явно закрывайте файлы и сокеты после завершения работы с ними. Это правило естественным образом распространяется на любые закрываемые (closeable) ресурсы, которые внутри используют сокеты (например, соединения с базами данных), а также на другие ресурсы, требующие аналогичного завершения работы. Примеры включают в себя mmap-отображения, объекты File в h5py и окна фигур matplotlib.pyplot.
Необоснованное оставление файлов, сокетов или других подобных объектов открытыми влечет за собой множество минусов:
- Потребление системных ресурсов: количество файловых дескрипторов ограничено. Код, работающий с множеством таких объектов, может преждевременно исчерпать этот лимит, если не будет возвращать ресурсы системе сразу после использования.
- Блокировка действий: открытый файл может препятствовать его перемещению, удалению или размонтированию файловой системы.
- Риск случайного использования: файлы и сокеты, доступные в разных частях программы, могут быть непреднамеренно использованы для чтения или записи уже после того, как работа с ними логически завершена. Если же ресурс закрыт физически, любая попытка обращения к нему вызовет исключение, что позволит обнаружить проблему гораздо раньше.
Более того, хотя файлы и сокеты автоматически закрываются при уничтожении объекта, связывать жизненный цикл объекта с состоянием ресурса — плохая практика:
- Нет никаких гарантий того, когда именно среда выполнения вызовет метод
__del__. Разные реализации Python используют разные техники управления памятью (например, отложенную сборку мусора), что может произвольно и на неопределенный срок продлить жизнь объекта. - Непредвиденные ссылки на файл (например, в глобальных переменных или трассировках исключений) могут удерживать его в памяти дольше, чем планировалось.
За десятилетия разработки на множестве языков программисты снова и снова убеждались, что использование финализаторов для автоматической очистки ресурсов с явными побочными эффектами приводит к серьезным проблемам (см., например, эту статью для Java).
Рекомендации по использованию
Предпочтительный способ управления файлами и аналогичными ресурсами — использование оператора [with](http://docs.python.org/reference/compound_stmts.html#the-with-statement\):
# Использование контекстного менеджера гарантирует закрытие файла
with open("hello.txt") as hello_file:
for line in hello_file:
print(line)Для объектов, похожих на файлы, но не поддерживающих протокол контекстного менеджера (оператор with), используйте contextlib.closing():
import contextlib
# closing() принудительно вызовет метод .close() по завершении блока
with contextlib.closing(urllib.urlopen("http://www.python.org/")) as front_page:
for line in front_page:
print(line)В редких случаях, когда управление ресурсами на основе контекста невозможно, документация к коду должна четко объяснять, как именно управляется жизненный цикл ресурса.
11. TODO комментарии
Используйте комментарии TODO для временного кода, краткосрочных решений или кода, который приемлем, но не идеален.
Комментарий TODO должен начинаться со слова TODO заглавными буквами, за которым следует двоеточие и ссылка на ресурс, содержащий контекст (в идеале — ссылка на баг-трекер). Ссылка на баг предпочтительнее, так как баги отслеживаются и содержат историю обсуждения. После контекста через дефис укажите пояснительную строку. Цель состоит в том, чтобы иметь единообразный формат TODO, по которому можно легко найти подробности.
# TODO: crbug.com/192795 - Изучить возможность оптимизации частоты процессора (cpufreq).Старый стиль (ранее рекомендованный, но нежелательный для нового кода):
# TODO(crbug.com/192795): Изучить возможность оптимизации cpufreq.
# TODO(yourusername): Использовать здесь "\*" для оператора конкатенации.Избегайте добавления TODO, в которых в качестве контекста указывается конкретный человек или команда:
# TODO: @yourusername - Создать задачу и использовать '*' для повторения.Если ваш TODO сформулирован в духе «сделать что-то в будущем», убедитесь, что вы указали либо конкретную дату («Исправить к ноябрю 2009»), либо очень специфическое событие («Удалить этот код, когда все клиенты смогут обрабатывать XML-ответы»), которое будет понятно будущим сопровождающим кода. Задачи в баг-трекере идеально подходят для отслеживания таких моментов.
12. Форматирование импортов
Импорты должны располагаться на отдельных строках; существуют [[#1812-импорты-для-типизации|исключения для модулей typing и collections.abc]].
Примеры:
# Так мы делаем:
from collections.abc import Mapping, Sequence
import os
import sys
from typing import Any, NewType# Так мы не делаем:
import os, sysИмпорты всегда размещаются в начале файла, сразу после комментариев к модулю и строк документации (docstrings), но перед глобальными переменными и константами модуля. Импорты следует группировать в порядке от наиболее общих к наиболее специфичным:
-
Инструкции Python
__future__.from __future__ import annotations -
Импорты стандартной библиотеки Python.
import sys -
Импорты сторонних модулей или пакетов.
import tensorflow as tf -
Импорты подпакетов из репозитория кода.
from otherproject.ai import mind -
Устаревшее: специфичные для приложения импорты, которые являются частью того же подпакета верхнего уровня, что и текущий файл. Например:
from myproject.backend.hgwells import time_machine
Вы можете встретить такое в старом коде, написанном по стилю Google, но это больше не требуется. В новом коде рекомендуется не выделять их в отдельную группу. Просто относите импорты подпакетов приложения к той же группе, что и остальные подпакеты проекта (пункт 4).
Внутри каждой группы импорты должны быть отсортированы лексикографически (по алфавиту), без учета регистра, в соответствии с полным путем к модулю (часть path в конструкции from path import ...). Между секциями импортов можно (опционально) оставлять пустую строку.
import collections
import queue
import sys
from absl import app
from absl import flags
import bs4
import cryptography
import tensorflow as tf
from book.genres import scifi
from myproject.backend import huxley
from myproject.backend.hgwells import time_machine
from myproject.backend.state_machine import main_loop
from otherproject.ai import body
from otherproject.ai import mind
from otherproject.ai import soul
# В коде старого стиля эти импорты могли находиться ниже:
#from myproject.backend.hgwells import time_machine
#from myproject.backend.state_machine import main_loop13. Инструкции
Как правило, на одной строке должна располагаться только одна инструкция.
Тем не менее, допускается размещать результат проверки на той же строке, что и сама проверка, но только в том случае, если вся инструкция целиком умещается на одной строке. В частности, это категорически запрещено для блоков try/except, поскольку они физически не могут уместиться на одной строке. Для оператора if такое допустимо только при условии, что у него нет блока else.
Примеры для наглядности:
# Так мы делаем:
if foo(): bar()
# стандартный способ:
if foo():
bar()# Так мы не делаем:
if foo(): bar()
else: baz() # ОШИБКА: 'else' требует новой строки
try:
bar()
except Exception: baz() # ОШИБКА: 'try/except' нельзя писать в одну строку14. Getter’s & Setter’s
Getter и setter (также называемые аксессорами и мутаторами) следует использовать только тогда, когда они выполняют значимую роль или обеспечивают определенное поведение при получении или установке значения переменной.
В частности, их стоит применять в тех случаях, когда процесс получения или установки переменной является сложным или ресурсозатратным — сейчас или в обозримом будущем.
Если, к примеру, пара getter/setter просто считывает и записывает внутренний атрибут, то такой атрибут следует сделать публичным. Если же установка переменной влечет за собой сброс какого-либо состояния или пересчет данных, это должна быть функция-сеттер. Сам вызов функции служит подсказкой, что происходит потенциально нетривиальная операция. В качестве альтернативы можно использовать свойства (properties), если требуется простая логика, или провести рефакторинг, чтобы полностью избавиться от необходимости в геттерах и сеттерах.
Геттеры и сеттеры должны соответствовать правилам именования, например: get_foo() и set_foo().
Если ранее доступ к данным осуществлялся через свойство (property), не привязывайте новые функции геттера/сеттера к этому свойству. Любой код, пытающийся обратиться к переменной старым способом, должен вызывать явную ошибку (ломаться), чтобы разработчики узнали об изменении сложности операции.
15. Наименования (нейминг)
module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_CONSTANT_NAME, global_var_name, instance_var_name, function_parameter_name, local_var_name, query_proper_noun_for_thing, send_acronym_via_https.
Имена должны быть описательными. Это относится к функциям, классам, переменным, атрибутам, файлам и любым другим именованным сущностям.
Избегайте сокращений. В частности, не используйте аббревиатуры, которые могут быть двусмысленными или незнакомыми читателям, не участвующим в вашем проекте. Также не сокращайте слова путем удаления букв из их середины.
Всегда используйте расширение .py для имен файлов. Никогда не используйте тире (-) в названиях файлов.
15.1. Имена, которых следует избегать
- Однострочные имена, за исключением:
- Счетчики и итераторы (например:
i,j,k,v, и т.д.) eв качеств идентификатора исключения в блокахtry/exceptfв качестве дескриптора файла в блокеwith- Приватные переменные типов без ограничений (например:
_T = TypeVar("_T"),_P = ParamSpec("_P")) - Имена, которые соответствуют общепринятым обозначениям в научной статье или алгоритме (см. раздел Математические обозначения)
Пожалуйста, не злоупотребляйте односимвольными именами. В общем смысле, описательность имени должна быть пропорциональна области его видимости. Например,
iможет быть отличным именем для блока кода в 5 строк, но в рамках нескольких вложенных областей видимости оно, скорее всего, будет слишком неопределенным.
- Счетчики и итераторы (например:
- Тире (
-) в именах пакетов или модулей - Имена с двойным подчеркиванием в начале и в конце (например,
__name__) - они зарезервированы языком Python - Имена, которые избыточно включают в себя тип переменной (например:
id_to_name_dictвместоid_to_name)
15.2. Правила именования
- internal - внутренний для модуля, либо защищенный(protected) или приватный(private) внутри класса
- Одиночное подчеркивание (
_) в начале имени частично поддерживается для защиты переменных и функций модуля (линтеры будут помечать обращения к таким «защищенным» членам как ошибку). Обратите внимание: в юнит-тестах допустимо обращаться к защищенным константам тестируемых модулей. - Двойное подчеркивание (
__, оно же «дандер») в начале имени переменной экземпляра или метода делает их фактически приватными для класса (используя механизм искажения имен — name mangling). Мы не рекомендуем использовать этот подход, так как он ухудшает читаемость, усложняет тестирование и не обеспечивает настоящей приватности. Отдавайте предпочтение одиночному подчеркиванию. - Группируйте связанные классы и функции верхнего уровня в одном модуле. В отличие от Java, в Python нет необходимости ограничиваться одним классом на модуль.
- Используйте CapWords (слова с заглавной буквы, верблюжий стиль) для имен классов, но
lower_with_under.py(нижний регистр с подчеркиваниями) для имен модулей. Хотя существуют старые модули с названиями видаCapWords.py, сейчас это не приветствуется, так как вносит путаницу, если модуль назван так же, как и класс («погодите, я написалimport StringIOилиfrom StringIO import StringIO?»). - Файлы юнит-тестов должны следовать именованию методов в стиле
lower_with_underсогласно PEP 8, например:test_<тестируемый_метод>_<состояние>. Для сохранения преемственности со старыми модулями (где функции названы в стиле CapWords), в именах методов, начинающихся сtest, допускаются подчеркивания для разделения логических компонентов. Один из возможных шаблонов:test<ТестируемыйМетод>_<состояние>.
15.3. Правила именования файлов
Имена файлов Python должны иметь расширение .py и не должны содержать тире (-). Это необходимо для того, чтобы их можно было импортировать и тестировать с помощью юнит-тестов. Если вы хотите, чтобы исполняемый файл был доступен без расширения, используйте символическую ссылку (symlink) или простую bash-обертку, содержащую команду exec "$0.py" "$@".
15.4. Рекомендации, основанные на советах Гвидо
В данной таблице приведена сводная информация по стилям именования для различных типов сущностей в Python.
| Тип сущности | Публичные (Public) | Внутренние (Internal) |
|---|---|---|
| Пакеты | lower_with_under | |
| Модули | lower_with_under | _lower_with_under |
| Классы | CapWords | _CapWords |
| Исключения | CapWords | |
| Функции | lower_with_under() | _lower_with_under() |
| Глобальные константы / константы класса | CAPS_WITH_UNDER | _CAPS_WITH_UNDER |
| Глобальные переменные / переменные класса | lower_with_under | _lower_with_under |
| Переменные экземпляра (поля объекта) | lower_with_under | _lower_with_under (защищенные) |
| Имена методов | lower_with_under() | _lower_with_under() (защищенные) |
| Параметры функций / методов | lower_with_under | |
| Локальные переменные | lower_with_under |
15.5. Математические именования
Для кода с обилием математических вычислений допускается использование коротких имен переменных, которые в иных случаях нарушали бы руководство по стилю. Это приветствуется тогда, когда такие имена соответствуют общепринятым обозначениям из научной статьи или алгоритма.
При использовании имен, основанных на устоявшихся обозначениях, соблюдайте следующие правила:
- Указывайте источник всех используемых соглашений об именовании в комментарии или строке документации (docstring). Предпочтительно давать прямую гиперссылку на сам академический ресурс. Если источник недоступен в сети, подробно опишите принятые обозначения в документации.
- Отдавайте предпочтение описательным именам, соответствующим PEP 8 (
descriptive_names), для публичных API — их гораздо чаще используют вне контекста конкретного алгоритма. - Используйте точечную директиву
pylint: disable=invalid-name, чтобы скрыть предупреждения линтера. Если таких переменных немного, добавьте директиву в конце строки для каждой из них; если их много, примените директиву в начале блока кода.
16. Главная функция (main)
В Python такие инструменты, как pydoc и библиотеки для юнит-тестирования, требуют, чтобы модули были доступны для импорта. Если файл предназначен для использования в качестве исполняемого скрипта, его основная функциональность должна быть вынесена в функцию main().
При этом ваш код всегда должен проверять условие if __name__ == '__main__' перед запуском основной программы — это гарантирует, что она не будет выполнена при импорте модуля.
При использовании библиотеки absl используйте app.run:
from absl import app
...
def main(argv: Sequence[str]):
# обработка аргументов, не являющихся флагами
...
if __name__ == '__main__':
app.run(main)В остальных случаях используйте стандартную конструкцию:
def main():
...
if __name__ == '__main__':
main()Весь код, находящийся на верхнем уровне (вне функций и классов), будет выполнен сразу при импорте модуля. Будьте осторожны: не вызывайте функции, не создавайте объекты и не выполняйте другие операции, которые не должны запускаться в процессе генерации документации через pydoc или при запуске тестов.
17. Длина функций
Отдавайте предпочтение коротким и узкоспециализированным функциям.
Мы признаем, что длинные функции иногда уместны, поэтому жесткого ограничения на их размер не устанавливается. Однако, если длина функции превышает 40 строк, подумайте, нельзя ли разбить её на части без ущерба для структуры программы.
Даже если ваша длинная функция сейчас работает безупречно, кто-то другой, внося изменения через несколько месяцев, может добавить в неё новую логику. Это может привести к трудноуловимым багам. Сохранение функций короткими и простыми облегчает другим людям чтение и модификацию вашего кода.
В процессе работы вы можете столкнуться с длинными и запутанными функциями. Не бойтесь модифицировать существующий код: если работа с такой функцией оказывается сложной, ошибки трудно поддаются отладке или вы хотите использовать фрагмент этой функции в нескольких разных контекстах — рассмотрите возможность разделения её на более мелкие и управляемые части.
18. Аннотации типов
18.1. Основа
Ознакомьтесь с аннотациями типов (type hints).
- Аннотировать
selfилиcls, как правило, не нужно. ТипSelfможно использовать в тех случаях, когда это необходимо для корректной передачи информации о типе, например:
from typing import Self
class BaseClass:
@classmethod
def create(cls) -> Self:
# Возвращает экземпляр того класса, у которого вызван метод
...
def difference(self, other: Self) -> float:
# Ожидает другой экземпляр того же самого класса
...- Точно так же не обязательно аннотировать возвращаемое значение метода
__init__(где единственным допустимым вариантом являетсяNone). - Если тип какой-либо переменной или возвращаемого значения невозможно или не нужно выразить точно, используйте
Any - Вы не обязаны аннотировать абсолютно все функции в модуле
- Аннотируйте как минимум ваши публичные API.
- Руководствуйтесь здравым смыслом, чтобы найти баланс между безопасностью и ясностью с одной стороны и гибкостью кода — с другой
- Аннотируйте код, склонный к ошибкам, связанным с типами (где ранее возникали баги или высокая сложность)
- Аннотируйте сложный код
- Добавляйте аннотации по мере того, как код становится стабильным с точки зрения типов. Во многих случаях в зрелом коде можно аннотировать все функции, не теряя при этом слишком много в гибкости.
18.2. Переносы строк
Старайтесь следовать существующим правилам отступов
После добавления аннотаций многие сигнатуры функций превратятся в формат «один параметр на строку». Чтобы гарантировать, что возвращаемый тип также получит свою отдельную строку, после последнего параметра можно поставить запятую.
def my_method(
self,
first_var: int,
second_var: Foo,
third_var: Bar | None,
) -> int:
...Всегда отдавайте предпочтение переносу строки между переменными, а не, к примеру, между именем переменной и её аннотацией типа. Однако, если всё умещается на одной строке — используйте одну строку.
def my_method(self, first_var: int) -> int:
...Если комбинация имени функции, последнего параметра и возвращаемого типа слишком длинная, сделайте отступ в 4 пробела на новой строке. При использовании переносов строк предпочтительно размещать каждый параметр и возвращаемый тип на отдельных строках, выравнивая закрывающую скобку по уровню ключевого слова def:
# Правильно:
def my_method(
self,
other_arg: MyLongType | None,
) -> tuple[MyLongType1, MyLongType1]:
...Опционально возвращаемый тип можно оставить на одной строке с последним параметром:
# Допустимо:
def my_method(
self,
first_var: int,
second_var: int) -> dict[OtherLongType, MyLongType]:
...pylint позволяет переносить закрывающую скобку на новую строку и выравнивать её по открывающей, но это менее читабельно.
# Так мы не делаем:
def my_method(self,
other_arg: MyLongType | None,
) -> dict[OtherLongType, MyLongType]:
...Как и в примерах выше, старайтесь не разрывать типы. Однако иногда они слишком длинные для одной строки (в таких случаях старайтесь не разрывать хотя бы подтипы).
def my_method(
self,
first_var: tuple[list[MyLongType1],
list[MyLongType2]],
second_var: list[dict[
MyLongType3, MyLongType4]],
) -> None:
...Если комбинация имени и типа слишком длинная, рассмотрите возможность использования псевдонима (alias) для типа. В крайнем случае допускается перенос после двоеточия с отступом в 4 пробела.
# Так мы делаем:
def my_function(
long_variable_name:
long_module_name.LongTypeName,
) -> None:
...# Так мы не делаем:
def my_function(
long_variable_name: long_module_name.
LongTypeName,
) -> None:
...18.3. Опережающие объявления (Forward Declarations)
Если вам нужно использовать имя класса (из того же модуля), который еще не определен — например, если имя класса требуется внутри объявления самого этого класса или если вы используете класс, который определен ниже по коду — используйте либо инструкцию from __future__ import annotations, либо указывайте имя класса в виде строки.
# Так мы делаем:
from __future__ import annotations
class MyClass:
# Благодаря импорту annotations выше, мы можем писать MyClass напрямую,
# даже если определение класса еще не завершено.
def __init__(self, stack: Sequence[MyClass], item: OtherClass) -> None:
...
class OtherClass:
...
class MyClass:
# Использование строковых литералов ('MyClass') — классический способ
# обойти проблему еще не определенных имен.
def __init__(self, stack: Sequence['MyClass'], item: 'OtherClass') -> None:
...
class OtherClass:
...18.4. Значения по умолчанию
Согласно PEP 8, используйте пробелы вокруг знака = только для тех аргументов, у которых одновременно есть и аннотация типа, и значение по умолчанию.
# Так мы делаем:
def func(a: int = 0) -> int:
...# Так мы не делаем:
def func(a:int=0) -> int:
...18.5. NoneType
В системе типов Python NoneType является типом «первого класса», и для целей аннотирования None считается псевдонимом для NoneType. Если аргумент может принимать значение None, это обязательно должно быть указано в объявлении! Вы можете использовать выражения объединения типов через оператор | (рекомендуется для нового кода на Python 3.10+), либо более старый синтаксис с использованием Optional и Union.
Используйте явное указание X | None вместо неявного. Ранние версии инструментов проверки типов позволяли интерпретировать a: str = None как a: str | None = None, но такое поведение больше не является предпочтительным.
# Так мы делаем:
def modern_or_union(a: str | int | None, b: str | None = None) -> str:
# Используется современный синтаксис Python 3.10+
...
def union_optional(a: Union[str, int, None], b: Optional[str] = None) -> str:
# Используется классический синтаксис модуля typing
...# Так мы не делаем:
def nullable_union(a: Union[None, str]) -> str:
# Не рекомендуется ставить None в начало Union
...
def implicit_optional(a: str = None) -> str:
# ОШИБКА: Тип указан как str, но значение по умолчанию — None.
# Это "неявный Optional", которого следует избегать.
...18.6. Псевдонимы типов
Вы можете объявлять псевдонимы для сложных типов. Имя псевдонима должно быть написано в стиле CapWords (с заглавных букв). Если псевдоним используется только внутри текущего модуля, он должен быть «приватным» (начинаться с подчеркивания: _Private).
Обратите внимание, что аннотация : TypeAlias поддерживается только в Python версии 3.10 и выше.
from typing import TypeAlias
# Приватный псевдоним для кортежа из двух тензоров
_LossAndGradient: TypeAlias = tuple[tf.Tensor, tf.Tensor]
# Публичный псевдоним для словаря со строковыми ключами
ComplexTFMap: TypeAlias = Mapping[str, _LossAndGradient]18.7. Игнорирование типов
Вы можете отключить проверку типов для конкретной строки с помощью специального комментария # type: ignore.
В инструменте pytype предусмотрена возможность отключения конкретных ошибок (по аналогии с линтерами):
# pytype: disable=attribute-error18.8. Типизация переменных
Аннотированные присваивания
Если тип внутренней переменной сложно или невозможно определить автоматически (вывести из контекста), укажите его с помощью аннотированного присваивания. Для этого используйте двоеточие и тип между именем переменной и её значением (точно так же, как это делается для аргументов функций со значениями по умолчанию):
a: Foo = SomeUndecoratedFunction()Комментарии к типам (Type Comments)
Хотя вы всё ещё можете встретить их в кодовой базе (они были необходимы до выхода Python 3.6), больше не добавляйте комментарии вида # type: <имя типа> в конце строки:
# НЕПРАВИЛЬНО:
a = SomeUndecoratedFunction() # type: Foo18.9. Кортежи vs Списки
Типизированные списки могут содержать объекты только одного типа. Типизированные кортежи могут содержать либо один повторяющийся тип, либо фиксированное количество элементов с разными типами. Последний вариант часто используется для описания типа возвращаемого значения функции.
# Список целых чисел (неопределенной длины)
a: list[int] = [1, 2, 3]
# Кортеж из целых чисел произвольной длины
b: tuple[int, ...] = (1, 2, 3)
# Кортеж фиксированной длины с элементами разных типов
c: tuple[int, str, float] = (1, "2", 3.5)18.10. Переменные типа (Type variables)
Система типов Python поддерживает дженерики (generics). Переменные типа, такие как TypeVar и ParamSpec, являются распространенным способом их использования.
Пример:
from collections.abc import Callable
from typing import ParamSpec, TypeVar
# Объявление переменных типа
_P = ParamSpec("_P")
_T = TypeVar("_T")
def next(l: list[_T]) -> _T:
"""Извлекает и возвращает последний элемент списка любого типа."""
return l.pop()
def print_when_called(f: Callable[_P, _T]) -> Callable[_P, _T]:
"""Декоратор, который печатает сообщение при вызове функции,
сохраняя её сигнатуру и тип возвращаемого значения."""
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _T:
print("Функция была вызвана")
return f(*args, **kwargs)
return innerПеременная TypeVar может быть ограничена набором типов:
# Ограничиваем переменную только числами или строками
AddableType = TypeVar("AddableType", int, float, str)
def add(a: AddableType, b: AddableType) -> AddableType:
"""Складывает два аргумента одного и того же типа (числа или строки)."""
return a + bВ модуле typing есть часто используемая предопределенная переменная типа — AnyStr. Используйте её для случаев, когда аннотации могут быть либо bytes, либо str, но при этом все они должны относиться к одному и тому же типу.
from typing import AnyStr
def check_length(x: AnyStr) -> AnyStr:
"""Проверяет длину строки или байтов и возвращает тот же тип данных."""
if len(x) <= 42:
return x
raise ValueError()Переменная типа должна иметь описательное имя, за исключением случаев, когда она соответствует всем следующим критериям:
- не видна извне (является внутренней для модуля);
- не имеет ограничений (unconstrained).
# Так мы делаем:
_T = TypeVar("_T") # Внутренняя и без ограничений
_P = ParamSpec("_P") # Внутренняя и без ограничений
AddableType = TypeVar("AddableType", int, float, str) # Есть ограничения — имя описательное
AnyFunction = TypeVar("AnyFunction", bound=Callable) # Есть верхняя граница — имя описательное# Так мы не делаем:
T = TypeVar("T") # Видна извне (нет подчеркивания), должно быть описательным
P = ParamSpec("P") # Видна извне, должно быть описательным
_T = TypeVar("_T", int, float, str) # Есть ограничения, должно быть описательным
_F = TypeVar("_F", bound=Callable) # Есть верхняя граница, должно быть описательным18.11. Строковые типы
Не используйте typing.Text в новом коде. Этот тип предназначен исключительно для обеспечения совместимости между Python 2 и 3.
Используйте str для строковых или текстовых данных. Для кода, работающего с бинарными данными, используйте bytes.
def deals_with_text_data(x: str) -> str:
"""Работает с текстовыми данными."""
...
def deals_with_binary_data(x: bytes) -> bytes:
"""Работает с бинарными (двоичными) данными."""
...Если все строковые типы в функции всегда должны совпадать (например, если возвращаемый тип должен быть таким же, как тип аргумента в примере выше), используйте AnyStr.
18.12. Импорты для типизации
Для символов (включая типы, функции и константы) из модулей typing или collections.abc, используемых для статического анализа и проверки типов, всегда импортируйте сам символ напрямую. Это делает аннотации более лаконичными и соответствует общемировой практике типизации. Вам явно разрешено импортировать несколько конкретных символов в одной строке из этих модулей. Например:
from collections.abc import Mapping, Sequence
from typing import Any, Generic, cast, TYPE_CHECKINGУчитывая, что такой способ импорта добавляет элементы в локальное пространство имен, имена из typing или collections.abc следует рассматривать аналогично зарезервированным ключевым словам и не переопределять их в своем коде. Если возникает конфликт между типом и существующим именем в модуле, используйте конструкцию import x as y.
from typing import Any as AnyTypeПри аннотировании сигнатур функций отдавайте предпочтение абстрактным типам контейнеров (например, collections.abc.Sequence) вместо конкретных (например, list). Если же вам необходимо использовать конкретный тип (например, tuple с типизированными элементами), отдавайте предпочтение встроенным типам Python вместо параметрических псевдонимов из модуля typing (например, используйте tuple вместо typing.Tuple).
# НЕ РЕКОМЕНДУЕТСЯ: использование конкретных типов и устаревших псевдонимов из typing
from typing import List, Tuple
def transform_coordinates(original: List[Tuple[float, float]]) -> List[Tuple[float, float]]:
...# ПРАВИЛЬНО: использование абстрактных контейнеров и встроенных типов
from collections.abc import Sequence
def transform_coordinates(original: Sequence[tuple[float, float]]) -> Sequence[tuple[float, float]]:
...18.13. Условные импорты
Используйте условные импорты только в исключительных случаях, когда необходимо избежать загрузки дополнительных модулей для проверки типов во время выполнения программы (runtime). Использование этого паттерна не приветствуется; предпочтительнее провести рефакторинг кода, чтобы разрешить импорты на верхнем уровне.
Импорты, необходимые исключительно для аннотаций типов, можно поместить в блок if TYPE_CHECKING:.
- Типы, импортированные условно, должны указываться в виде строк. Это обеспечивает обратную совместимость с Python 3.6, где выражения аннотаций вычисляются фактически при выполнении (если вы не используете
from __future__ import annotations). - В этом блоке должны определяться только те сущности, которые используются исключительно для типизации (включая псевдонимы). В противном случае возникнет ошибка выполнения, так как модуль не будет импортирован в рантайме.
- Блок должен располагаться сразу после всех обычных импортов.
- В списке импортов внутри блока не должно быть пустых строк.
- Сортируйте этот список так же, как и обычный список импортов.
import typing
# Блок выполняется только инструментами статического анализа (mypy, pyright)
if typing.TYPE_CHECKING:
import sketch
# Используем строку для ссылки на тип, чтобы избежать ошибки в рантайме
def f(x: "sketch.Sketch"): ...18.14. Циклические зависимости
Циклические зависимости, вызванные необходимостью типизации, являются «запашком кода» (code smell). Такой код — отличный кандидат на рефакторинг. Хотя технически сохранить циклические зависимости возможно, многие системы сборки не позволят вам этого сделать, так как модули будут жестко зависеть друг от друга.
Заменяйте импорты модулей, создающих циклическую зависимость, на Any. Создайте псевдоним (alias) с осмысленным именем и используйте реальное имя типа из этого модуля (любой атрибут Any также является Any). Определение псевдонима должно быть отделено от последнего импорта одной пустой строкой.
from typing import Any
# Модуль some_mod.py импортирует текущий модуль, создавая цикл.
# Поэтому мы используем Any вместо реального импорта.
some_mod = Any
...
def my_method(self, var: "some_mod.SomeType") -> None:
# Используем строковую аннотацию для ссылки на тип
...18.15. Дженерики (Generics)
При аннотировании старайтесь всегда указывать параметры типов для дженериков в списке параметров; в противном случае параметры дженерика будут по умолчанию считаться равными Any.
# Так мы делаем:
def get_names(employee_ids: Sequence[int]) -> Mapping[int, str]:
...
# Так мы не делаем:
# Это интерпретируется как get_names(employee_ids: Sequence[Any]) -> Mapping[Any, Any]
def get_names(employee_ids: Sequence) -> Mapping:
...Если наиболее подходящим параметром типа для дженерика действительно является Any, укажите это явно. Однако помните, что во многих случаях использование [[#1810-переменные-типа-type-variables|TypeVar]] может быть более уместным:
# Так мы не делаем:
def get_names(employee_ids: Sequence[Any]) -> Mapping[Any, str]:
"""Возвращает словарь соответствия ID сотрудника и его имени для данных ID."""# Так мы делаем:
_T = TypeVar('_T')
def get_names(employee_ids: Sequence[_T]) -> Mapping[_T, str]:
"""Возвращает словарь соответствия ID сотрудника и его имени для данных ID."""От себя
Длина строки
Такое чувство, словно правило делалось любителями писать код на планшетах или маленьких ноутбуках, я бы все-таки рекомендовал ограничиваться ~100 символами
Для игнорирования через ruff просто добавьте комментарий # noqa: E501
Отступы
Никогда не используйте пробелы, используйте табуляцию.
Математические именования
Используйте точечную директиву # noqa: N806, чтобы скрыть предупреждения ruff. Если таких переменных немного, добавьте директиву в конце строки для каждой из них; если их много, примените директиву в начале блока кода.