Search code examples
pythonpython-3.xfunctionglobalpython-turtle

Is it possible to automatically make all of a module's public class methods global functions without exec()? turtle.py as an example


I've seen it claimed that exec() is never necessary and should always be avoided. While there are clearly better ways to do things 99.9% of the time, I was looking at turtle.py and found this:

## The following mechanism makes all methods of RawTurtle and Turtle available
## as functions. So we can enhance, change, add, delete methods to these
## classes and do not need to change anything here.

__func_body = """\
def {name}{paramslist}:
    if {obj} is None:
        if not TurtleScreen._RUNNING:
            TurtleScreen._RUNNING = True
            raise Terminator
        {obj} = {init}
    try:
        return {obj}.{name}{argslist}
    except TK.TclError:
        if not TurtleScreen._RUNNING:
            TurtleScreen._RUNNING = True
            raise Terminator
        raise
"""

def _make_global_funcs(functions, cls, obj, init, docrevise):
    for methodname in functions:
        method = getattr(cls, methodname)
        pl1, pl2 = getmethparlist(method)
        if pl1 == "":
            print(">>>>>>", pl1, pl2)
            continue
        defstr = __func_body.format(obj=obj, init=init, name=methodname,
                                    paramslist=pl1, argslist=pl2)
        exec(defstr, globals())
        globals()[methodname].__doc__ = docrevise(method.__doc__)

_make_global_funcs(_tg_screen_functions, _Screen,
                   'Turtle._screen', 'Screen()', _screen_docrevise)
_make_global_funcs(_tg_turtle_functions, Turtle,
                   'Turtle._pen', 'Turtle()', _turtle_docrevise)

This code is why using the turtle module is so convenient for beginners. In addition to Turtle class instances automatically assigning themselves Screens as necessary, this "mechanism" automatically makes every public class method into a global function. This is why code like this, which never directly instantiates any turtle classes, works:

import turtle
turtle.forward(100)
turtle.mainloop()

This will instantiate a Screen, instantiate a Turtle, assign the Screen to the Turtle, call a method of that Turtle instance to draw a line, and call a method of that Screen to keep the window open, all with automatically generated global functions. This is the best use case for exec() I've ever seen.

Is there a way to do this more pythonically without exec()?


Solution

  • Here is how this could be replicated without exec()... somewhat.

    exec() is still necessary for assigning the old call signature to the new global functions in an explicit way, there is no convenient way to give a function a new call signature.

    However, as an answer to my question, a closure works for making the methods into global functions, the call signature just has to be *args, **kwargs.

    The following seems to work in the place of the code I posted and answers my question's title, how to replicate making methods global functions without exec, though I would argue it is not more pythonic due to the lack of accurate signatures:

    def __turtle_func_closure(obj, init, name):
        """Wraps class methods as global functions."""
    
        def func(*args, **kwargs):
            """Global function equivalent ofor class methods."""
            obj_ = getattr(Turtle, obj, None)
            if obj_ is None:
                if not TurtleScreen._RUNNING:
                    TurtleScreen._RUNNING = True
                    raise Terminator
                obj_ = init()
            try:
                return getattr(obj_, name)(*args, **kwargs)
            except TK.TclError:
                if not TurtleScreen._RUNNING:
                    TurtleScreen._RUNNING = True
                    raise Terminator
                raise
    
        return func
    
    
    def _make_global_funcs(functions, cls, obj, init, docrevise):
        for methodname in functions:
            method = getattr(cls, methodname)
    
            globals()[methodname] = __turtle_func_closure(obj, init, methodname)
            globals()[methodname].__doc__ = docrevise(method.__doc__)
    
    _make_global_funcs(_tg_screen_functions, _Screen,
                       'Turtle._screen', Screen, _screen_docrevise)
    _make_global_funcs(_tg_turtle_functions, Turtle,
                       'Turtle._pen', Turtle, _turtle_docrevise)