美文网首页
Python数据类型提示痛点的解决方案探讨

Python数据类型提示痛点的解决方案探讨

作者: m2fox | 来源:发表于2019-04-07 22:14 被阅读0次

    参考:
    PEP 484 Type Hints:https://www.python.org/dev/peps/pep-0484/
    numpy项目代码文档注释样例:https://github.com/numpy/numpy/blob/master/numpy/matrixlib/defmatrix.py

    几个月前,你写了一段Python代码,当时只有你和上帝能看懂。几个月后,这段代码就只有上帝能看懂了。

    痛点是什么

    Python是一门弱类型的动态语言,在看其他人写的一些Python项目的代码、特别是大型项目的代码的时候,是不是会有这样的体验:很多函数的参数超多,而且很难看出来每个参数代表什么含义、是什么数据类型的、字典类型的参数究竟可以传哪些参数等等等。如果写这些代码的人习惯还不好,没有写什么注释或注释写的比较糟糕,那看这种代码简直就是一种折磨了,这样的情况下一开篇所描述的情景就并不算夸张了。这样的代码可读性、可维护性几乎为0。

    解决方案

    PEP 484类型提示语法

    那么怎么解决上述痛点呢?Python 3.5版本在语言层面加入了一个新特性:类型提示(Type Hints,详情见开篇引文),可以以一种非常优雅的方式来对代码中变量的类型进行提示,大大提高了Python代码的可读性,例如下面这段代码:

    def hello(name:str, age:int) -> str:
        return 'hello, {0}! your age is: {1}'.format(name, age)
    

    在IPython中输入这段代码,然后用help函数查看hello函数的帮组文档,显示内容如下:

    In [10]: help(hello)
    Help on function hello in module __main__:
    
    hello(name: str, age: int) -> str
    

    类型提示语法中,在变量或函数形参后面加:类型名称可以指明变量的数据类型,在函数定义def语句的后面加上-> 类型名称可以指明函数返回值的数据类型,非常方便。

    PEP 484类型提示的不足之处

    要注意的是,PEP 484的类型提示语言特性只在Python 3.5或更高版本才支持,而且仅仅是作为注释性的语法元素进行解析,在编译的时候类型提示相关的代码都是会被当做注释而忽略掉的,Python编译器并不会因为有了类型提示而对代码做任何类型检查。

    更通用的解决方案:文档注释

    那么还有没有更好、更通用的办法来解决上述类型提示痛点呢?有的,文档注释就是一个非常好的解决办法。

    文档注释的优点

    • 清晰、准确、完善的文档注释可以让Python代码的可读性、可维护性非常高。
    • 不受Python语言版本的限制。
    • 按照一定约定规范书写的文档注释,可以被一些第三方IDE或语法检查工具利用,从而可以对代码做静态类型检查,在语法层面检查代码是否有类型错误,也可以做一些代码补全方面的事情。

    PyCharm风格的文档注释

    这是一种个人比较推崇的文档注释格式,PyCharm IDE可以识别这种文档注释格式,从而进行代码静态语法类型检查、代码自动补全等操作。

    下面是一个简单的示例,在示例中用文档注释标注了hello函数的功能描述、每个形参变量的含义和类型、函数的返回值类型等信息:

    def hello(name, age, habits = []):
        '''
        Say hello to someone, and print some info.
    
        :param name: name of someone
        :type name: str
    
        :param age: age of someone
        :type age: int
    
        :param habits: habits of someone
        :type habits: List[str]
    
        :return: a hello sentence
        :rtype: str
        '''
        return 'Hello, {name}! Your age is {age}, and your habits are {habits}.'.format(name = name, age = age, habits = habits)
    

    将上述代码输入到IPython中,然后执行hello?命令来查看hello函数的文档注释,结果如下:

    In [28]: hello?
    Signature: hello(name, age, habits=[])
    Docstring:
    Say hello to someone, and print some info.
    
    :param name: name of someone
    :type name: str
    
    :param age: age of someone
    :type age: int
    
    :param habits: habits of someone
    :type habits: List[str]
    
    :return: a hello sentence
    :rtype: str
    

    可以看出函数的功能描述、每个参数的含义和数据类型、函数返回值的含义和数据类型都非常清晰明了。

    numpy风格的文档注释

    还有一种numpy项目中所使用的文档注释风格,也非常不错,可以参考。一个示例如下:

    def squeeze(self, axis=None):
            """
            Return a possibly reshaped matrix.
            Refer to `numpy.squeeze` for more documentation.
            Parameters
            ----------
            axis : None or int or tuple of ints, optional
                Selects a subset of the single-dimensional entries in the shape.
                If an axis is selected with shape entry greater than one,
                an error is raised.
            Returns
            -------
            squeezed : matrix
                The matrix, but as a (1, N) matrix if it had shape (N, 1).
            See Also
            --------
            numpy.squeeze : related function
            Notes
            -----
            If `m` has a single column then that column is returned
            as the single row of a matrix.  Otherwise `m` is returned.
            The returned matrix is always either `m` itself or a view into `m`.
            Supplying an axis keyword argument will not affect the returned matrix
            but it may cause an error to be raised.
            Examples
            --------
            >>> c = np.matrix([[1], [2]])
            >>> c
            matrix([[1],
                    [2]])
            >>> c.squeeze()
            matrix([[1, 2]])
            >>> r = c.T
            >>> r
            matrix([[1, 2]])
            >>> r.squeeze()
            matrix([[1, 2]])
            >>> m = np.matrix([[1, 2], [3, 4]])
            >>> m.squeeze()
            matrix([[1, 2],
                    [3, 4]])
            """
            return N.ndarray.squeeze(self, axis=axis)
    

    Emacs编辑器自动插入PyCharm风格的文档注释模板

    如果你平时经常使用Emacs编辑器写Python代码,那么我写了一个简单的函数,可以方便地在python-mode一键插入上面所讲的PyCharm风格的文档注释。只需将下面的elisp代码加入你的Emacs配置文件中,然后在python-mode写完一个函数的def语句后,光标在def语句所在的行时按快捷键C-c C-a即可快速插入文档注释模板:

    (defun insert-doc-annotation-below-current-line ()
      "Insert doc annotations for python class or function below current line."
      (interactive)
      ;; first, get current line text
      (let ((cur-line-str (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
        (cond
         ;; judge whether current line text match python function define pattern
         ((string-match "^[[:space:]]*def.+?(\\(.*\\))[[:space:]]*:" cur-line-str)
          ;; first capture group of regex above is params list string
          (setq params-str (match-string 1 cur-line-str))
          ;; split params list string to list, and do some strip operation
          (setq params (split-string params-str ",[[:space:]]*" t "[[:space:]]*=.+?"))
          ;; go to end of current line and go to new line and indent
          (goto-char (line-end-position))
          (newline-and-indent)
          ;; insert head of doc annotation `'''`
          (insert "'''")
          (goto-char (line-end-position))
          (newline-and-indent)
          ;; record current line number, jump back to here after inserting annotation
          (setq annotation-top-line (line-number-at-pos))
          (newline-and-indent)
          (newline-and-indent)
          ;; insert each params annotation
          (while params
        ;; NOT insert param `self`
        (when (not (string-equal (car params) "self"))
          (progn
            (setq param (car params))
            ;; insert param name annotation line
            (insert (format ":param %s: " param))
            (newline-and-indent)
            ;; insert param type annotation line
            (insert (format ":type %s: " param))
            (newline-and-indent)
            (newline-and-indent)))
        (setq params (cdr params)))
          ;; insert return and return type annotation line
          (insert ":return: ")
          (newline-and-indent)
          (insert ":rtype:")
          (newline-and-indent)
          ;; insert tail of doc annotation
          (insert "'''")
          ;; jump back to the position of annotation top
          (goto-line annotation-top-line)
          (indent-for-tab-command))
         ((string-match "^[[:space:]]*class.+?:" cur-line-str)
          (goto-char (line-end-position))
          (newline-and-indent)
          ;; insert head of doc annotation `'''`
          (insert "'''")
          (goto-char (line-end-position))
          (newline-and-indent)
          ;; record current line number, jump back to here after inserting annotation
          (setq annotation-top-line (line-number-at-pos))
          (newline-and-indent)
          ;; insert tail of doc annotation
          (insert "'''")
          ;; jump back to the position of annotation top
          (goto-line annotation-top-line)
          (indent-for-tab-command))
         (t (message "current line NOT match neither function nor class!")))))
    
    (add-hook 'python-mode-hook
          (lambda ()
            (local-set-key (kbd "C-c C-a") 'insert-doc-annotation-below-current-line)))
    

    总结

    最终决定采用文档注释这种更通用的方式在Python代码中做类型提示,虽然无法做到要求其他的人都能为他们自己的Python代码提供完备、清晰的文档注释,但提升代码的可读性可以先从自身做起,起码可以防止那种自己也读不懂自己的代码、只有上帝才能看懂的尴尬局面的发生。

    相关文章

      网友评论

          本文标题:Python数据类型提示痛点的解决方案探讨

          本文链接:https://www.haomeiwen.com/subject/keieiqtx.html