วิธีที่มีประสิทธิภาพที่สุดในการสร้างคำสั่ง if-elif-elif-else เมื่อทำคำสั่งอื่นมากที่สุด?


102

ฉันมีคำสั่ง if-elif-elif-else ซึ่ง 99% ของเวลาคำสั่ง else จะถูกดำเนินการ:

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

โครงสร้างนี้ทำไปได้มากแต่เนื่องจากมันผ่านทุกเงื่อนไขก่อนที่จะถึงจุดอื่นฉันรู้สึกว่านี่ไม่ค่อยมีประสิทธิภาพเลยนับประสาอะไรกับ Pythonic ในทางกลับกันก็จำเป็นต้องรู้ว่าตรงตามเงื่อนไขเหล่านั้นหรือไม่ดังนั้นจึงควรทดสอบต่อไป

มีใครทราบบ้างไหมว่าสามารถทำได้อย่างมีประสิทธิภาพมากขึ้นและอย่างไรหรือนี่เป็นเพียงวิธีที่ดีที่สุดที่จะทำได้


คุณสามารถsortใช้สิ่งที่คุณกำลังเรียกใช้ if / else ... chain ได้หรือไม่เพื่อให้องค์ประกอบทั้งหมดที่เงื่อนไขข้อใดข้อหนึ่งตรงกันนั้นอยู่ที่ปลายด้านหนึ่งและส่วนที่เหลือทั้งหมดอยู่ที่อีกด้านหนึ่งหรือไม่? ถ้าเป็นเช่นนั้นคุณสามารถดูได้ว่าเร็วขึ้น / สง่างามมากขึ้นหรือไม่ แต่โปรดจำไว้ว่าหากไม่มีปัญหาด้านประสิทธิภาพก็ยังเร็วเกินไปที่จะกังวลเกี่ยวกับการเพิ่มประสิทธิภาพ
Patashu


4
มีบางอย่างที่กรณีพิเศษทั้งสามมีเหมือนกันหรือไม่? ตัวอย่างเช่นคุณสามารถทำการif not something.startswith("th"): doThisMostOfTheTime()เปรียบเทียบในส่วนelseคำสั่งอื่นได้
Tim Pietzcker

3
@ kramer65 หากเป็น if / elif ที่ยาว ... อาจจะช้า แต่ต้องแน่ใจว่าได้สร้างรายละเอียดโค้ดของคุณจริงๆแล้วเริ่มต้นด้วยการเพิ่มประสิทธิภาพส่วนใดก็ตามที่ใช้เวลามากที่สุด
jorgeca

1
การเปรียบเทียบเหล่านี้ดำเนินการเพียงครั้งเดียวต่อค่าของsomethingหรือการเปรียบเทียบที่คล้ายกันดำเนินการหลายครั้งในค่าเดียวกันหรือไม่
Chris Pitman

คำตอบ:


101

รหัส...

options.get(something, doThisMostOfTheTime)()

... ดูเหมือนว่ามันควรจะเร็วขึ้น แต่จริง ๆ แล้วมันช้ากว่าif... elif... elseสร้างเพราะมีการเรียกฟังก์ชั่นซึ่งจะมีค่าใช้จ่ายในการปฏิบัติงานที่สำคัญในห่วงคับ

ลองพิจารณาตัวอย่างเหล่านี้ ...

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]

... และจดระยะเวลา CPU ที่ใช้ ...

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

... โดยใช้เวลาผู้ใช้จากtime(1).

ตัวเลือก # 4 มีค่าใช้จ่ายของหน่วยความจำเพิ่มเติมในการเพิ่มรายการใหม่สำหรับการพลาดคีย์ที่แตกต่างกันทุกครั้งดังนั้นหากคุณคาดหวังว่าจะมีจำนวนคีย์ที่แตกต่างกันไม่ถูก จำกัด ฉันจะใช้ตัวเลือก # 3 ซึ่งยังคงเป็นการปรับปรุงที่สำคัญใน โครงสร้างดั้งเดิม


2
python มีคำสั่ง switch หรือไม่?
ธานเฮย์ฟิลด์

เอ่อ ... จนถึงตอนนี้มีสิ่งเดียวที่ฉันเคยได้ยินเกี่ยวกับงูหลามที่ฉันไม่สนใจ ... เดาว่าต้องมีอะไรบางอย่าง
ธานเฮย์ฟิลด์

2
-1 คุณบอกว่าการใช้ a dictช้าลง แต่การกำหนดเวลาของคุณแสดงให้เห็นว่าเป็นตัวเลือกที่เร็วที่สุดเป็นอันดับสอง
Marcin

11
@ Marcin ฉันกำลังบอกว่าdict.get()มันช้าลงซึ่ง2.pyช้าที่สุดในบรรดาทั้งหมด
อายะ

สำหรับเร็กคอร์ดสามและสี่นั้นเร็วกว่าการจับข้อผิดพลาดที่สำคัญในโครงสร้าง try / except อย่างมาก
Jeff

80

ฉันจะสร้างพจนานุกรม:

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

ตอนนี้ใช้เพียง:

options.get(something, doThisMostOfTheTime)()

หากsomethingไม่พบในoptionsdict dict.getจะคืนค่าเริ่มต้นdoThisMostOfTheTime

การเปรียบเทียบเวลาบางอย่าง:

สคริปต์:

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()

ผล:

>>> 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

สำหรับ10**5คีย์ที่ไม่มีอยู่จริงและคีย์ที่ถูกต้อง 100 คีย์ ::

>>> %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

ดังนั้นสำหรับการตรวจสอบพจนานุกรมปกติโดยใช้คีย์key in optionsเป็นวิธีที่มีประสิทธิภาพที่สุดที่นี่:

if key in options:
   options[key]()
else:
   doSomethingElse()

options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]()มีประสิทธิภาพมากกว่าเล็กน้อย
อายะ

ไอเดียเจ๋ง แต่อ่านไม่ออก นอกจากนี้คุณอาจต้องการแยกคำสั่งoptionsเพื่อหลีกเลี่ยงการสร้างใหม่ดังนั้นการย้ายส่วน (แต่ไม่ใช่ทั้งหมด) ของตรรกะให้ไกลจากจุดที่ใช้งาน ยังคงเป็นเคล็ดลับที่ดี!
Anders Johansson

7
คุณรู้หรือไม่ว่ามีประสิทธิภาพมากกว่านี้ ฉันเดาว่ามันช้าลงเนื่องจากมันทำการค้นหาแฮชมากกว่าการตรวจสอบเงื่อนไขง่ายๆ คำถามเกี่ยวกับประสิทธิภาพมากกว่าความกะทัดรัดของโค้ด
Bryan Oakley

2
@BryanOakley ฉันได้เพิ่มการเปรียบเทียบเวลาแล้ว
Ashwini Chaudhary

2
จริงๆแล้วมันควรจะมีประสิทธิภาพมากกว่าที่จะทำtry: options[key]() except KeyError: doSomeThingElse()(เนื่องจากif key in options: options[key]()คุณกำลังค้นหาพจนานุกรมสองครั้งสำหรับkey
hardmooth

9

คุณสามารถใช้ pypy ได้หรือไม่?

การรักษารหัสเดิมของคุณ แต่ใช้งานบน pypy ช่วยให้ฉันเพิ่มความเร็วได้ 50 เท่า

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 ขอบคุณสำหรับทิป. อันที่จริงฉันใช้ pypy อยู่แล้ว (ชอบมาก) แต่ฉันยังต้องการการปรับปรุงความเร็ว .. :)
kramer65

โอ้ดี! ก่อนหน้านี้ฉันได้ลองคำนวณแฮชล่วงหน้าสำหรับ 'this', 'that' และ 'there' - จากนั้นเปรียบเทียบรหัสแฮชแทนสตริง นั่นกลายเป็นว่าช้ากว่าต้นฉบับถึงสองเท่าดังนั้นดูเหมือนว่าการเปรียบเทียบสตริงจะได้รับการปรับให้เหมาะสมภายในเรียบร้อยแล้ว
foz

4

นี่คือตัวอย่างของ if ที่มีเงื่อนไขแบบไดนามิกที่แปลเป็นพจนานุกรม

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]

มันเป็นวิธีหนึ่ง แต่อาจไม่ใช่วิธีที่ยิ่งใหญ่ที่สุดในการทำเพราะอ่านได้น้อยสำหรับผู้ที่ไม่ถนัด Python


0

ผู้คนเตือนเกี่ยวกับexecเหตุผลด้านความปลอดภัย แต่นี่เป็นกรณีที่เหมาะสำหรับกรณีนี้
มันเป็นเครื่องรัฐที่ง่าย

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])
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.