Скорость извлечения элементов словаря с помощью try ... кроме vs. key в dict? - PullRequest
3 голосов
/ 28 апреля 2020

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

if key in dict_:
   value = dict_[key]
   do_something
else:
   do_something_else

или

try:
    value = dict_[key]
except KeyError:
    do_something_else
else:
    do_something

Какой из них быстрее / предпочтительнее? Зависит ли это от размера словаря?

Кажется, что здесь может быть два конкурирующих эффекта: 1) необходимость дважды искать ключ, против 2) настройка трассировки стека исключений.

1 Ответ

3 голосов
/ 28 апреля 2020

Вы можете сравнить три различных метода с помощью timeit. get1 - это try...except, get2 использует встроенные .get и get3 первые проверки.

In [1]: def get1(d, k): 
......:     try: 
......:         return d[k] 
......:     except KeyError: 
......:         return None 

In [3]: def get2(d, k): 
......:     return d.get(k) 

In [4]: def get3(d, k): 
......:    if k in d: return d[k]  
......:    else: return None 

В небольшом словаре (100 элементов)

In [8]: %timeit -n 100 [get1(little_d, e) for e in range(len(little_d))]                
18.8 µs ± 270 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [9]: %timeit -n 100 [get2(little_d, e) for e in range(len(little_d))]                
22.5 µs ± 352 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [10]: %timeit -n 100 [get3(little_d, e) for e in range(len(little_d))]               
19.3 µs ± 862 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

И в большем (1M элементов)

In [11]: %timeit -n 100 [get1(big_d, e) for e in range(len(little_d))]
19.4 µs ± 469 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [12]: %timeit -n 100 [get2(big_d, e) for e in range(len(little_d))]
21.8 µs ± 241 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [13]: %timeit -n 100 [get3(big_d, e) for e in range(len(little_d))]
19.2 µs ± 128 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

В словаре (1M) с ~ 50% "промахов" (random.choices(range(0, 2*len(big_id)), k=len(big_d))) видно больше различий:

In [20]: %timeit -n 100 [get1(big_d, e) for e in choices]                              
514 ms ± 10.4 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [21]: %timeit -n 100 [get2(big_d, e) for e in choices]                              
416 ms ± 4.54 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [22]: %timeit -n 100 [get3(big_d, e) for e in choices]                              
367 ms ± 4.89 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

.get все равно будет быстрее в этот случай.

In [23]: %timeit -n 100 [big_id.get(e) for e in choices]                                
334 ms ± 3.6 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

Они кажутся близкими, одинаково подвержены влиянию размера словаря, но по-разному подвержены промахам. Единственный реальный размер воздействия будет иметь место, когда python начинается побеждает (как это, вероятно, здесь происходит). Важным примечанием является то, что get2 просто медленнее, поскольку вызваны накладные расходы двух вызовов функций (get2 и .get), которые стоят дорого - i sh в python (как показано в последнем тесте). Промахи приведут к более медленным результатам по разным причинам, как указывает @ user2864740 .

tl; dr

Я бы использовал .get.


Ответ также будет в значительной степени зависеть от скорости реализации ваших ключей в __hash__ и __eq__. Если __hash__ и __eq__ медленные, различия в двух вызовах hash могут показать улучшение метода 1 или 2 (без дополнительной функции).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...