Python小微坑填坑系列

Python3.6.3版本下运行,不定期更新,寻找坑位中

坑位1:
from random import shuffle

l = [1, 3, 4, 5]
new_l = []
for _ in range(5):
    shuffle(l)
    new_l.append(l)

print(new_l)

预期输出:
[[3, 4, 1, 5], [3, 5, 4, 1], [3, 4, 5, 1], [1, 4, 3, 5], [5, 1, 4, 3]]

但是坑出现了,大家注意看实际输出:
[[4, 5, 1, 3], [4, 5, 1, 3], [4, 5, 1, 3], [4, 5, 1, 3], [4, 5, 1, 3]]
虽然原列表的顺序的确变了,但是我并不是想要new_l中所有的的子列表都是一样的呀,那如果这样,我何必每次迭代都shuffle呢。这是为什么呢?

坑解答: shuffle()函数一般无返回值,除非你自己设定返回随机浮点数。所以,shuffle一个列表,这个列表在内存地址中的id是不变的,你每次用新的列表new_l.append(l) 的循环添加的时候,其实都是添加了同一个id的列表,而每次这个列表里面的内容都发生变化,当然新列表里面的值也都发生变化。不信,自己运行一下下面代码,自己感受一下看看各个变量的id变化(内存地址变化)。

from random import shuffle

l = [1, 3, 4, 5]
print(id(l))
new_l = []
print(id(new_l))
for _ in range(5):
    shuffle(l)
    print('each shuffle old list id:')
    print(id(l))
    new_l.append(l)

for i in new_l:
    print('each list id in new_l ')
    print(id(i))

print(id(new_l))

为了让每次append的子列表都是新开辟的内存地址,而不是原list我们只需要加入list(),记得曾经看到过,list()函数可以相当是一个列表的拷贝,内容一样,内存地址不一样。正确代码:

from random import shuffle

l = [1, 3, 4, 5]
new_l = []
for _ in range(5):
    shuffle(l)
    new_l.append(list(l))

print(new_l)
坑位 2:

无论try语句中是否抛出异常,finally中的语句一定会被执行。我们来看下面的例子:

try:
    f = open("/tmp/output", "w")
    f.write("hello")
    #raise Exception("something wrong")
finally:
    print("closing file")
    f.close()

不论try中写文件的过程中是否有异常,finally中关闭文件的操作一定会执行。由于finally的这个特性,finally经常被用来做一些清理工作。
我们再来看下面的例子

def func1():
    try:
        return 1
    finally:
        return 2

def func2():
    try:
        raise ValueError()
    except:
        return 1
    finally:
        return 3

print(func1())
print(func2())

这个例子中 func1() 和 func2() 返回什么呢?
答案是 func1() 返回2, func2() 返回3。为什么是这样的呢?我们先来看一段Python官网上对于finally的解释:

A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. When an exception has occurred in the try clause and has not been handled by an except clause (or it has occurred in a except or else clause), it is re-raised after the finally clause has been executed. The finally clause is also executed “on the way out” when any other clause of the try statement is left via a break, continue or return statement.

重点部分用粗体标出了,翻成中文就是try块中包含break、continue或者return语句的,在离开try块之前,finally中的语句也会被执行。
所以在上面的例子中,func1() 中,在try块return之前,会执行finally中的语句,try中的return被忽略了,最终返回的值是finally中return的值。func2() 中,try块中抛出异常,被except捕获,在except块return之前,执行finally中的语句,except中的return被忽略,最终返回的值是finally中return的值。
我们在上面的例子中加入print语句,可以更清楚地看到过程

def func1():
    try:
        print 'in func1 try: try statement, will return 1'
        return 1
    finally:
        print 'in func1 finally: try statement, will return 2'
        return 2

def func2():
    try:
        print 'in func2 try: raise error'
        raise ValueError()
    except:
        print 'in func2 except: caught error, will return 1!'
        return 1
    finally:
        print 'in func2 finally: will return 3'
        return 3
print func1()
print func2()

上面的代码输出

in func1 try: try statement, will return 1
in func1 finally: try statement, will return 2
2
in func2 try: raise error
in func2 except: caught error, will return 1!
in func2 finally: will return 3
3

我们对上面的func2做一些修改,如下

def func2():
    try:
        print 'in func2 try: raise error'
        raise ValueError()
    except IndexError:
        print 'in func2 except: caught error, will return 1!'
        return 1
    finally:
        print 'in func2 finally: will return 3'
        return 3

print func2()

输出如下

in func2 try: raise error
in func2 finally: will return 3
3

try中抛出的异常是ValueError类型的,而except中定位的是IndexError类型的,try中抛出的异常没有被捕获到,所以except中的语句没有被执行,但不论异常有没有被捕获,finally还是会执行,最终函数返回了finally中的返回值3。

这里还可以看到另外一个问题。try中抛出的异常没有被捕获到,按理说当finally执行完毕后,应该被再次抛出,但finally里执行了return,导致异常被丢失。

可以看到在finally中使用return会导致很多问题。实际应用中,不推荐在finally中使用return返回。

坑位3 —– 可变数据类型作为函数定义中的默认参数:

试试下面的代码:

def fn(var1, var2=[]):
    var2.append(var1)
    print var2

fn(3)
fn(4)
fn(5)

从表面看,这像是十分正常的 Python 代码,事实上它也是,而且是可以运行的。但是,这里有个问题。如果我们给 var2 参数提供了一个列表,它将按照我们预期的那样工作。但是,如果我们让它使用默认值,就会出现一些神奇的事情。

可能你认为我们将看到:

[3]
[4]
[5]

但实际上,我们看到的却是:

[3]
[3, 4]
[3, 4, 5]

为什么呢?如你所见,每次都使用的是同一个列表,输出为什么会是这样?在 Python 中,当我们编写这样的函数时,这个列表被实例化为函数定义的一部分。当函数运行时,它并不是每次都被实例化。这意味着,这个函数会一直使用完全一样的列表对象,除非我们提供一个新的对象:

fn(3, [4])

[4, 3]

答案正如我们所想的那样。要想得到这种结果,正确的方法是:

def fn(var1, var2=None):
    if not var2:
        var2 = []
    var2.append(var1)

这将在模块加载的时候移走实例化的内容,以便每次运行函数时都会发生列表实例化。请注意,对于不可变数据类型,比如元组、字符串、整型,是不需要考虑这种情况的。这意味着,像下面这样的代码是非常可行的:

def func(message="my message"):
    print message
坑位4 —– 写Python程序从菜鸟到老手进阶步骤

我们通常写一个程序的时候,都会经历从单->双->多,从硬编码到柔编码,基本上都会经历下面的这几步,其实代码是讲究柔术的。下面是总览图,我们接着来一 一解释。

写程序的蝶变
  1. 先让程序跑起来
  2. 初步重构代码
  3. 增加注释
  4. 一定要考虑异常
  5. 添加配置文件
  6. 测试用例很重要
  7. 日志模块很重要
  8. 性能上的优化
  9. 再次重构,函数变类
  10. 代码检查

1 先让程序跑起来
当我们刚开始写一个程序的时候,无论是一个自动化脚本,还是一个小游戏,还是一个爬虫,还是一个模块,对于新手来说最简单的方法就是先work:

  • 用最直接的方法,让程序先能work.
  • 比如直接用函数写,直接用假的变量,先把程序运行起来
  • 运行正常的逻辑,让程序跑通

2
初步重构代码

当你的代码已经初步run起来之后,接着我们要对代码进行简单的梳理和整形,会从下面几个地方修剪修剪:

  • 变量名,函数名字的重构
  • 函数的状态要不要有返回值
  • if else这样的嵌套太多,考虑提取
  • 函数进行提取和重构,每个函数的功能单一原则
  • 函数的参数有没有考虑缺省值
  • 全局变量,有没有大写,有没有写在开头

3 增加注释

代码修剪过之后,发现利索多了,这个时候要趁热打铁把注释写上去!

  • 很多同学不太爱写注释,觉得麻烦。如果这个代码就你一人用,或者说这个代码很短,确实可以不写!
  • 如果这个代码有几千行,并且会多个人合作开放,后续还要扩展功能,那么你花5分钟来写注释,未来会帮你节省5个小时的维护时间!
  • 代码首先是给人看的,然后才是给机器运行的!

4 一定要考虑异常

代码跑的好好的,看起来没有问题,是不是很有成就感!

  • 如果是爬虫,你把网络断了,看看有没有异常处理
  • 如果是文件,你把句柄改成None看看有没有问题
  • 如果取列表里面的item,当你获得列表为空,会发生什么

所有的这一切,你有没有考虑到异常,有没有考虑到程序的健壮性。

  • 要考虑异常分支,if 里面有没有else的情况
  • for里面如果出现了错误,有没有break
  • 判断list[],需要看一下这个列表是否为空
  • 文件读写,有没有try/except
  • 拿到一个句柄,比如SSH,SQL,这样有没有考虑到句柄的有效性

5 添加配置文件

我们刚从把局部变量,提到了全局变量。现在我们需要把这个全局变量放到一个配置文件里面,把实现和接口分离,降低耦合度。对于用户来说只要改配置文件就行了。

比如可以把整个的全局变量放到一个config.py里面,然后在主程序里面用from config import * 这样的话,对后续的修改方便很多。

6 测试用例很重要

程序虽然写好了,不管你的程序是几十行的小程序小脚本,还是几千上万行的项目,测试用例是一定要设计。

简单的程序可以设一些断言assert,看一些有无异常,对于复杂的逻辑,一定要针对性的设计多个分支回路来测一下代码。

7 日志模块很重要

有同学说上面6步之后,我感觉代码已经很不错了,这么还有进化!Python的代码很多都是在服务区上运行的,你总不能一直都是print吧,尤其是对大型的程序,没有日志怎么行,建议用logging模块进行日志的记录。

8 性能上的优化

如果你处理的任务仅仅是几百上千,对性能要求不高,对实时性要求不高那还好。如果你要处理几十万条数据呢!

我记得我有一次爬stackoverflow的数据,有96万的数据,你不用并发,估计等程序运行完,你已经睡着啦!

这个时候一定要考虑并发的处理,到底是用多进程,还是多线程,线程池,还是用协程,需要思考!

当然性能上的优化并不单是单线程变多线程,还有数据结构的优化,比如什么时候该用列表,什么时候用元组,哪一种对内存消耗少,查询快。

9 再次重构,函数变类

为了让我们的代码更加易于扩展,适应变化!我们需要用类把变量和函数进行封装,设计一些接口,那些是对外开发的,那些是对外封闭的。

哪些用静态函数包裹,哪些用实例方法。是不是需要用一些装饰器来简化代码。

  • 相同类别的函数,进行整合,合并要一个类里面。
  • 多个功能用多个类来表示,方便维护和扩展。
  • 类与类之间,考虑他们的内在关系。用组合还是继承,用一些简单的设计模式,根据程序的特性用比如工厂模式,观察者。

10 代码检查

代码到这里应该是比较优美了,等一下是不是忘记了一个很重要的东西,我们有没有遵循PEP8的代码风格。

比如命名规范,每一行的长度,看似是细节,但是很多时候细节决定成败。为啥不用这个神器检查一下Pylint库,它能快速帮你查缺补漏。

你可能感兴趣的:(Python)