Python 纯函数与副作用,可变参数与不可变参数

Python 纯函数与副作用,可变参数与不可变参数

Python语言是一个以一切皆对象的面向对象的动态型语言。Python的对象可以根据其是否可以变化划分为可变对象和不可变对象。

对象类型

不可变对象(值类型)

  • Numbers:数值类型
    • int: 整型数
    • float: 浮点数
    • complex: 复数
  • bool: 布尔类型
  • str: 字符型
  • tuple: 元组
  • range: 范围对象
  • frozenset: 不可变集合
  • bytes: 不可变字节数组

可变对象(引用类型)

  • list: 列表
  • dict: 字典
  • set: 可变集合
  • bytearray: 可变字节数组
  • 用户自定义类

在编码中,我们可以对如str, int等对象进行修改,那为什么这些类型还是不可变对象。

实际上,当我们对这些不可变对象进行操作时,我们更改的并不是变量指向对象具体的值,而是使变量重新指向了一个不可变对象。

python
1
2
3
4
5
6
a = 123456789
b = 123456789

print(f"id(a) = {id(a)}, id(b) = {id(b)}")

# id(a) = 140219002169648, id(b) = 140219002169648

如上我们可以看到,当Python变量指向相同不可变对象时,不同的变量实际上是引用的相同的对象。

python
1
2
3
4
5
6
7
8
a = 123456789
print(f"id(a) = {id(a)}")

a = 987654321
print(f"update_id(a) = {id(a)}")

# id(a) = 140504869153072
# update_id(a) = 140504869153008

如上我们可以看到,当Python修改不可变对象时,实际上只是改变的变量指向的对象,而不是原来的不可变对象。

不可变对象的特例

元组作为Python中的容器类型,其本身是不可变的,但容器中存储的对象不一定都是不可变对象。 考虑的一下这种情形:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a = (1, 2, 3, [1, 2, 3])
print(f"a = {a}")
print(f"id(a) = {id(a)}")
a[3].append(4)
print(f"update_a = {a}")
print(f"update_id(a) = {id(a)}")

# a = (1, 2, 3, [1, 2, 3])
# id(a) = 140188517471056
# update_a = (1, 2, 3, [1, 2, 3, 4])
# update_id(a) = 140188517471056

可以看到,元组明明是不可变对象,但我们却对其值进行了修改。主要原因是,元组作为容器,内部存储的都是其保存数据的内存地址,我们对该地址上的数据进行了操作修改,但没有改变其内存地址本身,所以元组的值自然而然的就发生了变化。 所以,不可变对象的“值”不可以改变,但其组成对象的“值”可以进行修改。在处理不可变对象时,要注意考虑到这种情形。

Python变量

Python中的变量都是指针,因为Python的变量都是指针,所以Python变量上无类型限制的,它是可以指向任意对象的,Python对象只是保存了指向数据的内存地址。

不可变对象(值类型)

在Python中,因为值类型作为不可变对象,他们本身的值是不可以修改的。所以对值类型的修改实际上是让变量指向了新的对象,原始对象会被Python的GC回收

python
1
2
3
4
5
6
a = 1
b = a
a = 2

print(b)
# 1

修改值类型的值,因为是让变量指向了新的对象,不会对原始对象的属性造成影响。

可变对象(引用类型)

在Python中,当修改引用类型即可变对象时,因为对象是可变的的,所以可以直接对引用的对象进行操作。因为引用对象是对该实例的内存空间上的值进行修改,所以当有多个变量引用同一个引用实例时,对一个变量的修改,其他引用该实例的变量也会发生相应的变化。

python
1
2
3
4
5
6
a = [1, 2, 3]
b = a
a = a.append(4)

print(b)
# [1, 2, 3, 4]

可以看到通过对变量a进行操作,变量b的值也发生了相应的变化。

参数传递

函数按照传值的方式分为:

  • 值传递:把调用函数时传递的值赋值到形参当中,对形参的操作不会影响外部的实参变量。
  • 引用传递:把实参引用的内存地址赋值给形参,当对该内存的值进行修改时,会相应的影响到外部实参变量。

参数传递方式还包括地址传递,在Python这种无指针的高级语言中,不考虑辨别区分地址传递这种类型

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
a = 123
b = [1, 2, 3]

print(f"id(a) = {id(a)}, id(b) = {id(b)}")


def test(a, b):
    print(f"func id(a) = {id(a)}, id(b) = {id(b)}")


test(a, b)
# id(a) = 140362719137840, id(b) = 140362724287744
# func id(a) = 140362719137840, id(b) = 140362724287744

通过上面的代码,可以看到Python可变对象和不可变对象在传递参数时,都是传递的变量指向的内存地址而不是进行的值传递。

Python为了方便内存的管理,都是采用的引用传递。在传递参数时,都传递的是对应的内存地址,所以在Python中对可变对象的修改,会引起外部对象的改变。不可变对象因为其特性,对不可变对象的操作效果和值传递具有相同的效果。

Licensed under CC BY-NC-SA 4.0
陕ICP备2023020057号
Built with Hugo
主题 StackJimmy 设计