1

As I was writing another question, I started to wonder whether I could not simplify/rationalize my code. The idea is to have decorators that centralize the reading and writing of a database

I have the following (simple, home-grade) problem: I keep the state of a program in a JSON file and have several functions that make use of that "database". Some just need to load the DB, some need to load it, and then write back to file.

I wanted to use decorators on these functions to centralize the reading and writing of the database. Below is a simplified version of my code, with two functions: one that only consumes the DB, and another one that also modifies it. This code works and returns the expected values

def load_db(func):
    def wrapper():
        print("loading DB")
        db = 5
        # the db was loaded and is now passed to the function actually making use of it
        func(db)
    return wrapper

def load_and_write_db(func):
    def wrapper():
        print("loading DB")
        db = 5
        # the db was loaded and is now passed to the function actually making use of it
        # we will get back the changed database
        db = func(db)
        # now we write the DB to the disk
        print(f"writing DB: {db}")
    return wrapper

@load_db
def do_stuff_load_only(*db):
    # a function that just consumes the DB, without changing it
    print(f"initial DB is {db}")

@load_and_write_db
def do_stuff_load_and_write(*db):
    # a function that consumes and chnages the DB (which then needs to be updated on disk)
    print(f"initial DB is {db}")
    db = 10
    print(f"changed DB to {db}")
    # returning the new DB
    return db


do_stuff_load_only()
do_stuff_load_and_write()

# Output:
# 
# loading DB
# initial DB is (5,)
# loading DB
# initial DB is (5,)
# changed DB to 10
# writing DB: 10

I have therefore two decorators:

  • one to read the DB, and pass it to the function
  • another one to read the DB, pass it to the function, and retrieve the modified DB to write it back

Would it be possible to change these decorators so that one does the reading, and the other one the writing of the DB? And decorate function either with one or both?

The point that is not clear to me is that in each of the decorators, there is the actual call to the function. If I have two decorators I assume that I would need to call the function twice (one from each decorator - something that I of course do not want to do). Or is there a better approach?

WoJ
  • 27,165
  • 48
  • 180
  • 345
  • Yes, it's possible to use more than one decorator at once. Note that the topmost one gets priority. – fluffyyboii Feb 21 '21 at 13:07
  • @fluffyyboii: yes, I know that I can use two or more decorators. My question is about the logistics of calling the actual function from each of them. I updated the title to make it clear – WoJ Feb 21 '21 at 13:09
  • The second decorator takes in the original function and returns a decorated version. Then, the first decorator decorates that new function. The original function is still only called once. – fluffyyboii Feb 21 '21 at 13:18

1 Answers1

0

You could break these functions into two simple functions that does one job only:

  1. Load db
  2. Write db
def load_db(func):
    def wrapper():
        print("loading DB")
        db = 5
        # the db was loaded and is now passed to the function actually making use of it
        ret_db = func(db)

        # Check if returns a DB, this is required if it is written
        if ret_db:
            return ret_db
        return db
    return wrapper


def write_db(func):
    def wrapper():
        db = func()
        # now we write the DB to the disk
        print(f"writing DB: {db}")
    return wrapper


@load_db
def do_stuff_load_only(*db):
    # a function that just consumes the DB, without changing it
    print(f"initial DB is {db}")


@write_db
@load_db
def do_stuff_load_and_write(*db):
    # a function that consumes and chnages the DB (which then needs to be updated on disk)
    print(f"initial DB is {db}")
    db = 10
    print(f"changed DB to {db}")
    # returning the new DB
    return db


do_stuff_load_only()
do_stuff_load_and_write()

This prints the same output as yours.

In this case, we can use two decorators or one based on requirement. There are two things to note here:

  1. If you make a change, you need to return the db, or else it original unmodified db will be returned (see the wrapper for load_db)
  2. You have to use the decorators in this order only: write_db and then load_db, switching order won't work because of arguments required for wrapper functions.