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
Author: Community, 2012-08-15

3 answers

Rozszerzając odpowiedź , wyrażenia generatora są często wolniejsze niż składanie list ze względu na narzut wywołań funkcji. W tym przypadku zachowanie zwarcia in kompensuje tę powolność, jeśli element zostanie znaleziony dość wcześnie, ale poza tym wzór utrzymuje się.

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.

 35
Author: senderle,
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...

 13
Author: the wolf,
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.

 10
Author: Paulo Scardine,
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