动态函数文档字符串

问题描述 投票:0回答:4

我想编写一个具有动态创建的文档字符串的 python 函数。 本质上,对于函数

func()
我希望
func.__doc__
是一个描述符,调用自定义
__get__
函数根据请求创建文档字符串。 然后
help(func)
应该返回动态生成的文档字符串。

这里的上下文是在现有的分析包中编写一个包装大量命令行工具的python包。 每个工具都成为一个类似命名的模块函数(通过函数工厂创建并插入到模块命名空间中),并通过分析包动态生成函数文档和接口参数。

python
4个回答
16
投票

你无法以你想要的方式做你想做的事。

从你的描述看来你可以做这样的事情:

for tool in find_tools():
    def __tool(*arg):
        validate_args(tool, args)
        return execute_tool(tool, args)
    __tool.__name__ = tool.name
    __tool.__doc__ = compile_docstring(tool)
    setattr(module, tool.name, __tool)

即创建函数时,预先动态创建文档字符串。 这就是文档字符串从一次调用到下一次调用必须是动态的原因吗?


假设有,您必须将函数包装在一个类中,使用

__doc__

来触发操作。


但即便如此,你还是遇到了问题。当调用 help() 来查找文档字符串时,它是在类上调用的,而不是实例上的,所以这种事情:

__call__

不起作用,因为属性是实例,而不是类属性。您可以通过将 doc 属性放在元类(而不是类本身)上来使其起作用

class ToolWrapper(object): def __init__(self, tool): self.tool = tool self.__name__ = tool.name def _get_doc(self): return compile_docstring(self.tool) __doc__ = property(_get_doc) def __call__(self, *args): validate_args(args) return execute_tool(tool, args)

现在你可以做

for tool in find_tools(): # Build a custom meta-class to provide __doc__. class _ToolMetaclass(type): def _get_doc(self): return create_docstring(tool) __doc__ = property(_get_doc) # Build a callable class to wrap the tool. class _ToolWrapper(object): __metaclass__ = _ToolMetaclass def _get_doc(self): return create_docstring(tool) __doc__ = property(_get_doc) def __call__(self, *args): validate_args(tool, args) execute_tool(tool, args) # Add the tool to the module. setattr(module, tool.name, _ToolWrapper())

并获取自定义文档字符串,或者

help(my_tool_name)

对于同样的事情。 
my_tool_name.__doc__

属性位于

__doc__
类中,需要捕获后一种情况。
    


3
投票
(Python 3解决方案)

您可以利用 Python 的鸭子类型来实现动态字符串:

_ToolWrapper

注意事项:

这假设 import time def fn(): pass class mydoc( str ): def expandtabs( self, *args, **kwargs ): return "this is a dynamic strting created on {}".format( time.asctime() ).expandtabs( *args, **kwargs ) fn.__doc__ = mydoc() help( fn ) 函数正在调用

help
.expandtabs
对象获取文本,这适用于 Python 3.7。更强大的解决方案将实现其他
__doc__
方法,以便即使
str
方法发生变化,我们的鸭子也能继续像鸭子一样行事。另请注意,我们的
help
类派生自
mydoc
,这是因为
str
(有点不典型)通过断言
help
强制执行强类型。与所有解决方案一样,这有点棘手,但这是否是一个问题很大程度上取决于完整的项目需求。
    


0
投票

不过,这个问题的解决方案对于记录“传递”到其他函数的 args/kwargs 很有用,而无需将文档字符串耦合到底层函数的 API。

一种解决方案如下:此代码提供了一个装饰器isinstance(thing.__doc__, str),它动态地将来自一个或多个指定函数的参数附加到被装饰函数的文档字符串中。

dynamic_helpstring

使用示例:

使用单个函数的参数装饰函数:

  1. import inspect from typing import Callable, List, Union def dynamic_helpstring(functions: Union[Callable, List[Callable]]): """ A decorator that dynamically appends the arguments (wargs) from one or more specified functions to the docstring of the decorated function. Args: functions (Union[Callable, List[Callable]]): A function or a list of functions whose kwargs will be appended to the docstring of the decorated function. Returns: Callable: The original function with an updated docstring. """ if not isinstance(functions, list): functions = [functions] def decorator(func): # Generate the additional docstring with args from the specified functions additional_doc = "\n\nAvailable args from specified functions:\n" for function in functions: additional_doc += f"\nFrom {function.__module__}.{function.__name__}():\n" sig = inspect.signature(function) params = sig.parameters function_doc = "\n".join( _get_arg_docstring(name, param) for name, param in params.items() ) additional_doc += function_doc + "\n" if func.__doc__ is None: func.__doc__ = "" func.__doc__ += additional_doc return func return decorator def _get_arg_docstring(name: str, param: inspect.Parameter): annote = ( f"{param.annotation.__module__}.{param.annotation.__name__}" if hasattr(param.annotation, "__name__") and hasattr(param.annotation, "__module__") else param.annotation ) annote = annote.replace("builtins.","") if param.default is inspect.Parameter.empty: return f"\t{name} ({annote})" return f"\t{name} ({annote}): {param.default}"
用多个函数的参数装饰一个函数:
  1. @dynamic_helpstring(bar) def foo(...): pass
带有来自多个函数的参数的完整示例:
    @dynamic_helpstring([bar, bas]) def foo(...): pass
  1. 结果:
from typing import List, Union
from modeling.dynamic_helpstring import dynamic_helpstring

def foo(*, a: int, b: Union[int,str]):
    pass

def bar(c: list, d: str = None):
    pass

@dynamic_helpstring([foo,bar])
def bas(**kwargs):
    """default help string"""
    pass

help(bas)

与其搞乱函数,为什么不编写自己的 
Help on function bas in module __main__: bas(**kwargs) default help string Available args from specified functions: From __main__.foo(): a (int) b (typing.Union) From __main__.bar(): c (list) d (str): None

-4
投票

help

    

© www.soinside.com 2019 - 2024. All rights reserved.