Reputation: 59426
Sometimes I have a cascade of different things I can try to accomplish a task, e. g. If I need to get a record I can first try to find the record, and if this fails, I can create the missing record, and if this also fails, I can use a tape instead.
Failing is represented by throwing an exception my code needs to catch.
In Python this looks something like this:
try:
record = find_record()
except NoSuchRecord:
try:
record = create_record()
except CreateFailed:
record = tape
This already has the disadvantage of piling-up indentations. If I have five options, this code won't look good.
But I find it even more problematic when there are also else
clauses to the try
-except
clauses:
try:
record = find_record()
except NoSuchRecord:
try:
record = create_record()
except CreateFailed:
record = tape
logger.info("Using a tape now")
else:
logger.info("Created a new record")
else:
logger.info("Record found")
The find_record()
and the corresponding Record found
message are as far apart as possible which makes it hard to read code. (Moving the code of the else
clause directly into the try
clause is only an option if this code is definitely not raising one of the exceptions caught in the except
statement, so this is no general solution.)
Again, this ugliness gets worse with each added level of nesting.
Is there a nicer way to put this into Python code
try
and the except
clauses of one topic closely together and/orUpvotes: 3
Views: 118
Reputation: 5451
I think following code is more readable and clean. Also I am sure in real problem we need some parameters to be sent to "find_record" and "create_record" functions like id, some, values to create new record. The factory solution need those parameters also be listed in tuple
def try_create(else_return):
try:
record = create_record()
except CreateFailed:
record = else_return
logger.info("Using a tape now")
else:
logger.info("Created a new record")
def try_find(else_call= try_create, **kwargs):
try:
record = find_record()
except NoSuchRecord:
try_create(**kwargs)
else:
logger.info("Record found")
try_find(else_call=try_create, else_return=tape)
Upvotes: 0
Reputation: 50076
You can use a for
loop to successively try variants:
for task, error in ((find_record, NoSuchRecord), (create_record, CreateFailed)):
try:
result = task()
except error:
continue
else:
break
else:
# for..else is only entered if there was no break
result = tape
If you need an else
clause, you can provide it as a separate function:
for task, error, success in (
(find_record, NoSuchRecord, lambda: logger.info("Record found")),
(create_record, CreateFailed, lambda: logger.info("Created a new record"))
):
try:
result = task()
except error:
continue
else:
success()
break
else:
result = tape
logger.info("Using a tape now")
Take note that the default case tape
is not part of the variants - this is because it has no failure condition. If it should execute with the variants, it can be added as (lambda: tape, (), lambda: None)
.
You can put this all into a function for reuse:
def try_all(*cases):
for task, error, success in cases:
try:
result = task()
except error:
continue
else:
success()
return result
try_all(
(find_record, NoSuchRecord, lambda: logger.info("Record found")),
(create_record, CreateFailed, lambda: logger.info("Created a new record")),
(lambda: tape, (), lambda: logger.info("Using a tape now")),
)
In case the tuples seem difficult to read, a NamedTuple
can be used to name the elements. This can be mixed with plain tuples:
from typing import NamedTuple, Callable, Union, Tuple
from functools import partial
class Case(NamedTuple):
task: Callable
error: Union[BaseException, Tuple[BaseException, ...]]
success: Callable
try_all(
Case(
task=find_record,
error=NoSuchRecord,
success=partial(logger.info, "Record found")),
(
create_record, CreateFailed,
partial(logger.info, "Created a new record")),
Case(
task=lambda: tape,
error=(),
success=partial(logger.info, "Using a tape now")),
)
Upvotes: 6
Reputation: 6590
You could break that into multiple functions ?
def handle_missing():
try:
record = create_record()
except CreateFailed:
record = tape
logger.info("Using a tape now")
else:
logger.info("Created a new record")
return record
def get_record():
try:
record = find_record()
except NoSuchRecord:
record = handle_missing()
else:
logger.info("Record found")
return record
And then you'd use it like,
record = get_record()
Upvotes: 3