1

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

  • 2
    Not to start a flamewar, but php really encourages bad habits. Go for option two. – klutt Apr 09 '18 at 10:32
  • 1
    As the author of the accepted answer in the linked question, I can only second klutt's comment and Nils Werner's (excellent) answer: Python is a totally different language with a totally different execution model and a totally different philosophy so forget PHP and learn to write things the Pythonic way. – bruno desthuilliers Apr 09 '18 at 10:49
  • Thank you. You're right and I like the way Nils put that. – Albert Uler Silva Melo Apr 09 '18 at 12:22

1 Answers1

4

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
Nils Werner
  • 34,832
  • 7
  • 76
  • 98