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

参考:
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代码提供完备、清晰的文档注释,但提升代码的可读性可以先从自身做起,起码可以防止那种自己也读不懂自己的代码、只有上帝才能看懂的尴尬局面的发生。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容