Python 代码规范

规范参考
  • 《8 号 Python 增强规范》(Python Enhacement Proposal #8),简称 PEP8;
  • 《Google Python 风格规范》(Google Python Style Guide),源自 Google 内部的风格规范。

统一的编程规范的重要性

统一的编程规范能提高开发效率。而开发效率,关乎三类对象,也就是阅读者、编程者和机器。

他们的优先级是 阅读者的体验 >> 编程者的体验 >> 机器的体验

实际工作中,真正在打字的时间,远比阅读或者 debug 的时间要少。研究表明,软件工程中 80% 的时间都在阅读代码。 所以,为了提高效率,我们要优化的,不是你的打字时间,而是团队阅读的体验。


举例说明

命名规范

命名必须有意义,不能是无意义的单字母。

# 错误示例
if (a <= 0):
    return
elif (a > b):
    return
else:
    b -= a

# 正确示例
if (transfer_amount <= 0):
    raise Exception('...')
elif (transfer_amount > balance):
    raise Exception('...')
else:
    balance -= transfer_amount

导包

Google Style 中规定,Python 代码中的 import 对象,只能是 package 或者 module 。

# 错误示例
from mypkg import Obj
from mypkg import my_func
my_func([1, 2, 3])

# 正确示例
import numpy as np
import mypkg
np.array([6, 7, 8])
mypkg.my_func([1, 2, 3])

因为 my_func 这样的名字,如果没有一个 package name 提供上下文语境,读者很难单独通过 my_func 这个名字来推测它的可能功能,也很难在 debug 时根据 package name 找到可能的问题。

勿过度简化代码

代码应该写起来让人看着简洁明了,而非看着很累。

# 错误示范
result = [(x, y) for x in range(10) for y in range(5) if x * y > 20]

# 正确示范
result = []
for x in range(10):
    for y in range(5):
        if x * y > 20:
            result.append((x, y))

机器体验

避免去用 is 比较两个 Python 整数的地址。

# 错误示例
x = 27
y = 27
print(x is y)
x = 721
y = 721
print(x is y)

在 CPython 的实现中,把 -5 到 256 的整数做成了 singleton,也就是说,这个区间里的数字都会引用同一块内存区域,所以上面的 27 和下面的 27 会指向同一个地址,运行结果为 True。但是 -5 到 256 之外的数字,会因为你的重新定义而被重新分配内存,所以两个 721 会指向不同的内存地址,结果也就是 False 了。

# 正确示例
x = 27
y = 27
print(x == y)
x = 721
y = 721
print(x == y)

和 None 比较时候永远使用 is 。

# 错误示范
x = MyObject()
print(x == None)

# 正确示例
x = MyObject()
print(x is None)

Python 中的隐式布尔转换

# 错误示范
def pay(salary=None):
    if not salary:
        salary = 1
    print(salary)

# 正确示范
def pay(salary=None):
    if salary is not None:
        salary = 1
    print(salary)

不规范的编程习惯也会导致程序效率问题

# 错误示例
adict = {i: i * 2 for i in range(100)}
for key in adict.keys():
    print(f"{key}:{adict[key]}")

# 正确示范
for key in adict:
    print(f"{key}:{adict[key]}")

keys() 方法会在遍历前生成一个临时的列表,导致上面的代码消耗大量内存并且运行缓慢。正确的方式,是使用默认的 iterator。默认的 iterator 不会分配新内存,也就不会造成上面的性能问题。


整合进开发流程的自动化工具

强制代码评审和强制静态或者动态 linter 。

在代码评审工具里,添加必须的编程规范环节;把团队确定的代码规范写进 Pylint 里(https://www.pylint.org/),能够在每份代码提交前自动检查,不通过的代码无法提交。


合理分解代码

编程中一个核心思想是,不写重复代码。重复代码大概率可以通过使用条件、循环、构造函数和类来解决。

另一个核心思想则是,减少迭代层数,尽可能让 Python 代码扁平化,毕竟,人的大脑无法处理过多的栈操作。

一个函数的粒度应该尽可能细,不要让一个函数做太多的事情。所以,对待一个复杂的函数,我们需要尽可能地把它拆分成几个功能简单的函数,然后合并起来。


合理利用 assert

Python 的 assert 语句,可以说是一个 debug 的好工具,主要用于测试一个条件是否满足。如果测试的条件满足,则什么也不做,相当于执行了 pass 语句;如果测试条件不满足,便会抛出异常 AssertionError,并返回具体的错误信息(optional)。

assert 的合理使用,可以增加代码的健壮度,同时也方便了程序出错后的定位排查。

但是,也不能滥用 assert。很多情况下,程序中出现的不同情况都是意料之中的,需要用不同的方案去处理,这时候用条件语句进行判断更为合适。而对于程序中的一些 run-time error,最好使用异常处理。


巧用上下文管理器和with精简代码

上下文管理器,能够自动分配并且释放资源,其中最典型的应用便是 with 语句。

with open('test.txt', 'w') as f:
    f.write('hello')
    
# 相当于
f = open('test.txt', 'w')
try:
    f.write('hello')
finally:
    f.close()
some_lock = threading.Lock()
with somelock:
    ...

# 相当于
some_lock = threading.Lock()
some_lock.acquire()
try:
    ...
finally:
    some_lock.release()

上下文管理器的例子

class FileManager:
    def __init__(self, file_name, open_mode):
        print("__init__")
        self.file_name = file_name
        self.open_mode = open_mode
        self.file = None

    def __enter__(self):
        print("__enter__")
        self.file = open(self.file_name, mode=self.open_mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__")
        if self.file:
            self.file.close()


if __name__ == '__main__':
    with FileManager('test.txt', 'w') as f:
        f.write('aaa')

基于生成器的上下文管理器

使用装饰器 contextlib.contextmanager,来定义自己所需的基于生成器的上下文管理器,用以支持 with 语句。

from contextlib import contextmanager

@contextmanager
def file_manager(file_name, open_mode):
    f = None
    try:
        f = open(file_name, mode=open_mode)
        yield f
    finally:
        if f:
            f.close()

if __name__ == '__main__':
    with file_manager('test.txt', 'w') as f:
        f.write("file_manager")

函数 file_manager() 是一个生成器,当执行 with 语句时,便会打开文件,并返回文件对象 f;当 with 语句执行完后,finally block 中的关闭文件操作便会执行。

基于类的上下文管理器更加灵活,适用于大型的系统开发;而基于生成器的上下文管理器更加方便、简洁,适用于中小型程序。


单元测试

编写测试来验证某一个模块的功能正确性,一般会指定输入,验证输出是否符合预期。

实际生产环境中,会对每一个模块的所有可能输入值进行测试。这样虽然显得繁琐,增加了额外的工作量,但是能够大大提高代码质量,减小 bug 发生的可能性,也更方便系统的维护。

import unittest

# 将要被测试的排序函数
def sort(arr):
    l = len(arr)
    for i in range(0, l):
        for j in range(i + 1, l):
            if arr[i] >= arr[j]:
                tmp = arr[i]
                arr[i] = arr[j]
                arr[j] = tmp

# 编写子类继承 unittest.TestCase
class TestSort(unittest.TestCase):
    # 以 test 开头的函数将会被测试
    def test_sort(self):
        arr = [3, 4, 1, 5, 6]
        sort(arr)
        # assert 结果和期待的一样
        self.assertEqual(arr, [1, 3, 4, 5, 6])

if __name__ == '__main__':
    unittest.main()
mock

mock 是单元测试中最核心重要的一环。mock 的意思,便是通过一个虚假对象,来代替被测试函数或模块需要的对象。

比如要测一个后端 API 逻辑的功能性,但一般后端 API 都依赖于数据库、文件系统、网络等。这样,就需要通过 mock,来创建一些虚假的数据库层、文件系统层、网络层对象,以便可以简单地对核心后端逻辑单元进行测试。

Python mock 则主要使用 mock 或者 MagicMock 对象 ,举例:

import unittest
from unittest.mock import MagicMock

class A(unittest.TestCase):
    def m1(self):
        val = self.m2()
        self.m3(val)

    def m2(self):
        print("m2")

    def m3(self, val):
        print("m3")

    def test_m1(self):
        a = A()
        a.m2 = MagicMock(return_value="custom_val")
        a.m3 = MagicMock()
        a.m1()
        self.assertTrue(a.m2.called)  # 验证 m2 被 call 过
        a.m3.assert_called_with("custom_val")  # 验证 m3 被指定参数 call 过

if __name__ == '__main__':
    # unittest.main(argv=['first-arg-is-ignored'], exit=False)
    # unittest.main()
    a = A()
    a.test_m1()

这段代码中,定义了一个类的三个方法 m1()、m2()、m3()。需要对 m1() 进行单元测试,但是 m1() 取决于 m2() 和 m3()。如果 m2() 和 m3() 的内部比较复杂,就不能只是简单地调用 m1() 函数来进行测试,可能需要解决很多依赖项的问题。

但是,有了 mock 其实就很好办了。我们可以把 m2() 替换为一个返回具体数值的 value,把 m3() 替换为另一个 mock(空函数)。这样,测试 m1() 就很容易了,我们可以测试 m1() 调用 m2(),并且用 m2() 的返回值调用 m3()。

真正工业化的代码,都是很多层模块相互逻辑调用的一个树形结构。单元测试需要测的是某个节点的逻辑功能,mock 掉相关的依赖项是非常重要的。

Mock Side Effect

含义:mock 的函数,属性是可以根据不同的输入,返回不同的数值,而不只是一个 return_value 。

from unittest import mock

mock_obj = mock.Mock(side_effect=[1, 2, 3])
print(mock_obj())
print(mock_obj())
print(mock_obj())
# print(mock_obj())  # StopIteration
import unittest
from unittest import mock

class AddClass(object):
    def add(self, a, b):
        s = a + b
        print(f"s is {s}")
        return s

class TestAdd(unittest.TestCase):
    def test_add1(self):
        sub = AddClass()
        sub.add = mock.Mock(return_value=10)
        result = sub.add(1, 2)
        self.assertEqual(result, 10)  # 断言实际结果和预期结果

    def test_add2(self):
        sub = AddClass()  # 初始化被测函数类实例
        sub.add = mock.Mock(return_value=10,
                            side_effect=[1, 2, 3])  # 传递side_effect关键字参数, 会覆盖return_value参数值
        result1 = sub.add(1, 10)
        self.assertEqual(result1, 1)  # test passed
        result2 = sub.add(2, 10)
        self.assertEqual(result2, 2)  # test passed
        result3 = sub.add(3, 10)
        self.assertEqual(result3, 2)  # test failed

    def test_add3(self):
        sub = AddClass()
        sub.add = mock.Mock(return_value=10, side_effect=sub.add)  # 使用真实的add方法测试
        result1 = sub.add(1, 10)
        self.assertEqual(result1, 11)
        result2 = sub.add(2, 10)
        self.assertEqual(result2, 12)
        result3 = sub.add(3, 10)
        self.assertEqual(result3, 13)
        
if __name__ == '__main__':
    unittest.main()
patch

patch 可以应用 Python 的 decoration 模式或是 context manager 概念,快速自然地 mock 所需的函数。


调试代码
如何使用 pdb
import pdb

def func(a, b):
    s = a + b
    print('enter func()')
    return s

a, b = 1, 2

pdb.set_trace()
ret = func(a, b)
c = 3
print(ret + c)
D:\Python_basis>python test_data.py
> d:\python_basis\test_data.py(13)<module>()
-> ret = func(a, b)
(Pdb) s
--Call--
> d:\python_basis\test_data.py(4)func()
-> def func(a, b):
(Pdb) l
  1     import pdb
  2
  3
  4  -> def func(a, b):
  5         s = a + b
  6         print('enter func()')
  7         return s
  8
  9
 10     a, b = 1, 2
 11
(Pdb) n
> d:\python_basis\test_data.py(5)func()
-> s = a + b
(Pdb) p a
1
(Pdb) p b
2
(Pdb) n
> d:\python_basis\test_data.py(6)func()
-> print('enter func()')
(Pdb) p s
3
(Pdb) n
enter func()
> d:\python_basis\test_data.py(7)func()
-> return s
(Pdb) n
--Return--
> d:\python_basis\test_data.py(7)func()->3
-> return s
(Pdb) n
> d:\python_basis\test_data.py(14)<module>()
-> c = 3
(Pdb) p ret
3
(Pdb) n
> d:\python_basis\test_data.py(15)<module>()
-> print(ret + c)
(Pdb) n
6
--Return--
> d:\python_basis\test_data.py(15)<module>()->None
-> print(ret + c)

pdf 官方文档:https://docs.python.org/3/library/pdb.html#modulepdb


用 cProfile 进行性能分析

profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。这样你就可以知道程序的瓶颈所在,从而对其进行修正或优化 。

方式一:在代码中加入 import cProfile,并将执行语句放入 cProfile.run()

import cProfile

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n - 1))
        res.append(fib(n))
    return res

cProfile.run('fib_seq(30)')
D:\compile\Python3.6.0\python.exe D:/Python_basis/test_data.py
         7049216 function calls (94 primitive calls) in 6.177 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    6.177    6.177 <string>:1(<module>)
     31/1    0.000    0.000    6.177    6.177 test_data.py:13(fib_seq)
7049122/30    6.177    0.000    6.177    0.206 test_data.py:4(fib)
        1    0.000    0.000    6.177    6.177 {built-in method builtins.exec}
       30    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       30    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}

Process finished with exit code 0

方式二:直接在运行脚本的命令中,加入选项 -m cProfile 。不用在脚本中加入 import CProfile 等了。

python3 -m cProfile test_data.py

方式三:直接使用 Pycharm 中的 Run-Profile 执行脚本。

参数解析

  • ncalls,是指相应代码 / 函数被调用的次数;
  • tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间);
  • tottime percall,就是上述两者相除的结果,也就是 tottime / ncalls;
  • cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时间;
  • cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。

通过解析发现,fib 函数被调用次数太多,7049122次,需要改进。

# 改进版
from functools import lru_cache

@lru_cache()
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n - 1))
        res.append(fib(n))
    return res

fib_seq(30)
D:\Python_basis>python -m cProfile test_data.py
         144 function calls (113 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:989(_handle_fromlist)
        1    0.000    0.000    0.000    0.000 functools.py:44(update_wrapper)
        1    0.000    0.000    0.000    0.000 functools.py:449(lru_cache)
        1    0.000    0.000    0.000    0.000 functools.py:480(decorating_function)
        1    0.000    0.000    0.000    0.000 test_data.py:1(<module>)
     31/1    0.000    0.000    0.000    0.000 test_data.py:14(fib_seq)
    31/30    0.000    0.000    0.000    0.000 test_data.py:4(fib)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        7    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.setattr}
       30    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       30    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}

常见问题
异常处理
  • 在代码中对数据进行检测,并直接处理与抛出异常。

    if [condition1]:
        raise Exception1('exception 1')
    elif [condition2]:
        raise Exception2('exception 2')
    

    一旦抛出异常,那么程序就会终止。

  • 在异常处理代码中进行处理。

    try:
       do something
    except Exception as e:
       other
    

    如果抛出异常,会被程序捕获(catch),程序还会继续运行。


代码优化时间

先写出能跑起来的代码,后期再优化。很明显,这种认知是错误的。

从一开始写代码时,就必须对功能和规范这两者双管齐下。

代码功能完整和规范完整的优先级是不分先后的,应该是同时进行的。如果你一开始只注重代码的功能完整,而不关注其质量、规范,那么规范问题很容易越积越多。这样就会导致产品的 bug 越来越多,相应的代码库越发难以维护,到最后不得已只能推倒重来。


代码中写多少注释合适

通常来说,会在类、函数的开头或者是某一个功能块的开头加上一段描述性的注释,来说明这段代码的功能,并指明所有的输入和输出。

除此之外,也要求在一些比较复杂、可能有歧义的代码上方加上注释,帮助阅读者理解代码的含义。

在写好之后修改了代码,代码对应的注释一定也要做出相应的修改,不然很容易造成“文不对题”的现象,给别人也给你自己带来困扰。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值