Лучший способ вернуть несколько значений из функции?

Канонический способ возврата нескольких значений на языках, которые его поддерживают, часто бывает кортеж.

Вариант: использование кортежа

Рассмотрим этот тривиальный пример:

def f(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return (y0, y1, y2)

Однако это быстро становится проблематичным по мере увеличения количества возвращаемых значений. Что, если вы хотите вернуть четыре или пять значений? Конечно, вы можете и дальше использовать их кортежи, но легко забыть, какое значение где находится. Также довольно некрасиво распаковывать их, где бы вы их ни хотели получить.

Вариант: Использование словаря

Следующим логическим шагом, кажется, будет введение некой «записи записи». В Python очевидный способ сделать это - использовать dict.

Учтите следующее:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0': y0, 'y1': y1 ,'y2': y2}

(Для ясности, y0, y1 и y2 предназначены только как абстрактные идентификаторы. Как уже указывалось, на практике вы должны использовать значимые идентификаторы.)

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

result['y0']

Вариант: использование класса

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

class ReturnValue:
  def __init__(self, y0, y1, y2):
     self.y0 = y0
     self.y1 = y1
     self.y2 = y2

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return ReturnValue(y0, y1, y2)

В Python два предыдущих, возможно, очень похожи с точки зрения сантехники - в конце концов, { y0, y1, y2 } просто попадают в записи во внутреннем __dict__ файла ReturnValue.

Однако для крошечных объектов Python предоставляет одну дополнительную функцию - атрибут __slots__. Класс можно выразить как:

class ReturnValue(object):
  __slots__ = ["y0", "y1", "y2"]
  def __init__(self, y0, y1, y2):
     self.y0 = y0
     self.y1 = y1
     self.y2 = y2

Из Справочного руководства по Python:

Объявление __slots__ принимает последовательность переменных экземпляра и резервирует в каждом экземпляре ровно достаточно места для хранения значения для каждой переменной. Место сохраняется, потому что __dict__ не создается для каждого экземпляра.

Вариант: использование класса данных (Python 3.7+)

Используя новые классы данных Python 3.7, верните класс с автоматически добавленными специальными методами, набором текста и другими полезными инструментами:

@dataclass
class Returnvalue:
    y0: int
    y1: float
    y3: int

def total_cost(x):
    y0 = x + 1
    y1 = x * 3
    y2 = y0 ** y3
    return ReturnValue(y0, y1, y2)

Вариант: использование списка

Еще одно предложение, которое я упустил из виду, исходит от Ящера Билла:

def h(x):
  result = [x + 1]
  result.append(x * 3)
  result.append(y0 ** y3)
  return result

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

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

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

Вопрос

После пространной преамбулы возникает неизбежный вопрос. Как вы думаете, какой метод лучше?


person saffsd    schedule 10.12.2008    source источник
comment
В ваших прекрасных примерах вы используете переменную y3, но если y3 не объявлен глобальным, это приведет к NameError: global name 'y3' is not defined, возможно, просто используйте 3?   -  person hetepeperfan    schedule 19.12.2013
comment
Многие замечательные вопросы с отличными ответами закрываются, потому что возникает ключевое слово «мнение». Вы можете утверждать, что вся SO основана на мнении, но это мнение, основанное на фактах, ссылках и конкретном опыте. Тот факт, что кто-то спрашивает, что, по вашему мнению, лучше, не означает, что они спрашивают личное мнение, основанное на реальных фактах, ссылках и конкретном опыте. Они почти наверняка запрашивают именно такое мнение, основанное на фактах, ссылках и конкретных знаниях, которые человек использовал для формирования мнения, и документально подтвержденный ими.   -  person NeilG    schedule 08.08.2019
comment
@hetepeperfan не нужно менять 3, и ни один из них не определяет y3 в глобальном масштабе, вы также можете использовать локальное имя y3, которое также будет выполнять ту же работу.   -  person okie    schedule 28.10.2019


Ответы (14)


Именованные кортежи были добавлен в 2.6 для этой цели. См. Также аналогичный встроенный пример в os.stat.

>>> import collections
>>> Point = collections.namedtuple('Point', ['x', 'y'])
>>> p = Point(1, y=2)
>>> p.x, p.y
1 2
>>> p[0], p[1]
1 2

В последних версиях Python 3 (я думаю, 3.6+) новая библиотека typing получила _ 3_, чтобы упростить создание именованных кортежей и сделать их более мощными. Наследование от typing.NamedTuple позволяет использовать строки документации, значения по умолчанию и аннотации типов.

Пример (из документации):

class Employee(NamedTuple):  # inherit from typing.NamedTuple
    name: str
    id: int = 3  # default value

employee = Employee('Guido')
assert employee.id == 3
person A. Coady    schedule 10.12.2008
comment
Это единственно правильный ответ, потому что это единственная каноническая структура, которую OP не рассматривал, и потому, что она решает его проблему управления длинными кортежами. Должен быть отмечен как принятый. - person airstrike; 20.07.2014
comment
Что ж, логическим обоснованием дизайна namedtuple является меньший объем памяти след для массовых результатов (длинные списки кортежей, например, результаты запросов к БД). Для отдельных элементов (если рассматриваемая функция не часто вызывается) словари и классы тоже подойдут. Но именованные кортежи и в этом случае являются хорошим решением. - person Lutz Prechelt; 23.02.2016
comment
Думаю, лучший ответ. Я не сразу понял одну вещь - вам не нужно объявлять namedtuple во внешней области видимости; ваша функция сама может определять контейнер и возвращать его. - person wom; 15.12.2016
comment
@wom: Не делай этого. Python не прилагает усилий для унификации определений namedtuple (каждый вызов создает новое), создание класса namedtuple относительно дорого как с точки зрения ЦП, так и с точки зрения памяти, а все определения классов по своей сути включают циклические ссылки (поэтому на CPython вы ждете циклического GC run для их выпуска). Это также делает невозможным pickle класс (и, следовательно, в большинстве случаев невозможно использовать экземпляры с multiprocessing). Каждое создание класса в моем 3.6.4 x64 потребляет ~ 0,337 мс и занимает чуть менее 1 КБ памяти, что сводит на нет любую экономию экземпляра. - person ShadowRanger; 20.04.2018
comment
Отмечу, Python 3.7 улучшил скорость создания новых namedtuple классы; затраты на ЦП снижаются примерно в 4 раза, но они по-прежнему примерно в 1000 раз выше, чем стоимость создания экземпляра и стоимость памяти для каждого класса остаются высокими (я ошибся в своем последнем комментарии по поводу менее 1 КБ для класса, _source сам по себе обычно составляет 1,5 КБ; _source удален в 3.7, поэтому он, вероятно, ближе к первоначальному требованию чуть менее 1 КБ на создание класса). - person ShadowRanger; 20.04.2018
comment
Как жаль, что этого нет в стандартной библиотеке. Нужно import collections. Поэтому продолжаю пользоваться словарями. - person Serge Stroobandt; 16.09.2018
comment
@SergeStroobandt - это часть стандартной библиотеки, но не встроенная. Вам не нужно беспокоиться, что он может быть не установлен в другой системе с Python ›= 2.6. Или вы просто возражаете против лишней строчки кода? - person Justin; 09.10.2018
comment
Разве сейчас не будет лучшим вариантом types.SimpleNamespace? - person jaaq; 29.07.2019
comment
@jaaq Почему так лучше? - person endolith; 26.10.2019
comment
@endolith, потому что вы можете добавлять значения после его создания, то есть вы можете добавлять результаты в пространство имен retval, как только вы их получите, и вам не нужно ждать, пока вы сможете поместить их в именованный кортеж сразу. Иногда может значительно уменьшить беспорядок в более крупных функциях. - person jaaq; 27.10.2019
comment
@jaaq: Проблема в том, что types.SimpleNamespace - прославленный dict. Это означает значительно более высокую стоимость памяти на один экземпляр, повышенную стоимость создания, отсутствие возможности распаковки, как tuple, и т. Д. - person ShadowRanger; 27.11.2019

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

Возврат словаря с ключами "y0", "y1", "y2" и т. Д. Не дает никаких преимуществ перед кортежами. Возврат экземпляра ReturnValue со свойствами .y0, .y1, .y2 и т. Д. Также не дает никаких преимуществ перед кортежами. Вам нужно начать называть вещи, если вы хотите чего-то достичь, и вы в любом случае можете сделать это с помощью кортежей:

def get_image_data(filename):
    [snip]
    return size, (format, version, compression), (width,height)

size, type, dimensions = get_image_data(x)

ИМХО, единственный хороший метод, помимо кортежей, - это возвращать реальные объекты с соответствующими методами и свойствами, как вы получаете от re.match() или open(file).

person too much php    schedule 10.12.2008
comment
Вопрос - есть ли разница между size, type, dimensions = getImageData(x) и (size, type, dimensions) = getImageData(x)? То есть, имеет ли значение перенос левой части кортежного присваивания? - person Reb.Cabin; 28.12.2014
comment
@ Reb.Cabin Нет разницы. Кортеж обозначается запятой, а круглые скобки используются только для группировки элементов. Например ,(1) - это целое число, а (1,) или 1, - кортеж. - person phil; 14.05.2015
comment
Повторный возврат словаря с ключами y0, y1, y2 и т. Д. Не дает никаких преимуществ перед кортежами: словарь имеет то преимущество, что вы можете добавлять поля в возвращаемый словарь, не нарушая существующий код. - person ostrokach; 29.08.2016
comment
Повторный возврат словаря с ключами y0, y1, y2 и т. Д. Не дает никаких преимуществ перед кортежами: он также более читабелен и менее подвержен ошибкам, поскольку вы получаете доступ к данным на основе его имени, а не положения. - person Denis Dollfus; 16.01.2020

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

def f():
    return True, False
x, y = f()
print(x)
print(y)

дает:

True
False
person Joe    schedule 14.04.2016
comment
Вы все еще возвращаете коллекцию. Это кортеж. Я предпочитаю скобки, чтобы сделать его более ясным. Попробуйте следующее: type(f()) возвращает <class 'tuple'>. - person Igor; 17.05.2016
comment
@Igor: Нет причин делать аспект tuple явным; На самом деле не важно, чтобы вы возвращали tuple, это идиома для возврата периода с несколькими значениями. По той же причине вы опускаете скобки с идиомой свопа, x, y = y, x, множественная инициализация x, y = 0, 1 и т. Д .; конечно, это делает tuples под капотом, но нет причин делать это явным, поскольку tuples вовсе не главное. В учебнике Python множественное назначение задолго до того, как это даже касается tuples. - person ShadowRanger; 20.04.2018
comment
@ShadowRanger любая последовательность значений, разделенных запятой в правой части =, является кортежем в Python с круглыми скобками или без них. Так что на самом деле здесь нет явного или неявного. a, b, c - такой же кортеж, как (a, b, c). Когда вы возвращаете такие значения, под капотом также не создается кортеж, потому что это просто простой кортеж. OP уже упоминал кортежи, поэтому на самом деле нет разницы между тем, что он упомянул, и тем, что показывает этот ответ. Никто - person Ken4scholars; 23.01.2019
comment
@ Ken4scholars: Да, определение языка рассматривает их как эквивалентные. Тем не менее, настоящие программисты склонны рассматривать круглые скобки как указание на структуру данных последовательности, тогда как без них это просто несколько вещей. Я согласен, что нет никакой разницы в поведении, но лишние скобки меняют то, как большинство людей, которых я знаю, думают об этом. Согласен, это не отличается от примера OP по поведению (со всеми теми же ограничениями, которые OP не любил). - person ShadowRanger; 24.01.2019
comment
Это буквально первый вариант, предложенный в вопросе - person endolith; 26.10.2019
comment
@endolith Два раза, когда парень задает вопрос (Как мне вернуть несколько значений? и Как вы вернуть несколько значений?), отвечает этот ответ. Иногда менялся текст вопроса. И это вопрос, основанный на мнении. - person Joe; 19.02.2020

Голосую за словарь.

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

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

Если вас беспокоит поиск типа, используйте описательные ключи словаря, например, «список значений x».

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }
person monkut    schedule 10.12.2008
comment
После многих лет программирования я стремлюсь к любой структуре данных и функциям, которые требуются. Сначала функция, вы всегда можете выполнить рефакторинг по мере необходимости. - person monkut; 03.06.2014
comment
Как мы можем получить значения в словаре, не вызывая функцию несколько раз? Например, если я хочу использовать y1 и y3 в другой функции? - person Matt; 02.11.2014
comment
присвоить результаты отдельной переменной. result = g(x); other_function(result) - person monkut; 04.11.2014
comment
@monkut да. Этот способ также позволяет передавать результат нескольким функциям, которые принимают разные аргументы из результата, без необходимости каждый раз специально ссылаться на определенные части результата. - person Gnudiff; 08.01.2018

Другой вариант - использовать генераторы:

>>> def f(x):
        y0 = x + 1
        yield y0
        yield x * 3
        yield y0 ** 4


>>> a, b, c = f(5)
>>> a
6
>>> b
15
>>> c
1296

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

person rlms    schedule 23.02.2014
comment
Это кажется наиболее чистым решением с чистым синтаксисом. Есть ли у этого недостатки? Если вы не используете всю прибыль, есть ли «неизрасходованные» доходы, которые могут вам навредить? - person Jiminion; 16.12.2015
comment
Это может быть понятно, но совсем не интуитивно. Как может кто-то, кто никогда раньше не сталкивался с этим шаблоном, знал, что автоматическая распаковка кортежа вызовет каждый yield? - person coredumperror; 07.01.2016
comment
@CoreDumpError, генераторы - это всего лишь ... генераторы. Нет никакой внешней разницы между def f(x): …; yield b; yield a; yield r и (g for g in [b, a, r]), и оба легко преобразуются в списки или кортежи и, как таковые, будут поддерживать распаковку кортежей. Форма генератора кортежей следует функциональному подходу, в то время как форма функции является обязательной и позволяет управлять потоком и назначать переменные. - person sleblanc; 28.01.2019

Я предпочитаю:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }

Кажется, все остальное - это просто дополнительный код для того же.

person UnkwnTech    schedule 10.12.2008
comment
Кортежи легче распаковать: y0, y1, y2 = g () с диктовкой, которую вы должны сделать: result = g () y0, y1, y2 = result.get ('y0'), result.get ('y1' ), result.get ('y2'), что немного некрасиво. У каждого решения есть свои «плюсы» и «минусы». - person Oli; 10.12.2008

Я предпочитаю использовать кортежи, когда кортеж кажется «естественным»; координаты являются типичным примером, когда отдельные объекты могут стоять сами по себе, например в одноосных вычислениях только масштабирование, и порядок важен. Примечание: если я могу отсортировать или перемешать элементы без отрицательного влияния на значение группы, то мне, вероятно, не следует использовать кортеж.

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

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

person tzot    schedule 10.12.2008

Как правило, «специализированная структура» на самом деле ЯВЛЯЕТСЯ разумным текущим состоянием объекта со своими собственными методами.

class Some3SpaceThing(object):
  def __init__(self,x):
    self.g(x)
  def g(self,x):
    self.y0 = x + 1
    self.y1 = x * 3
    self.y2 = y0 ** y3

r = Some3SpaceThing( x )
r.y0
r.y1
r.y2

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

person S.Lott    schedule 10.12.2008

Кортежи, диктовки и объекты Python предлагают программисту плавный компромисс между формальностью и удобством для небольших структур данных («вещей»). Для меня выбор того, как изобразить вещь, в основном продиктован тем, как я собираюсь использовать структуру. В C ++ принято использовать struct для элементов, содержащих только данные, и class для объектов с методами, даже если вы можете на законных основаниях помещать методы в struct; моя привычка похожа на Python, с dict и tuple вместо struct.

Для наборов координат я буду использовать tuple, а не точку class или dict (и обратите внимание, что вы можете использовать tuple в качестве словарного ключа, поэтому dicts составляют отличные разреженные многомерные массивы).

Если я собираюсь перебирать список вещей, я предпочитаю распаковывать tuples на итерации:

for score,id,name in scoreAllTheThings():
    if score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(score,id,name)

... поскольку версия объекта более загромождена для чтения:

for entry in scoreAllTheThings():
    if entry.score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry.score,entry.id,entry.name)

... не говоря уже о dict.

for entry in scoreAllTheThings():
    if entry['score'] > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry['score'],entry['id'],entry['name'])

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

Наконец, если я собираюсь обмениваться данными с системными компонентами, не относящимися к Python, я чаще всего сохраняю их в dict, потому что это лучше всего подходит для сериализации JSON.

person Russell Borogove    schedule 13.08.2013

+1 к предложению С.Лотта именованного контейнерного класса.

Для Python 2.6 и выше именованный кортеж предоставляет полезный способ легко создавать эти классы-контейнеры, и результаты будут «легковесными и не требуют больше памяти, чем обычные кортежи».

person John Fouhy    schedule 10.12.2008

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

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

person fluffels    schedule 10.12.2008

Я бы использовал dict для передачи и возврата значений из функции:

Используйте переменную форму, как определено в форме.

form = {
    'level': 0,
    'points': 0,
    'game': {
        'name': ''
    }
}


def test(form):
    form['game']['name'] = 'My game!'
    form['level'] = 2

    return form

>>> print(test(form))
{u'game': {u'name': u'My game!'}, u'points': 0, u'level': 2}

Для меня и для процессора это наиболее эффективный способ.

Вам нужно передать только один указатель и вернуть только один указатель.

Вам не нужно изменять аргументы функций (тысячи из них) всякий раз, когда вы вносите изменения в свой код.

person Elis Byberi    schedule 30.10.2017
comment
Dicts изменчивы. Если вы передаете dict функции, и эта функция редактирует dict, изменения будут отражены за пределами этой функции. Если функция возвращает dict в конце, это может означать, что функция не имеет побочных эффектов, поэтому значение не должно возвращаться, что дает понять, что test напрямую изменит значение. Сравните это с dict.update, который не возвращает значения. - person sleblanc; 28.01.2019
comment
@sleblanc Если функция возвращает dict в конце, это может означать, что функция не имеет побочных эффектов. Это не означает, что, как вы сказали, dict изменяем. Однако возвращение form не ухудшает читаемость или производительность. В случаях, когда вам может потребоваться переформатировать form, возвращая его [форма], убедитесь, что возвращается последний form, потому что вы не будете нигде отслеживать изменения формы. - person Elis Byberi; 28.01.2019

«Лучшее» - решение отчасти субъективное. Используйте кортежи для небольших возвращаемых наборов в общем случае, когда допустимо неизменяемое значение. Кортеж всегда предпочтительнее списка, если изменчивость не является обязательной.

Для более сложных возвращаемых значений или для случая, когда важна формальность (например, код высокого значения), лучше использовать именованный кортеж. В самом сложном случае лучше всего подходит объект. Однако действительно важна ситуация. Если имеет смысл возвращать объект, потому что это то, что у вас, естественно, есть в конце функции (например, шаблон Factory), тогда верните объект.

Как сказал мудрец:

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

person joel3000    schedule 29.08.2018
comment
Дональд Кнут сказал (в 1974 г.). - person Peter Mortensen; 20.06.2019

person    schedule
comment
@edouard Нет, он возвращает кортеж, а не список. - person Simon Hibbs; 17.02.2017
comment
деструктуризация - это аргумент для возврата списков, на мой взгляд - person semiomant; 08.06.2017