Cara paling efisien untuk membuat pernyataan if-elif-elif-else ketika else dilakukan paling banyak?

99

Saya mendapat pernyataan if-elif-elif-else yang 99% waktunya, pernyataan else dijalankan:

if something == 'this':
    doThis()
elif something == 'that':
    doThat()
elif something == 'there':
    doThere()
else:
    doThisMostOfTheTime()

Konstruksi ini banyak dilakukan , tetapi karena ia melewati setiap kondisi sebelum mencapai kondisi lain, saya merasa ini tidak terlalu efisien, apalagi Pythonic. Di sisi lain, ia perlu mengetahui apakah salah satu kondisi tersebut terpenuhi, jadi tetap harus mengujinya.

Adakah yang tahu jika dan bagaimana hal ini dapat dilakukan dengan lebih efisien atau apakah ini cara terbaik untuk melakukannya?

kramer65
sumber
Dapatkah Anda sortmelakukan hal-hal yang menjalankan rantai if / else ... Anda, sehingga semua elemen yang akan cocok dengan salah satu kondisinya ada di satu ujung, dan yang lainnya ada di sisi lain? Jika demikian, Anda dapat melihat apakah itu lebih cepat / lebih elegan atau tidak. Tapi ingat, jika tidak ada masalah kinerja, terlalu dini untuk mengkhawatirkan pengoptimalan.
Patashu
4
Apakah ada kesamaan dari ketiga kasus khusus tersebut? Misalnya, Anda dapat melakukan if not something.startswith("th"): doThisMostOfTheTime()dan melakukan perbandingan lain di elseklausa.
Tim Pietzcker
3
@ kramer65 Jika rantai panjang if / elif ... itu bisa lambat, tapi pastikan untuk benar-benar membuat profil kode Anda dan mulai dengan mengoptimalkan bagian mana pun yang membutuhkan waktu paling lama.
jorgeca
1
Apakah perbandingan ini dilakukan hanya sekali per nilai something, atau apakah perbandingan serupa dilakukan beberapa kali pada nilai yang sama?
Chris Pitman

Jawaban:

98

Kode...

options.get(something, doThisMostOfTheTime)()

... sepertinya seharusnya lebih cepat, tetapi sebenarnya lebih lambat daripada konstruksi if... elif... else, karena harus memanggil fungsi, yang dapat menjadi overhead performa yang signifikan dalam loop yang ketat.

Pertimbangkan contoh-contoh ini ...

1.py

something = 'something'

for i in xrange(1000000):
    if something == 'this':
        the_thing = 1
    elif something == 'that':
        the_thing = 2
    elif something == 'there':
        the_thing = 3
    else:
        the_thing = 4

2.py

something = 'something'
options = {'this': 1, 'that': 2, 'there': 3}

for i in xrange(1000000):
    the_thing = options.get(something, 4)

3.py

something = 'something'
options = {'this': 1, 'that': 2, 'there': 3}

for i in xrange(1000000):
    if something in options:
        the_thing = options[something]
    else:
        the_thing = 4

4.py

from collections import defaultdict

something = 'something'
options = defaultdict(lambda: 4, {'this': 1, 'that': 2, 'there': 3})

for i in xrange(1000000):
    the_thing = options[something]

... dan catat jumlah waktu CPU yang mereka gunakan ...

1.py: 160ms
2.py: 170ms
3.py: 110ms
4.py: 100ms

... menggunakan waktu pengguna dari time(1).

Opsi # 4 memang memiliki overhead memori tambahan untuk menambahkan item baru untuk setiap kehilangan kunci yang berbeda, jadi jika Anda mengharapkan sejumlah kesalahan kunci berbeda yang tidak terbatas, saya akan menggunakan opsi # 3, yang masih merupakan peningkatan yang signifikan pada konstruksi aslinya.

Aya
sumber
2
apakah python memiliki pernyataan switch?
nathan hayfield
ugh ... sejauh ini hanya itulah satu-satunya hal yang saya dengar tentang python yang tidak saya pedulikan ... kira pasti ada sesuatu
nathan hayfield
2
-1 Anda mengatakan bahwa menggunakan a dictlebih lambat, tetapi pengaturan waktu Anda benar-benar menunjukkan bahwa ini adalah opsi tercepat kedua.
Marcin
11
@Marcin Saya katakan itu dict.get()lebih lambat, yang 2.py- paling lambat dari semuanya.
Aya
Sebagai catatan, tiga dan empat juga secara dramatis lebih cepat daripada menangkap kesalahan kunci dalam konstruksi coba / kecuali.
Jeff
78

Saya akan membuat kamus:

options = {'this': doThis,'that' :doThat, 'there':doThere}

Sekarang gunakan saja:

options.get(something, doThisMostOfTheTime)()

Jika somethingtidak ditemukan di optionsdict maka dict.getakan mengembalikan nilai defaultdoThisMostOfTheTime

Beberapa perbandingan waktu:

Naskah:

from random import shuffle
def doThis():pass
def doThat():pass
def doThere():pass
def doSomethingElse():pass
options = {'this':doThis, 'that':doThat, 'there':doThere}
lis = range(10**4) + options.keys()*100
shuffle(lis)

def get():
    for x in lis:
        options.get(x, doSomethingElse)()

def key_in_dic():
    for x in lis:
        if x in options:
            options[x]()
        else:
            doSomethingElse()

def if_else():
    for x in lis:
        if x == 'this':
            doThis()
        elif x == 'that':
            doThat()
        elif x == 'there':
            doThere()
        else:
            doSomethingElse()

Hasil:

>>> from so import *
>>> %timeit get()
100 loops, best of 3: 5.06 ms per loop
>>> %timeit key_in_dic()
100 loops, best of 3: 3.55 ms per loop
>>> %timeit if_else()
100 loops, best of 3: 6.42 ms per loop

Untuk 10**5kunci yang tidak ada dan 100 kunci yang valid ::

>>> %timeit get()
10 loops, best of 3: 84.4 ms per loop
>>> %timeit key_in_dic()
10 loops, best of 3: 50.4 ms per loop
>>> %timeit if_else()
10 loops, best of 3: 104 ms per loop

Jadi, untuk kamus normal memeriksa penggunaan kunci key in optionsadalah cara paling efisien di sini:

if key in options:
   options[key]()
else:
   doSomethingElse()
Ashwini Chaudhary
sumber
options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()sedikit lebih efisien.
Aya
Ide keren, tapi tidak bisa dibaca. Juga Anda mungkin ingin memisahkan optionsdict untuk menghindari membangunnya kembali, sehingga memindahkan sebagian (tetapi tidak semua) logika jauh dari titik penggunaan. Tetap saja, trik yang bagus!
Anders Johansson
7
Anda tahu apakah ini lebih efisien? Dugaan saya adalah lebih lambat karena melakukan pencarian hash daripada pemeriksaan bersyarat sederhana atau tiga. Pertanyaannya adalah tentang efisiensi daripada kekompakan kode.
Bryan Oakley
2
@BryanOakley Saya telah menambahkan beberapa perbandingan waktu.
Ashwini Chaudhary
1
sebenarnya ini akan lebih efisien untuk dilakukan try: options[key]() except KeyError: doSomeThingElse()(karena dengan if key in options: options[key]()Anda menelusuri kamus dua kalikey
hardmooth
8

Apakah Anda dapat menggunakan pypy?

Menyimpan kode asli Anda tetapi menjalankannya di pypy memberikan kecepatan 50x bagi saya.

CPython:

matt$ python
Python 2.6.8 (unknown, Nov 26 2012, 10:25:03)
[GCC 4.2.1 Compatible Apple Clang 3.0 (tags/Apple/clang-211.12)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from timeit import timeit
>>> timeit("""
... if something == 'this': pass
... elif something == 'that': pass
... elif something == 'there': pass
... else: pass
... """, "something='foo'", number=10000000)
1.728302001953125

Pypy:

matt$ pypy
Python 2.7.3 (daf4a1b651e0, Dec 07 2012, 23:00:16)
[PyPy 2.0.0-beta1 with GCC 4.2.1] on darwin
Type "help", "copyright", "credits" or "license" for more information.
And now for something completely different: ``a 10th of forever is 1h45''
>>>>
>>>> from timeit import timeit
>>>> timeit("""
.... if something == 'this': pass
.... elif something == 'that': pass
.... elif something == 'there': pass
.... else: pass
.... """, "something='foo'", number=10000000)
0.03306388854980469
foz
sumber
Hai Foz. Terima kasih atas tipnya. Sebenarnya saya sudah menggunakan pypy (love it), tapi saya masih butuh peningkatan kecepatan .. :)
kramer65
Baiklah! Sebelum ini saya mencoba pra-komputasi hash untuk 'ini', 'itu', dan 'di sana' - dan kemudian membandingkan kode hash alih-alih string. Itu ternyata menjadi dua kali lebih lambat dari aslinya, jadi sepertinya perbandingan string sudah dioptimalkan dengan cukup baik secara internal.
foz
3

Berikut contoh jika dengan kondisi dinamis diterjemahkan ke kamus.

selector = {lambda d: datetime(2014, 12, 31) >= d : 'before2015',
            lambda d: datetime(2015, 1, 1) <= d < datetime(2016, 1, 1): 'year2015',
            lambda d: datetime(2016, 1, 1) <= d < datetime(2016, 12, 31): 'year2016'}

def select_by_date(date, selector=selector):
    selected = [selector[x] for x in selector if x(date)] or ['after2016']
    return selected[0]

Ini adalah cara, tetapi mungkin bukan cara paling pythonic untuk melakukannya karena kurang terbaca bagi mereka yang tidak fasih dengan Python.

Arthur Julião
sumber
0

Orang-orang memperingatkan tentang execalasan keamanan, tetapi ini adalah kasus yang ideal untuk itu.
Ini adalah mesin negara yang mudah.

Codes = {}
Codes [0] = compile('blah blah 0; nextcode = 1')
Codes [1] = compile('blah blah 1; nextcode = 2')
Codes [2] = compile('blah blah 2; nextcode = 0')

nextcode = 0
While True:
    exec(Codes[nextcode])
pengguna3319934
sumber