Как вернуть несколько значений из функции? [закрыто]


1067

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

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

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

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 заключается в том, что списки являются изменяемыми , а кортежи - нет.

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

Вопрос

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


10
В ваших превосходных примерах вы используете переменную y3, но если y3 не объявлен как глобальный, это может привести к NameError: global name 'y3' is not definedпростому использованию 3?
hetepeperfan

11
Многие великие вопросы с хорошими ответами закрыты, потому что возникает ключевое слово «мнение». Вы можете утверждать, что вся SO основана на мнении, но это мнение основано на фактах, ссылках и конкретных знаниях. Тот факт, что кто-то спрашивает «что, по вашему мнению, лучше всего», не означает, что он запрашивает личное мнение, абстрагированное от реальных фактов, ссылок и конкретных знаний. Они почти наверняка спрашивают именно такого рода мнение, которое основано на фактах, ссылках и конкретных знаниях, которые человек использовал для формирования мнения, и задокументировал их.
NeilG

@hetepeperfan не нужно менять 3, и ни один не определяет y3 в глобальном, вы также можете использовать локальное имя y3, которое будет делать то же самое.
Ока

Ответы:


637

Именованные кортежи были добавлены в 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библиотеке появился NamedTupleкласс, облегчающий создание именованных кортежей и более мощный. Наследование от typing.NamedTupleпозволяет использовать строки документов, значения по умолчанию и аннотации типов.

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

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

employee = Employee('Guido')
assert employee.id == 3

68
Это только правильный ответ, потому что это единственная каноническая структура, которую OP не рассматривал, и потому что она решает его проблему управления длинными кортежами. Должен быть помечен как принятый.
авиаудар

7
Что ж, обоснование дизайна namedtupleзаключается в том, что для массовых результатов требуется меньший объем памяти (длинные списки кортежей, например результаты запросов к БД). Для отдельных элементов (если рассматриваемая функция вызывается не часто) словари и классы также хороши. Но именованные кортежи - это хорошее и приятное решение и в этом случае.
Лутц Пречелт

8
@wom: не делай этого. Python не предпринимает усилий для унификации namedtupleопределений (каждый вызов создает новое), создание namedtupleкласса является относительно дорогостоящим как в ЦП, так и в памяти, и все определения классов по своей природе включают циклические ссылки (поэтому в CPython вы ожидаете циклического запуска GC чтобы их выпустили). Это также делает невозможным для pickleкласса (и, следовательно, невозможно использовать экземпляры с multiprocessingв большинстве случаев). Каждое создание класса на моем 3.6.4 x64 потребляет ~ 0.337 мс и занимает чуть менее 1 КБ памяти, убивая любую экономию экземпляра.
ShadowRanger

3
Отмечу, Python 3.7 улучшил скорость создания новых namedtupleклассов ; затраты на ЦП снижаются примерно в 4 раза , но они все еще примерно в 1000 раз превышают затраты на создание экземпляра, а стоимость памяти для каждого класса остается высокой (я ошибался в своем последнем комментарии о том, что «меньше 1 КБ») для класса, _sourceкак правило, он равен 1,5 КБ; _sourceудаляется в 3,7, так что он, вероятно, ближе к первоначальной заявке, составляющей чуть менее 1 КБ на создание класса).
ShadowRanger

4
@SergeStroobandt - это часть стандартной библиотеки, но не встроенная. Вам не нужно беспокоиться о том, что он не может быть установлен в другой системе с Python> = 2.6. Или вы просто возражаете против дополнительной строки кода?
Джастин

234

Для небольших проектов мне проще всего работать с кортежами. Когда это становится слишком сложным для управления (и не раньше), я начинаю группировать вещи в логические структуры, однако я думаю, что вы предлагаете использовать словари и 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).


6
Вопрос - есть ли разница между size, type, dimensions = getImageData(x)и (size, type, dimensions) = getImageData(x)? То есть, имеет ли значение перенос левой части кортежного присваивания?
Reb.Cabin

11
@ Реб. Кабин Нет разницы. Кортеж идентифицируется запятой, а использование скобок - просто для группировки вещей. Например, (1)является int while (1,)или 1,является кортежем.
Фил

19
«Возвращение словаря с ключами y0, y1, y2 и т. Д. Не дает никаких преимуществ перед кортежами»: словарь имеет то преимущество, что вы можете добавлять поля в возвращаемый словарь, не нарушая существующий код.
Острокач

«Возвращение словаря с ключами y0, y1, y2 и т. Д. Не дает никаких преимуществ перед кортежами»: оно также более читабельно и менее подвержено ошибкам, поскольку вы получаете доступ к данным на основе их имени, а не позиции.
Денис Доллфус

204

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

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

дает:

True
False

24
Вы все еще возвращаете коллекцию. Это кортеж Я предпочитаю скобки, чтобы сделать его более явным. Попробуйте это: type(f())возвращается <class 'tuple'>.
Игорь

20
@ Игорь: нет никаких причин, чтобы сделать tupleаспект явным; это не очень важно, что вы возвращаете tuple, это идиома для возврата периода нескольких значений. По той же причине, по которой вы опускаете парены с идиомой подстановки x, y = y, x, множественной инициализацией x, y = 0, 1и т. Д .; конечно, он делает tuples под капотом, но нет никакой причины делать это явным, так как tuples не имеет никакого значения. Учебник по Python вводит множественные назначения задолго до того, как он затрагивает tuples.
ShadowRanger

@ShadowRanger - любая последовательность значений, разделенных запятой в правой части, =является кортежем в Python с или без скобок вокруг них. Так что на самом деле здесь нет явного или неявного. a, b, c такой же кортеж, как (a, b, c). При возврате таких значений также нет создания кортежа «под капотом», потому что это просто простой кортеж. ОП уже упоминал кортежи, так что на самом деле нет разницы между тем, что он упомянул, и тем, что показывает этот ответ. Нет
Ken4scholars

2
Это буквально первый вариант , предложенный в вопросе
эндолиты

1
@endolith The два раза парень задает вопрос ( «Как вернуть несколько значений?» и «Как вы вернуть несколько значений?») отвечает на этот ответ. Текст вопроса иногда менялся. И это вопрос, основанный на мнении.
Джозеф Хансен

74

Я голосую за словарь.

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

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

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

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

5
после многих лет программирования я склоняюсь к тому, что когда-либо требуется структура данных и функций. Функция сначала, вы всегда можете рефакторинг по мере необходимости.
Monkut

Как бы мы получили значения в словаре, не вызывая функцию несколько раз? Например, если я хочу использовать y1 и y3 в другой функции?
Мэтт

3
присвойте результаты отдельной переменной. result = g(x); other_function(result)
monkut

1
@monkut да. Этот способ также позволяет передавать результат нескольким функциям, которые получают разные аргументы из результата, без необходимости каждый раз специально ссылаться на определенные части результата.
Гнудифф

38

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

>>> 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 обычно лучше, за исключением случаев, когда возвращаемые значения являются кандидатами для инкапсуляции в классе.


1
Это кажется самым чистым решением и имеет чистый синтаксис. Есть ли минусы в этом? Если вы не используете все возвраты, есть ли «неизрасходованные» доходы, ожидающие вас?
Jiminion

24
Это может быть "чисто", но это не кажется интуитивно понятным. Как кто-то, кто никогда раньше не сталкивался с этим шаблоном, знал, что автоматическая распаковка кортежей вызовет каждую из них yield?
coredumperror

1
@CoreDumpError, генераторы - это просто ... генераторы. Внешнего различия между def f(x): …; yield b; yield a; yield rvs. нет (g for g in [b, a, r]), и оба они легко конвертируются в списки или кортежи и, следовательно, будут поддерживать распаковку кортежей. Форма генератора кортежей следует функциональному подходу, тогда как форма функции является обязательной и позволит управлять потоком и назначать переменные.
sleblanc

30

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

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

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


29

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

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

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


22
Кортежи проще распаковать: y0, y1, y2 = g () с указанием, которое вам нужно сделать: result = g () y0, y1, y2 = result.get ('y0'), result.get ('y1' ), result.get ('y2'), что немного некрасиво. У каждого решения есть свои «плюсы» и «минусы».
Оли

27
>>> def func():
...    return [1,2,3]
...
>>> a,b,c = func()
>>> a
1
>>> b
2
>>> c
3

@edouard Нет, это не так, он возвращает кортеж, а не список.
Саймон Хиббс

1
По моему мнению, деструктуризация - это аргумент для возвращения списков
полуомант

21

Как правило, «специализированная структура» фактически представляет собой разумное текущее состояние объекта со своими собственными методами.

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

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


20

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

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

Если я собираюсь перебирать список вещей, я предпочитаю распаковывать 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.


19

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

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


4

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

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


4

Я бы использовал 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}

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

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

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


Dicts изменчивы. Если вы передаете dict функции, и эта функция редактирует dict, изменения будут отражены за пределами этой функции. Наличие функции, возвращающей dict в конце, может означать, что функция не имеет побочных эффектов, поэтому значение не должно возвращаться, давая понять, что testэто непосредственно изменит значение. Сравните это с тем dict.update, что не возвращает значение.
sleblanc

@sleblanc "Наличие функции, возвращающей dict в конце, может означать, что функция не имеет побочных эффектов". Это не означает, что, как вы сказали, dict изменчив. Тем не менее, возвращаясьform не ухудшает читаемость и производительность. В тех случаях, когда вам может потребоваться переформатировать form, возвращая его [форма] действительно гарантирует, что последний formбудет возвращен, потому что вы нигде не будете отслеживать изменения формы.
Элис Бибери

3

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

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

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

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


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.