wydajność w składaniu list i wyrażeniach generatora

Następujące zachowanie wydaje mi się raczej sprzeczne z intuicją (Python 3.4):

>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]

Wartości pośrednie ostatniego wiersza nie zawsze są None, są tym, co my send w generatorze, równoważne (chyba) Następującemu generatorowi:

def f():
   for i in range(3):
      yield (yield i)
Wydaje mi się zabawne, że te trzy wersy w ogóle działają. Referencja mówi, że yield jest dozwolone tylko w definicji funkcji (choć może źle ją odczytuję i / lub może być po prostu skopiowane ze starszej wersji). Pierwsze dwie linie tworzą SyntaxError w Pythonie 2.7, ale trzecia linia nie.

Również, wydaje się dziwne

  • że lista zwraca generator, a nie listę
  • i że wyrażenie generatora przekonwertowane na listę i odpowiadające im zrozumienie listy zawierają różne wartości.
Czy ktoś mógłby podać więcej informacji?
Author: Martijn Pieters, 2015-08-21

1 answers

Uwaga: był to błąd w obsłudze yield CPython w wyrażeniach złożonych i generatorów, naprawiony w Pythonie 3.8, z ostrzeżeniem o dezaktualizacji w Pythonie 3.7. Zobacz raport o błędach Pythona oraz Co nowego dla Python 3.7 i Python 3.8 .

Wyrażenia generatora oraz składnia set i dict są kompilowane do (generatora) obiektów funkcyjnych. W Pythonie 3 składanie list otrzymuje to samo leczenie; wszystkie one są w istocie nowym zagnieżdżonym zakresem.

Możesz to zobaczyć, jeśli spróbujesz zdemontować wyrażenie generatora:

>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (range)
             12 LOAD_CONST               2 (3)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               3 (None)
             26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Powyższe pokazuje, że wyrażenie generatora jest kompilowane do obiektu kodu, ładowanego jako funkcja (MAKE_FUNCTION tworzy obiekt funkcji z obiektu kodu). Odniesienie .co_consts[0] pozwala nam zobaczyć obiekt kodu wygenerowany dla wyrażenia i używa YIELD_VALUE tak, jak zrobiłaby to funkcja generatora.

Jako takie wyrażenie yield działa w ten kontekst, ponieważ kompilator postrzega je jako funkcje-w-ukryciu.

To jest błąd; yield nie ma miejsca w tych wyrażeniach. Python gramatyka przed Pythonem 3.7 pozwala na to (dlatego kod jest kompilowalny), ale yield Specyfikacja wyrażenia pokazuje, że użycie yield tutaj nie powinno działać:

Wyrażenie wydajności jest używane tylko przy definiowaniu funkcji generatora i dlatego może być używane tylko w ciele funkcji definicja.

Jest to błąd, który został potwierdzony w numerze 10544. Rozwiązanie błędu polega na tym, że użycie yield i yield from spowoduje podniesienie SyntaxError w Pythonie 3.8 ; w Pythonie 3.7 podniesienie DeprecationWarning aby Kod przestał używać tej konstrukcji. Zobaczysz to samo ostrzeżenie w Pythonie 2.7.15 i nowszych, jeśli użyjesz -3 przełącznik wiersza poleceń włączający Ostrzeżenia zgodności Pythona 3.

Ostrzeżenie 3.7.0b1 wygląda tak; obracanie ostrzeżenie o błędach daje wyjątek SyntaxError, tak jak w 3.8:

>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: 'yield' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter('error')
>>> [(yield i) for i in range(3)]
  File "<stdin>", line 1
SyntaxError: 'yield' inside list comprehension

Różnice między sposobem działania yield w zrozumieniu listy i yield w wyrażeniu generatora wynikają z różnic w implementacji tych dwóch wyrażeń. W Pythonie 3 zrozumienie listy wykorzystuje wywołania LIST_APPEND, aby dodać górną część stosu do budowanej listy, podczas gdy wyrażenie generatora daje tę wartość. Dodanie w (yield <expr>) po prostu dodaje kolejny YIELD_VALUE opcode do albo:

>>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                13 (to 22)
              9 STORE_FAST               1 (i)
             12 LOAD_FAST                1 (i)
             15 YIELD_VALUE
             16 LIST_APPEND              2
             19 JUMP_ABSOLUTE            6
        >>   22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 YIELD_VALUE
             14 POP_TOP
             15 JUMP_ABSOLUTE            3
        >>   18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

Kod opcode YIELD_VALUE w indeksach bajtowych 15 i 12 jest dodatkowy, kukułka w gnieździe. Tak więc dla generatora list-comprehension-turned-generator MASZ 1 Wydajność produkującą wierzchołek stosu za każdym razem (zastępując wierzchołek stosu zwracaną wartością yield), a dla wariantu wyrażenia generatora dajesz wierzchołek stosu (liczbę całkowitą), a następnie wydajesz ponownie, ale teraz stos zawiera wartość zwracaną yield i otrzymujesz None tę sekundę. czas.

Dla zrozumienia listy, zamierzone list wyjście obiektu jest nadal zwracane, ale Python 3 widzi to jako generator, więc zwracana wartość jest dołączona doStopIteration wyjątek jako atrybut value:

>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3))  # avoid exhausting the generator
[0, 1, 2]
>>> try:
...     next(listgen)
... except StopIteration as si:
...     print(si.value)
... 
[None, None, None]

Te None obiekty są wartościami zwracanymi z wyrażeń yield.

I powtórzyć to jeszcze raz; ta sama kwestia dotyczy słownika i zestawu rozumienia w Pythonie 2 i Pythonie 3, a także w Pythonie 2 yield zwracane wartości są nadal dodawane do zamierzonego słownika lub obiektu set, a wartość zwracana jest "yielded" last zamiast dołączona do wyjątku StopIteration:

>>> list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
['bar', 'foo', 'eggs', 'spam', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]
 59
Author: Martijn Pieters,
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
2018-03-16 15:33:30