4

I'm new to conditional importing in Python, and am considering two approaches for my module design. I'd appreciate input on why I might want to go with one vs. the other (or if a better alternative exists).

The problem

I have a program that will need to call structurally identical but distinct modules under different conditions. These modules all have the same functions, inputs, outputs, etc., the only difference is in what they do within their functions. For example,

# module_A.py
def get_the_thing(input):
    # do the thing specific to module A
    return thing

# module_B.py
def get_the_thing(input):
    # do the thing specific to module B
    return thing

Option 1

Based on an input value, I would just conditionally import the appropriate module, in line with this answer.

if val == 'A':
    import module_A
if val == 'B':
    import module_B

Option 2

I use the input variable to generate the module name as a string, then I call the function from the correct module based on that string using this method. I believe this requires me to import all the modules first.

import module_A
import module_B

in_var = get_input() # Say my input variable is 'A', meaning use Module A
module_nm = 'module_' + in_var
function_nm = 'get_the_thing'

getattr(globals()[module_nm], function_nm)(my_args)

The idea is this would call module_A.get_the_thing() by generating the module and function names at runtime. This is a frivolous example for only one function call, but in my actual case I'd be working with a list of functions, just wanted to keep things simple.

Any thoughts on whether either design is better, or if something superior exists to these two? Would appreciate any reasons why. Of course, A is more concise and probably more intuitive, but wasn't sure this necessarily equated to good design or differences in performance.

Julian Drago
  • 719
  • 9
  • 23
  • Option 1 seems overwhelmingly better. Python uses it in some cases when loading platform specific stuff. I can't see why 2 would ever be preferred. Personally, I gag a little everytime I see `globals()[]`. I don't think I've ever seen it's use where it was the best option for any problem (not saying that they don't exist, but they seem to be quite rare) – Carcigenicate Dec 24 '19 at 00:48
  • For 1 to work though, you'd probably want to do `if val == 'A': import module_A as some_name; elif val == 'B': import module_B as some_name` instead. It won't work well if the imports aren't given the same name, since then you'll need to figure out later which was imported. – Carcigenicate Dec 24 '19 at 00:52
  • @Carcigenicate Would it work OK if it were `from module_A import *` and `from module_B import *`? Then you can just call `get_the_thing()` without knowing the module names. – Barmar Dec 24 '19 at 00:55
  • @Barmar That would work, but `import *` isn't good practice. Iirc, PEP8 explicitly discourages it since it pollutes the namespace. – Carcigenicate Dec 24 '19 at 00:56
  • An alternative would be to define multiple classes, then `if val == 'A': thing = classA() elif val == 'B': thing = classB()` – Barmar Dec 24 '19 at 00:57

1 Answers1

5

I'd go with Option 1. It's significantly neater, and you aren't needing to fiddle around with strings to do lookups. Dealing with strings, at the very least, will complicate refactoring. If you ever change any of the names involved, you must remember to update the strings as well; especially since even smart IDEs won't be able to help you here with typical shift+F6 renaming. The less places that you have difficult to maintain code like that, the better.

I'd make a minor change to 1 though. With how you have it now, each use of the module will still require using a qualified name, like module_A.do_thing(). That means whenever you want to call a function, you'll need to first figure out which was imported in the first place, which leads to more messy code. I'd import them under a common name:

if val == 'A':
    import module_A as my_module

if val == 'B':
    import module_B as my_module

. . .

my_module.do_thing()  # The exact function called will depend on which module was imported as my_module

You could also, as suggested in the comments, use a wildcard import to avoid needing to use a name for the module:

if val == 'A':
    from module_A import *

if val == 'B':
    from module_B import *

. . .

do_thing()

But this is discouraged by PEP8:

Wildcard imports (from <module> import *) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.

It also pollutes the namespace that you're importing into, making it easier to accidentally shadow a name from the imported file.

Carcigenicate
  • 43,494
  • 9
  • 68
  • 117