Python 中值得注意的几个问题

可变和不可变对象

参考文章: 这里

可变:list, dict, set
不可变:int, str, float, tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def int_test():  # 不可变,会复制一份 
i = 77
j = 77
print(id(77)) #140396579590760
print('i id:' + str(id(i))) #i id:140396579590760
print('j id:' + str(id(j))) #j id:140396579590760
print i is j #True
j = j + 1
print('new i id:' + str(id(i))) #new i id:140396579590760
print('new j id:' + str(id(j))) #new j id:140396579590736
print i is j #False

def dict_test(): # 可变,不会复制一份
a = {}
b = a # 浅复制
print(id(a)) # 140367329543360
a['a'] = 'hhhh'
print('id a:' + str(id(a))) # id a:140367329543360
print('a:' + str(a))
b['hah'] = 50
print('id b:' + str(id(b))) # id b:140367329543360
print('b:' + str(b))
print('a:' + str(a)) # 这里a也跟着变了!

函数的参数传递

由于 Python 规定参数传递都是传递引用,也就是传递给函数的是原变量实际所指向的内存空间,修改的时候就会根据该引用的指向去修改该内存中的内容,所以按道理说我们在函数内改变了传递过来的参数的值的话,原来外部的变量也应该受到影响。但是上面我们说到了 Python 中有可变类型和不可变类型,这样的话,当传过来的是可变类型list、dict时,我们在函数内部修改就会影响函数外部的变量。而传入的是不可变类型时在函数内部修改变量并不会影响函数外部的变量,因为修改的时候会先复制一份。

在很多的其他语言中在传递参数的时候允许程序员选择值传递还是引用传递(比如 C 语言加上*号传递指针就是引用传递,而直接传递变量名就是值传递),而 Python 只允许使用引用传递,但是它加上了可变类型和不可变类型,让我们感觉有点混乱了。听说 Python 只允许引用传递是为方便内存管理,因为 Python 使用的内存回收机制是计数器回收,就是每块内存上有一个计数器,表示当前有多少个对象指向该内存。每当一个变量不再使用时,就让该计数器-1,有新对象指向该内存时就让计数器+1,当计时器为0时,就可以收回这块内存了。

Copy & Deepcopy

Python 中的对象之间赋值时是按引用传递的,如果需要拷贝对象,需要使用标准库中的 copy 模块。

  1. copy.copy(浅拷贝):只拷贝父对象,不会拷贝对象的内部的子对象。
  2. copy.deepcopy(深拷贝):拷贝对象及其子对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import copy
>>> a = [1, 2, 3, 4, ['a', 'b']] #原始对象
>>> b = a #赋值,传对象的引用
>>> c = copy.copy(a)
>>> d = copy.deepcopy(a)
>>> a.append(5)
>>> a[4].append('c')

>>> print 'a=',a # id: 4529531272
a= [1, 2, 3, 4, ['a', 'b', 'c'], 5]
>>> print 'b=',b # id: 4529531272
b= [1, 2, 3, 4, ['a', 'b', 'c'], 5]
>>> print 'c=',c # id: 4529487240
c= [1, 2, 3, 4, ['a', 'b', 'c']] # 为什么还是这样abc呢?因为子对象没有被复制
>>> print 'd=',d # id: 4529611080
d= [1, 2, 3, 4, ['a', 'b']]

is 和 == 的区别

is比较的是 id 是不是一样,==比较的是值是不是一样。在 Python 中,万物皆对象!万物皆对象!万物皆对象!(很重要,重复 3 遍)

每个对象包含 3 个属性:id,type,value。id 就是对象地址,可以通过内置函数id()查看对象引用的地址;type 就是对象类型,可以通过内置函数type()查看对象的类型;value 就是对象的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = 1
b = a # 如果a是-10,a is b 为 False
c = 1
d = 1.0

id(a) # 35556792L
id(b) # 35556792L
id(c) # 35556792L
id(d) # 21253459L

>>> a is b # id(a) == id(b)
True
>>> a is d
False
>>> a == d
True

所以大多数情况下当用is==的结果是一样时,用is的效率是会高于==的效率。==是通过对象的一个__eq__()方法实现的。

By The Way

Python为了实现对内存的有效利用,对小整数[-5,256]内的整数会进行缓存,不在该范围内的则不会缓存,具体如下:

1
2
3
4
5
6
7
8
>>> a = 255
>>> b = 255
>>> a is b
True
>>> c = 257
>>> d = 257
>>> c is d
False

为什么 Python 不支持函数重载?

这个问题,最近在 cpyug 上面讨论得很火热。为了考虑为什么 Python 不提供函数重载,首先我们要研究为什么需要提供函数重载。函数重载主要是为了解决两个问题:

  • 可变参数类型
  • 可变参数个数

另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。

好吧,那么对于情况 1,函数功能相同,但是参数类型不同,Python 如何处理?答案是根本不需要处理,因为 Python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在 Python 中很可能是相同的代码,没有必要做成两个不同函数。

那么对于情况 2,函数功能相同,但参数个数不同,Python 如何处理?大家知道,答案就是缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的。

好了,鉴于情况 1 跟情况 2 都有了解决方案,Python 自然就不需要函数重载了。