Skip to content

Latest commit

 

History

History
3772 lines (2953 loc) · 167 KB

bars_python_style_guide_ru.md

File metadata and controls

3772 lines (2953 loc) · 167 KB

Гайд по стилю Python-кода от Google

Содержание

1 Введение

Python - основной динамический язык, используемый в Google. Это руководство по стилю кода представляет собой перечень из разрешений и запретов для программ на Python.

Чтобы помочь вам правильно отформатировать код, мы создали файл настроек для Vim. Для Emacs должны подойти настройки по умолчанию.

Многие команды используют yapf для автоматического форматирования кода, чтобы избежать лишних споров.

2 Правила написания Python-кода

2.1 Линтер

Запустите pylint в своем проекте, используя это pylintrc.

2.1.1 Описание

pylint это инструмент для поиска ошибок и стилистических проблем в исходном коде Python. Он находит проблемы, которые обычно обнаруживаются компилятором в менее динамических языках, таких как C и C ++. Из-за динамической природы Python некоторые предупреждения могут быть неверными; однако, такие ситуации обычно возникают достаточно редко.

2.1.2 Плюсы

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

2.1.3 Минусы

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

2.1.4 Решение

Убедитесь, что запускаете pylint в вашем проекте.

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

dict = 'something awful'  # Плохая идея... pylint: disable=redefined-builtin

Каждое предупреждение pylint имеет свой собственный строковый идентификатор (например, empty-docstring). Предупреждения по спецификации Google начинаются с g-.

If the reason for the suppression is not clear from the symbolic name, add an explanation.

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

Вы можете получить список предупреждений pylint выполнив команду:

pylint --list-msgs

Чтобы получить дополнительную информацию о конкретном сообщении, используйте:

pylint --help-msg=C6409

Предпочтительно использовать pylint: disable взамен устаревшей формы pylint: disable-msg.

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

def viking_cafe_order(spam, beans, eggs=None):
    del beans, eggs  # Не используется викингами.
    return spam + spam + spam

Другим распространенным способом отключения этого типа предупреждения является использование '_' в качестве идентификатора для неиспользуемого аргумента или постфикса аргумента 'unused_', или его присвоение '_'. Все эти способы допустимы, но не являются предпочтительными, поскольку могут некорректно работать для вызовов, которые передают аргументы по имени и не гарантируют, что они фактически не используются.

2.2 Импорты

2.2.1 Описание

Механизм повторного использования кода из одного модуля в другом.

2.2.2 Плюсы

Соглашение об управлении пространством имен - простое. Все члены модуля должны быть явно прописаны в импорте.

2.2.3 Минусы

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

2.2.4 Решение

  • Используйте from x import y, где x - это префикс модуля, а y - имя члена без префикса.
  • Используйте from x import y as z, если импортируются два члена модулей с именем y или если y - это слишком длинное имя.
  • Используйте import y as z только тогда, когда z - это стандартное сокращение (например, np для numpy).

Например, класс sound.effects.echo.EchoFilter может быть импортирован так:

from sound.effects.echo import EchoFilter
...
EchoFilter(input, output, delay=0.7, atten=4)

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

2.3 Пакеты

При импорте модуля всегда используйте полный путь к нему.

2.3.1 Плюсы

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

2.3.2 Минусы

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

2.3.3 Решение

Весь новый код должен импортировать каждый модуль по его полному имени пакета.

Импорт должен выглядеть следующим образом:

# Ссылка на absl.flags в коде через указание полного пути (более подробно).
import absl.flags
from doctor.who import jodie

FLAGS = absl.flags.FLAGS
# Ссылка на flags в коде через указание только имени модуля (более общее).
from absl import flags
from doctor.who import jodie

FLAGS = flags.FLAGS

Пример неправильного использования (предполагается, что этот файл находится по пути doctor/who/, где также находится jodie.py):

# Непонятно, какой модуль подразумевал автор и что будет импортировано.  
# Фактическое поведение импорта зависит от настроек sys.path.
# Какой из возможных модулей jodie намеревался импортировать автор?
import jodie

Не следует рассчитывать на то, что каталог, в котором находится основной двоичный файл, находится в sys.path, несмотря на то, что это по умолчанию происходит в некоторых средах. В этом случае код должен предполагать, что import jodie относится к стороннему пакету или пакету верхнего уровня с именем jodie, а не к локальному jodie.py.

2.4 Исключения

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

2.4.1 Описание

Исключение - это способ выхода из обычного потока управления для обработки ошибок или других исключительных условий.

2.4.2 Плюсы

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

2.4.3 Минусы

Может сделать поток управления менее предсказуемым. Легко пропустить ошибку при использовании сторонних функций или классов.

2.4.4 Решение

Исключения должны соответствовать определенным условиям:

  • Используйте встроенные классы исключений, когда это действительно имеет смысл. Например, сделайте вызов исключения ValueError, чтобы указать на ошибку программирования, такую как нарушенное предусловие (например, если вам передали отрицательное число, а требовалось положительное). Не используйте выраженияassert для валидации значений аргументов публичного API. assert используется для поддержания внутренней корректности кода, а не для обеспечения правильности его использования или для указания того, что произошло какое-то непредвиденное событие. Если в указанных случаях требуется исключение, используйте оператор raise. Например:

    Правильно:
      def connect_to_next_port(self, minimum):
        """Подключение к следующему доступному порту.
    
        Args:
          minimum: Значение порта, большее или равное 1024.
    
        Returns:
          Новый минимальный порт.
    
        Raises:
          ConnectionError: Если не найден доступный порт.
        """
        if minimum < 1024:
          # Обратите внимание, что вызов исключения ValueError не указан в 
          # описании секции "Raises:", поскольку нет необходимости обрабатывать 
          # его как случай неверного использования API.
          raise ValueError(f'Минимальный порт должен быть не меньше 1024, а не {minimum}.')
        port = self._find_next_open_port(minimum)
        if not port:
          raise ConnectionError(
              f'Не удалось подключиться к сервису по порту {minimum} или выше.')
        assert port >= minimum, (
            f'Некорректный порт {port}, поскольку минимальный порт равен {minimum}.')
        return port
    Неправильно:
      def connect_to_next_port(self, minimum):
        """Подключение к следующему доступному порту.
    
        Args:
          minimum: Значение порта, большее или равное 1024.
    
        Returns:
          Новый минимальный порт.
        """
        assert minimum >= 1024, 'Минимальный порт должен быть не меньше 1024.'
        port = self._find_next_open_port(minimum)
        assert port is not None
        return port
  • Библиотеки или пакеты могут определять свои собственные исключения. При этом они должны наследовать их от существующего класса исключения. Имена исключений должны заканчиваться на Error и не должны дублировать название модуля (foo.FooError).

  • Никогда не используйте выражение except:, которое перехватывает все исключения, или перехват Exception или StandardError, если вы не

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

    Python - очень толерантный язык в этом отношении и выражение except: действительно будет перехватывать все, включая ошибки именования, вызовы sys.exit(), прерывания Ctrl+C, ошибки юниттестов и иные виды исключений которые вы на самом деле не планировали перехватывать.

  • Минимизируйте количество кода в блоке try/except. Чем больше тело try, тем больше вероятность того, что исключение будет вызвано строкой кода, от которой вы этого не ожидали. В этом случае, блок try/except скроет реальную ошибку.

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

2.5 Глобальные переменные

Избегайте глобальных переменных.

2.5.1 Описание

Глобальными считаются переменные, которые объявлены на уровне модуля или как атрибуты класса.

2.5.2 Плюсы

Подобные переменные иногда бывают полезны.

2.5.3 Минусы

Могут изменить поведение модуля во время импорта, поскольку присвоение глобальным переменным выполняется при первом импорте модуля.

2.5.4 Решение

Избегайте глобальных переменных.

При этом константы уровня модуля разрешены и приветствуются несмотря на то, что они технически являются переменными. Например: MAX_HOLY_HANDGRENADE_COUNT = 3. Константы должны именоваться заглавными буквами с подчеркиванием. Подробнее см. Именование.

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

2.6 Вложенные / локальные / внутренние классы и функции

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

2.6.1 Описание

Класс может быть определен внутри метода, функции или класса. Функция может быть определена внутри метода или функции. Вложенные функции имеют доступ только для чтения к переменным, определенным в той же области видимости, что и они сами.

2.6.2 Плюсы

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

2.6.3 Минусы

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

2.6.4 Решение

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

2.7 Comprehensions и генераторные выражения

Можно использовать в простых случаях.

2.7.1 Описание

List, Dict, и Set comprehensions, а также генераторные выражения обеспечивают выразительный и эффективный способ создания контейнерных типов и итераторов, без необходимости использования традиционных циклов, функций map(), filter(), или lambda.

2.7.2 Плюсы

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

2.7.3 Минусы

Сложные comprehensions или выражения генератора могут быть трудночитаемыми..

2.7.4 Решение

Можно использовать в простых случаях. Каждая часть выражения должна умещаться в одной строке: сопоставление, выражение for, фильтр. Использование нескольких выражений for или фильтров не допускается. Когда выражения становятся более сложными - используйте обычные циклы. [Локальное дополнение] Важное замечание: несмотря на то, что блок фильтрации может быть реализован как тернарная операция - запрещается разбивать его на несколько строк, поскольку в данном случае это атомарная конструкция.

Примеры правильного использования:

result = [mapping_expr for value in iterable if filter_expr]
result = [
    {'key': value} for value in iterable
    if a_long_filter_expression(value)
]
result = [
    complicated_transform(x)
    for x in iterable if predicate(x)
]
descriptive_name = [
    transform({'key': key, 'value': value}, color='black')
    for key, value in generate_iterable(some_input)
    if complicated_condition_is_met(key, value)
]
result = []
for x in range(10):
    for y in range(5):
        if x * y > 10:
            result.append((x, y))
return {
    x: complicated_transform(x)
    for x in long_generator_function(parameter)
    if x is not None
}
squares_generator = (x**2 for x in range(10))
unique_names = {user.name for user in users if user is not None}
eat(
    jelly_bean for jelly_bean in jelly_beans
    if jelly_bean.color == 'black'
)

Примеры неправильного использования:

  result = [complicated_transform(
                x, some_argument=x+1)
            for x in iterable if predicate(x)]
  result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
  return ((x, y, z)
          for x in range(5)
          for y in range(5)
          if x != y
          for z in range(5)
          if y != z)

2.8 Итераторы и операторы по умолчанию

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

2.8.1 Описание

Контейнерные типы, такие как словари или списки, определяют итераторы по умолчанию и операторы проверки вхождения ("in" и "not in").

2.8.2 Плюсы

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

2.8.3 Минусы

Вы не можете определить тип объекта, прочитав имя метода (например, has_key() относится к словарю). Одновременно, это можно считать преимуществом.

2.8.4 Решение

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

Примеры правильного использования:

for key in adict: ...
if key not in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in adict.items(): ...
for k, v in six.iteritems(adict): ...

Примеры неправильного использования:

for key in adict.keys(): ...
if not adict.has_key(key): ...
for line in afile.readlines(): ...
for k, v in dict.iteritems(): ...

2.9 Генераторы

При необходимости используйте генераторы.

2.9.1 Описание

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

2.9.2 Плюсы

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

2.9.3 Минусы

Не обнаружены.

2.9.4 Решение

Используйте. Указывайте "Yields:" вместо "Returns:" в docstring для функций-генераторов.

2.10 Лямбда-функции

Применимы для однострочных выражений. Используйте генераторные выражения вместо map() или filter() с lambda.

2.10.1 Описание

Лямбды определяют анонимные функции в выражении без отдельного объявления.

2.10.2 Плюсы

Они удобны.

2.10.3 Минусы

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

2.10.4 Решение

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

Для обычных операций, таких как умножение, вместо лямбда-функций используйте функции из модуля operator. Например, нужно использовать operator.mul вместо lambda x, y: x * y.

2.11 Условные выражения

Применимы для простых случаев.

2.11.1 Описание

Условные выражения (иногда называемые «тернарными операторами») - это механизмы, которые обеспечивают более короткий синтаксис для операторов if. Например: x = 1 if cond else 2.

2.11.2 Плюсы

Короче и удобнее, чем оператор if.

2.11.3 Минусы

В определенных случаях труднее читать, чем оператор if. Иногда условие сложно выделить, если выражение длинное.

2.11.4 Решение

Применимы для простых случаев. Каждая часть должна умещаться на одной линии: true-выражение, if-выражение, else-выражение. Используйте стандартную форму оператора if, если выражение становится сложнее.

Примеры правильного использования:

# Однострочное выражение
one_line = 'yes' if predicate(value) else 'no'

# Разделение выражения на две строки
slightly_split = (
    'yes' if predicate(value)
    else 'no, nein, nyet'
)

# Самый длинный вариант выражения, который можно использовать 
the_longest_ternary_style_that_can_be_done = (
    'yes, true, affirmative, confirmed, correct'
    if predicate(value)
    else 'no, false, negative, nay'
)

Примеры неправильного использования:

# Неверный перенос строки
bad_line_breaking = ('yes' if predicate(value) else
                     'no')

# Слишком длинное выражение
portion_too_long = ('yes'
                    if some_long_module.some_long_predicate_function(
                        really_long_variable_name)
                    else 'no, false, negative, nay')

2.12 Значения аргументов по умолчанию

Применимы в большинстве случаев.

2.12.1 Описание

Вы можете указать значения для переменных в конце списка параметров функции, например, def foo(a, b=0):. Если foo вызывается только с одним аргументом, то b принимает значение 0. Если функция вызывается с двумя аргументами, b принимает значение второго аргумента.

2.12.2 Плюсы

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

2.12.3 Минусы

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

2.12.4 Решение

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

Примеры правильного использования:

def foo(a, b=None):
    if b is None:
        b = []
def foo(a, b: Optional[Sequence] = None):
    if b is None:
        b = []
# Пустой кортеж можно использовать, поскольку он неизменяемый
def foo(a, b: Sequence = ()):  
    ...

Примеры неправильного использования:

def foo(a, b=[]):
    ...
def foo(a, b=time.time()):  # Был ли загружен модуль time???
    ...
def foo(a, b=FLAGS.my_thing):  # sys.argv еще не прочитан...
    ...
def foo(a, b: Mapping = {}):  # Используется "небезопасный" изменяемый тип
    ...

2.13 Свойства

Используйте свойства для доступа или заполнения данных там, где вы хотели использовать простые геттеры или сеттеры.

2.13.1 Описание

Способ декорации вызовов методов для получения и установки атрибута через стандартный доступ к атрибуту, когда логика его вычисления является простой.

2.13.2 Плюсы

Читаемость повышается за счет исключения явных вызовов методов get и set для простого доступа к атрибутам. Позволяет реализовать ленивые вычисления. Считается Pythonic-подходом для объявления интерфейса класса. С точки зрения производительности, разрешение свойств убирает необходимость в тривиальных методах доступа, когда прямой доступ к переменной является обоснованным. Также позволяют добавлять методы доступа в будущем без нарушения интерфейса.

2.13.3 Минусы

Может скрывать побочные эффекты, такие как перегрузка оператора. Может сбивать с толку при создании дочерних классов.

2.13.4 Решение

Используйте свойства для доступа или заполнения данных там, где вы хотели использовать простые геттеры или сеттеры. Свойства должны объявляться при помощи декоратора @property.

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

Пример:

 import math

 class Square:
     """Квадрат с двумя свойствами: площадь - изменяемое, периметр - только для чтения.

     Пример использования:
     >>> sq = Square(3)
     >>> sq.area
     9
     >>> sq.perimeter
     12
     >>> sq.area = 16
     >>> sq.side
     4
     >>> sq.perimeter
     16
     """

     def __init__(self, side):
         self.side = side

     @property
     def area(self):
         """Площадь квадрата."""
         return self._get_area()

     @area.setter
     def area(self, area):
         return self._set_area(area)

     def _get_area(self):
         """Косвенный метод доступа для вычисления свойства 'area'."""
         return self.side ** 2

     def _set_area(self, area):
         """Косвенный метод установки значения свойства 'area'."""
         self.side = math.sqrt(area)

     @property
     def perimeter(self):
         return self.side * 4

2.14 Определение True/False

Если возможно, используйте «неявное» логическое значение "ложь".

2.14.1 Описание

Python определяет ряд значений как False. Практическое правило: все «пустые» значения определяются как False. Например, 0, None, [], {}, ''.

2.14.2 Плюсы

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

2.14.3 Минусы

Могут показаться странным для разработчиков C/C++.

2.14.4 Решение

По возможности используйте "неявное" логическое значение "ложь", например, if foo: вместо if foo != []:. Однако, следует иметь в виду несколько условий:

  • Всегда используйте if foo is None: (или is not None), чтобы проверить значение None. Например, при проверке того, было ли для переменной или аргумента со значением по умолчанию None установлено другое значение. Другое значение может быть ложным при приведении к типу bool!

  • Никогда не сравнивайте логическую переменную с False, используя==. Вместо этого используйте if not x:. Если вам нужно отличить False от None, тогда объедините выражения в цепочку, например, if not x and x is not None:.

  • Для последовательностей (строк, списков, кортежей), используйте тот факт, что пустые последовательности являются ложными, поэтому if seq: и if not seq: предпочтительнее, чем if len(seq): и if not len(seq):
    соответственно.

  • При обработке целых чисел использование неявного значения "ложь" может быть связано с большим риском (т.е. None может быть ошибочно обработано как 0). Вы можете сравнить значение, которое, как известно, является целым числом (и не является результатом len()), с целым числом 0.

  • Обратите внимание, что '0' (т.е. 0 в виде строки) приводится к логическому значению "истина".

Примеры правильного использования:

if not users:
    print('no users')
if foo == 0:
    self.handle_zero()
if i % 10 == 0:
    self.handle_multiple_of_ten()
def f(x=None):
    if x is None:
        x = []

Примеры неправильного использования:

if len(users) == 0:
    print('no users')
if foo is not None and not foo:
    self.handle_zero()
if not i % 10:
    self.handle_multiple_of_ten()
def f(x=None):
    x = x or []

2.16 Лексическая область видимости

Допустимо к использованию.

2.16.1 Описание

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

Пример использования:

def get_adder(summand1):
    """Возвращает функцию, которая прибавляет число к переданному числу."""
    def adder(summand2):
        return summand1 + summand2

    return adder

2.16.2 Плюсы

В большинстве случаев позволяет реализовать более четкий и элегантный код. Особенно удобен для опытных программистов на Lisp и Scheme (а также Haskell, ML и др.).

2.16.3 Минусы

Может привести к неочевидным ошибкам. Например, таким как пример из PEP-0227:

i = 4
def foo(x):
    def bar():
        print(i, end='')
    # ...
    # Большой блок кода
    # ...
    for i in x:  # Ах, i *является* локальной для foo, что и увидит bar
        print(i, end='')
    bar()

В результате, foo([1, 2, 3]) выведет 1 2 3 3, а не 1 2 3 4.

2.16.4 Решение

Допустимо к использованию.

2.17 Декораторы функций и методов

Используйте декораторы разумно: только тогда, когда от этого есть явная польза. Избегайте staticmethod и ограничьте использование classmethod.

2.17.1 Описание

Декораторы для функций и методов (т.е. "объявление через @"). Один из распространенных декораторов - это декоратор @property, используется для преобразования обычных методов в динамически вычисляемые атрибуты. Синтаксис декоратора также позволяет использовать декораторы, определяемые пользователем. В частности, для некоторой функции my_decorator, пример использования в качестве декоратора будет выглядеть так:

class C:
    @my_decorator
    def method(self):
        # тело метода ...

что аналогично:

class C:
    def method(self):
        # тело метода ...
    method = my_decorator(method)

2.17.2 Плюсы

Удобным образом определяет некоторые преобразования над методом; преобразование может помочь устранить повторение кода, принудительно применить инварианты и т.д.

2.17.3 Минусы

Декораторы могут выполнять произвольные операции с аргументами функции или возвращаемыми значениями, что приводит к неявному поведению. Кроме того, декораторы выполняются во время импорта. Failures in decorator code are pretty much impossible to recover from.

2.17.4 Решение

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

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

Декораторы - это частный случай "кода верхнего уровня" - подробнее смотрите в разделе Основное.

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

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

2.18 Потоки

Не полагайтесь на атомарность встроенных типов.

Несмотря на то, что встроенные типы данных Python, такие как словари, на первый взгляд, содержат атомарные операции, есть ряд случаев, когда они не атомарны ( например если __hash__ или __eq__ реализованы как методы Python), и на их атомарность не следует полагаться. Вы также не должны полагаться на атомарность присвоения переменной (поскольку это, в свою очередь, зависит от реализации словарей).

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

2.19 "Мощные" функции

Избегайте этих функций.

2.19.1 Описание

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

2.19.2 Плюсы

Это мощные языковые функции. Они могут сделать ваш код более компактным.

2.19.3 Минусы

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

2.19.4 Решение

Избегайте этих функций в своем коде.

Стандартные библиотечные модули и классы, которые внутренне используют эти функции, можно использовать (например, abc.ABCMeta, dataclasses и enum).

2.20 Современный Python: Python 3 и from __future__ imports

Python 3 здесь! Проект БАРС.Бюджет-онлайн полностью перешел с Python 2.7 на Python 3.x, и в связи с этим в проекте могут попадаться старые варианты кода.

2.20.1 Описание

Python 3 - значительное изменение в языке Python. Существующий код не полностью адаптирован под версии 3.x, но нужды в хранении универсальных подходов и старых вариантов кода уже нет.

2.20.2 Плюсы

Код, написанный для Python 3, более понятен и в нем нет зависимости от необходимости проставлять кодировку utf-8, т. к. она теперь является стандартной.

2.20.3 Минусы

Не обнаружены.

2.20.4 Решение

from __future__ imports

В прошлом был добавлен код from __future__ import в некоторые модули. На текущий момент в нем нет нужды, поэтому его нужно удалять из файлов, с которыми производится работа в рамках задачи

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

Также в версии Python 3 нет необходимости прописывать в начале файла # coding: utf-8 и любые его варианты, поэтому эту строку также нужно удалять.

Unicode-строки теперь являются стандартными в Python 3, поэтому нет необходимости дополнительно указывать это в начале строки с помощью u''. Все указания u перед началом строки нужно удалять:

# Python 2.7:
# Unicode-строка:
a = u'unicode string'
# Байтовая строка (стандартная):
a = 'byte string'

# Python 3.x:
# Unicode-строка (стандартная):
a = 'unicode string'
# Байтовая строка:
a = b'byte string'
Библиотека six

Ранее в проекте была нужда в активной поддержке использования Python 2 и 3 одновременно, и для этого использовалась библиотека six, однако сейчас в ее наличии нет необходимости. Нужно преобразовывать ее использования в обычный Python 3-код. Примеры:

# Удалять импорты builtin-функций:
from six.moves import (
    filter,
    map,
    zip,
    range,
    # и т. д.
)

# Заменять использования универсальных операторов на методы, присущие объектам:
# Старый вариант:
import six
for x, y in six.iteritems(some_dict):
   ...

for value in six.itervalues(some_dict):
   ...

# Должно стать:
for x, y in some_dict.items():
   ...

for value in some_dict.values():
   ...

# Заменять использования универсальных определений типов на типы Python 3:
# Старый вариант:
import six
int_as_str = six.text_type(1)

# Должно стать:
int_as_str = str(1)

2.21 Аннотация типов в коде

Вы можете дополнять аннотациями код Python 3 с помощью "подсказок типов" в соответствии с PEP-484, и проверить код во время сборки с помощью инструмента проверки типов, например pytype.

Аннотации типов могут быть указаны в исходном коде или в файле stub pyi. По возможности, аннотации типов должны быть указаны в исходном коде. Используйте файлы pyi для сторонних модулей или модулей расширения.

2.21.1 Описание

Аннотации типов (или "подсказки типов") предназначены для аргументов функции или методов и возвращаемых значений:

def func(a: int) -> List[int]:

Вы также можете объявить тип переменной, используя синтаксис, аналогичный PEP-526:

a: SomeType = some_func()

Или используя комментарии типов в коде, который должен поддерживать устаревшие версии Python:

a = some_func()  # type: SomeType

2.21.2 Плюсы

Аннотации типов улучшают читаемость и поддерживаемость вашего кода. Средство проверки типов преобразует многие ошибки времени выполнения в ошибки времени сборки и уменьшает свободу использования "мощных" функций.

2.21.3 Минусы

Вам нужно будет поддерживать объявления типов в актуальном состоянии. Вы можете увидеть ошибки типов, которые, по вашему мнению, являются допустимыми. Использование проверки типов может затруднить использование "мощных" функций.

2.21.4 Решение

Настоятельно рекомендуем включить анализ типов Python при обновлении кода. При добавлении или изменении общедоступных API включите аннотации типов, а также проверку через pytype в системе сборки. Поскольку статический анализ является относительно новым для Python, мы признаем, что возможны нежелательные побочные эффекты (например, ошибочно выведенные типы), которые могут помешать некоторым проектам. В таких ситуациях авторам рекомендуется добавить комментарий с TODO или ссылку на ошибку, описывающую проблему, которая в настоящее время препятствуют принятию аннотации типов в файле BUILD или в самом коде, если это необходимо.

3 Правила оформления Python-кода

3.1 Точка с запятой

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

3.2 Длина строки

Максимальная длина строки - 120 символов.

Исключениями из ограничения в 120 символов являются:

  • Длинные операторы импорта.
  • URL-адреса, пути или длинные флаги в комментариях.
  • Константы уровня модуля с длинным именем без пробелов, которые нежелательно разбивать на строки, такие как URL-адрес или пути.
    • Комментарии отключения Pylint. (например, # pylint: disable=invalid-name)

Не используйте продолжение строки с обратной косой чертой, за исключением операторов with, использующих три и более менеджеров контекста.

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

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'
):

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

x = (
    'This will build a very long long long long long long long long long long long long 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

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

Примеры правильного использования:

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)
with very_long_first_expression_function() as spam:
    with very_long_second_expression_function() as beans:
        place_order(beans, spam)

Пример неправильного использования:

with VeryLongFirstExpressionFunction() as spam, \
     VeryLongSecondExpressionFunction() as beans:
        PlaceOrder(eggs, beans, spam, beans)

Обратите внимание на отступ элементов в приведенных выше примерах продолжения строки; подробную информацию смотрите в разделе отступы.

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

3.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)

3.4 Отступы

Формируйте отступы в кодовых блоках 4 пробелами.

Никогда не используйте табуляции и не смешивайте табуляции и пробелы. В случаях неявного продолжения строки следует выровнять используя отступ из 4 пробелов, и в этом случае после открытой круглой скобки или скобки в первой строке не должно быть ничего. Рекомендуется оставлять максимально возможное количество символов в строке, если все выражение не занимает более одной линии кода. Это правило не распространяется на объявления функций - для упрощения чтения при наличии аннотаций типов каждый аргумент должен быть на своей строке; также оно не распространяется на импорты.

# Выравнивание с отступом в случае одной линии кода
foo = long_function_name(
    var_one, var_two, var_three, var_four, var_five, var_six, var_seven, var_eight, var_nine
)
# Если заполненные подряд линии кода занимают более 1й строки, нужен 
# перенос каждого элемента
meal = (
    var_one, 
    var_two, 
    var_three, 
    var_four, 
    var_five, 
    var_six, 
    var_seven, 
    var_eight, 
    var_nine, 
    var_ten,
    var_eleven,
)
# Отступ в 4 пробела в словаре
foo = {
    long_dictionary_key: (
        long_dictionary_value
    ),
    ...
}

Примеры неправильного использования:

# Объекты в первой строке запрещены, если они полностью не входят в строку
foo = long_function_name(var_one, var_two,
    var_three, var_four)

meal = (spam,
    beans)
# Отступ в 2 пробела запрещен
foo = long_function_name(
  var_one, var_two, var_three, var_four
)
# Отсутствует отступ в словаре
foo = {
    long_dictionary_key:
    long_dictionary_value,
    ...
}

3.4.1 Замыкающие запятые в последовательности элементов?

Замыкающие запятые в последовательностях элементов рекомендуется использовать только в том случае, когда закрывающий символ контейнера ], ) или } не находится в той же строке, что и последний элемент. Наличие замыкающей запятой также используется в качестве подсказки инструменту автоматического форматирования кода Python YAPF, чтобы направить его на автоматическое форматирование контейнера элементов до одного элемента в строке, когда присутствует , после последнего элемента.

golomb3 = [0, 1, 3]
golomb4 = [
    0,
    1,
    4,
    6,
]

Пример неправильного использования:

golomb4 = [
    0,
    1,
    4,
    6
]

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

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

3.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)

Пример неправильного использования:

spam (1)

Пример правильного использования:

dict['key'] = list[index]

Пример неправильного использования:

dict ['key'] = list [index]

Не используйте пробелы в конце строк.

Обозначьте бинарные операторы одним пробелом с обеих сторон для присваивания (=), сравнения (==, <, >, !=, <>, <=, >=, in, not in, is, is not) и Boolean-операторов (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)

Не используйте пробелы для вертикального выравнивания символов в последовательных строках, так как это очень тяжело поддерживать ( приминительно к :, #, =, etc.):

Пример правильного использования:

foo = 1000  # комментарий
long_name = 2  # комментарий, который не следует выравнивать

dictionary = {
    'foo': 1,
    'long_name': 2,
}

Пример неправильного использования:

foo       = 1000  # комментарий
long_name = 2     # комментарий, который не следует выравнивать

dictionary = {
    'foo'      : 1,
    'long_name': 2,
}

3.7 Шебанг-строка

Большинство файлов .py не нужно начинать со строки#!. Начните основной файл программы с #!/usr/bin/env python3 (для поддержки виртуальных окружений) или #!/usr/bin/python3 согласно PEP-394.

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

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

Убедитесь, что вы используете правильный стиль для docstring уровня модуля, функции, метода и встроенных комментариев.

3.8.1 Docstrings

Python использует docstrings для документирования кода. Docstring - это строка, которая является первым оператором в пакете, модуле, классе или функции. Эти строки могут быть извлечены автоматически через атрибут __doc__ объекта и используются pydoc (попробуйте запустить pydoc в своем модуле, чтобы посмотреть, как он выглядит.) Всегда используйте формат трех двойных кавычек """ для строк документации (подробнее в PEP 257). Docstring должна быть организована в виде итоговой строки (одна физическая строка, не превышающая 120 символов), оканчивающаяся точкой, вопросительным знаком или восклицательным знаком. При написании дополнительных строк - они должны быть отделены пустой строкой, за которой следует остальная часть docstring, начиная с той же позиции курсора, что и первый символ первой строки. Ниже приведены дополнительные рекомендации по форматированию docstring.

3.8.2 Модули

Каждый файл должен содержать шаблон лицензии. Выберите соответствующий шаблон для лицензии, используемой в проекте (например, Apache 2.0, BSD, LGPL, GPL).

Файлы должны начинаться с docstring, описывающей содержимое и использование модуля. Например:

"""Однострочное описание модуля или программы, оканчивающееся точкой.

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

  Пример использования:

  foo = ClassFoo()
  bar = foo.FunctionBar()
"""

3.8.3 Функции и Методы

В этом разделе «функция» означает метод, функцию или генератор.

Функция должна иметь docstring, если она не соответствует всем следующим критериям:

  • не видна снаружи
  • очень короткая
  • очевидная

Docstring должен содержать достаточно информации для записи вызова функции без чтения кода функции. Docstring должен быть описательным, ("""Выбирает строки из Большой таблицы.""") а не императивным ( """Получить строки из Большой таблицы."""), за исключением дескрипторов данных @property, которые должны использовать тот же стиль, что и атрибуты. Docstring должен описывать синтаксис вызова функции и ее семантику, а не ее реализацию. Для сложного кода комментарии рядом с кодом более уместны, чем использование docstring.

Метод, который переопределяет метод из базового класса, может иметь простую строку документации, отправляющую читателя в строку документации своего переопределенного метода, такую как """См. базовый класс.""". Причина в том, что нет необходимости во многих местах повторять документацию, которая уже присутствует в docstring базового метода. Однако, если поведение переопределяющего метода существенно отличается от переопределенного, или необходимо указать подробности (например, задокументировать дополнительные side-эффекты), для переопределенного метода требуется указать хотя бы эти различия в docstring.

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

Args: : Расположите все параметры в списке по имени. Описание должно следовать за именем и разделяться двоеточием, за которым следует либо пробел, либо новая строка. Если описание слишком длинное и не помещается в одну строку из 120 символов, используйте отступ на 2 или 4 пробела больше, чем имя параметра (должно быть согласовано с остальными строками документации в файле). Описание должно включать требуемый тип(ы), если код не содержит аннотации соответствующего типа. Если функция принимает *foo (списки аргументов переменной длины) и/или **bar (произвольные именованные аргументы), они должны быть указаны как *foo и **bar.

Returns: (или Yields: для генераторов) : Описывает тип и семантику возвращаемого значения. Если функция возвращает только None, этот раздел не является обязательным. Его также можно пропустить, если docstring начинается с результата работы (например, """Возвращает строку из Bigtable как кортеж строк."""`) и этого достаточно для описания возвращаемого значения.

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

def fetch_smalltable_rows(
    table_handle: smalltable.Table,
    keys: Sequence[Union[bytes, str]],
    require_all_keys: bool = False,
) -> Mapping[bytes, Tuple[str]]:
    """Возвращает строки из Smalltable.

    Извлекает строки, относящиеся к указанным ключам, из экземпляра 
    таблицы, представленного table_handle. Строковые ключи будут 
    закодированы в UTF-8.

    Args:
        table_handle: Открытый экземпляр smalltable.Table.
        keys: Последовательность строк, представляющая собой ключ 
            каждой строки таблицы для выборки. Строковые ключи 
            будут закодированы в UTF-8.
        require_all_keys: Необязательный; Если 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[Union[bytes, str]],
    require_all_keys: bool = False,
) -> Mapping[bytes, Tuple[str]]:
    """Возвращает строки из Smalltable.

    Извлекает строки, относящиеся к указанным ключам, из экземпляра 
    таблицы, представленного table_handle. Строковые ключи будут 
    закодированы в UTF-8.

    Args:
      table_handle:
        Открытый экземпляр smalltable.Table.
      keys:
        Последовательность строк, представляющая собой ключ каждой 
        строки таблицы для выборки. Строковые ключи будут 
        закодированы в UTF-8.
      require_all_keys:
        Необязательный; Если 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.
    """

3.8.4 Классы

Классы должны иметь docstring под определением класса. Если у вашего класса есть общедоступные атрибуты, они должны быть задокументированы в секции Attributes и следовать тому же формату, что и секции Args у функций.

class SampleClass:
    """Краткое описание класса.

    Подробная информация о классе....
    Подробная информация о классе....

    Attributes:
        likes_spam: Булевый флаг, указывающий на то, 
            нравится нам СПАМ или нет.
        eggs: Целое число отложенных нами яиц.
    """

    def __init__(self, likes_spam=False):
        """Инициализация SampleClass."""
        self.likes_spam = likes_spam
        self.eggs = 0

    def public_method(self):
        """Выполнение операции."""

3.8.5 Блочные и Inline-комментарии

Последнее место для комментариев - сложные части кода. Если вам придется объяснять это на следующем ревью кода, вы должны прокомментировать это прямо сейчас. Сложные операции получают несколько строк комментариев до начала операций. Неочевидные получают комментарии в конце строки.

# Мы используем взвешенный поиск по словарю, чтобы узнать, где 
# i находится в массиве. Мы экстраполируем позицию на основе 
# наибольшего числа в массиве и размера массива, а затем 
# выполняем двоичный поиск, чтобы получить точное число.

if i & (i-1) == 0:  # True, если i равна 0 или степени 2.

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

С другой стороны, никогда не описывайте код. Предполагайте, что человек, читающий код, знает Python (а не то, что вы пытаетесь сделать) лучше, чем вы.

# ПЛОХОЙ КОММЕНТАРИЙ: Теперь просмотрим массив b и убедимся, 
# что всякий раз, когда встречается i, следующим элементом 
# является i + 1

3.8.6 Пунктуация, правописание и грамматика

Обратите внимание на пунктуацию, орфографию и грамматику; хорошо написанные комментарии читать легче, чем плохо написанные.

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

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

3.10 Строки

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

Примеры правильного использования:

x = f'{a}{b}'
# Нежелательно, но допустимо
x = '{}, {}'.format(first, second)
x = f'name: {name}; score: {n}'

Примеры неправильного использования:

x = '%s %s' % (a, b)  # ленивое форматирование не рекомендуется
x = first + ', ' + second
x = 'name: ' + name + '; score: ' + str(n)
# Экранированные символы нельзя использовать внутри выражения, 
# подставляемого с помощью f-строки 
x = f'log: {"\n".join([error1, error2])}'

Избегайте использования операторов + и + = для накопления строки в цикле. В некоторых условиях соединение строк может привести к квадратичному, а не линейному времени работы. Несмотря на то, что такие соединения строк могут быть оптимизированы на CPython, не стоит на это рассчитывать, так как это является деталями реализации. Условия, при которых применяется эта оптимизация, предсказать непросто, и они могут измениться.

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

Пример правильного использования:

items = ['<table>']
for last_name, first_name in employee_list:
    items.append(f'<tr><td>{last_name}, {first_name}</td></tr>')
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('Why are you hiding your eyes?')
Gollum("I'm scared of lint errors.")
Narrator('"Good!" thought a happy Python reviewer.')

Пример неправильного использования:

Python("Why are you hiding your eyes?")
Gollum('The lint. It burns. It burns us.')
Gollum("Always the great lint. Watching. Watching.")

Для многострочных строк предпочтительнее """, чем '''. Проекты могут использовать ''' для всех многострочных строк без документации тогда и только тогда, когда они также используют ' для обычных строк. Docstring должны использовать """ всегда.

Многострочные строки не очень хорошо сочетаются с отступами в остальном коде. Если вам нужно избежать встраивания лишних пробелов в строку, используйте либо конкатенированные однострочные строки, либо многострочные строки с textwrap.dedent() для удаления начального пробела в каждой строке:

Пример неверного использования

  long_string = """This is pretty ugly.
Don't do this.
"""

Пример правильного использования

long_string = """This is fine if your use case can accept
    extraneous leading spaces."""

Пример правильного использования

long_string = ("And this is fine if you cannot accept\n" +
               "extraneous leading spaces.")

Пример правильного использования

long_string = ("And this too is fine if you cannot accept\n"
               "extraneous leading spaces.")

Пример правильного использования

import textwrap

long_string = textwrap.dedent("""\
    This is also fine, because textwrap.dedent()
    will collapse common leading spaces in each line.""")

3.10.1 Логирование

Для логирования функций, которые ожидают шаблон-строку (с %-заполнителем) как их первый аргумент: Всегда в качестве первого аргумента передавайте строковый литерал (не f-строку!), а в качестве последующих аргументов - параметры шаблона. Некоторые реализации логирования сохраняют неразвернутую шаблон-строку именно как шаблон, не подставляя значения параметров сразу. Это также предотвращает трату времени на рендеринг сообщения для вывода которого не сконфигурирован логгер.

Пример правильного использования

  import tensorflow as tf
  logger = tf.get_logger()
  logger.info('TensorFlow Version is: %s', tf.__version__)

Пример правильного использования

  import os
  from absl import logging

  logging.info('Current $PAGER is: %s', os.getenv('PAGER', default=''))

  homedir = os.getenv('HOME')
  if homedir is None or not os.access(homedir, os.W_OK):
    logging.error('Cannot write to home directory, $HOME=%r', homedir)

Пример неправильного использования

  import os
  from absl import logging

  logging.info('Current $PAGER is:')
  logging.info(os.getenv('PAGER', default=''))

  homedir = os.getenv('HOME')
  if homedir is None or not os.access(homedir, os.W_OK):
    logging.error(f'Cannot write to home directory, $HOME={homedir!r}')

3.10.2 Сообщения об ошибках

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

  1. Сообщение должно точно отражать фактическое состояние ошибки.

  2. Фрагменты сообщения об ошибке, вставленные из переменных должны быть легко определяемы.

  3. Они должны позволять простую автоматическу обработку (например, парсинг с использованием grep).

Пример правильного использования

  if not 0 <= p <= 1:
    raise ValueError(f'Not a probability: {p!r}')

  try:
    os.rmdir(workdir)
  except OSError as error:
    logging.warning('Could not remove directory (reason: %r): %r',
                    error, workdir)

Пример неправильного использования

  if p < 0 or p > 1:  # PROBLEM: also false for float('nan')!
    raise ValueError(f'Not a probability: {p!r}')

  try:
    os.rmdir(workdir)
  except OSError:
    # PROBLEM: Message makes an assumption that might not be true:
    # Deletion might have failed for some other reason, misleading
    # whoever has to debug this.
    logging.warning('Directory already was deleted: %s', workdir)

  try:
    os.rmdir(workdir)
  except OSError:
    # PROBLEM: The message is harder to grep for than necessary, and
    # not universally non-confusing for all possible values of `workdir`.
    # Imagine someone calling a library function with such code
    # using a name such as workdir = 'deleted'. The warning would read:
    # "The deleted directory could not be deleted."
    logging.warning('The %s directory could not be deleted.', workdir)

3.11 Файлы и сокеты

Явно закрывайте файлы и сокеты, когда завершаете работу с ними.

Если оставить файлы, сокеты или другие файловые объекты открытыми без необходимости, это может привести к следующим проблемам:

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

Кроме того, хотя файлы и сокеты автоматически закрываются при удалении файлового объекта, привязка времени жизни файлового объекта к состоянию файла является плохой практикой:

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

Предпочтительный способ управления файлами - использование менеджера контекста через with:

with open("hello.txt") as hello_file:
    for line in hello_file:
        print(line)

Для файловых объектов, которые не поддерживают оператор with, используйте contextlib.closing():

import contextlib

with contextlib.closing(urllib.urlopen("http://www.python.org/")) as front_page:
    for line in front_page:
        print(line)

3.12 Комментарии TODO

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

Комментарий TODO должен начинаться со строки TODO заглавными буквами c последующим двоеточием и номером задачи. Далее следует объяснение того, что нужно делать.

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

В ходе выполнения указанной в TODO задачи комментарий должен быть удален.

# TODO: BOBUH-12345 Используйте "*" для повторения строк.
# TODO: BOZIK-54321 Измените это, чтобы использовать отношения.

3.13 Форматирование импортов

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

Примеры правильного использования:

import os
import sys
from typing import (
    Mapping, 
    Sequence,
)

Примеры неправильного использования:

import os, sys

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

  1. Импорты стандартной библиотеки Python. Например:

    import sys
  2. Импорты внешних модулей или пакетов. Например:

    import tensorflow as tf
  3. Импорты суб-пакетов основного кода проекта. Например:

    from otherproject.ai import mind
  4. Устарело: специфичный для приложения импорт, который является частью того же суб-пакета верхнего уровня, что и этот файл. Например:

    from myproject.backend.hgwells import time_machine

Вы можете найти старый стиль кода Google Python, который делает это, но это больше не требуется. Новому коду рекомендуется не обращать на это внимание. Просто относитесь к импорту суб-пакетов для конкретного приложения так же, как и к импорту других суб-пакетов.

Внутри каждой группы импорты должны быть отсортирован лексикографически, без учета регистра, в соответствии с полным путем к пакету каждого модуля ( 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

# Older style code may have these imports down here instead:
#from myproject.backend.hgwells import time_machine
#from myproject.backend.state_machine import main_loop

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

3.14 Выражения

Обычно должен использоваться только один оператор в строке.

Однако, вы можете поместить результат теста в ту же строку, что и тест, только если весь оператор умещается в одной строке. В частности, вы никогда не сможете сделать это с try / except, поскольку try и except не могут одновременно уместиться в одной строке, но у вас может получиться это сделать с выражением if, если у него нет else.

Пример правильного использования:

if foo: bar(foo)

Примеры неправильного использования:

if foo: bar(foo)
else:   baz(foo)
try:               bar(foo)
except ValueError: baz(foo)
try:
    bar(foo)
except ValueError: baz(foo)

3.15 Способы доступа

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

С другой стороны, если доступ сложен или стоимость доступа к переменной значительна, вы должны использовать вызовы функций, (следуя правилам Именования) такие как get_foo() или set_foo(). Если предыдущее поведение разрешало доступ через свойство, не привязывайте новые функции доступа к свойству. Любой код, которые по прежнему пытается получить доступ к переменной старым методом, должен явным образом сломаться, чтобы стало известно об изменении уровня сложности.

3.16 Именование

имя_модуля, имя_пакета, ИмяКласса, имя_метода, ИмяИсключения, имя_функции, ИМЯ_КОНСТАНТЫ, имя_глобальной_переменной, имя_переменной_объекта, имя_параметра_функций, имя_локальной_переменной.

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

Всегда используйте расширение имени файла .py. Никогда не используйте дефис.

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

  • односимвольные имена, за исключением специально разрешенных случаев:

    • счетчики или итераторы (например i, j, k, v и др.)
    • e как идентификатор исключения в операторах try/except
    • f как дескриптор файла в операторах with

    Помните, что нельзя злоупотреблять односимвольными именами. Фактически, информативность должна быть пропорциональна области видимости имени. Например, i может быть хорошим именем для 5-строчного блока кода, но в пределах нескольких вложенных областей видимости оно будет слишком расплывчатым.

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

  • дефис (-) в любом имени пакета/модуля

  • имена __с_двойным_подчеркиванием_в_начале_и_в_конце__ (зарезервированы Python)

  • оскорбительные термины

  • имена, содержащие типы переменных, если на то нет причин (для примера: id_to_name_dict)

3.16.2 Соглашения об именовании

  • «Внутренний» означает внутренний по отношению к модулю, защищенный или приватный класс.

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

  • Поместите связанные классы и функции верхнего уровня вместе в модуль. В отличие от Java, нет необходимости ограничивать себя одним классом на модуль.

  • Используйте CapWords для имен классов, но lower_with_under.py для имен модулей. Хотя есть несколько старых модулей с именем CapWords.py, теперь это такое именование не рекомендуется, потому что это сбивает с толку, когда модуль назвали в честь класса. ("Подождите -- должен ли я написать import StringIO или from StringIO import StringIO?")

  • Подчеркивание может появляться в именах методов unittest, начинающихся с test, для разделения логических компонентов имени, даже если эти компоненты используют CapWords. Один из возможных шаблонов - это test<MethodUnderTest>_<state>; например, testPop_EmptyStack - это нормально. Не существует единственного правильного способа назвать методы тестирования.

3.16.3 Именование файлов

Имена файлов Python должны иметь расширение .py и не должны содержать дефисов (-). Это позволяет импортировать и тестировать их. Если вы хотите, чтобы исполняемый файл был доступен без расширения, используйте символическую ссылку или простой bash-скрипт, содержащий exec "$0.py" "$@".

3.16.4 Принципы, основанные на Рекомендациях Гвидо(https://en.wikipedia.org/wiki/Guido_van_Rossum)

Тип Общедоступный Внутренний
Пакеты 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 (protected)
Имена методов lower_with_under() _lower_with_under() (protected)
Параметры функции/метода lower_with_under
Локальные переменные lower_with_under

3.17 Main-функция

В Python pydoc, а также модульные тесты требуют, чтобы модули были импортируемыми. Если файл предназначен для использования в качестве исполняемого файла, его основная функциональность должна быть в функции main(), и ваш код должен всегда проверять if __name__ == '__main__' перед выполнением вашей основной программы, чтобы он не выполнялся при импорте модуля.

При использовании absl, используйте app.run:

from absl import app
...

def main(argv):
    # process non-flag arguments
    ...

if __name__ == '__main__':
    app.run(main)

В противном случае, используйте:

def main():
    ...

if __name__ == '__main__':
    main()

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

3.18 Длина функций

Отдавайте предпочтение небольшим и точечным функциям.

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

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

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

3.19 Аннотация типов

3.19.1 Общие правила

  • Ознакомьтесь с PEP-484.
  • В методах аннотируйте self или cls только если это необходимо для правильной информации о типе, например:
    @classmethod
    def create(cls: Type[T]) -> T:
        return cls()
  • Если какая-либо другая переменная или возвращаемый тип не должны быть явно объявлены, используйте Any.
  • От вас не требуется аннотировать все функции в модуле:
    • Аннотируйте свои общедоступные API.
    • Используйте здравый смысл, чтобы найти хороший баланс между безопасностью и ясностью с одной стороны, и гибкостью с другой.
    • Аннотируйте код, который подвержен ошибкам, связанным с типом (с учетом предыдущих ошибок или сложности).
    • Аннотируйте код, который трудно понять.
    • Аннотируйте код, когда он становится стабильным с точки зрения типов. Во многих случаях вы можете аннотировать все функции в давно написанном коде, не теряя при этом в гибкости.

3.19.2 Перенос строк

Постарайтесь следовать существующим правилам отступов.

После аннотирования многие сигнатуры функций примут вид «по одному параметру на строку».

def my_method(self,
              first_var: int,
              second_var: Foo,
              third_var: Optional[Bar]) -> int:
  ...

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

def my_method(self, first_var: int) -> int:
  ...

Если комбинация имени функции, последнего параметра и возвращаемого типа слишком длинная, сделайте отступ на 4 в новой строке.

def my_method(
    self, first_var: int) -> Tuple[MyLongType1, MyLongType1]:
  ...

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

Yes:
def my_method(
    self, other_arg: Optional[MyLongType]
) -> Dict[OtherLongType, MyLongType]:
  ...

pylint позволяет переместить закрывающую скобку на новую строку и выровнять ее с открывающей, но это менее читаемо:

def my_method(self,
              other_arg: Optional[MyLongType]
             ) -> Dict[OtherLongType, MyLongType]:
  ...

Как и в примерах выше, постарайтесь не разбивать типы. Однако, иногда они слишком длинные, чтобы находиться в одной строке (старайтесь, чтобы подтипы не прерывались).

def my_method(
    self,
    first_var: Tuple[List[MyLongType1],
                     List[MyLongType2]],
    second_var: List[Dict[
        MyLongType3, MyLongType4]]) -> None:
  ...

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

def my_function(
    long_variable_name:
        long_module_name.LongTypeName,
) -> None:
  ...

Ни в коем случае не делаете следующим образом:

def my_function(
    long_variable_name: long_module_name.
        LongTypeName,
) -> None:
  ...

3.19.3 Предварительные декларации

Если вам требуется использовать имя класса из того же модуля, который еще не определен - например, если вам нужен класс внутри объявления класса, или если вы используете класс, определенный ниже - используйте строку для имени класса.

class MyClass:

    def __init__(self,
                 stack: List["MyClass"]) -> None:

3.19.4 Значения по умолчанию

Согласно PEP-008, используйте пробелы вокруг = только для аргументов, которые имеют как аннотацию типа, так и значение по умолчанию.

Пример правильного оформления:

def func(a: int = 0) -> int:
  ...

Пример неправильного оформления:

def func(a:int=0) -> int:
  ...

3.19.5 NoneType

В системе типов Python NoneType является типом "первого класса", и для целей типизирования None является псевдонимом для NoneType. Если аргумент может иметь значение None, его нужно объявить! Вы можете использовать Union, но если есть только один другой тип, используйте Optional.

Используйте явный Optional вместо неявного Optional. В более ранних версиях PEP 484 a: Text = None можно было интерпретировать как a: Optional[Text] = None, но это больше не является предпочтительным поведением.

Примеры правильного форматирования:

def func(a: Optional[Text], b: Optional[Text] = None) -> Text:
  ...
def multiple_nullable_union(a: Union[None, Text, int]) -> Text
  ...

Примеры неправильного форматирования:

def nullable_union(a: Union[None, Text]) -> Text:
  ...
def implicit_optional(a: Text = None) -> Text:
  ...

3.19.6 Псевдонимы типов

Вы можете объявлять псевдонимы сложных типов. Имя псевдонима должно быть CapWorded. Если псевдоним используется только в этом модуле, он должен быть _Приватным.

Например, если имя модуля вместе с именем типа слишком длинное:

_ShortName = module_with_long_name.TypeWithLongName
ComplexMap = Mapping[Text, List[Tuple[int, int]]]

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

3.19.7 Игнорирование типов

Вы можете отключить проверку типов в строке специальным комментарием # type: ignore.

В pytype есть опция отключения для определенных ошибок (аналогично lint):

# pytype: disable=attribute-error

3.19.8 Типизация переменных

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

Комментарий с типом - используйте комментарий # type: в конце строки:

a = SomeUndecoratedFunction()  # type: Foo

Анотация присвоения - используйте двоеточие и укажите тип между именем переменной и значением, как с аргументами функции:

a: Foo = SomeUndecoratedFunction()

3.19.9 Кортежи или строки?

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

a = [1, 2, 3]  # type: List[int]
b = (1, 2, 3)  # type: Tuple[int, ...]
c = (1, "2", 3.5)  # type: Tuple[int, Text, float]

3.19.10 TypeVars

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

from typing import List, TypeVar
T = TypeVar("T")
...
def next(l: List[T]) -> T:
  return l.pop()

TypeVar может быть ограничен:

AddableType = TypeVar("AddableType", int, float, Text)
def add(a: AddableType, b: AddableType) -> AddableType:
  return a + b

Стандартным предопределенным типом переменной в модуле typing является AnyStr. Используйте его для нескольких аннотаций, которые могут быть bytes или unicode, и все они должны быть одного типа.

from typing import AnyStr
def check_length(x: AnyStr) -> AnyStr:
  if len(x) <= 42:
    return x
  raise ValueError()

3.19.11 Строковые типы

Правильный способ аннотирования строк зависит от того, для каких версий Python предназначен код.

Для кода Python 3 предпочтительно использовать str. Text также применим. Будьте последовательны в использовании того или иного способа.

Для кода, совместимого с Python 2, используйте Text. В некоторых редких случаях, str может иметь смысл; обычно для обеспечения совместимости, когда типы возвращаемых значений не совпадают между двумя версиями Python. Избегайте использования unicode: его нет в Python 3.

Причина этого несоответствия заключается в том, что str имеет различное значение в зависимости от версии Python.

Пример неправильного использования:

def py2_code(x: str) -> unicode:
  ...

Для кода, который имеет дело с бинарными данными, используйте bytes:

def deals_with_binary_data(x: bytes) -> bytes:
  ...

Для кода, совместимого с Python 2, который обрабатывает текстовые данные (str или unicode для Python 2, str для Python 3), используйте Text. Для Python 3 кода, который обрабатывает текстовые данные, используйте str.

from typing import Text
...
def py2_compatible(x: Text) -> Text:
  ...
def py3_only(x: str) -> str:
  ...

Если тип может быть байтовым или текстовым, используйте Union, с соответствующим типом текста.

from typing import Text, Union
...
def py2_compatible(x: Union[bytes, Text]) -> Union[bytes, Text]:
  ...
def py3_only(x: Union[bytes, str]) -> Union[bytes, str]:
  ...

Если все строковые типы определенной функции всегда одинаковы, например, если тип возвращаемого значения совпадает с типом аргумента (как в приведенном выше коде), используйте AnyStr.

Соблюдение этих правил упростит процесс переноса кода на Python 3.

3.19.12 Импорты для типов

Для классов из модуля typing всегда импортируйте сам класс. Явно разрешено импортировать несколько определенных классов в одной строке из модуля typing. Например:

from typing import (
    Any, 
    Dict, 
    Optional,
)

Учитывая, что этот способ импорта из typing добавляет элементы в локальное пространство имен, любые имена в typing должны обрабатываться так же, как ключевые слова, и не переопределяться в вашем типизированном или не типизированном коде Python. Если есть конфликт между типом и существующим именем в модуле, импортируйте его, используя import x as y.

from typing import (
    Any as AnyType,
)

3.19.13 Условные импорты

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

Импорты, которые необходимы только для аннотаций типов, могут быть помещены в блок if TYPE_CHECKING:.

  • На условно импортированные типы нужно ссылаться как на строки, чтобы они были совместимы с Python 3.6, где аннотации фактически выполняются.
  • Следует определять только те объекты, которые используются исключительно для типизации; это правило распространяется на псевдонимы. В противном случае это будет ошибка времени выполнения, поскольку модуль не был импортирован во время выполнения.
  • Блок должен быть размещен сразу после обычных импортов.
  • В списке импорта для типизации не должно быть пустых строк.
  • Отсортируйте этот список, как если бы это был обычный список импортов.
import typing
if typing.TYPE_CHECKING:
  import sketch
def f(x: "sketch.Sketch"): ...

3.19.14 Циклически зависимости

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

Замените модули, которые создают циклические зависимости, на Any. Установите псевдоним с понятным именем, и используйте настоящее имя типа из этого модуля (любой атрибут у Any считается за Any). Определения псевдонимов следует отделять от последнего импорта одной строкой.

from typing import Any

some_mod = Any  # some_mod.py импортирует этот модуль.
...

def my_method(self, var: "some_mod.SomeType") -> None:
    ...

3.19.15 Generic-типы

При аннотации предпочитайте указывать параметры типа для универсальных типов; в противном случае, предполагается, что параметры generic-типов будут равны Any.

def get_names(employee_ids: List[int]) -> Dict[int, Any]:
    ...
# Обе функции интерпретируются как get_names(employee_ids: List[Any]) -> Dict[Any, Any]
def get_names(employee_ids: list) -> Dict:
    ...

def get_names(employee_ids: List) -> Dict:
    ...

Если лучшим параметром типа для generic-типа является Any, сделайте его явным, но помните, что во многих случаях TypeVar может быть более подходящим:

def get_names(employee_ids: List[Any]) -> Dict[Any, Text]:
    """Возвращает соответствие ID и имен сотрудников для соответствующих ID сотрудников."""
T = TypeVar('T')
def get_names(employee_ids: List[T]) -> Dict[T, Text]:
    """Возвращает соответствие ID и имен сотрудников для соответствующих ID сотрудников."""

4 Заключение

БУДЬТЕ ПОСЛЕДОВАТЕЛЬНЫМИ.

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

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

5 Словарь терминов

  • Docstring - это строковый литерал, являющийся первой инструкцией в определении модуля, функции, класса или метода. Такая строка становится доступна при обращении с специальному атрибуту __doc__ этого объекта.

  • Pythonic (Pythonic-way) - это код или подход к кодированию, который не только позволяет правильно выстроить синтаксис, но и следует соглашениям сообщества Python, а также использует язык именно так, как и задумывалось изначально.