Search code examples
pythonpython-3.xparameter-passingdecoratorpython-decorators

is *args the proper way to pass parameters to/from decorated functions?


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(*)

Please note how the database (db) is passed between the decorators and the function

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

Is this the proper approach to pass information between the decorators and the function? Specifically, should I rely on *db to indicate only the decorator passes an argument to the function, and nothing is passed when it is actually called from the code (last two lines)?

This answer explains very nicely how arguments should be passed, it just fails short to address my question about decorated functions sometomes receiving an argument, and sometimes not.


(*) Almost. db passed to the function arrives as a tuple, something I can live with


Solution

  • The way your decorator is written, do_stuff_load_only can be defined with a regular parameter, even if you won't actually pass an argument when you call it. That's because the name do_stuff_load_only isn't going to be bound to a one-argument function; it's going to be bound to the zero-argument function wrapper defined inside load_db. wrapper itself will take care of passing an argument to the actual one-argument function being decorated.

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

    Defining do_stuff_load_only(*db) would work, but changes what is actually bound to db; in this case, it would be the singleton tuple (5,) rather than the integer 5.

    If you think this looks awkward, that's because it is. load_db has a "hidden" side effect that you shouldn't have to worry about. A context manager would probably be more fitting here:

    from contextlib import contextmanager
    
    
    @contextmanager
    def load_db():
        print("Initializing the database")
        yield 5  # provide it
        print("Tearing down the database")
    
    
    def do_stuff_load_only(*db):
        # a function that just consumes the DB, without changing it
        print(f"initial DB is {db}")
    
    
    with load_db() as db:
        do_stuff_load_only(db)
    

    Now the definition and use of the function bound to do_stuff_load_only agree, with the details of how the database is created and destroyed hidden by the context manager. The output of the above code is

    Initializing the database
    initial DB is 5
    Tearing down the database