更好的 Python 代码

Table of Contents

这里,逐步记录一些 Python 代码优化经验和想法。

1 小工具

  • flake8:检查 Python 代码标准
  • pep8:检查 Python 代码是否符合 PEP8

2 减少无用变量

变量增强了表达式的可读性,但过度使用变量也会造成困扰,定义无用的中间变量是很常见的做法,如:

def this_is_a_function(a_list):
    result = False

    for i in a_list:
        if x > 0:
            result = True
            return result

    return result

可以简化:

def this_is_a_function(a_list):
    for i in a_list:
        if x > 0:
            return True

    return False

有时我也会在函数中看到类似这样的代码:

info = {}
info["name"] = user.get_name()
info["age"] = user.get_age()
return info

可简化为:

return {
    "name": user.get_name(),
    "age": user.get_age()
}

3 让结构更美观一点

字段太多时,我喜欢用这样的格式来美化:

info = {
    "name": "lu4nx",
    "website": "www.shellcodes.org"
}

而不是:

info = {"name": "lu4nx", "website": "www.shellcodes.org"}

或者:

info = {"name": "lu4nx",
        "website": "www.shellcodes.org"}

甚至函数参数太多的情况下,也是,这样避免挤在一起:

db.update(
    "User",
    name="lu4nx",
    website="www.shellcodes.org",
    email="lx_at_shellcodes.org",
    ...
)

反正我是不喜欢这样的:

db.update("User", \
          name="lu4nx", \
          website="www.shellcodes.org", \
          email="lx_at_shellcodes.org", \
          ...)

4 尽早提前结束

def this_is_a_function():
    if xxx:
        ...此处省略N行

    do_something

这样会更好:

def this_is_a_function():
    if not xxx:
        return

    ...此处省略N行
    do_something

5 尽量避免 if..elif..

Python 没有 switch 语法,只有 if..elif,经常会在代码中看到类似:

if input_data == "a":
    return a_handler()
elif input_data == "b":
    return b_handler()
elif input_data == "c":
    return c_handler()
else:
    return default_handler()

如果判断条件太多,可用字典来简化:

handler_functions = {
    "a": a_handler,
    "b": b_handler,
    "c": c_handler
}

func = handler_functions.get(input_data)
if not func:
    return default_handler()

return func()

6 捕获 Exception 异常的陷阱

我在一段多线程代码里偷了个懒:

try:
    resp = http_get(url)
    lock.acquire()
    ....
    lock.realease()
except Exception:
    pass

后来死活都出现死锁问题,最后发现 lock.release 写成 lock.realease 了。但由于被异常捕获并忽略了,导致调试很久。

所以:

  1. Exception 异常尽可能留在最后捕获,先捕获一些已知会发生的异常。
  2. 不要轻易捕获所有异常并且还不做任何处理,至少也该把异常记录到日志或者打印出来。

7 编写 Docstring

7.1 代码风格检查工具:pydocstyle

这两点可以不用遵守:

  • D400: First line should end with a period.
  • D401: First line should be in imperative mood.

D203 和 D211 有冲突,选其一(参数:–ignore=D203):

  • D203: 1 blank line required before class docstring (found 0)
  • D203: 1 blank line required before class docstring (found 0)

D212 和 D213 有冲突,选其一(参数:–ignore=D213):

  • D212: Multi-line docstring summary should start at the first line

-D213: Multi-line docstring summary should start at the second line

我的默认忽略:

--ignore=D400,D401,D203,D213

7.2 Docstring 风格

主流风格有:

  • Google 风格
  • Epytext/JavaDoc 风格
  • reST(我比较推荐)

注意:如果字符串中有反斜杠,字符串前加 r:

r""" ... """

7.2.1 模块

在模块的 __init__.py 中添加 docstring,可以将模块的简单使用示例等信息填入,例 requests 的 __init__.py 文件:

"""
Requests HTTP Library
~~~~~~~~~~~~~~~~~~~~~
Requests is an HTTP library, written in Python, for human beings. Basic GET
usage:
   >>> import requests
   >>> r = requests.get('https://www.python.org')
   >>> r.status_code
   200
   >>> 'Python is a programming language' in r.content
   True
... or POST:
   >>> payload = dict(key1='value1', key2='value2')
   >>> r = requests.post('https://httpbin.org/post', data=payload)
   >>> print(r.text)
   {
     ...
     "form": {
       "key2": "value2",
       "key1": "value1"
     },
     ...
   }
The other HTTP methods are supported - see `requests.api`. Full documentation
is at <http://python-requests.org>.
:copyright: (c) 2017 by Kenneth Reitz.
:license: Apache 2.0, see LICENSE for more details.
"""

7.2.2 单文件

可以把使用方法放在顶层的 docstring 中,例如:

"""
关于本程序
...

usage: filename.py [download|check]

- download: xxx
- check: zzz
"""

import sys

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print(__doc__)

7.2.3

  • 字符串开头不包含空格
  • 类的三引号单独一行闭合
  • __init__ 参数的解释、使用示例写进去
  • docstring 和第一个方法之间空一行
class Request(RequestHooksMixin):
    """A user-created :class:`Request <Request>` object.
    Used to prepare a :class:`PreparedRequest <PreparedRequest>`, which is sent to the server.
    :param method: HTTP method to use.
    :param url: URL to send.
    :param headers: dictionary of headers to send.
    :param files: dictionary of {filename: fileobject} files to multipart upload.
    :param data: the body to attach to the request. If a dictionary or
        list of tuples ``[(key, value)]`` is provided, form-encoding will
        take place.
    :param json: json for the body to attach to the request (if files or data is not specified).
    :param params: URL parameters to append to the URL. If a dictionary or
        list of tuples ``[(key, value)]`` is provided, form-encoding will
        take place.
    :param auth: Auth handler or (user, pass) tuple.
    :param cookies: dictionary or CookieJar of cookies to attach to this request.
    :param hooks: dictionary of callback hooks, for internal usage.
    Usage::
      >>> import requests
      >>> req = requests.Request('GET', 'https://httpbin.org/get')
      >>> req.prepare()
      <PreparedRequest [GET]>
    """

    def __init__(self,
                 method=None, url=None, headers=None, files=None, data=None,
                 params=None, auth=None, cookies=None, hooks=None, json=None):

7.2.4 单行 docstring

  • 字符串开头不包含空格
  • 三引号闭合仍然是单独一行

示例:

class RequestException(IOError):
    """There was an ambiguous exception that occurred while handling your
    request.
    """

    def __init__(self, *args, **kwargs):
        """Initialize RequestException with `request` and `response` objects."""
        response = kwargs.pop('response', None)
        self.response = response
        self.request = kwargs.pop('request', None)
        if (response is not None and not self.request and
            hasattr(response, 'request')):
            self.request = self.response.request
            super(RequestException, self).__init__(*args, **kwargs)

7.2.5 函数/方法

示例,如果是单行的情况:

def prepare_body(self, data, files, json=None):
    """Prepares the given HTTP body data."""

示例,如果是多行的情况:

def get_cookie_header(jar, request):
    """
    Produce an appropriate Cookie header string to be sent with `request`, or None.
    :rtype: str
    """
    r = MockRequest(request)
    jar.add_cookie_header(r)
    return r.get_new_headers().get('Cookie')