Search code examples
pythonframeworkspygamedesktopautoloader

Should I always use the most pythonic way to import modules?


I am making a tiny framework for games with pygame, on which I wish to implement basic code to quickly start new projects. This will be a module that whoever uses should just create a folder with subfolders for sprite classes, maps, levels, etc. My question is, how should my framework module load these client modules? I was considering to design it so the developer could just pass to the main object the names of the directories, like:

game = Game()
game.scenarios = 'scenarios'

Then game will append 'scenarios' to sys.path and use __import__(). I've tested and it works. But then I researched a little more to see if there were already some autoloader in python, so I could avoid to rewrite it, and I found this question Python modules autoloader? Basically, it is not recommended to use a autoloader in python, since "explicit is better than implicit" and "Readability counts".

That way, I think, I should compel the user of my module to manually import each of his/her modules, and pass these to the game instance, like:

import framework.Game
import scenarios
#many other imports
game = Game()
game.scenarios = scenarios
#so many other game.whatever = whatever

But this doesn't looks good to me, not so confortable. See, I am used to work with php, and I love the way it works with it's autoloader. So, the first exemple has some problability to crash or be some trouble, or is it just not 'pythonic'?

note: this is NOT an web application


Solution

  • I wouldn't consider letting a library import things from my current path or module good style. Instead I would only expect a library to import from two places:

    1. Absolute imports from the global modules space, like things you have installed using pip. If a library does this, this library must also be found in its install_requires=[] list

    2. Relative imports from inside itself. Nowadays these are explicitly imported from .:

      from . import bla
      from .bla import blubb
      

    This means that passing an object or module local to my current scope must always happen explicitly:

    from . import scenarios
    import framework
    
    scenarios.sprites  # attribute exists
    game = framework.Game(scenarios=scenarios)
    

    This allows you to do things like mock the scenarios module:

    import types
    import framework
    
    # a SimpleNamespace looks like a module, as they both have attributes
    scenarios = types.SimpleNamespace(sprites='a', textures='b')
    scenarios.sprites  # attribute exists
    game = framework.Game(scenarios=scenarios)
    

    Also you can implement a framework.utils.Scenario() class that implements a certain interface to provide sprites, maps etc. The reason being: Sprites and Maps are usually saved in separate files: What you absolutely do not want to do is look at the scenarios's __file__ attribute and start guessing around in its files. Instead implement a method that provides a unified interface to that.

    class Scenario():
        def __init__(self):
            ...
    
        def sprites(self):
            # optionally load files from some default location
            # If no such things as a default location exists, throw a NotImplemented error
            ...
    

    And your user-specific scenarios will derive from it and optionally overload the loading methods

    import framework.utils
    class Scenario(framework.utils.Scenario):
        def __init__(self):
            ...
    
        def sprites(self):
            # this method *must* load files from location
            # accessing __file__ is OK here
            ...
    

    What you can also do is have framework ship its own framework.contrib.scenarios module that is used in case no scenarios= keyword arg was used (i.e. for a square default map and some colorful default textures)

    from . import contrib
    
    class Game()
        def __init__(self, ..., scenarios=None, ...):
            if scenarios is None:
                scenarios = contrib.scenarios
            self.scenarios = scenarios