1

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

WoJ
  • 27,165
  • 48
  • 180
  • 345
  • It's not clear what you are asking. The title asks about using ``*`` to pass things *to the decorator*, the code shows using ``*`` to pass things *to the decorated function* . Note that ``*`` is not directly tied to decorators, it indicates variadic arguments – decorators often are variadic in their arguments, but do not have to be. – MisterMiyagi Feb 21 '21 at 14:14
  • Why does `load_db` define a wrapper that ignores the argument passed to the decorated function and replace it with a hard-coded value instead? – chepner Feb 21 '21 at 14:17
  • 1
    What do you mean by *"my question about decorated functions sometimes receiving an argument, and sometimes not"*? It seems to me that your decorated function **always** receives the `db` argument, because the decorator always passes it. If you want a single parameter that may or may not be passed as an argument, it would be more sensible to give it a default value (i.e. `def foo(db=None): ...`) rather than use the `*args` syntax which allows an arbitrary number of arguments (not just 0 or 1). – kaya3 Feb 21 '21 at 14:27
  • @kaya3: * It seems to me that your decorated function always receives the db argument,* - please look at the last two lines of my code, nothing is passed. Now, the named parameters is indeed a much better idea, did not think about that, thanks! – WoJ Feb 21 '21 at 14:40
  • @chepner: this is to simulate the load of the data from disk, and assigning it to `db` (and then passing for the actual use) – WoJ Feb 21 '21 at 14:41
  • @MisterMiyagi: yes you are right, I mistyped what I meant, I will correct that right away. – WoJ Feb 21 '21 at 14:42
  • 1
    @WoJ I looked at your code already; please read my comment more carefully. Just because you call the (decorated) function without passing an argument, doesn't mean the function doesn't *receive* the argument; the decorator passes it. The decorator **always** passes it, so the function always receives it. – kaya3 Feb 21 '21 at 21:42

1 Answers1

1

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
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thank you for the context manager information - this looks very promising. As a side note, shouldn't the decorator be `@contextmanager`? Wile for the "readonly" version the context manager is straightforward, is it possible to use it in the "read, then write" one? – WoJ Feb 22 '21 at 13:22
  • To answer my own question: probably yes, I am checking with class-based context managers that handle `__enter__` and `__exit__` – WoJ Feb 22 '21 at 13:30
  • I would try to use `contextmanager` first, as it's less verbose. Basically, anything before the `yield` is what goes into the resulting `__enter__` method; the thing yielded is what gets returned by `__enter__`, and everything after the `yield` is the `__exit__` method. `contextmanager` just generates the methods for you. – chepner Feb 22 '21 at 14:07
  • (Unless, of course, you already have an existing class to which it makes sense to add `__enter__` and `__exit__` methods.) – chepner Feb 22 '21 at 14:08
  • Thank you, this is very clear - I already wrote the class but I will refactor it to use `contextmanager` – WoJ Feb 22 '21 at 14:14