Export decorator that manages __all__

2019-01-26 14:19发布

问题:

A proper Python module will list all its public symbols in a list called __all__. Managing that list can be tedious, since you'll have to list each symbol twice. Surely there are better ways, probably using decorators so one would merely annotate the exported symbols as @export.

How would you write such a decorator? I'm certain there are different ways, so I'd like to see several answers with enough information that users can compare the approaches against one another.

回答1:

You could simply declare the decorator at the module level like this:

__all__ = []

def export(obj):
    __all__.append(obj.__name__)
    return obj

This is perfect if you only use this in a single module. At 4 lines of code (plus probably some empty lines for typical formatting practices) it's not overly expensive to repeat this in different modules, but it does feel like code duplication in those cases.



回答2:

In Is it a good practice to add names to __all__ using a decorator?, Ed L suggests the following, to be included in some utility library:

import sys

def export(f):
    """Use a decorator to avoid retyping function/class names.

    * Based on an idea by Duncan Booth:
      http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a
    * Improved via a suggestion by Dave Angel:
      http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1
    """
    mod = sys.modules[fn.__module__]
    if hasattr(mod, '__all__'):
        name, all_ = f.__name__, mod.__all__
        if name not in __all__:
            all_.append(name)
    else:
        mod.__all__ = [fn.__name__]
    return f

We've adapted the name to match the other examples. With this in a local utility library, you'd simply write

from .utility import export

and then start using @export. Just one line of idiomatic Python, you can't get much simpler than this. On the downside, the module does require access to the module by using the __module__ property and the sys.modules cache, both of which may be problematic in some of the more esoteric setups (like custom import machinery, or wrapping functions from another module to create functions in this module).

The python part of the atpublic package by Barry Warsaw does something similar to this. It offers some keyword-based syntax, too, but the decorator variant relies on the same patterns used above.

This great answer by Aaron Hall suggests something very similar, with two more lines of code as it doesn't use __dict__.setdefault. It might be preferable if manipulating the module __dict__ is problematic for some reason.



回答3:

https://github.com/russianidiot/public.py has yet another implementation of such a decorator. Its core file is currently 160 lines long! The crucial points appear to be the fact that it uses the inspect module to obtain the appropriate module based on the current call stack.



回答4:

You could define the following in some utility library:

def exporter():
    all = []
    def decorator(obj):
        all.append(obj.__name__)
        return obj
    return decorator, all

export, __all__ = exporter()
export(exporter)

# possibly some other utilities, decorated with @export as well

Then inside your public library you'd do something like this:

from . import utility

export, __all__ = utility.exporter()

# start using @export

Using the library takes two lines of code here. It combines the definition of __all__ and the decorator. So people searching for one of them will find the other, thus helping readers to quickly understand your code. The above will also work in exotic environments, where the module may not be available from the sys.modules cache or where the __module__ property has been tampered with or some such.