欢迎光临
生活尽了力,其他靠佛系

【Python】可/不可变对象、作用域

了解可/不可变对象以及作用域对python编程意义重大,也可以在一定程度上减少编写错误代码,然后自己却找不到bug的境况。

注:本站如不经特殊说明,使用的Python版本都是3.x

可变对象与不可变对象

Python的所有对象可分为可变对象和不可变对象,如下表。而所谓的可变对象是指,对象的内容可变,反之不可变对象指的是对象的内容不可变。

不可变对象 可变对象
数值类型,字符串,元组 字典,列表

你可能不理解数值类型为什么是不可变对象,我们来了解一下Python变量的操作,假如我们有一个变量x赋值3,既有如下操作:

x = x*2

执行该操作后,程序将会为变量x申请地址并存储它。你可能会想到如果我们执行,x= x*x的操作只有x的值发生了变化,为什么说这种类型是不可变的呢?实际上,如果变量的值发生改变,Python会自动创建另一个队形申请另一个内存,并改变变量的对象引用,如下图

这样做的优点是减少了重复的值对内存的占用,而缺点则是每次修改变量都需要重新开辟内存单元,给执行效率带来一定影响。

Python函数的参数都是对象的引用,这是我们需要明确的。如果引用不可变对象中尝试修改对象,程序会在函数中生成新的对象,函数外被引用的对象则不会被改变。例如:

num = 1
def fun1(num):
    num += 1
fun1(num)
print(num)

输出结果:1。这是因为主程序中的num与函数中的num是不一样的,具体来说,它们的地址不一样,所以改变函数中的num值时并不会影响函数外的num值,而如果想改变函数外的num,则可通过返回值实现。

而对于一个列表来说,则有:

L = [1, 2, 3]
def fun2(list):
    list.append(4)
fun2(L)
print(L)

输出的结果为:[1, 2, 3, 4],这个就很好理解了,list是可变的对象。

如果我们希望赋值时可变对象不进行引用,而是重新分配地址空间并将数据复制,我们可以利用Python中的copy模块。其中主要函数有copy.copycopy.deepcopy

  • copy.copy仅复制父对象,不会复制对象内部的子对象,也就是浅复制
  • copy.deepcopy复制父对象和子对象,也就是深复制

例如:

# 深复制与浅复制
import copy

list1 = [1, 2, 3, 4, ['a', 'b']]
list2 = list1                    # 指向list1

list3 = copy.copy(list1)         # 浅拷贝
list4 = copy.deepcopy(list1)     # 深拷贝
list1.append(5)
list1[4].append('c')

print("list1:", list1)
print("list2:", list2)
print("list3:", list3)
print("list4:", list4)

# 结果
# list1: [1, 2, 3, 4, ['a', 'b', 'c'], 5]
# list2: [1, 2, 3, 4, ['a', 'b', 'c'], 5]
# list3: [1, 2, 3, 4, ['a', 'b', 'c']]
# list4: [1, 2, 3, 4, ['a', 'b']]

对于简单的 object,用 shallow copy 和 deep copy 没区别
复杂的 object, 如 list 中套着 list 的情况,shallow copy 中的 子list,并未从原 object 真的「独立」出来。也就是说,如果你改变原 object 的子 list 中的一个元素,这也就是为什么list3中有[‘a’, ‘b’, ‘c’],而无5的原因,但经过深copy则,无 [‘a’, ‘b’, ‘c’],也无5.更多可以参考文献1.

作用域

Python在创建、改变或查找变量名时都是在命名空间中进行的,更准确地说,使在特定作用域下进行的。所以,我们需要使用某个变量名时,应清晰地知道其作用域。由于Python不能声明变量,所以变量第一次被赋值的时候已经与一个特定作用域绑定了。也就是说,在代码中给一个变量赋值的地方决定了这个变量存在于哪个作用域,它可见的范围在哪。
假设我们有一个函数:

def defint_x():
    x = 2

接着,我们执行

x = 1
defint_x()
print(x)

则其输出结果为:1
分析:执行函数defint_x()后,函数外的x的值没有变化,这是因为整段程序中存在两个x,起初在函数体外,创建了一个x,接着执行defint_x()时,又在函数的内部代码块中进行了,赋值语句x=2仅在局部作用域(也就是函数内部)起作用。所以它不会使得函数外的x发生变化。我们把函数内的变量称为局部变量(Local Variable),而在主程序中的变量称为全局变量(Global Variable)。在函数内部是可以访问全局的,如:

def print_x():
    print(x)
x = 1
print_x()

执行结果就是:1,程序没有报错,所以说明在函数内部同样可以使用全局变量。那么,函数内部既然可访问局部变量,也可以访问全局变量,如果出现重名了该怎么办?实际上,在最先开始的那个例子就已经回答了这个问题,在局部作用域中,如果出现重名的变量,则全局变量会被局部变量屏蔽,如果想使用全局变量,我们需要使用globals()函数,例如:

def print_x():
    x = 2
    print(globals()['x'])
x = 1
print_x()

输出结果就是:1
有时候,我们也希望在函数内部去创建一个全局变量,这时我们就使用global关键字进行声明即可,例如:

def defint_x():
    global x
    x = 2
x = 1
defint_x()
print(x)

输出结果就是:2
函数内部使用global声明了变量x的作用域是全局的,因而程序访问的是全局的x。看起来global很好用,但是在实际开发中还是少使用global为好,否则会导致代码变得混乱,可读性低,你都不知道哪里突然出现了一个变量。相反,局部变量会使代码更加抽象,封装性更好。一个好的函数只使用输入和输出和函数外的程序进行联系。

Reference

1.python中copy()和deepcopy()详解

赞(1) 打赏
未经允许不得转载:AIAS编程有道 | Artificial Intelligence Algorithm Scientist » 【Python】可/不可变对象、作用域
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用,就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏