I am dogmatic about never using global variables. But when people way smarter than me use them, like Brian Kernighan does when he builds a command-line interpreter in that chapter of The Unix Programming Environment, I wonder if maybe I’m being too reluctant.
I was looking at a python module vaguely like this:
def foo():
do_foo_stuff()
def bar():
do_bar_stuff()
def baz():
do_baz_stuff()
def quux():
do_quux_stuff()
I had a bunch of independent functions. I wanted to add logging. I saw two easy ways to do it:
def foo():
logger = get_logging_singleton()
logger.log_stuff()
do_foo_stuff()
def bar():
logger = get_logging_singleton()
logger.log_stuff()
do_bar_stuff()
def baz():
logger = get_logging_singleton()
logger.log_stuff()
do_baz_stuff()
def quux():
logger = get_logging_singleton()
logger.log_stuff()
do_quux_stuff()
In the above code, I would get a reference to my logger object in each function call. No globals. Maybe I am violating some tenet of dependency injection, but I’ll talk about that later. Anyhow, the point I want to make is that the above approach is the way I would do it in the past.
Here’s how I decided to write it this time:
logger = get_logging_singleton()
def foo():
logger.log_stuff()
do_foo_stuff()
def bar():
logger.log_stuff()
do_bar_stuff()
def baz():
logger.log_stuff()
do_baz_stuff()
def quux():
logger.log_stuff()
do_quux_stuff()
All the functions access the logger created in the main namespace of the module. It feels a tiny bit wrong, but I think it is the right thing to do. The other way violates DRY in a big fat way.
So, a third option would be to require the caller to pass in the logging object in every function call, like this:
def quux(logger):
logger.log_stuff()
do_quux_stuff()
This seems like the best possible outcome — it satisfies my hangup about avoiding global variables and the caller can make decisions about log levels by passing any particular logger it wants to.
There’s two reasons why I didn’t take this approach:
- I was working on existing code, and I didn’t have the option of cramming in extra parameters in the calling library. So, I could do something like
def quux(logger=globally_defined_logger)
but I’m trying to make this prettier, not uglier. The whole reason that I wanted to add logging was that I wanted some visibility into what what the heck was going wrong in my app. I didn’t have time to monkey with overhauling the whole system.
- I plan to control my logger behavior from an external configuration system. I don’t want to change code inside the caller every time I want to bump the log level up or down. It is the conventional wisdom in my work environment that I face less risk just tweaking a configuration file setting and restarting my app rather than editing my code*.
[*]I suspect that in the final analysis, this belief will be exposed as garbage. But for right now, it seems pretty true that bugs occur more frequently after editing code than after editing config files.
UPDATE: Apparently, I’m not just talking to myself here! Gary Bernhardt linked to this post and added some really interesting points. Also, his link to the post on the origin of the phrase now you have two problems was something I hadn’t heard of before.