Як я можу знайти відсутнє значення більш стисло?


76

Наступний код перевіряє , якщо xі yє різними значення (змінні x, y, zможуть мати тільки значення a, bабо c) , і якщо так, то набори zдля третього символу:

if x == 'a' and y == 'b' or x == 'b' and y == 'a':
    z = 'c'
elif x == 'b' and y == 'c' or x == 'c' and y == 'b':
    z = 'a'
elif x == 'a' and y == 'c' or x == 'c' and y == 'a':
    z = 'b'

Чи можливо це зробити більш стислим, зрозумілим та ефективним способом?


6
Коротка відповідь - "Так!" Набори Python чудово підходять для перевірки чіткості та для обчислення невикористаних елементів.
Реймонд Хеттінгер,

1
Дякую усім за відповіді, мабуть, я буду використовувати рішення, використовуючи set, як його досить швидкий і читабельний. Відповідь на основі таблиці пошуку Оскара Лопеса також цікава.
Bunny Rabbit

Відповіді:


62
z = (set(("a", "b", "c")) - set((x, y))).pop()

Я припускаю, що один із трьох випадків у вашому коді має місце. Якщо це так, набір set(("a", "b", "c")) - set((x, y))буде складатися з одного елемента, який повертаєтьсяpop() .

Редагувати: Як пропонував Реймонд Хеттінгер у коментарях, ви також можете скористатися розпаковуванням кортежів для вилучення окремого елемента з набору:

z, = set(("a", "b", "c")) - set((x, y))

26
Якщо ви використовуєте Python 2.7 / 3.1 або пізнішої версії, ви можете написати це ще коротше, використовуючи набір літералів, наприклад так:z = ({'a', 'b', 'c'} - {x, y}).pop()
Taymon

7
Поп () є непотрібним і повільно. Замість цього використовуйте розпакування кортежів. Крім того, set(("a", "b", "c"))інваріант є, тому його можна попередньо обчислити один раз, залишивши лише набір різниць, який буде використовуватися в циклі (якщо він не використовується в циклі, то ми не так сильно дбаємо про швидкість).
Реймонд Хеттінгер,

3
@Ed: Я знаю, але в OP не вказано, що коли робити x == y, тому я пропустив тест. Це досить просто додати, if x != y:якщо це потрібно.
Sven Marnach

4
Особисто я вибрав би перший, оскільки це більш очевидно, ніж випадкова кома.
Джон

1
Можливо, ви захочете замінити set(("a", "b", "c"))на set("abc").
kasyc

47

stripМетод є ще одним варіантом , який працює швидко для мене:

z = 'abc'.strip(x+y) if x!=y else None

2
+1 Це також дуже прозоро, і на відміну від більшості відповідей, воно стосується x == y.
Ed Staub

1
Приємна ідея, +1; хоча я на самому ділі думаю , що "a", "b"і "c"в вихідному пості просто наповнювачі для реальних значень. Це рішення не узагальнює для будь-якого іншого типу значення, крім рядків довжиною 1.
Свен Марнах,

@chepner це креативно! Дякуємо за відповідь Чепнер.
Bunny Rabbit

27

Відмінний код Свена зробив трохи занадто багато роботи і міг би використовувати розпакування кортежу замість pop () . Крім того, він міг би додати захисник if x != yдля перевірки різниці x та y . Ось як виглядає покращена відповідь:

# create the set just once
choices = {'a', 'b', 'c'}

x = 'a'
y = 'b'

# the main code can be used in a loop
if x != y:
    z, = choices - {x, y}

Ось порівняльні терміни з набором синхронізації, щоб показати відносну ефективність:

import timeit, itertools

setup_template = '''
x = %r
y = %r
choices = {'a', 'b', 'c'}
'''

new_version = '''
if x != y:
    z, = choices - {x, y}
'''

original_version = '''
if x == 'a' and y == 'b' or x == 'b' and y == 'a':
    z = 'c'
elif x == 'b' and y == 'c' or x == 'c' and y == 'b':
    z = 'a'
elif x == 'a' and y == 'c' or x == 'c' and y == 'a':
    z = 'b'
'''

for x, y in itertools.product('abc', repeat=2):
    print '\nTesting with x=%r and y=%r' % (x, y)
    setup = setup_template % (x, y)
    for stmt, name in zip([original_version, new_version], ['if', 'set']):
        print min(timeit.Timer(stmt, setup).repeat(7, 100000)),
        print '\t%s_version' % name

Ось результати таймінгу:

Testing with x='a' and y='a'
0.0410830974579     original_version
0.00535297393799    new_version

Testing with x='a' and y='b'
0.0112571716309     original_version
0.0524711608887     new_version

Testing with x='a' and y='c'
0.0383319854736     original_version
0.048309803009      new_version

Testing with x='b' and y='a'
0.0175108909607     original_version
0.0508949756622     new_version

Testing with x='b' and y='b'
0.0386209487915     original_version
0.00529098510742    new_version

Testing with x='b' and y='c'
0.0259420871735     original_version
0.0472128391266     new_version

Testing with x='c' and y='a'
0.0423510074615     original_version
0.0481910705566     new_version

Testing with x='c' and y='b'
0.0295209884644     original_version
0.0478219985962     new_version

Testing with x='c' and y='c'
0.0383579730988     original_version
0.00530385971069    new_version

Ці терміни показують, що продуктивність оригінальної версії досить сильно варіюється залежно від того, які оператори if ініціюються різними вхідними значеннями.


2
Ваш тест здається упередженим. Так звана "set_version" лише іноді швидша, оскільки вона захищається додатковим ifтвердженням.
екхуморо

2
@ekhumoro До цього вимагала специфікація проблеми: "перевіряє, чи x та y є різними значеннями, і якщо так, то встановлює z для третього символу". Найшвидший (і найпростіший) спосіб перевірити, чи відрізняються значення, - це x != y. Лише коли вони різняться, ми встановлюємо різницю в множині для визначення третього символу :-)
Реймонд Хеттінгер

2
Суть, про який я говорив, полягає в тому, що ваші тести не показують, що ефективність set_versionпрацює краще, оскільки вона базується на множинах ; він працює лише краще завдяки ifзаяві про охорону .
екхуморо

1
@ekhumoro Це дивне читання результатів тесту. Код робить те, про що просив ОП. Часи показують порівняльну ефективність з усіма можливими групами вхідних даних. Це залежить від вас, як ви хочете їх інтерпретувати. Час для версії з використанням if x != y: z, = choices - {x, y}тарифів досить добре порівняно з оригінальним кодом OP. Я не знаю, звідки з’являється ваше поняття упередженості - терміни такі, якими вони є, і AFAICT, це все ще найкраща з відповідей, які були розміщені. Це і чисто, і швидко.
Реймонд Хеттінгер,

2
Кілька оптимізацій було додано до "встановленої версії" Свена, але те ж не було зроблено для "версії if". Додавання if x != yзапобіжника до "if-version", мабуть, зробить його більш послідовним і ефективнішим, ніж усі інші рішення, які пропонувались до цього часу (хоча, очевидно, не настільки читабельними та стислими). Ваша "set_version" - дуже гарне рішення - воно просто не настільки гарне, як здаються тести ;-)
ekhumoro

18
z = (set('abc') - set(x + y)).pop()

Ось усі сценарії, щоб показати, що це працює:

>>> (set('abc') - set('ab')).pop()   # x is a/b and y is b/a
'c'
>>> (set('abc') - set('bc')).pop()   # x is b/c and y is c/b
'a'
>>> (set('abc') - set('ac')).pop()   # x is a/c and y is c/a
'b'

15

Якби три розглянуті елементи не були "a", "b"а "c", а, скоріше 1, 2і 3, ви також можете використовувати двійковий XOR:

z = x ^ y

Більш загально, якщо ви хочете встановити zодне з трьох чисел a, що залишилось , bі cдавши два числа xі yз цього набору, ви можете використовувати

z = x ^ y ^ a ^ b ^ c

Звичайно, ви можете попередньо обчислити, a ^ b ^ cякщо цифри фіксовані.

Цей підхід також можна використовувати з оригінальними літерами:

z = chr(ord(x) ^ ord(y) ^ 96)

Приклад:

>>> chr(ord("a") ^ ord("c") ^ 96)
'b'

Не чекайте, що хтось, хто читає цей код, негайно зрозуміє, що це означає :)


+1 Це рішення здається приємним та елегантним; і якщо ви зробите це власною функцією і дасте магічному числу 96 ім’я, логікою досить легко керуватися / підтримувати ( xor_of_a_b_c = 96 # ord('a') ^ ord('b') ^ ord('c') == 96). Однак з точки зору швидкості сировини це приблизно на 33% повільніше, ніж довгий ланцюг if / elifs; але на 500% швидше, ніж setметод.
dr jimbob

@sven Дякую, що представив мені оператор XOR, Ваше рішення чисте та елегантне, я думаю, що цей приклад змусить його прилипати до мого мозку, ще раз дякую :)
Bunny Rabbit

13

Я думаю, що рішення Свена Марнаха та FJ є прекрасним, але це не швидше у моєму маленькому тесті. Це оптимізована версія Реймонда з використанням заздалегідь обчисленого set:

$ python -m timeit -s "choices = set('abc')" \
                   -s "x = 'c'" \
                   -s "y = 'a'" \
                      "z, = choices - set(x + y)"
1000000 loops, best of 3: 0.689 usec per loop

Оригінальне рішення:

$ python -m timeit -s "x = 'c'" \
                   -s "y = 'a'" \
                      "if x == 'a' and y == 'b' or x == 'b' and y == 'a':" \
                      "    z = 'c'" \
                      "elif x == 'b' and y == 'c' or x == 'c' and y == 'b':" \
                      "    z = 'a'" \
                      "elif x == 'a' and y == 'c' or x == 'c' and y == 'a':" \
                      "    z = 'b'"
10000000 loops, best of 3: 0.310 usec per loop

Зверніть увагу, що це найгірший можливий вхід для if-значень, оскільки всі шість порівнянь доведеться випробувати. Тестування з усіма значеннями для xі yдає:

x = 'a', y = 'b': 0.084 usec per loop
x = 'a', y = 'c': 0.254 usec per loop
x = 'b', y = 'a': 0.133 usec per loop
x = 'b', y = 'c': 0.186 usec per loop
x = 'c', y = 'a': 0.310 usec per loop
x = 'c', y = 'b': 0.204 usec per loop

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

Я думаю, що обидва типи рішень цінні: важливо знати, що створення "складних" структур даних, таких як набори, коштує вам чогось у продуктивності - в той час як вони дають вам багато в читабельності та швидкості розробки . Складні типи даних також набагато кращі, коли код змінюється: легко розширити рішення на основі набору до чотирьох, п’яти, ... змінних, тоді як оператори if швидко перетворюються на кошмар обслуговування.


1
@martinGeisler велике спасибі за вашу відповідь, я навіть не підозрював, що ми можемо вимірювати подібні речі на python. У мене є відчуття, що рішення Chessmasters буде просто працювати нормально та ефективно, я спробую перевірити його, як і ви інші відповіді та повідомте вас.
Bunny Rabbit

1
Рішення, що базується на наборі, оптимізує лаконічність і читабельністьелегантність ). Але також була згадана ефективність , тому я пішов і дослідив ефективність запропонованих рішень.
Мартін Гейслер

1
@MartinGeisler: Так, коли я це помітив, я видалив свій коментар. І мені зазвичай цікаво хоча б знати, що швидше.
Sven Marnach

1
@BunnyRabbit: модуль timeit чудово підходить для таких мікро-тестів. Звичайно, спочатку слід сформулювати загальну програму, щоб визначити, де є вузькі місця, але коли вони виявлені, тоді час це може бути чудовим способом швидкого випробування різних реалізацій один проти одного.
Мартін Гейслер

1
+1 - контрольний тест, який підтверджує прості серії порівнянь, логічний та швидкий.
Jeff Ferland

8

Спробуйте цей варіант, використовуючи словники:

z = {'ab':'c', 'ba':'c', 'bc':'a', 'cb':'a', 'ac':'b', 'ca':'b'}[x+y]

Звичайно, якщо x+yключа немає на карті, він видасть ключ, з KeyErrorяким вам доведеться обробляти.

Якщо словник попередньо обчислюється і зберігається для подальшого використання, доступ буде набагато швидшим, оскільки для кожної оцінки не потрібно буде створювати нових структур даних, потрібна лише конкатенація рядків та пошук словника:

lookup_table = {'ab':'c', 'ba':'c', 'bc':'a', 'cb':'a', 'ac':'b', 'ca':'b'}
z = lookup_table[x+y]

2
Для розваги, ось ще один варіант дикту: {1: 'c', 2: 'b', 3: 'a'}[ord(x)+ord(y)-ord('a')*2]додаткова складність, мабуть, не варта заощадженого місця.
Ендрю Кларк

2
@FJ: z = {1: 'a', 2: 'b', 3: 'c'}[2*('a' in x+y)+('b' in x+y)] це весело ...
ChessMaster

Це швидше, ніж оригінальний код OP? якщо так, то чому? Як обчислення хеш-значень може бути швидшим, ніж просте порівняння?
максимум

@max це єдиний хеш-розрахунок, а не ціла купа порівнянь та умовних виразів
Оскар Лопес,

Класно .. Не розумів, наскільки швидкою є функція хешування!
максимум

8
z = 'a'*('a' not in x+y) or 'b'*('b' not in x+y) or 'c'

або менш хакерський і використовуючи умовне призначення

z = 'a' if ('a' not in x+y) else 'b' if ('b' not in x+y) else 'c'

але, мабуть, вирішення дикту швидше ... вам доведеться його встигнути.


2

Я думаю, це повинно виглядати так:

z = (set(("a", "b", "c")) - set((x, y))).pop() if x != y else None

12
len(set((x, y))) == 2це найбільш нечитабельний спосіб писати, який x != yя коли-небудь бачив :)
Свен Марнах,

Так, Свен))) Дякую за ваш коментар. Цей сценарій мав ще одну основну ідею, коли я почав писати його)) І нарешті я забув це відредагувати.
самоназва

1

Використовуючи розуміння списку, припускаючи, як і інші, що має місце один із трьох випадків у вашому коді:

l = ['a', 'b', 'c']
z = [n for n in l if n not in [x,y]].pop()

Або, як у прийнятій відповіді, скориставшись кортежем, щоб розпакувати його,

z, = [n for n in l if n not in [x,y]]

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