Kodomo

Пользователь

Вызовы функций: вся правда

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

Даже в такой вещи, как определение функции, в питоне есть ещё несколько полезных вещей.

Функции с неопределённым количеством аргументов

Вспомним древний пример. Функция min. Некоторые из вас, прочитав постановку задачи и сравнив её с встроенной функцией, сказали, что нее, наш результат не совпадает с ней.

С встроенной функцией min можно писать:

   1 >>> min([1, 2])
   2 1
   3 >>> min(2, 3)
   4 2

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

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

   1 def min(*args):
   2     if len(args) == 1:
   3         args = args[0]
   4     ... # дальше так же, как и раньше

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

Ну а имея такое средство, мы можем уже, наконец, свести функцию min, как она описана в питоне, к нашей функции min в две строчки.

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

Теперь вспомним, как мы решали задачу про графы на далёком 10-м занятии. Мы тогда вершины описали, кажется, так:

   1 class Vertex(object):
   2     def __init__(self, neighbours):
   3         self.neighbours = neighbours

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

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

   1 class Vertex(object):
   2     def __init__(self, neighbours=[]):
   3         self.neighbours = neighbours

Читается это так:

  1. определить класс Vector,
  2. определить в нём конструктор, который получает необязательный аргумент neighbours со значением по умолчанию []
  3. положить значение аргумента neighbours в self в поле neighbours

Теперь мы можем пользоваться этим классом, что указывая список соседей, что не указывая:

   1 >>> v1 = Vertex()
   2 >>> v2 = Vertex()
   3 >>> v3 = Vertex([v1, v2])

Осторожно, списки!

Тут мы, правда, сразу напоролись на одну очень тонкую и неприятную мелочь (которая имеет свои причины, но вопреки стилю всего остального питона, ведёт себя неожиданно для начинающего программиста). Дело в том, что значение по умолчанию вычисляется один раз: в тот момент, когда мы описываем функцию. То есть, если рисовать "диаграмму отношений" (оно же "картинку с червяками и стрелочками"), то получится, что у всех, кто использовал значение по умолчанию, это будет один и тот же объект (все стрелочки указывают на одного червяка). И если мы этот объект начнём менять, получится скорее всего не то, чего мы хотим. В нашем случае, если мы скажем: v1.neighbours.append(v3), и посмотрим в v2.neighbours, то мы и там увидим вершину v3. Что никуда не годится.

Поэтому принято, чтобы значения по умолчанию всегда были объектами неизменяемых типов: None, числа, строки, логические константы, тупли.

В нашем примере правильно было бы написать так:

   1 class Vertex(object):
   2     def __init__(self, neighbours=None):
   3         if neighbours is None:
   4             neighbours = []
   5         self.neighbours = neighbours

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

Именованная передача аргументов

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

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

   1 class Vertex(object):
   2     def __init__(self, neighbours=None, name=None, sw_id=None, pdb_id=None):
   3         if neighbours is None:
   4             neighbours = []
   5         self.neighbours = neighbours
   6         self.name = name
   7         self.sw_id = sw_id
   8         self.pdb_id = pdb_id

Как же нам теперь создать вершину, для которой нам пока известен только её pdb-код?

Можно так:

   1 >>> v4 = Vertex(None, None, None, None, "1m5x")

Только это выглядит ужасно, да ещё нам нужно точно знать, что используется в качестве значения по умолчания для какого аргумента. Это убивает весь смысл значений по умолчанию: какое же это умолчание, если мы их явно все передали?

На этот случай в питоне есть специальный синтаксис: именованная передача параметра (named parameter passing – можно перевести и как "передача именованного параметра", английский язык в этом месте коварно расплывчат). Пишется это столь же просто:

   1 >>> v4 = Vertex(pdb_id="1m5x")

Функции с неопределённым количеством именованных аргументов

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

Мы можем сказать так:

   1 class Vertex(object):
   2     def __init__(self, neighbours=None, **kwargs):
   3         if neighbours is None:
   4             neighbours = []
   5         self.neighbours = neighbours
   6         self.data = kwargs

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

   1 >>> v1 = Vertex()
   2 >>> v2 = Vertex()
   3 >>> v3 = Vertex([v1, v2])
   4 >>> v4 = Vertex(pdb_id="1m5x")

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

   1 >>> v5 = Vertex(neighbours=[v1, v2], name="v5", taxon="e.coli")

В переменной kwargs внутри нашей функции при этом оказывается словарь, где ключи – имена передаваемых параметров (строки), а значения ключей и есть значения передаваемых параметров. В этот словарь попадают только те аргументы, которые не были отловлены каким-нибудь другим правилом предачи параметров. В нашем примере в v5.data окажется: { 'name': 'v5', 'taxon': 'e.coli } – а параметр neighbours туда не попадёт.

Dirty hack

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

Есть один грязный хак, который поможет нам вернуть всё обратно. Помните, что делает функция vars? (Если нет, вспоминайте). Второе, что нужно вспомнить (или подглядеть в хелпах) – это метод update для словарей. Объединив эти знания с нашей функцией, мы получим:

   1 class Vertex(object):
   2     def __init__(self, neighbours=None, **kwargs):
   3         if neighbours is None:
   4             neighbours = []
   5         vars(self).update(kwargs)
   6         self.neighbours = neighbours

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

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

Зло в ней кроется в двух местах:

  1. мы никак не контролируем, что именно пользователь положит в наш объект. Это значит, что он может переписать поверх уже существующих данных – и даже если мы передвинем vars(self).update(kwargs) в начало функции, чтобы это не пользователь переписывал существенные для нас данные, а мы переписывали существенные для пользователя данные, хорошо от этого никому не будет.

  2. если мы при такой записи где-то дальше по коду используем какие-нибудь из полей, которые предполагается, что созданы вызовом vars(self).update(kwargs), то наш код становится очень трудно читать; даже тщательная документация всё равно не будет лишать читателя ощущения, что что-то тут не так.

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

Распаковка параметров

Предположим, у нас есть словарь:

   1 >>> data = { 'name': 'v5', 'taxon': 'e.coli', 'neighbours': [v1, v2] }

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

   1 >>> v5 = Vertex(name=data['name'], taxon=data['taxon'], neighbours=data['neighbours'])

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

   1 >>> v5 = Vertex(**data)

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

   1 >>> args = ["file.txt", "w"]
   2 >>> file = open(*args)

"Обёртки" к функциям

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

Обёрткой к функции f называют функцию, которая вызывает f, и либо как-нибудь для неё готовит аргументы, либо как-нибудь обрабатывает её результат.

Для написания обёрток зачастую бывает очень полезно совмещать получение произвольных аргументов и распаковку аргументов.

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

Положим, мы хотим сделать функцию, которая сортирует список задом-наперёд. Мы можем посмотреть в описание встроенной питонской функции sorted и увидеть такой заголовок: sorted(list, cmp=None, key=None, reversed=False). (Я не буду тут пока что рассказывать про то, зачем у неё есть параметры cmp и key – это тема одного-двух рассказов следующего семестра). Соответственно, мы можем в описании нашей функции дословно воспроизвести точно такое же описание аргументов и передавать их все в sorted. А можно сделать проще:

   1 def rsorted(*args, **kw):
   2     return reversed(sorted(*args, **kw))

Такую запись я бы читал так: мы описали функцию rsorted, которая принимает те же параметры, что и sorted, и передаёт их ей, а затем разворачивает полученный из неё отсортированный список.