如何编写优雅(地道)的Python代码 - 第三部分

3. 组织代码

3.1 模块和包

Python语言支持面向对象编程,不过不是必须要使用的。很多有经验的Python程序员相对较少使用类和多态。有很多这方面的原因。

大部分存储在类中的属性可以用列表、字典和集合类型来表示。Python有很多优化(设计和实现层面)的内置函数和标准库来处理这些数据。类仅当需要的时候才使用,并且永远不要超过出API边界。

在Java语言中,类是封装的基本单元。每个文件都代表一个Java类,不管解决的问题是否有意义。如果有个方便的工具函数,那么将会封装到Utility类中。即使我们不能直观地理解Utility对象代表什么意思,也没有关系。当然,我夸大了,但重点是明确的,一旦习惯把所有内容封装到一个类中,就会很容易把这个概念带到其他的编程语言。

在Python中,一组相关的函数和数据通常封装在模块中。如果使用MVC的WEB框架来构建"Chirp"应用,可能会存在一个包含模型、视图和控制器的chirp包。如果"Chirp"是一个特别宏大的项目,代码库很大,这些模块可以很容易自己打包。 控制器包可能有一个持久性模块和处理模块。除了直觉上属于控制者之外,这些都不需要以任何方式相关。

如果所有这些模块都变成了类,那么互操作性立即就会变成了一个问题。 我们必须认真准确地确定我们将要对外公开的方法,状态如何更新,以及类支持的测试方法。不像一个字典或列表,我们必须编写代码来处理和持久性化对象。

请注意,在"Chirp"的描述中,没有任何需要使用类的地方。简单的导入语句使代码共享和封装变得简单。将状态显式作为参数传递给函数让一切变得松耦合。系统更容易接收,处理和转换数据流。

可以确切的说,类可能是更清晰或自然地表示一种东西的方式。在许多情况下,面向对象编程是一个很方便的范例,只是不要让它成为使用的唯一范例。

3.2 格式化

3.2.1 使用大写字母来声明全局常量

为了区分从定义在模块级别中(或者单个文件中的全局变量)导入的常量名字,都使用大写字母。

3.2.1.1 不好的风格

seconds_in_a_day = 60 * 60 * 24
# ...
def display_uptime(uptime_in_seconds):
    percentage_run_time = (
        uptime_in_seconds/seconds_in_a_day) * 100
    # "Huh!? Where did seconds_in_a_day come from?"
    return 'The process was up {percent} percent of the day'.format(
        percent=int(percentage_run_time))
# ...
uptime_in_seconds = 60 * 60 * 24
display_uptime(uptime_in_seconds)

3.2.1.2 Python的风格

SECONDS_IN_A_DAY = 60 * 60 * 24
# ...
def display_uptime(uptime_in_seconds):
    percentage_run_time = (
        uptime_in_seconds/SECONDS_IN_A_DAY) * 100
    # "Clearly SECONDS_IN_A_DAY is a constant defined
    # elsewhere in this module."
    return 'The process was up {percent} percent of the day'.format(
        percent=int(percentage_run_time))
    # ...
uptime_in_seconds = 60 * 60 * 24
display_uptime(uptime_in_seconds)

3.2.2 避免在同一行上放置多条语句

尽管语言定义允许放置多条,没有理由地使用,使得代码很难读,无法较好表述语句。当同一行上出现像if, else 或 elif 多条语句时,情形变得更加困惑。

3.2.2.1 不好的风格

if this_is_bad_code: rewrite_code(); make_it_more_readable();

3.2.2.2 Python的风格

if this_is_bad_code:
    rewrite_code()
    make_it_more_readable()

3.2.3 依据PEP8规则调整代码风格

Python定义了一套代码风格规则,俗称PEP8。如果你浏览Python工程的commit信息时,你将会发现,里面散落对PEP8的引用。原因很简单:如果我们都同意通用的命名和格式约定,那么所有的Python代码对新手和经验丰富的开发者而言,可读性更好。PEP8也许是Python社区中习语最明显的例子。阅读PEP,需要为编辑器安装PEP8的样式检查插件(一般编辑器都有),用其它程序员都欣赏的风格编写代码。下面列举几个例子:

标识符类型 格式 示例
首字母大写的Camel风格 class StringManipulator():
变量 单词之间使用_连接 joined_by_underscore = True
函数 单词之间使用_连接 def multi_word_name(words):
常量 所有字母大写 SECRET_KEY = 42

一般其它没有列出的遵循变量和函数命名习惯:单词之间使用下划线连接。

3.3 可执行脚本

3.3.1 在脚本中使用sys.exit函数返回恰当的错误码

Python脚本应该是一个好的shell公民。在if __name__ == '__main__'后面执行一段代码,不返回任何结果,看起来挺神奇的。要避免这种做法。

在包含代码的运行脚本中创建一个主函数。如果有错误,在主函数中使用sys.exit返回错误码,否则,返回0。if __name__ =='__main__'下的唯一代码语句应该调用sys.exit作为主函数的返回值参数。

通过这样做,允许脚本在Unix管道中被使用,进行监控,失败的时候不需要自定义规则,可以被其他程序安全地调用。

3.3.1.1 不好的风格

if __name__ == '__main__':
    import sys
    # What happens if no argument is passed on the
    # command line?
    if len(sys.argv) > 1:
        argument = sys.argv[1]
        result = do_stuff(argument)
        # Again, what if this is False? How would other
        # programs know?
        if result:
            do_stuff_with_result(result)

3.3.1.2 python的风格

def main():
    import sys
    if len(sys.argv) < 2:
        # Calling sys.exit with a string automatically
        # prints the string to stderr and exits with
        # a value of '1' (error)
        sys.exit('You forgot to pass an argument')
    argument = sys.argv[1]
    result = do_stuff(argument)
    if not result:
        sys.exit(1)
        # We can also exit with just the return code
    do_stuff_with_result(result)
    # Optional, since the return value without this return
    # statment would default to None, which sys.exit treats
    # as 'exit with 0'
    return 0
# The three lines below are the canonical script entry
# point lines. You'll see them often in other Python scripts
if __name__ == '__main__':
    sys.exit(main())

3.3.2 在文件中使用if name == 'main'使得文件即可以被导入也可以直接运行

不像某些语言中的main()函数,Python没有内建的主入口点。相反,Python依赖加载的文件来执行语句。如果想要文件既可以作为模块导入,也可以作为一个独立的脚本,请使用 if __name__== '__main__'。

3.3.2.1 不好的风格

import sys
import os
FIRST_NUMBER = float(sys.argv[1])
SECOND_NUMBER = float(sys.argv[2])
def divide(a, b):
    return a/b
# I can't import this file (for the super
# useful 'divide' function) without the following
# code being executed.
if SECOND_NUMBER != 0:
    print(divide(FIRST_NUMBER, SECOND_NUMBER))

3.3.2.2 Python的风格

import sys
import os
def divide(a, b):
return a/b
# 仅当脚本直接运行的时候执行,作为模块导入的时候不执行
if __name__ == '__main__':
    first_number = float(sys.argv[1])
    second_number = float(sys.argv[2])
    if second_number != 0:
        print(divide(first_number, second_number))

3.4 导入

3.4.1 相比相对导入,最好使用绝对导入

有两种导入模块的风格:绝对导入和相对导入。绝对导入依据sys.path指定导入模块位置(像\.\.\)。

相对模块导入的模块相对于当前模块在文件系统中的位置。假设有模块package.sub_package.module,想要导入package.other_module,可以使用.风格的相对导入语法:from ..other_module import foo。单一.表示当前模块包含的包。每一个额外的.意味着包的父亲一级,一级一个点。请注意,相对导入必须使用from ... import ...风格。import foo通常会被认为是绝对导入。

相应地,使用绝对导入应该这样写:import package.other_module(有可能使用子句给模块命名一个短的名字)。

那么,为什么更倾向于使用绝对导入呢?相对导入会搞混模块的命名空间。from foo import bar语句将bar绑定到自己模块的命名空间下。对于那些阅读你代码的读者而言,不太清楚bar来自哪里,尤其是当应用在复杂函数或大模块中。然而,boo.bar,很清晰地知道bar在哪里定义。Python编程常见问题甚至可以这样说:永远不要使用相对包导入。

3.4.1.1 不好的风格

# My location is package.sub_package.module
# and I want to import package.other_module.
# The following should be avoided:
from ...package import other_module

3.4.1.2 Python的风格

# My location is package.sub_package.another_sub_package.module
# and I want to import package.other_module.
# Either of the following are acceptable:
import package.other_module
import package.other_module as other

3.4.2 不要使用from foo import * 导入模块中的内容

考虑之前的习惯,该规则就会很明显。在导入的时候(from foo import *),使用*号很容易混乱命名空间。如果自己定义和包中定义的名称出现冲突,可能会导致问题。

不过,如果想要从foo包中导入大量的名字怎么办?很简单。利用括号来组合导入语句。你没有必要从一个模块中写10行导入语句,命名空间仍然相对比较干净。

更好的是,只需简单使用绝对导入。如果包/模块名称很长,用as从句命名一个短点的变量名。

3.4.2.1 不好的风格

from foo import *

3.4.2.2 Python的风格

from foo import (bar, baz, qux, quux, quuux)
# or even better
import foo

3.4.3 使用标准的顺序排列导入语句

随着工程规模的增长(尤其是那些WEB框架),导入语句的数量也会变大。让所有的导入语句在文件的开头,使用标准的顺序给import语句排序,并坚持下去。不过实际的顺序并不是太重要,以下是Python编程常见问题推荐的顺序:

  1. 标准库模块
  2. 安装在site-packages的第三方库
  3. 本工程中的本地模块

许多人选择按照(大致)按字母顺序排列导入语句。其他人会认为这是可笑的。 实际上,这并不重要。 重要的是你选择一个标准的顺序,并遵守它。

3.4.3.1 不好风格

import os.path
# Some function and class definitions,
# one of which uses os.path
# ....
import concurrent.futures
from flask import render_template
# Stuff using futures and Flask's render_template
# ....
from flask import (Flask, request, session, g,
    redirect, url_for, abort,
    render_template, flash, _app_ctx_stack)
import requests
# Code using flask and requests
# ....
if __name__ == '__main__':
    # Imports when imported as a module are not so
    # costly that they need to be relegated to inside
    # an 'if __name__ == '__main__'' block...
    import this_project.utilities.sentient_network as skynet
    import this_project.widgets
    import sys

3.4.3.2 Python的风格

Easy to see exactly what my dependencies are and where to
# make changes if a module or package name changes
import concurrent.futures
import os.path
import sys
from flask import (Flask, request, session, g,
    redirect, url_for, abort,
    render_template, flash, _app_ctx_stack)
import requests
import this_project.utilities.sentient_network as skynet
import this_project.widgets

猜你喜欢

转载自blog.csdn.net/zhujf21st/article/details/79126033