'Multiple try blocks with similar code and a single exception

I have a set of slightly different operations I want to perform on a bunch of different files, some of which may or may not exist at any given time. In all cases, if a file doesn't exist (returning FileNotFoundError), I want it to just log that that particular file doesn't exist, then move on, i.e. identical exception handling for all FileNotFoundError cases.

The tricky part is that what I want to do with each set of file is slightly different. So I can't simply do:

for f in [f1, f2, f3, f4]:
    try:
        do_the_thing(f)
    except FileNotFoundError:
        print(f'{f} not found, moving on!')

Instead I want something like:

try:
    for f in [f_list_1]:
        do_thing_A(f)
    for f in [f_list_2]:
        do_thing_B(f)
    for f in [f_list_3]:
        do_thing_C(f)
except FileNotFoundError:
    print(f'{f} not found, moving on!')

But where each for block is tried regardless of success / failure of previous block.

Obviously I could use a whole lot of separate try-except sets:

for f in [f_list_1]:
    try:
        do_thing_A(f)
    except FileNotFoundError:
        print(f'{f} not found, moving on!')
for f in [f_list_2]:
    try:
        do_thing_B(f)
    except FileNotFoundError:
        print(f'{f} not found, moving on!')
for f in [f_list_3]:
    try:
        do_thing_C(f)
    except FileNotFoundError:
        print(f'{f} not found, moving on!')

But is there a more elegant or Pythonic way to do this, given that it's the same exception handling in each case?



Solution 1:[1]

I think I would start with something like this that wraps your functions.

def do_thing_A(f):
    print(f"do_thing_A({f})")

def do_thing_B(f):
    print(f"do_thing_B({f})")

def do_by_thing(fn, file_path):
    try:
        fn(file_path)
    except FileNotFoundError:
        print(f'{file_path} not found, moving on!')

tasks = [
    (do_thing_A, ["hello", "word"]), #(function, list_of_paths)
    (do_thing_B, ["foo", "bar"]),
]

for fn, file_list in tasks:
    for file_path in file_list:
        do_by_thing(fn, file_path)

Having done so though I think this begs the question of decorators. Maybe something like:

import os

def if_file_exists(fn):
    def _inner(f):
        if not os.path.isfile(f):
            print(f'{f} not found, moving on!')
            return
        fn(f)
    return _inner

@if_file_exists
def do_thing_A(f):
    print(f"do_thing_A({f})")

@if_file_exists
def do_thing_B(f):
    print(f"do_thing_B({f})")

tasks = [
    (do_thing_A, ["./results.csv", "word"]), #(function, list_of_paths)
    (do_thing_B, ["./sample.txt", "bar"]),
]

for fn, file_list in tasks:
    for file_path in file_list:
        fn(file_path)

Of course, either option could be altered to use exceptions or os.path.isfile(). I just did one each way.

If you decided that wrappers and decorators was overkill, then you could also just simply do:

def do_thing_A(f):
    print(f"do_thing_A({f})")

def do_thing_B(f):
    print(f"do_thing_B({f})")

tasks = [
    (do_thing_A, ["hello", "word"]), #(function, list_of_paths)
    (do_thing_B, ["foo", "bar"]),
]

for fn, file_list in tasks:
    for file_path in file_list:
        try:
            fn(file_path)
        except FileNotFoundError:
            print(f'{file_path} not found, moving on!')

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1