- 与 for、while、try 搭配的 else语句
在 Python 中 else 除了与 if 匹配外,还可以与 for、while、try 语句匹配。
- for/else
只有当 for 语句执行完毕的时候,else 才会执行。除非被 break 语句打断
In [1]: for i in range(3):
...: print i
...: else:
...: print "end"
...:
0
1
2
end
In [2]: for i in range(3):
...: print i
...: if i == 2:
...: break
...: else:
...: print "end"
...:
0
1
2
- while/else
运行原理同 for/else
In [3]: i = 0
In [4]: while i < 3:
...: print i
...: i += 1
...: else:
...: print "end"
...:
0
1
2
end
In [6]: i = 0
In [7]: while i < 3:
...: print i
...: i += 1
...: if i == 1:
...: break
...: else:
...: print "end"
...:
0
- try/else
只有 try 语句中没有发生异常时,else 才会执行。
In [8]: try:
...: print "haha"
...: except Exception as e:
...: print "Error"
...: else:
...: print "end"
...:
haha
end
如果 try 语句中有异常,则 else 语句则不会执行。如果存在异常而 except 模块没捕获到,那么 else 代码块中的代码也不会执行。
In [9]: try:
...: print "haha"
...: raise Exception
...: except Exception as e:
...: print "Error"
...: else:
...: print "end"
...:
haha
Error
- 可变数据类型作为函数的默认值
In [10]: def fun(a, b=[]):
...: b.append(a)
...: print b
...:
In [11]: fun(3)
[3]
In [12]: fun(4)
[3, 4]
In [13]: fun(5)
[3, 4, 5]
为啥不是下面的这个呢?
[3]
[4]
[5]
为什么呢?如你所见,每次都使用的是同一个列表,输出为什么会是这样?之所以得到这个结果,是因为在Python中,一个函数参数的默认值,仅仅在该函数定义的时候,被初始化一次。
在 Python 中,当我们编写这样的函数时,这个列表被实例化为函数定义的一部分。当函数运行时,它并不是每次都被实例化。这意味着,这个函数会一直使用完全一样的列表对象,除非我们提供一个新的对象:
In [14]: fun(1, [2])
[2, 1]
In [15]: fun(1, [3])
[3, 1]
答案是一个函数参数的默认值,仅仅在该函数定义的时候,被赋值一次。如此,只有当函数 fun()第一次被定义的时候,才讲参数 b 的默认值初始化到它的默认值(即一个空的列表)。当调用fun()的时候(不给参数 b),会继续使用 b 最早初始化时的那个列表。
要想得到这样的结果,正确的做法是:
In [16]: def fun(a, b=None):
...: if not b: # or if b is None:
...: b = []
...: b.append(a)
...: print b
...:
In [17]: fun(3)
[3]
In [18]: fun(4)
[4]
In [19]: fun(5)
[5]
- 可变类型作为类变量
class URLCatcher(object):
urls = []
def add_url(self, url):
self.urls.append(url)
这段代码看起来非常正常。我们有一个储存 URL 的对象。当我们调用 add_url 方法时,它会添加一个给定的 URL 到存储中。看起来非常正确吧?让我们看看实际是怎样的:
a = URLCatcher()
a.add_url('http://www.google.com')
b = URLCatcher()
b.add_url('http://www.bbc.co.hk')
实际结果
# b.urls:
['http://www.google.com', 'http://www.bbc.co.uk']
# a.urls:
['http://www.google.com', 'http://www.bbc.co.uk']
等等,怎么回事?!我们想的不是这样啊。我们实例化了两个单独的对象 a 和 b。把一个 URL 给了 a,另一个给了 b。这两个对象怎么会都有这两个 URL 呢?
这和第一个错例是同样的问题。创建类定义时,URL 列表将被实例化。该类所有的实例使用相同的列表。在有些时候这种情况是有用的,但大多数时候你并不想这样做。你希望每个对象有一个单独的储存。为此,我们修改代码为:
class URLCatcher(object):
def __init__(self):
self.urls = []
def add_url(self, url):
self.urls.append(url)
现在,当创建对象时,URL 列表被实例化。当我们实例化两个单独的对象时,它们将分别使用两个单独的列表。
- 错误的使用类变量
In [1]: class A(object):
...: x = 1
...:
In [2]: class B(A):
...: pass
...:
In [3]: class C(A):
...: pass
...:
In [4]: A.x, B.x, C.x
Out[4]: (1, 1, 1)
In [5]: B.x = 2
In [6]: A.x, B.x, C.x
Out[6]: (1, 2, 1)
In [7]: A.x = 3
In [8]: A.x, B.x, C.x
Out[8]: (3, 2, 3)
仅仅是改变了A.x,为什么C.x也跟着改变了。
当在 Python 值使用继承的时候,也要注意类属性的隐藏。对于一个类,可以通过类的 __ dict __ 属性来查看所有的类属性。
当通过类名来访问一个类属性的时候,会首先查找类的 __ dict __ 属性,如果没有找到类属性,就会继续查找父类。但是,如果子类定义了跟父类同名的类属性后,子类的类属性就会隐藏父类的类属性。
在Python中,类变量都是作为字典进行内部处理的,并且遵循方法解析顺序(MRO)。在上面这段代码中,因为属性x没有在类C中发现,它会查找它的基类(在上面例子中只有A,尽管Python支持多继承)。换句话说,就是C自己没有x属性,独立于A,因此,引用 C.x其实就是引用A.x。
- 捕获多个异常
In [10]: try:
...: l = ["a", "b"]
...: int(l[2])
...: except ValueError, IndexError: # 想捕捉两个异常
...: print "error"
...:
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-10-6f6b12c012b2> in <module>()
1 try:
2 l = ["a", "b"]
----> 3 int(l[2])
4 except ValueError, IndexError: # 想捕捉两个异常
5 print "error"
IndexError: list index out of range
这里的问题在于except语句不会像这样去接受一系列的异常。并且,在Python 2.x里面,语法except Exception, e是用来将异常和这个可选的参数绑定起来(即这里的e),以用来在后面查看的。因此,在上面的代码中,IndexError异常不会被except语句捕捉到;而最终ValueError这个异常被绑定在了一个叫做IndexError的参数上。
在except语句中捕捉多个异常的正确做法是将所有想要捕捉的异常放在一个元组(tuple)里并作为第一个参数给except语句。并且,为移植性考虑,使用as关键字,因为Python 2和Python 3都支持这样的语法,例如:
In [11]: try:
...: l = ["a", "b"]
...: int(l[2])
...: except (ValueError, IndexError) as e: # 想捕捉两个异常
...: print "error"
...: print e
...:
error
list index out of range
- 误解 Python 的作用域规则
Python的作用域解析是基于叫做LEGB(Local(本地),Enclosing(封闭),Global(全局),Built-in(内置))的规则进行操作的。
In [12]: x = 1
In [13]: def fun():
...: x += 1
...: print x
...:
In [14]: fun()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-14-69e6a439c52d> in <module>()
----> 1 fun()
<ipython-input-13-d176b55edb95> in fun()
1 def fun():
----> 2 x += 1
3 print x
4
UnboundLocalError: local variable 'x' referenced before assignment
这是因为,在一个作用域里面给一个变量赋值的时候,Python****自动认为这个变量是这个作用域的本地变量,并屏蔽作用域外的同名的变量。
使用列表时同样会存在这样的问题
In [15]: a = [1, 2, 3]
In [16]: def fun1():
...: a.append(10)
...:
In [17]: fun1()
In [18]: a
Out[18]: [1, 2, 3, 10]
In [19]: a = [1, 2, 3]
In [20]: def fun2():
...: a += [10]
...:
In [21]: fun2()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-21-32d138681f85> in <module>()
----> 1 fun2()
<ipython-input-20-c050916b939b> in fun2()
1 def fun2():
----> 2 a += [10]
3
UnboundLocalError: local variable 'a' referenced before assignment
为什么 fun2 有问题,而 fun1 没有问题?
答案和上一个例子一样,但是更加不易察觉。fun1 并没有给 a 赋值,但是 fun2 尝试给 a 赋值。注意 a += [10] 只是 a = a + [10] 的简写,由此可以看到我们尝试给 a 赋值(因此Python假设作用域为本地)。但是,这个要赋给 a 的值是基于 a 本身的(这里的作用域仍然是本地),而 a 却没有被定义,这就出错了。
- 在遍历列表的同时又通过下标修改列表
来看个例子
In [27]: aa = [1,2,3,4,5]
In [28]: for i, v in enumerate(aa):
...: if i % 2 == 0: # 本想删除下标为偶数位的值
...: aa.pop(i)
...:
In [29]: aa
Out[29]: [2, 3, 5]
遍历一个列表或者数组的同时又删除里面的元素,对任何有经验的软件开发人员来说这是个很明显的错误。
- 与 Python 标准库模块命名冲突
Python的一个优秀的地方在于它提供了丰富的库模块。但是这样的结果是,如果你不下意识的避免,很容易你会遇到你自己的模块的名字与某个随Python附带的标准库的名字冲突的情况(比如,你的代码中可能有一个叫做email.py的模块,它就会与标准库中同名的模块冲突)。
这会导致一些很粗糙的问题,例如当你想加载某个库,这个库需要加载Python标准库里的某个模块,结果呢,因为你有一个与标准库里的模块同名的模块,这个包错误的将你的模块加载了进去,而不是加载Python标准库里的那个模块。这样一来就会有麻烦了。 - 元组中含有列表时,列表中的元素是可变的
In [30]: aa = (1, 2, 3, [4, 5, 6])
In [31]: id(aa)
Out[31]: 61949896L
In [32]: id(aa[3])
Out[32]: 62077384L
In [34]: aa[3].extend([10])
In [35]: aa
Out[35]: (1, 2, 3, [4, 5, 6, 10])
- 模块循环依赖
在编码的过程中就应当避免循环依赖的情况,或者代码重构的过程中消除循环依赖。如果出现循环依赖时,常用的解决办法就是把引用关系搞清楚,让某个模块在真正需要的时候再导入(一般放到函数里面) - x += y 与 x = x + y
一般来说,二者是等价的,至少看起来是等价的。
In [36]: x=1;x += 1;print x
2
In [37]: x=1;x = x+1;print x
2
In [38]: x=[1];x+=[2];print x
[1, 2]
In [39]: x=[1];x=x+[2];print x
[1, 2]
我们可以看下其 id 值
In [40]: x=[1];print id(x);x+=[2];print id(x)
61650568
61650568
In [41]: x=[1];print id(x);x=x+[2];print id(x)
61652040
61651528
可以看到 += 是在原有对象上进行修改,后面一种 + 其实是一个新的对象。
扩展:列表的 append 和 extend 方法也是在原有列表上进行操作的。
- 生成器不会保留迭代过后的值
In [42]: g = (x for x in range(5))
In [43]: 3 in g
Out[43]: True
In [44]: 4 in g
Out[44]: True
In [45]: 1 in g
Out[45]: False
为什么 1 in g 会返回 False 呢? 因为在调用 3 in g,4 in g 后 1 已经不再迭代器里面了。
可以使用如下方法保存对应的值。
In [46]: g = (x for x in range(5))
In [47]: aa = list(g)
In [48]: aa
Out[48]: [0, 1, 2, 3, 4]
- lambda 在闭包中会保存局部变量
In [67]: vv = [lambda: i for i in range(5)]
In [68]: for f in vv:
...: print f()
...:
4
4
4
4
4
当赋值给 vv 的时候,lambda 表达式会执行 i 循环,直到 i = 4 的时候保留。
可以使用生成器替换,但是也只能调用一次,代码如下:
In [69]: vv = (lambda: i for i in range(5))
In [70]: for f in vv:
...: print f()
...:
0
1
2
3
4
In [71]: for f in vv:
...: print f()
...:
- 拷贝可变对象
In [72]: a = [[1,2]] * 2
In [73]: a
Out[73]: [[1, 2], [1, 2]]
In [74]: a[1][0] = 'a' # 只修改其中的一项
In [75]: a
Out[75]: [['a', 2], ['a', 2]] # 两项都被修改了
使用下面的方法可以避免这种情况发生
In [76]: b = [[1,2] for i in range(2)]
In [77]: b
Out[77]: [[1, 2], [1, 2]]
In [78]: b[1][0] = 'a'
In [79]: b
Out[79]: [[1, 2], ['a', 2]]