We can define a decorator that takes an ordinary instance method and turns it into one with your desired prologue and epilogue. Here's what our decorator will look like
def wrapped_app(fn):
def wrapped_fn(self, *args, **kwargs):
self.step_one()
fn(self, *args, **kwargs)
self.step_three()
return wrapped_fn
So given a function fn, wrapped_app returns a new function (which we locally call wrapped_fn) which does self.step_one(), then fn, then self.step_three(). We can use it with decorator syntax inside the class now
class Main:
def step_one(self):
print("STEP 1")
def step_three(self):
print("STEP 3")
@wrapped_app
def app_one(self):
print("App one")
@wrapped_app
def app_two(self):
print("App two")
@wrapped_app
def app_three(self):
print("App three")
We can check that it works as follows
>>> main = Main()
>>> main.app_one()
STEP 1
App one
STEP 3
We can also go one step further. Your current app_one et al don't return anything, but it is possible that we may want such functions to be capable of returning. A slight modification to wrapped_fn will take care of that eventuality. Similarly, we may also want step_three to be guaranteed to run, even in the case that our app function fails or raises an exception. We can use try ... finally to do so.
def wrapped_app(fn):
def wrapped_fn(self, *args, **kwargs):
self.step_one()
try:
result = fn(self, *args, **kwargs)
finally:
self.step_three()
return result
return wrapped_fn