If you need to always do some cleanup, consider using a context manager:
from contextlib import contextmanager
@contextmanager
def normalized(sub_item):
... # normalize
yield resource
... # cleanup
If the cleanup is necessary even with an exception:
@contextmanager
def normalized(sub_item):
... # normalize
try:
yield resource
finally:
... # cleanup
You can change your other functions so that they require the resource from the context as an argument, preventing them to be called outside of that context. Then:
for index, sub_item in enumerate(item.sub_items):
with normalized(sub_item) as resource:
process(sub_item)
if index == 0:
calculator(sub_item, resource)
I.e., use data flow dependencies to enforce a particular ordering. This isn't perfect in Python because the scope of the resource
variable will be quite wide, but it's a lot harder to make an accidental mistake.
Testing for index == 0
is the cleanest way to add extra processing for the first item. There are alternatives such as explicitly using iterators, but they imply some amount of code duplication and are more difficult to read:
sub_items_iter = iter(item.sub_items)
# consume first item from iterator
for sub_item in sub_items_iter:
with normalized(sub_item) as resource:
process(sub_item)
calculator(sub_item, resource)
break
# consume remaining items
for sub_item in sub_items_iter:
with normalized(sub_item) as resource:
process(sub_item)
Using the for-loop to consume a single item from an iterator isn't obvious, but it's still clearer than the de-sugared variant:
try:
sub_item = next(sub_items_iter)
has_first = True
except StopIteration:
has_first = False
if has_first:
with normalized(sub_item) as resource:
process(sub_item)
calculator(sub_item, resource)