Сообщить об ошибке.

Способы создания строгих сигнатур функций Python

Содержание:


Зачем использовать строгие сигнатуры функций?

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

Меньше нужно учитывать, когда функция меняется.

Пример из реальной жизни. Например есть метод API под названием get(), который извлекает документ из Elasticsearch по его идентификатору. В разработанном API, сигнатура функции должна была измениться из-за того, что аргумент doc_type устарел на стороне сервера и запланирован для удаления в следующей версии.

# Старая сигнатура функции
def get(index, id, doc_type=None, params=None, ...):

# Новая сигнатура функции
def get(index, id, params=None, ...):

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

client.get("1", "2", "3")

# В старой версии, указанные выше аргументы будут назначены так:
# {index=1, id=2, doc_type=3}

# А в новой - аргументы будут назначены так:
# {index=1, id=2, params=3} (нехорошо!)

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

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

def process_data(data, /, *, encoding="utf-8"):

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

# Можно переименовать 'data' -> 'data_or_list', не нарушая чей-либо код.
def process_data(data_or_list, /, *, encoding="utf-8"):

Переименование позиционного аргумента data ничего не нарушит. А вот если бы data был ключевым аргументом, то возник бы риск нарушения работы пользовательского кода.

Соответствие между документацией и использованием.

В идеале документация должна выбирать единственный способ использования каждой функции и быть последовательной внутри себя. Почему бы не потребовать от пользователей использовать функции так, как они задокументированы? Если бы urllib3 писалась сегодня, то сигнатура функции для request() могла бы выглядеть так: method и url были бы только позиционными, а все остальные параметры были бы только ключевыми аргументами:

def request(method, url, /, *, headers=None, ...):

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

# Мы привыкли видеть такой код повсюду:
request("GET", "https://example.com", headers={...})

# А это, не сразу узнаваемо:
request(method="GET", url="https://example.com")
request("GET", "https://example.com", {...})

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

Ограничение способов передачи аргументов в функцию Python.

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

def process_data(data, encoding="ascii"):
    # здесь идет обработка данных
    pass

Есть один аспект, о котором многие программисты знают, но не понимают, как его можно записать в определении функции: это то, как вызывающая сторона должна передавать каждый аргумент в функцию? Для приведенной выше функции можно задокументировать следующие правильные варианты передачи аргументов:

process_data("input")
# и
process_data("input", encoding="utf-8")

Здесь не нужно указывать ключевое слово data=, чтобы пользователи могли сделать вывод о том, каким может быть первый аргумент. Имя параметра подсказывает имя функции: process_data(). С другой стороны, значение "utf-8" аргумента encoding не очевидно, если видеть только значение аргумента. Учитывая это, для значения "utf-8" лучше использовать ключевой аргумент.

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

# Все позиционные параметры, здесь сложнее
# определить параметр для чего нужен 'utf-8'.
process_data("input", "utf-8")

# Использование `data` в качестве ключевого аргумента, 
# но не так чисто, так как дублируется термин "data".
process_data(data="input")
process_data(data="input", encoding="utf-8")

# Все аргументы передаются как ключевые и являются триггерными.
process_data(encoding="utf-8", data=b"input")

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

Передача в функцию только ключевых аргументов.

Определение в функции только ключевых аргументов выглядит следующим образом:

def process_data(data, *, encoding="ascii"):

Обратите внимание на символ звездочки * между data и encoding. Звездочка означает, что все параметры справа в сигнатуре функции не могут передаваться как позиционные аргументы. Эти параметры теперь относятся только к ключевым словам.

Теперь, когда параметр encoding содержит только ключевое слово, как изменится список возможных вариантов использования?

# Правильное использование функции пользователями:
process_data("input")
process_data("input", encoding="utf-8")

# Поднимет исключение `TypeError`:
process_data("input", "utf-8")

# Варианты использования функции:
process_data(data="input")
process_data(data="input", encoding="utf-8")
process_data(encoding="utf-8", data="input")

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

Передача аргументов в функцию только по позиции.

Передача аргументов в функцию только по позиции стала возможна с версии Python 3.8, следовательно ее нельзя использовать в проектах, поддерживающих Python 3.7.

Определить аргумент "только для позиции" в Python можно следующим образом:

def process_data(data, /, encoding="ascii"):

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

# Это вызовет ошибку TypeError
process_data(data=b"input")

Многие функции в стандартной библиотеке не следуют типичным правилам для параметров. Примером может служить функция pow(). При вызове с ключевыми аргументами pow() завершится ошибкой, потому что базовая реализация C принимает только позиционные аргументы:

# Вывод `help()` для `pow()` использовал символ `/` даже до Python 3.8.
>>> help(pow)
...
pow(x, y, z=None, /)
...

>>> pow(x=5, y=3)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: pow() takes no keyword arguments

Объединяем два правила.

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

def process_data(data, /, *, encoding="ascii"):

А теперь, когда data являются только позиционным, а encoding только по ключевым аргументом, посмотрим, как можно использовать эту функцию:

# Правильное использование функции пользователями:
process_data("input")
process_data("input", encoding="utf-8")

# Остальные варианты использования 
# вызывают ошибку `TypeError`:
process_data("input", "utf-8")
process_data(data="input")
process_data(data="input", encoding="utf-8")
process_data(encoding="utf-8", data="input")