Як повернути кілька значень з функції? [зачинено]


1067

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

Варіант: Використання кортежу

Розглянемо цей тривіальний приклад:

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

Однак це швидко стає проблематичним, оскільки кількість повернених значень збільшується. Що робити, якщо ви хочете повернути чотири-п’ять значень? Звичайно, ви могли б продовжувати їх обробляти, але легко забути, яке значення є де. Також досить некрасиво розпакувати їх там, де ви хочете їх отримати.

Варіант: Використання словника

Наступним логічним кроком, здається, є введення якогось "позначення запису". У Python очевидний спосіб зробити це за допомогою a 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
Багато чудових питань із великими відповідями закриті, оскільки виникає ключове слово "думка". Ви можете стверджувати, що вся ПС базується на думці, але це думка, поінформована фактами, посиланнями та спеціальною експертизою. Тільки тому, що хтось запитує, "який ти вважаєш найкращим", це не означає, що він запитує особисту думку, абстраговану фактичними фактами, посиланнями та конкретними знаннями. Вони майже напевно просять саме такої думки, такої, яка ґрунтується ґрунтовно на документах та документально підтверджених фактах, посиланнях та специфічній експертизі, яку використовувала людина для формування думки.
NeilG

@hetepeperfan не потрібно змінювати 3, і жодне з них не визначає y3 у глобальному масштабі, ви також можете використовувати локальну назву y3, яка також зробить ту саму роботу.
okie

Відповіді:


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
Це лише правильна відповідь, оскільки це єдина канонічна структура, яку ОП не враховувала і тому, що вона вирішує його проблему управління довгими кортежами. Потрібно позначити як прийняте.
авіаудар

7
Що ж, обґрунтування дизайну namedtupleмає менший слід пам’яті для масових результатів (довгі списки кортежів, такі як результати запитів БД). Для окремих елементів (якщо цю проблему не називають часто), словники та класи також відмінно підійдуть. Але іменіtutuples є приємним / приємним рішенням і в цьому випадку.
Lutz Prechelt

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
@ Reb.Cabin Різниці немає. Кортеж ідентифікується комою, а використання дужок - це просто групування речей. Наприклад (1), це int while (1,)або 1,є кортеж.
філ

19
Повторне "повернення словника з ключами y0, y1, y2 тощо не пропонує жодної переваги перед кортежами": словник має перевагу в тому, що ви можете додавати поля до поверненого словника без порушення існуючого коду.
ostrokach

Повторне "повернення словника з ключами y0, y1, y2 тощо не пропонує жодної переваги перед кортежами": це також більш читабельне та менше схильність до помилок, коли ви отримуєте доступ до даних на основі його імені, а не позиції.
Деніс Доллфус

204

Багато відповідей свідчать про те, що вам потрібно повернути якусь колекцію, як-от словник чи список. Ви можете залишити зайвий синтаксис і просто виписати зворотні значення, розділені комами. Примітка. Це технічно повертає кортеж.

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

дає:

True
False

24
Ви все ще повертаєте колекцію. Це кортеж. Я вважаю за краще дужки, щоб зробити це більш явним. Спробуйте це: type(f())повертається <class 'tuple'>.
Ігор

20
@Igor: Немає підстав робити tupleчіткий аспект; не дуже важливо, що ви повертаєтесь tuple, це ідіома для повернення кількох значень періоду. З тієї ж причини ви опускаєте паролі з ідіомою swap 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 так. Цей спосіб також дозволяє передавати результат декільком функціям, які беруть різні аргументи від результату, не потребуючи конкретного посилання на окремі частини результату кожен раз.
Gnudiff

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
Це, здається, є найчистішим рішенням і має чистий синтаксис. Чи є в цьому недоліки? Якщо ви не використовуєте всі прибутки, чи чекають "невитрачені" врожаї, щоб завдати вам шкоди?
Джімініон

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чи точку 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.


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}

Це найефективніший спосіб для мене та для технологічного блоку.

Ви повинні пропустити лише один вказівник і повернути лише один вказівник назовні.

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


Дикти змінюються. Якщо ви передасте дікт функції, і ця функція редагує дікт, зміни будуть відображені поза межами цієї функції. Якщо функція поверне дік в кінці, це може означати, що функція не має побічних ефектів, тому значення не слід повертати, даючи зрозуміти, що це testбуде безпосередньо змінювати значення. Порівняйте це з тим dict.update, що не повертає значення.
sleblanc

@sleblanc "Наявність функції повернення дикту в кінці може означати, що функція не має побічних ефектів". Це не означає, що, як ви сказали, диктант є змінним. Однак повернення formне шкодить читабельності чи продуктивності. У випадках, коли вам може знадобитися переформатувати form, повертаючи його [форму], переконайтеся, що formповернуто останнє , оскільки ви ніде не будете відслідковувати зміни форми.
Elis Byberi

3

"Найкраще" - частково суб'єктивне рішення. Використовуйте кортежі для невеликих наборів повернення в загальному випадку, коли незмінний прийнятний. Кортеж завжди кращий до списку, коли змінність не є необхідною умовою.

Для більш складних повернутих значень або для випадку, коли формальність є цінною (тобто код високого значення), краще названий кортеж. Для найскладнішого випадку об'єкт, як правило, найкращий. Однак насправді важлива ситуація. Якщо є сенс повернути об'єкт, оскільки це, природно, ви маєте в кінці функції (наприклад, заводський зразок), тоді поверніть об'єкт.

Як сказав мудрець:

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.