强类型与动态类型的 Python

2019/11/10
设计理解
Python

在编译的时候,变量的类型就可以被编译器确定,并且运行时该变量不经过强制转换将类型无法发生改变,这种禁止错误类型的参数继续运算的语言我们称为强类型语言

弱类型语言的变量在定义后,可以根据环境变化不需要通过显式的强制转换,可以进行隐式的转换类型,这一种变量类型为弱类型。

本次主要对Python进行探讨,不对强弱类型、动态类型、安全类型这些进行讲解,关于类型系统的相关内容参考wiki类型系统强类型和弱类型 (opens new window)

以下是来自知乎问题“弱类型、强类型、动态类型、静态类型语言的区别是什么?” (opens new window)中的一张图片。

language

从这张图中可以看出 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 每一个变量都是引用,对于多维列表,表层元素的值本身是一个引用,引用被复制后还是引用。

由于拷贝后,拷贝出来的多维列表是一个新引用,即指向了一个新列表的地址,但是两个列表表层元素的值相同,而该值又是一个引用,因此表层元素所指向的深层列表也相同,当我们对深层列表(引用的子列表)进行操作时,由于两个引用相同,所以操作的依旧是同一个列表。

可以通过下图来帮助理解这一内容:

dynamic

例如:

我将 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)

Post Order
By Time : DESC
Article Statistics
Article: 35
Categories: 4
Tags: 18