强类型与动态类型的 Python
在编译的时候,变量的类型就可以被编译器确定,并且运行时该变量不经过强制转换将类型无法发生改变,这种禁止错误类型的参数继续运算的语言我们称为强类型语言。
弱类型语言的变量在定义后,可以根据环境变化不需要通过显式的强制转换,可以进行隐式的转换类型,这一种变量类型为弱类型。
本次主要对Python进行探讨,不对强弱类型、动态类型、安全类型这些进行讲解,关于类型系统的相关内容参考wiki类型系统强类型和弱类型 (opens new window)
以下是来自知乎问题“弱类型、强类型、动态类型、静态类型语言的区别是什么?” (opens new window)中的一张图片。
从这张图中可以看出 Python 属于强类型语言。
例如在 Python 里面定义了进行不同类型的运算
1 + 'b'
就会报如下的类型错误 (TypeError)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
因为 a 和 b 是两个不同类型的变量,在不可以进行加号操作,但是在 C 语言里面可以,因为它会进行隐式转换,而在 Python 里面不会进行类型隐式转换。
对于 Python 来说,变量不通过 int() 或 str() 等方法进行转换的话,那么该变量的类型将无法发生改变。因此 Python 属于强类型语言。
但强类型是针对类型检查的严格程度而言的,它指任何变量在使用的时候必须要指定这个变量的类型,而且在程序的运行过程中这个变量只能存储这个类型的数据。
但是在使用 Python 的时候可以发现,变量定义不需要指定变量的类型,一个变量可以赋其它类型的值的,例如可以这样写
a = 1
a = False
b = "b"
a = b
这看起来和强类型的定义发生冲突了,但其实这涉及到 Python 的另一个特性。
Python 的变量并不是指定了类型的,Python 的变量在进行赋值的时候,是指向了对象的地址,在进行重新赋值的时候,Python 变量并不关心值的类型,因为它只是改变了地址的指向。这种赋值方式报错导致的类型错误,都是在运行的时候才会发生,包括上面举的例子的。
在编译的时候不对变量类型进行识别,而是在运行时期调度已标记的资料,称为动态检查,这一类语言称为动态语言。
详情参考wiki类型系统静态和动态检查 (opens new window)
因为 Python 的这个特性,所以可以抛开强类型语言对于定于及赋值时声明类型的要求,转而在运行时会对变量指向地址的值的类型进行判别,这导致 Python 中的变量几乎都是引用类型。
对于 Python 这种动态指向,在编写程序的时候,给我们带来了很多方便,但是稍不注意,也会让程序发生一些难以察觉的错误。
例如在对列表操作的时候
a = [0, 0, 0]
b = a
b[0] = 1
print("a =", a)
print("b =", b)
此时将会输出
a = [1, 0, 0]
b = [1, 0, 0]
当 Python 将一个列表赋给 a 时,a 并不等于 [0, 0, 0], 而是指向了这个列表的地址。
将 a 的值赋给 b 的时,相当于将 b 的引用修改成 a 的引用,即 b 也是指向这个列表的地址。当你对 b 进行操作的时候,就是在对存放在这个地址里的值进行操作,所以 a 的值也会发生改变,当程序在递归或者用树遍历的时候,如果这样对列表元素进行操作,将可能出现逻辑的错误。
为了避免这种情况,Python 也提供了一些方法给我们进行赋值使用,当我们希望将 a 和 b 不是指向同一个列表,但两个列表的值又要一样的时候,我们可以用 list.copy()
函数来对列表进行浅拷贝。这个函数生成了一个新的列表赋给了 b, 所以对 b 操作的时候不会改变 a 的值。
a = [0, 0, 0]
b = a.copy()
b[0] = 1
print(b) # [0, 0, 0]
另外一种方法就是直接对整个列表进行截取:
当我们使用 a[x:y] 时,可以截取到 a 中下标 x 到下标 y 的片段(不包含 y,即 [x,y)
);
当 x 或 y 的参数放空时,表示从头开始截取或者截取到尾;
例:a[:y] 就是截取下标 0~y 的片段,a[x:] 就是从 x 开始截取到列表结束;
而 a[:] 就是从头截取到最后一个元素,也就是整个列表都截取下来。
c = a[:]
c[0] = 1
print(a) # [0, 0, 0]
print(c) # [1, 0, 0]
但这些是浅拷贝,如果你打算对复制后的多维列表进行操作,你需要判断你要操作的元素所在的维度。
当你对多维列表下第一层元素进行操作时
a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b = a.copy()
c = a[:]
b[0] = [1, 1, 1]
c[1] = [1, 1, 1]
print("a =", a)
print("b =", b)
print("c =", c)
a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b = [[1, 1, 1], [0, 0, 0], [0, 0, 0]]
c = [[0, 0, 0], [1, 1, 1], [0, 0, 0]]
这两种方法同样适用,但当你打算对复制后的多维列表的子列表进行操作时,你会发现这两个方法对于多维列表操作单个元素来说,都不起作用。
d = a.copy()
e = a[:]
d[0][0] = 1
e[1][1] = 1
print("d =", d)
print("e =", e)
print("a =", a)
d = [[1, 0, 0], [0, 1, 0], [0, 0, 0]]
e = [[1, 0, 0], [0, 1, 0], [0, 0, 0]]
a = [[1, 0, 0], [0, 1, 0], [0, 0, 0]]
这个原因是:对于 Python 每一个变量都是引用,对于多维列表,表层元素的值本身是一个引用,引用被复制后还是引用。
由于拷贝后,拷贝出来的多维列表是一个新引用,即指向了一个新列表的地址,但是两个列表表层元素的值相同,而该值又是一个引用,因此表层元素所指向的深层列表也相同,当我们对深层列表(引用的子列表)进行操作时,由于两个引用相同,所以操作的依旧是同一个列表。
可以通过下图来帮助理解这一内容:
例如:
我将 a 复制给了 b, 虽然生成的是新列表,但是列表里面元素指向的地址是相同的,当我修改 b[0]
的时候,相当于是把 b[0]
指向了其它列表,此时 a 并不会收到影响。
但是如果我修改的是 b[1][1]
, 此时我修改的是 b[1]
指向的列表下标为 1 的元素,但是 a[1]
和 b[1]
指向的是同一列表,相当于也是在修改 a[1]
的元素。
这种情况下,也有应对的方法,用 for 循环来进行深度复制
a = [[0,0,0],[0,0,0],[0,0,0]]
b = []
for data in a:
b.append(data.copy())
但是这种办法对于对于 n 维列表,需要内嵌的 for 循环为 n-1 个,如果是维度高的列表,就不是很切实际了。
但 Python 提供了另外一种方法, copy.deepcopy()
函数,这个方法在 Python 3 自带的包 copy
里面。
import copy
a = [[0,0,0],[0,0,0],[0,0,0]]
b = copy.deepcopy(a)
b[0][0] = 1
b[1][1] = 2
b[2][2] = 3
print("a =",a)
print("b =",b)
此时将会输出
a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b = [[1, 0, 0], [2, 0, 0], [3, 0, 0]]
可以看到操作列表 b 并不会对列表 a 造成影响。
参考链接:
谈谈python中的深拷贝和浅拷贝@会发光的二极管 (opens new window) Wiki 类型系统 (opens new window) 弱类型、强类型、动态类型、静态类型语言的区别是什么? (opens new window)