Почему функции всегда должны возвращать один и тот же тип? - PullRequest
24 голосов
/ 03 декабря 2009

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

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

Полагаю, лучшим решением было бы

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

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

Ответы [ 7 ]

35 голосов
/ 03 декабря 2009

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

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

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

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

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

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

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

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

18 голосов
/ 03 декабря 2009

Не очень понятно, что функция всегда должна возвращать объекты ограниченного типа, или что возвращение 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'>

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

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, я золотой ...»

10 голосов
/ 03 декабря 2009

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

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

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

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

for child in x():
    do_other_stuff(child)

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

7 голосов
/ 20 сентября 2015

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

Прежде всего 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 / C ++).

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

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

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

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

5 голосов
/ 03 декабря 2009

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

4 голосов
/ 03 декабря 2009

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

foo, bar = x(foo)

возврат 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
2 голосов
/ 06 июля 2010

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

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

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