Python如何实现docstring

doc

Python语言从排版上看强制要求了一些书写规范,算是强制赋予了每个程序员一个"代码洁癖"。作为规范的一部分,可以在在类函数的开始增加注释,并且语言本身为这种注释做了"背书":可以通过help展示这个帮助文档的内容。
这个本来是Python一个很细小的功能,也是一个很有意思的语法糖(因为其它语言并不常见这种语法),所以还是可以看看这个语法的一些实现细节。
这里主要考虑的问题是文档如何存储的问题。其实识别这个注释字符串是一个语法层面的内容判断,存储文档字符串也并不麻烦(在运行时存储字符串是基本操作,各种属性名字符串操作不在话下)。更重要的是一个运行时的处理逻辑,最简单的想法就是增加一个虚拟机指令,用来安装文档信息。

测试可以看到,对于文件(模块)级别的文档,的确是通过抓们的STORE_NAME指令来完成的,但是对于函数的实现并没有对应的虚拟机指令来安装。

tsecer@harry: cat py_doc_str.py
"""file doc"""
def tsecer():
    """func doc"""
    pass

tsecer@harry: python3 -m dis py_doc_str.py
  1           0 LOAD_CONST               0 ('file doc')
              2 STORE_NAME               0 (__doc__)

  2           4 LOAD_CONST               1 (<code object tsecer at 0x7fa8ac8b0810, file "py_doc_str.py", line 2>)
              6 LOAD_CONST               2 ('tsecer')
              8 MAKE_FUNCTION            0
             10 STORE_NAME               1 (tsecer)
             12 LOAD_CONST               3 (None)
             14 RETURN_VALUE
tsecer@harry: python3 
>>> import dis, py_doc_str
>>> dis.dis(py_doc_str)
Disassembly of tsecer:
  4           0 LOAD_CONST               1 (None)
              2 RETURN_VALUE

>>> help(py_doc_str.tsecer)

>>> print(py_doc_str.__doc__)
file doc
>>> print(py_doc_str.tsecer.__doc__)
func doc
>>> 

语法

函数创建

从前面的虚拟机指令可以看到,def 函数执行的对应虚拟机指令是MAKE_FUNCTION,对应的动作会从代码的co_consts数组的第一个槽位(PyTuple_GetItem(consts, 0))中的内容,并把这个内容安装到定义函数的func_doc字段中(当然,这里检测了consts的数量是否大于0,并且第一个const是一个Unicode类型的对象)。

PyObject *
PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)
{
///...
    consts = ((PyCodeObject *)code)->co_consts;
    if (PyTuple_Size(consts) >= 1) {
        doc = PyTuple_GetItem(consts, 0);
        if (!PyUnicode_Check(doc))
            doc = Py_None;
    }
    else
        doc = Py_None;
    Py_INCREF(doc);
    op->func_doc = doc;
///...
}

每个function对象它类型描述中,描述了对象的doc从狗结构的func_doc位置取,也就是前面设置的字符串位置。

/* Methods */

#define OFF(x) offsetof(PyFunctionObject, x)

static PyMemberDef func_memberlist[] = {
    {"__closure__",   T_OBJECT,     OFF(func_closure),
     RESTRICTED|READONLY},
    {"__doc__",       T_OBJECT,     OFF(func_doc), PY_WRITE_RESTRICTED},
    {"__globals__",   T_OBJECT,     OFF(func_globals),
     RESTRICTED|READONLY},
    {"__module__",    T_OBJECT,     OFF(func_module), PY_WRITE_RESTRICTED},
    {NULL}  /* Sentinel */
};

验证

可以看到,在生成的function定义中,consts是一个tuple类型(有序),并且第一个元素是约定的函数注释。

>>> dir(py_doc_str.tsecer)            
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> dir(py_doc_str.tsecer.__code__)            
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> type(py_doc_str.tsecer.__code__.co_consts) 
<class 'tuple'>
>>> print(py_doc_str.tsecer.__code__.co_consts)
('func doc', None)
>>> 

parser

词法

在词法分析阶段,一个特殊的处理事判断如果是单引号的处理。可以看到,

  • 引号具体是使用单引号还是双引号并没有区别,只要结束位置相同即可。
  • 引号数量可以为1个,两个,最多三个。不论多少个,最后只识别相同符号(单引号/双引号)相同数量的符号作为结束。
  • 如果是单引号的情况下不接受中间换行。
  • 返回的词法单位是一个STRING类型。
static int
tok_get(struct tok_state *tok, char **p_start, char **p_end)
{
///...
        /* Get rest of string */
        while (end_quote_size != quote_size) {
            c = tok_nextc(tok);
            if (c == EOF) {
                if (quote_size == 3) {
                    tok->done = E_EOFS;
                }
                else {
                    tok->done = E_EOLS;
                }
                tok->cur = tok->inp;
                return ERRORTOKEN;
            }
            if (quote_size == 1 && c == '\n') {
                tok->done = E_EOLS;
                tok->cur = tok->inp;
                return ERRORTOKEN;
            }
            if (c == quote) {
                end_quote_size += 1;
            }
            else {
                end_quote_size = 0;
                if (c == '\\') {
                    tok_nextc(tok);  /* skip escaped char */
                }
            }
        }

        *p_start = tok->start;
        *p_end = tok->cur;
        return STRING;
    }

语法

语法分析阶段,字符串生成一个Str_kind类型节点。

/* Make a Str node, but decref the PyUnicode object being added. */
static expr_ty
make_str_node_and_del(PyObject **str, struct compiling *c, const node* n)
{
    PyObject *s = *str;
    *str = NULL;
    assert(PyUnicode_CheckExact(s));
    if (PyArena_AddPyObject(c->c_arena, s) < 0) {
        Py_DECREF(s);
        return NULL;
    }
    return Str(s, LINENO(n), n->n_col_offset, c->c_arena);
}
expr_ty
Str(string s, int lineno, int col_offset, PyArena *arena)
{
    expr_ty p;
    if (!s) {
        PyErr_SetString(PyExc_ValueError,
                        "field s is required for Str");
        return NULL;
    }
    p = (expr_ty)PyArena_Malloc(arena, sizeof(*p));
    if (!p)
        return NULL;
    p->kind = Str_kind;
    p->v.Str.s = s;
    p->lineno = lineno;
    p->col_offset = col_offset;
    return p;
}

doc_string

显然不是所有的字符节点都是docstring,只有一个block的第一条语句如果是Str_kind的话才算是doc-string,而这个就需要在需要支持docstring的上下文中自己判断是否把第一条语句的Str_kind作为docstring。可见下面的逻辑都是判断了是否是第一条语句。由于compiler_body只有在class和module中做了判断,所以也就只有这两种语法结构和函数定义中可以包含docstring。

static int
compiler_isdocstring(stmt_ty s)
{
    if (s->kind != Expr_kind)
        return 0;
    if (s->v.Expr.value->kind == Str_kind)
        return 1;
    if (s->v.Expr.value->kind == Constant_kind)
        return PyUnicode_CheckExact(s->v.Expr.value->v.Constant.value);
    return 0;
}


/* Compile a sequence of statements, checking for a docstring
   and for annotations. */

static int
compiler_body(struct compiler *c, asdl_seq *stmts)
{
    int i = 0;
    stmt_ty st;

    /* Set current line number to the line number of first statement.
       This way line number for SETUP_ANNOTATIONS will always
       coincide with the line number of first "real" statement in module.
       If body is empy, then lineno will be set later in assemble. */
    if (c->u->u_scope_type == COMPILER_SCOPE_MODULE &&
        !c->u->u_lineno && asdl_seq_LEN(stmts)) {
        st = (stmt_ty)asdl_seq_GET(stmts, 0);
        c->u->u_lineno = st->lineno;
    }
    /* Every annotated class and module should have __annotations__. */
    if (find_ann(stmts)) {
        ADDOP(c, SETUP_ANNOTATIONS);
    }
    if (!asdl_seq_LEN(stmts))
        return 1;
    st = (stmt_ty)asdl_seq_GET(stmts, 0);
    if (compiler_isdocstring(st) && c->c_optimize < 2) {
        /* don't generate docstrings if -OO */
        i = 1;
        VISIT(c, expr, st->v.Expr.value);
        if (!compiler_nameop(c, __doc__, Store))
            return 0;
    }
    for (; i < asdl_seq_LEN(stmts); i++)
        VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i));
    return 1;
}

static int
compiler_function(struct compiler *c, stmt_ty s, int is_async)
{
///...
    st = (stmt_ty)asdl_seq_GET(body, 0);
    docstring = compiler_isdocstring(st);
///...
}

help的doc由来

读取的就是对象的__doc__属性。

###:@file:Python-3.6.0\Lib\inspect.py
def _finddoc(obj):
    if isclass(obj):
        for base in obj.__mro__:
            if base is not object:
                try:
                    doc = base.__doc__
                except AttributeError:
                    continue
                if doc is not None:
                    return doc
        return None

区别

在函数中不适用LOADDOC可以节省一条虚拟机指令。那么为什么模块不也采用这种方法呢?毕竟module也是默认作为一个函数的。 或许是因为类似于builtin这种模块是通过C代码创建而,而不是通过Python代码创建;或者是因为NameSpace也是一个module而天生没有docstring;或者只是历史遗留问题?

posted on 2023-07-06 17:52  tsecer  阅读(75)  评论(0编辑  收藏  举报

导航