Czy można bezpiecznie poddawać się blokowi " z " w Pythonie (i dlaczego)?

Kombinacja koroutinów i pozyskiwania zasobów wydaje się mieć niezamierzone (lub nieintuicyjne) konsekwencje.

Podstawowe pytanie brzmi, czy coś takiego działa, czy nie:

def coroutine():
    with open(path, 'r') as fh:
        for line in fh:
            yield line
/ Align = "left" / (Możesz to przetestować!)

Głębsza troska polega na tym, że with ma być czymś alternatywnym dla finally, gdzie zapewniamy, że zasób zostanie wydany na końcu bloku. Coroutines może zawiesić i wznowić wykonywanie z w obrębie with blok, więc jak rozwiązuje się konflikt?

Na przykład, jeśli otworzysz plik z odczytem/zapisem zarówno wewnątrz, jak i na zewnątrz coroutine, podczas gdy coroutine jeszcze nie powrócił:

def coroutine():
    with open('test.txt', 'rw+') as fh:
        for line in fh:
            yield line

a = coroutine()
assert a.next() # Open the filehandle inside the coroutine first.
with open('test.txt', 'rw+') as fh: # Then open it outside.
    for line in fh:
        print 'Outside coroutine: %r' % repr(line)
assert a.next() # Can we still use it?

Update

W poprzednim przykładzie chodziło mi o zastrzeżenie obsługi plików z blokadą zapisu, ale ponieważ większość systemów operacyjnych przydziela filehandle dla każdego procesu, nie będzie tam żadnych zastrzeżeń. (Wyrazy uznania dla @ Miles za wskazanie przykładu nie miało zbyt dużego sensu.) Oto moja poprawiona przykład, który pokazuje rzeczywisty stan impasu:

import threading

lock = threading.Lock()

def coroutine():
    with lock:
        yield 'spam'
        yield 'eggs'

generator = coroutine()
assert generator.next()
with lock: # Deadlock!
    print 'Outside the coroutine got the lock'
assert generator.next()
Author: cdleary, 2009-03-26

5 answers

Nie bardzo rozumiem, o jaki konflikt pytasz, ani problem z przykładem: dobrze jest mieć dwa współistniejące, niezależne Uchwyty do tego samego pliku.

Jednej rzeczy nie wiedziałem, że dowiedziałem się w odpowiedzi na twoje pytanie, że istnieje nowa metoda close () na generatorach:

close() generuje nowy wyjątek GeneratorExit wewnątrz generatora, aby zakończyć iterację. Po otrzymaniu tego wyjątku kod generatora musi podnieść GeneratorExit lub StopIteration.

close() jest wywoływany, gdy generator jest zbierany śmieci, więc oznacza to, że kod generatora dostaje ostatnią szansę na uruchomienie, zanim generator zostanie zniszczony. Ta ostatnia szansa oznacza, że try...finally wypowiedzi w generatorach mogą być teraz zagwarantowane, że zadziałają; klauzula finally będzie teraz zawsze miała szansę działać. Wydaje się to drobną ciekawostką językową, ale użycie generatorów i try...finally jest w rzeczywistości konieczne, aby zaimplementować with oświadczenie opisane przez PEP 343.

Http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features

Tak, że obsługuje sytuację, w której instrukcja with jest używana w generatorze, ale daje w środku, ale nigdy nie powraca-metoda menedżera kontekstu __exit__ zostanie wywołana, gdy generator zostanie usunięty.


Edit :

Jeśli chodzi o problem z obsługą plików: czasami zapominam, że istnieją platformy, które nie są podobne do POSIX. :)

[13]}Jeśli chodzi o zamki, myślę, że Rafał Dowgird uderza głową w gwoździe, gdy mówi: "trzeba mieć świadomość, że generator jest jak każdy inny obiekt, który przechowuje zasoby."Nie wydaje mi się, aby with stwierdzenie było tutaj tak istotne, ponieważ ta funkcja cierpi na te same problemy z impasem:
def coroutine():
    lock.acquire()
    yield 'spam'
    yield 'eggs'
    lock.release()

generator = coroutine()
generator.next()
lock.acquire() # whoops!
 21
Author: Miles,
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
2009-03-26 18:56:11

Nie sądzę, żeby to był prawdziwy konflikt. Musisz tylko mieć świadomość, że generator jest jak każdy inny obiekt, który przechowuje zasoby, więc obowiązkiem twórcy jest upewnienie się, że jest on prawidłowo sfinalizowany (i aby uniknąć konfliktów/impasu z zasobami posiadanymi przez obiekt). Jedynym (drobnym) problemem, jaki tu widzę jest to, że generatory nie implementują protokołu zarządzania kontekstem (przynajmniej w Pythonie 2.5), więc nie można po prostu:

with coroutine() as cr:
  doSomething(cr)

Ale zamiast tego mają do:

cr = coroutine()
try:
  doSomething(cr)
finally:
  cr.close()

Śmieciarz i tak robi close(), ale to zła praktyka polegać na tym, aby uwolnić zasoby.

 9
Author: Rafał Dowgird,
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
2009-03-26 10:44:15

Ponieważ yield może wykonać dowolny kod, byłbym bardzo ostrożny trzymając blokadę nad instrukcją yield. Podobny efekt można uzyskać na wiele innych sposobów, włączając w to wywołanie metody lub funkcji, które mogły zostać nadpisane lub w inny sposób zmodyfikowane.

Generatory są jednak zawsze (prawie zawsze) "zamknięte", albo jawnym wywołaniem close(), albo po prostu zbieraniem śmieci. Zamknięcie generatora powoduje wyrzucenie GeneratorExit wyjątku wewnątrz generatora i w ten sposób uruchamia się w końcu klauzulach, ze stwierdzeniami, itp. Możesz przechwycić wyjątek, ale musisz rzucić lub zamknąć funkcję (np. rzucić wyjątek StopIteration), zamiast oddać. To chyba kiepska praktyka polegania na garbage collector, aby zamknąć generator w przypadkach, jak napisałeś, bo to może się zdarzyć później niż chcesz, a jeśli ktoś zadzwoni sys._exit (), wtedy twoje sprzątanie może w ogóle nie nastąpić.
 1
Author: Doug,
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
2009-03-26 10:44:54

Tak bym się tego spodziewał. Tak, Blok NIE WYDA swoich zasobów, dopóki się nie zakończy, więc w tym sensie zasób uciekł z leksykalnego zagnieżdżania. Jednak nie różni się to niczym od wywołania funkcji, która próbowała użyć tego samego zasobu w bloku with - nic nie pomaga w przypadku, gdy blok ma nie jeszcze zakończony, z bez względu na powód. To nie jest nic konkretnego do generatorów.

Jedna rzecz, która może być warto jednak martwić się o zachowanie, jeśli generator jest nigdy wznowione. Spodziewałbym się, że blok with będzie działał jak blok finally i wywoła __exit__ część po zakończeniu, ale tak się nie wydaje.

 0
Author: Brian,
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
2009-03-26 10:19:42

Dla TLDR, spójrz na to w ten sposób:

with Context():
    yield 1
    pass  # explicitly do nothing *after* yield
# exit context after explicitly doing nothing

Context kończy się po zakończeniu pass (tzn. nic), pass wykonuje po zakończeniu yield (tzn. wznawia wykonywanie). Tak więc with Kończy się po kontrola jest wznowiona w yield.

TLDR: a with kontekst pozostaje utrzymany, gdy yield zwalnia kontrolę.


Istnieją właściwie tylko dwie zasady, które są tu istotne:

  1. Kiedy with wypuści swój zasób?

    To tak raz i bezpośrednio po jego blok jest wykonywany. Pierwszy oznacza, że nie wypuszcza podczas a yield, ponieważ może się to zdarzyć kilka razy. Później oznacza, że wypuszcza po yield zakończono.

  2. Kiedy kończy się yield?

    Pomyśl o yield jako o wywołaniu odwrotnym: kontrola jest przekazywana do dzwoniącego, a nie do wywołanego. Podobnie, yield Kończy się, gdy kontrola jest przekazywana z powrotem do niej, tak jak gdy wywołanie powraca Kontrola.

Zauważ, że zarówno with jak i yield działają zgodnie z przeznaczeniem! Celem with lock jest ochrona zasobu i pozostaje on chroniony podczas yield. Zawsze można wyraźnie zwolnić tę ochronę:

def safe_generator():
  while True:
    with lock():
      # keep lock for critical operation
      result = protected_operation()
    # release lock before releasing control
    yield result
 0
Author: MisterMiyagi,
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-01-19 15:03:36