问题
I'm trying to write a small context manager that'll try to execute some code repeatedly until the code works or until a specified number of tries has been made. I have attempted to write this but am encountering a difficulty with having the context manager handle problems when yielding:
Exception RuntimeError: 'generator ignored GeneratorExit'
How should I code this?
import contextlib
import random
def main():
with nolube():
print(1 / random.randint(0, 1))
@contextlib.contextmanager
def nolube(
tries = None # None: try indefinitely
):
"""
Create a context for trying something repeatedly.
"""
tries_done = 0
rekt = True
if tries is None:
while rekt is True:
try:
yield
rekt = False
except:
tries_done += 1
pass
else:
while rekt is True and tries_done <= tries:
try:
yield
rekt = False
except:
tries_done += 1
pass
if __name__ == "__main__":
main()
回答1:
@contextlib.contextmanager
has a very clear contract; it'll only be resumed once. It can't be used to re-run code.
In fact, you can't use a context manager to control repetitions at all. You need a loop here, not a context manager. A context manager doesn't control the block, it is only informed when entering and exiting.
Use the tenacity package* instead; it provides a decorator. The decorator wraps a function in a while True loop that'll re-run the function for you.
You'd apply it to your case by moving the print()
statement into a function, decorated with @retry
, then calling that function:
import random
from tenacity import retry
@retry
def foo():
print(1 / random.randint(0, 1))
def main():
foo()
* This answer originally recommended the retrying package but this was forked into a new package with updated API when that project fell dormant.
回答2:
You can't do that. A context manager in Python is simply a protocol which:
- Calls
__enter__
- Executes one or more statements
- Calls
__exit__
Point 3. is guaranteed to happen, which makes it great for handling resource releasing, etc. But the important point here is point 2.: the context manager will run the code within the context, then process 3. By that point, the wrapped code is gone, forgotten and unreachable forever, so you can't 'call it again'. contextlib
offers a nice API for defining your context manager simply by doing it as a function:
@contextmanager
def ctxt():
# 1: __enter__ code
yield
# 3: __exit__ code
And the documentation clearly specifies:
The function being decorated must return a generator-iterator when called. This iterator must yield exactly one value, which will be bound to the targets in the with statement’s as clause, if any.
So the previous point remains.
What you can do to call something repeteadly is to put it in a function, and decorate that function with your 'repeat until success' logic:
def dec(f):
def decorated(*args, **kwargs):
while True:
try:
return f(*args, **kwargs)
except:
pass
return decorated
But this is totally unrelated to context managers.
回答3:
The short answer is: you can't really do this with context manager in Python.
We can only yield
once in a context manager, so it doesn't make sense to use yield
inside a while
loop. This is different from the yield
used in Ruby
block
s.
And we don't have access to the body of code either, for example, we don't automatically get something like a function
that we can reuse.
So, no, if you do want to implement a reusable retry
logic, use a function instead.
def retry(func, n_times=None):
i = 1
while True:
try:
return func()
except Exception:
i += 1
if n_times and i >= n_times:
raise
来源:https://stackoverflow.com/questions/36740887/how-can-a-python-context-manager-try-to-execute-code