Подтвердить что ты не робот

Почему функции всегда возвращают один и тот же тип?

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

def x(foo):
 if 'bar' in foo:
  return (foo, 'bar')
 return None

Я думаю, лучшее решение было бы

def x(foo):
 if 'bar' in foo:
  return (foo, 'bar')
 return ()

Разве не было бы более разумным, если бы разумно было вернуть None, чтобы создать новый пустой кортеж, или эта разница во времени слишком мала, чтобы заметить даже в больших проектах?

4b9b3361

Ответ 1

Почему функции возвращают значения согласованного типа? Чтобы выполнить следующие два правила.

Правило 1 - функция имеет "тип" - входы, отображаемые на выходы. Он должен возвращать согласованный тип результата, или он не является функцией. Это беспорядок.

Математически, мы скажем, что некоторая функция F является отображением из области D в диапазон R. F: D -> R. Область и диапазон образуют "тип" функции. Типы ввода и тип результата также важны для определения функции, как имя или тело.

Правило 2 - когда у вас есть "проблема" или вы не можете вернуть правильный результат, создайте исключение.

def x(foo):
    if 'bar' in foo:
        return (foo, 'bar')
     raise Exception( "oh, dear me." )

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

"Разве не разумнее было бы вернуть память" Нет "? Неверный вопрос.

Дело не в оптимизации памяти за счет ясного, понятного, очевидного кода.

Ответ 2

Не так ясно, что функция всегда должна возвращать объекты ограниченного типа или что возвращение None является неправильным. Например, re.search может возвращать объект _sre.SRE_Match или объект NoneType:

import re
match=re.search('a','a')

type(match)
# <type '_sre.SRE_Match'>

match=re.search('a','b')

type(match)
# <type 'NoneType'>

Разработанный таким образом, вы можете проверить соответствие с idiom

if match:
    # do xyz

Если разработчикам потребовался re.search для возврата объекта _sre.SRE_Match, тогда идиома должна была бы измениться на

if match.group(1) is None:
    # do xyz

Не было бы никакого существенного выигрыша, требуя, чтобы re.search всегда возвращал объект _sre.SRE_Match.

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

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

Сказав это, есть прекрасная простота для функций, возвращающих объекты, все из которых имеют одни и те же свойства. (Утиная печать, а не статическая типизация, является способом python!) Это может позволить вам объединить функции: foo (bar (baz))) и с уверенностью знать тип объекта, который вы получите на другом конце.

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

Ответ 3

Лучшая практика в том, что должна возвращать функция, сильно варьируется от языка к языку и даже между различными проектами Python.

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

ret_val = x()
if ret_val:
     do_stuff(ret_val)

и по-прежнему разрешать вам перебирать его без тестирования:

for child in x():
    do_other_stuff(child)

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

Ответ 4

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

Ответ 5

Вот мои мысли обо всем этом, и я попытаюсь также объяснить, почему я считаю, что принятый ответ в основном неверен.

Прежде всего programming functions != mathematical functions. Ближе всего вы можете перейти к математическим функциям, если вы выполняете функциональное программирование, но даже тогда есть много примеров, которые говорят иначе.

  • Функции не должны иметь вход
  • Функции не должны иметь вывод
  • Функции не должны отображать входные данные на выходе (из-за двух предыдущих точек пули)

Функция в терминах программирования должна рассматриваться просто как блок памяти с пуском (точка входа функции), тело (пустое или иное) и точка выхода (одна или несколько в зависимости от реализации) все которые существуют для повторного использования кода, который вы написали. Даже если вы не видите этого, функция всегда "возвращает" что-то. Это что-то на самом деле является адресом следующего оператора сразу после вызова функции. Это то, что вы увидите во всей своей славе, если вы сделаете какое-то действительно низкоуровневое программирование на языке ассемблера (я смею пойти на лишнюю милю и сделать какой-то машинный код вручную, как Линус Торвальдс, который так часто упоминает об этом во время его семинары и интервью: D). Кроме того, вы также можете внести некоторый вклад, а также вывести какой-то вывод. Вот почему

def foo():
  pass

является абсолютно правильной частью кода.

Итак, почему нужно было бы возвращать несколько типов? Ну... Это совсем не так, если ты не злоупотребляешь им. Это, конечно, проблема плохого программирования и/или незнание того, что может использовать язык, который вы используете.

Разве не было бы более разумным, если бы разумно было вернуть None, чтобы создать новый пустой кортеж, или эта разница во времени слишком мала, чтобы заметить даже в больших проектах?

Насколько я знаю - да, возвращение объекта NoneType было бы намного дешевле по памяти. Вот небольшой эксперимент (возвращаемые значения - байты):

>> sys.getsizeof(None)
16
>> sys.getsizeof(())
48

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

Однако вы также должны учитывать код, который находится вокруг вызова функции, и как он обрабатывает все ваши функции. Вы проверяете NoneType? Или вы просто проверяете, имеет ли возвращенный кортеж длину 0? Это распространение возвращаемого значения и его типа (NoneType по сравнению с пустым кортежем в вашем случае) может быть более утомительным для обработки и вдувания вашего лица. Не забывайте - сам код загружается в память, поэтому, если обработка NoneType требует слишком большого количества кода (даже небольших фрагментов кода, но в большом количестве), лучше оставить пустой кортеж, что также позволит избежать путаницы в умах люди используют вашу функцию и забывают, что она фактически возвращает 2 типа значений.

Говоря о возвращении нескольких типов значений, это та часть, в которой я согласен с принятым ответом (но только частично) - возврат одного типа делает код более удобным для обслуживания без сомнения. Гораздо проще проверить только для типа A, затем A, B, C,... и т.д.

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

Боковое примечание. Вы можете немного прочитать о метаклассах и динамических классах в этой хорошей обзорной статье с большим количеством примеров.

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

  • Duck typing - часто является частью динамических языков ввода, которые Python является представителем
  • Factory шаблон проектирования метода - в основном это функция, которая возвращает различные объекты на основе ввода, который он получает.

Наконец, ваша функция возвращает один или несколько типов, полностью зависит от проблемы, которую вы должны решить. Можно ли злоупотреблять этим полиморфным поведением? Конечно, как и все остальное.

Ответ 6

Если x вызывается как

foo, bar = x(foo)

return None приведет к

TypeError: 'NoneType' object is not iterable

если 'bar' не находится в foo.

Пример

def x(foo):
    if 'bar' in foo:
        return (foo, 'bar')
    return None

foo, bar = x(["foo", "bar", "baz"])
print foo, bar

foo, bar = x(["foo", "NOT THERE", "baz"])
print foo, bar

Это приводит к:

['foo', 'bar', 'baz'] bar
Traceback (most recent call last):
  File "f.py", line 9, in <module>
    foo, bar = x(["foo", "NOT THERE", "baz"])
TypeError: 'NoneType' object is not iterable

Ответ 7

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

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