Zrozumienie listy a dziwne wyniki wyrażeń generatora?
Odpowiadałem na to pytanie , wolałem tutaj wyrażenie generatora i użyłem tego, co myślałem, że będzie szybsze, ponieważ generator nie musi najpierw tworzyć całej listy:
>>> lis=[['a','b','c'],['d','e','f']]
>>> 'd' in (y for x in lis for y in x)
True
I Levon użył rozumienia listy w swoim rozwiązaniu ,
>>> lis = [['a','b','c'],['d','e','f']]
>>> 'd' in [j for i in mylist for j in i]
True
Ale kiedy zrobiłem wyniki timeit dla tych LC były szybsze niż generator:
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f']]" "'d' in (y for x in lis for y in x)"
100000 loops, best of 3: 2.36 usec per loop
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f']]" "'d' in [y for x in lis for y in x]"
100000 loops, best of 3: 1.51 usec per loop
Potem zwiększyłem rozmiar listy i zmierzyłem ją ponownie:
lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]
Tym razem na poszukiwanie 'd'
generator był szybszy od LC, ale kiedy przeszukałem środkowy element (11) i ostatni element, to LC ponownie bije wyrażenie generatora i nie mogę zrozumieć dlaczego?
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]" "'d' in (y for x in lis for y in x)"
100000 loops, best of 3: 2.96 usec per loop
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]" "'d' in [y for x in lis for y in x]"
100000 loops, best of 3: 7.4 usec per loop
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]" "11 in [y for x in lis for y in x]"
100000 loops, best of 3: 5.61 usec per loop
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]" "11 in (y for x in lis for y in x)"
100000 loops, best of 3: 9.76 usec per loop
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]" "18 in (y for x in lis for y in x)"
100000 loops, best of 3: 8.94 usec per loop
~$ python -m timeit -s "lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],[7,8,9],[10,11,12],[13,14,15],[16,17,18]]" "18 in [y for x in lis for y in x]"
100000 loops, best of 3: 7.13 usec per loop
3 answers
Przepuściłem prosty skrypt przez profiler dla bardziej szczegółowej analizy. Oto scenariusz:
lis=[['a','b','c'],['d','e','f'],[1,2,3],[4,5,6],
[7,8,9],[10,11,12],[13,14,15],[16,17,18]]
def ge_d():
return 'd' in (y for x in lis for y in x)
def lc_d():
return 'd' in [y for x in lis for y in x]
def ge_11():
return 11 in (y for x in lis for y in x)
def lc_11():
return 11 in [y for x in lis for y in x]
def ge_18():
return 18 in (y for x in lis for y in x)
def lc_18():
return 18 in [y for x in lis for y in x]
for i in xrange(100000):
ge_d()
lc_d()
ge_11()
lc_11()
ge_18()
lc_18()
Oto odpowiednie wyniki, uporządkowane tak, aby wzory jaśniej.
5400002 function calls in 2.830 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
100000 0.158 0.000 0.251 0.000 fop.py:3(ge_d)
500000 0.092 0.000 0.092 0.000 fop.py:4(<genexpr>)
100000 0.285 0.000 0.285 0.000 fop.py:5(lc_d)
100000 0.356 0.000 0.634 0.000 fop.py:8(ge_11)
1800000 0.278 0.000 0.278 0.000 fop.py:9(<genexpr>)
100000 0.333 0.000 0.333 0.000 fop.py:10(lc_11)
100000 0.435 0.000 0.806 0.000 fop.py:13(ge_18)
2500000 0.371 0.000 0.371 0.000 fop.py:14(<genexpr>)
100000 0.344 0.000 0.344 0.000 fop.py:15(lc_18)
Utworzenie wyrażenia generatora jest równoważne utworzenie funkcji generatora i wywołanie jej . To odpowiada za jedno wezwanie do <genexpr>
. Następnie, w pierwszym przypadku, next
jest wywoływany 4 razy, aż do osiągnięcia d
, W sumie 5 wywołań (razy 100000 iteracji = ncalls = 500000). W drugim przypadku jest on wywoływany 17 razy, w sumie 18 połączeń; a w trzecim, 24 razy, w sumie 25 połączeń.
Genex przewyższa zrozumienie listy w pierwszy przypadek, ale dodatkowe wywołania next
odpowiadają za większą różnicę między szybkością zrozumienia listy a szybkością wyrażenia generatora w drugim i trzecim przypadku.
>>> .634 - .278 - .333
0.023
>>> .806 - .371 - .344
0.091
Nie jestem pewien, co odpowiada za pozostały czas; wydaje się, że wyrażenia generatora byłyby wolniejsze nawet bez dodatkowych wywołań funkcji. Przypuszczam, że potwierdza to twierdzenie inspectorG4dget , że " tworzenie generatora ma bardziej natywne nad głową niż rozumienia listy."Ale w każdym razie pokazuje to dość wyraźnie, że wyrażenia generatora są wolniejsze głównie z powodu wywołań do next
.
Dodam, że gdy zwarcie nie pomaga, składanie list jest nadal szybsze, nawet dla bardzo dużych list. Na przykład:
>>> counter = itertools.count()
>>> lol = [[counter.next(), counter.next(), counter.next()]
for _ in range(1000000)]
>>> 2999999 in (i for sublist in lol for i in sublist)
True
>>> 3000000 in (i for sublist in lol for i in sublist)
False
>>> %timeit 2999999 in [i for sublist in lol for i in sublist]
1 loops, best of 3: 312 ms per loop
>>> %timeit 2999999 in (i for sublist in lol for i in sublist)
1 loops, best of 3: 351 ms per loop
>>> %timeit any([2999999 in sublist for sublist in lol])
10 loops, best of 3: 161 ms per loop
>>> %timeit any(2999999 in sublist for sublist in lol)
10 loops, best of 3: 163 ms per loop
>>> %timeit for i in [2999999 in sublist for sublist in lol]: pass
1 loops, best of 3: 171 ms per loop
>>> %timeit for i in (2999999 in sublist for sublist in lol): pass
1 loops, best of 3: 183 ms per loop
Jak widzisz, gdy zwarcie jest nieistotne, składanie list jest konsekwentnie szybsze nawet dla listy o długości miliona pozycji. Oczywiście dla rzeczywistych zastosowań in
w tych skalach, Generatory będą szybsze z powodu zwarcia. Jednak w przypadku innych zadań iteracyjnych, które są naprawdę liniowe pod względem liczby elementów, składanie list jest w zasadzie zawsze szybsze. Jest to szczególnie ważne, jeśli trzeba wykonać wiele testów na liście; można iterację nad już zbudowanym zrozumieniem listy bardzo szybko: {]}
>>> incache = [2999999 in sublist for sublist in lol]
>>> get_list = lambda: incache
>>> get_gen = lambda: (2999999 in sublist for sublist in lol)
>>> %timeit for i in get_list(): pass
100 loops, best of 3: 18.6 ms per loop
>>> %timeit for i in get_gen(): pass
1 loops, best of 3: 187 ms per loop
W tym przypadku lista jest rzędem wielkości szybciej!
Oczywiście, to pozostaje prawdą, dopóki nie zabraknie Ci pamięci. Co prowadzi mnie do ostatniego punktu. Istnieją dwa główne powody, dla których warto korzystać z generatora: aby wykorzystać zwarcie i zaoszczędzić pamięć. W przypadku bardzo dużych sekwencji/iterabli generatory są oczywistym sposobem, ponieważ oszczędzają pamięć. Ale jeśli zwarcie nie jest opcją, praktycznie nigdy nie wybieraj generatorów zamiast list dla prędkości. Wybrałeś je, aby zachować pamięć i zawsze jest wymiana.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:00:16
Całkowicie zależy od danych.
Generatory mają ustalony czas konfiguracji, który musi być amortyzowany w stosunku do liczby wywoływanych elementów; składanie List jest początkowo szybsze, ale znacznie zwalnia, ponieważ więcej pamięci jest używane w większych zestawach danych.
Przypomnijmy, że gdy listy cPython są rozszerzane, lista jest zmieniana w wzorze wzrostu4, 8, 16, 25, 35, 46, 58, 72, 88,.... W przypadku większych zestawień list, Python może przydzielać do 4x więcej pamięci niż rozmiar Twoje dane. Jak tylko trafisz w VM - - - naprawdę sloowww! Ale, jak wspomniano, składanie list jest szybsze niż generatory dla małych zestawów danych.
Rozważmy przypadek 1 , 2x26 lista list:
LoL=[[c1,c2] for c1,c2 in zip(string.ascii_lowercase,string.ascii_uppercase)]
def lc_d(item='d'):
return item in [i for sub in LoL for i in sub]
def ge_d(item='d'):
return item in (y for x in LoL for y in x)
def any_lc_d(item='d'):
return any(item in x for x in LoL)
def any_gc_d(item='d'):
return any([item in x for x in LoL])
def lc_z(item='z'):
return item in [i for sub in LoL for i in sub]
def ge_z(item='z'):
return item in (y for x in LoL for y in x)
def any_lc_z(item='z'):
return any(item in x for x in LoL)
def any_gc_z(item='z'):
return any([item in x for x in LoL])
cmpthese.cmpthese([lc_d,ge_d,any_gc_d,any_gc_z,any_lc_d,any_lc_z, lc_z, ge_z])
Wyniki w tych timings:
rate/sec ge_z lc_z lc_d any_lc_z any_gc_z any_gc_d ge_d any_lc_d
ge_z 124,652 -- -10.1% -16.6% -44.3% -46.5% -48.5% -76.9% -80.7%
lc_z 138,678 11.3% -- -7.2% -38.0% -40.4% -42.7% -74.3% -78.6%
lc_d 149,407 19.9% 7.7% -- -33.3% -35.8% -38.2% -72.3% -76.9%
any_lc_z 223,845 79.6% 61.4% 49.8% -- -3.9% -7.5% -58.5% -65.4%
any_gc_z 232,847 86.8% 67.9% 55.8% 4.0% -- -3.7% -56.9% -64.0%
any_gc_d 241,890 94.1% 74.4% 61.9% 8.1% 3.9% -- -55.2% -62.6%
ge_d 539,654 332.9% 289.1% 261.2% 141.1% 131.8% 123.1% -- -16.6%
any_lc_d 647,089 419.1% 366.6% 333.1% 189.1% 177.9% 167.5% 19.9% --
Teraz rozważmy przypadek 2, który wykazuje duże różnice między LC i gen. w tym przypadku szukamy jednego elementu w liście list o wymiarach 100 x 97 x 97:
LoL=[[str(a),str(b),str(c)]
for a in range(100) for b in range(97) for c in range(97)]
def lc_10(item='10'):
return item in [i for sub in LoL for i in sub]
def ge_10(item='10'):
return item in (y for x in LoL for y in x)
def any_lc_10(item='10'):
return any([item in x for x in LoL])
def any_gc_10(item='10'):
return any(item in x for x in LoL)
def lc_99(item='99'):
return item in [i for sub in LoL for i in sub]
def ge_99(item='99'):
return item in (y for x in LoL for y in x)
def any_lc_99(item='99'):
return any(item in x for x in LoL)
def any_gc_99(item='99'):
return any([item in x for x in LoL])
cmpthese.cmpthese([lc_10,ge_10,any_lc_10,any_gc_10,lc_99,ge_99,any_lc_99,any_gc_99],c=10,micro=True)
Wyniki w tych czas:
rate/sec usec/pass ge_99 lc_99 lc_10 any_lc_99 any_gc_99 any_lc_10 ge_10 any_gc_10
ge_99 3 354545.903 -- -20.6% -30.6% -60.8% -61.7% -63.5% -100.0% -100.0%
lc_99 4 281678.295 25.9% -- -12.6% -50.6% -51.8% -54.1% -100.0% -100.0%
lc_10 4 246073.484 44.1% 14.5% -- -43.5% -44.8% -47.4% -100.0% -100.0%
any_lc_99 7 139067.292 154.9% 102.5% 76.9% -- -2.4% -7.0% -100.0% -100.0%
any_gc_99 7 135748.100 161.2% 107.5% 81.3% 2.4% -- -4.7% -100.0% -100.0%
any_lc_10 8 129331.803 174.1% 117.8% 90.3% 7.5% 5.0% -- -100.0% -100.0%
ge_10 175,494 5.698 6221964.0% 4943182.0% 4318339.3% 2440446.0% 2382196.2% 2269594.1% -- -38.5%
any_gc_10 285,327 3.505 10116044.9% 8036936.7% 7021036.1% 3967862.6% 3873157.1% 3690083.0% 62.6% --
Jak widzisz -- to zależy i to jest kompromis...
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-17 14:45:59
Wbrew powszechnemu przekonaniu, składanie list jest dość dobre dla umiarkowanych zakresów. Protokół Iterator implikuje wywołania dla iterator.__next__()
, a wywołania funkcji w Pythonie są - prawdę mówiąc-nieprzyjemnie drogie.
Oczywiście w pewnym momencie wymiana pamięci/procesora generatorów zacznie się opłacać, ale w przypadku małych zestawów składanie list jest bardzo wydajne.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-08-09 17:31:02