Python重点回顾(一)

最近学习Python,整理出来一部分重难点与大家分享。

一、变量不是盒子

Lynn Stein教授曾指出,人们常将变量视为存放数据的盒子,但这一概念不利于理解面向对象语言中的变量。事实上,Python中的变量类似于Java中非基元类型的变量,默认传递引用而不是拷贝。这也就意味着,可变对象的使用要特别小心。
Lynn Stein建议将变量理解为对内存中数据的标签,也就是引用。
下面这个例子说明了原因。

a= [1, 2, 3]
b = a
a.append(4)
print(f"a:{a}")
print(f"b:{b}")
"""
输出:
a:[1, 2, 3, 4]
b:[1, 2, 3, 4]
"""

通过变量b就可以看出这个效果。如果你将b视作盒子,存储a盒子中[1, 2, 3]的拷贝,那这个行为是说不通的。因此b = a不是将盒子a中的数据复制一份再放在了新盒子b中,而是为内存中的[1, 2, 3]对象又贴上了一张标签b

二、相等性、统一性和别名

来看这个例子:

a = {"a": 1, "b": 2}
b = a
c = {"a" : 1, "b": 2}
print(f"a == b : {a == b}")
print(f"a is b : {a is b}")
print(f"a == c : {a == c}")
print(f"a is c : {a is c}")
"""
输出:
a == b : True
a is b : True
a == c : True
a is c : False
"""

从这个例子中,我们发现is==具有不同的行为。a is ca == c的值并不相同。自此,我们引入别名的概念。
在本例中,ab持有同一个对象的引用,所以可以说,ba的别名。而c不是a的别名,因为两者指向的对象持有的数据等价,但并没有指向相同的对象。
回到is==上来,is比较对象的id,在CPython中,对象的id就是对象的地址,不同的对象拥有不同的地址,但不可变对象具有一些特殊行为。而==默认比较id,但它可以被特殊方法__eq__重载。a == b其实是a.__eq__(b)的语法糖。对于dict而言,dict实现了__eq__方法,只比较对象持有的数据。
is适合比较一个对象与一个单例,例如x is None而不写作x == None。在一般情况下,我们只关注值,所以==更常用。

三、不可变对象的奇特行为

元组常被认作“不可变列表”,但这是错误的理解。元组只保证具有相对不可变性,因为它存储的仍然是对象的引用,元组只能保证引用不变,而非对象不变。
所以,如果元组中的元素是可变对象,元素仍然可以被修改。例如:

t = (1, 2, [3, 4])
t[2][1] = 5
print(t)

这样的操作是被允许的,因为元组内的列表元素仍然可以被修改。但是,如果尝试修改元组中的元素本身,会得到一个错误提示:

t[0] = 0  # TypeError: 'tuple' object does not support item assignment

还需要强调的是,tuple(tuple_name)构造器会返回tuple_name的别名。如下:

tuple_a = (1, 2, 3)
tuple_b = tuple(tuple_a)
print(f"tuple_a is tuple_b : {tuple_a is tuple_b}")
list_a = [1, 2, 3]
list_b = list(list_a)
print(f"list_a is list_b : {list_a is list_b}")

正如官方文档所说:

If the argument is a tuple, the return value is the same object.
如果实参是一个元组,返回值是同一个对象。

此外,setfrozenset也有此类行为。
CPython还存在称为驻留的机制(interning),会共享一些小整数和字符串字面量。
这是一个例子:

>>> a = "a"
>>> b = "a"
>>> a is b
True
>>> a == b
True

变量ab本应该不是同一个字符串对象的引用,但CPython悄悄做了手脚。
最后,驻留作为内部优化,不应被依赖或过多提起。

四、不要用可变对象充当默认参数

很多教程都谈到,不要使用可变对象作为默认参数,这里我们解释原因。
看这个例子:

class Bus:
    """
    传说中的幽灵巴士
    """
    def __init__(self, passengers = None):
        self.passengers = passengers
    def add(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.pop(name)


bus1 = Bus()
bus1.add("jack")
bus1.add("nick")
bus2 = Bus()
bus2.add("helen")

print(bus2.passengers)

一切的原因就在于,默认参数仅在函数声明时初始化(求值)一次。默认参数始终都是同一个对象,这将导致不经意间的共享数据。
在确实需要传递可变对象时,请将参数默认值设为一哨符对象,例如None。此外不要忘记进行一次浅拷贝。
这是正确的例子:

class Bus:
    """
    从来没有什么幽灵!
    """
    def __init__(self, passengers = None):
        self.passengers = list(passengers) if passengers is not None else []
    def add(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.pop(name)


bus1 = Bus()
bus1.add("jack")
bus1.add("nick")
bus2 = Bus()
bus2.add("helen")

print(bus2.passengers)

打字不易,多多支持!!!
笔者能力有限,错漏难免,请读者指出!

本文由小科云团队原创出品,作者陈祺嘉,未经许可禁止转载。

20260320092532160-image

 

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容