О некоторых причинах, по которым лучше использовать строгие сигнатуры функций, можно узнать прочитав разделы мотивация 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, которая охватывает все это:
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")