原文:
www.kdnuggets.com/5-common-python-gotchas-and-how-to-avoid-them
作者提供的图片
Python 是一种对初学者友好且多才多艺的编程语言,以其简洁性和可读性而闻名。然而,它优雅的语法并非没有奇怪之处,这可能会让即使是经验丰富的 Python 开发者感到意外。理解这些问题对于编写无错误的代码——或者说无痛调试——是至关重要的。
1. Google 网络安全证书 - 快速进入网络安全职业。
2. Google 数据分析专业证书 - 提升你的数据分析水平
3. Google IT 支持专业证书 - 支持你组织的 IT
本教程探讨了一些这些问题:可变的默认值、循环和推导式中的变量作用域、元组赋值等。我们将编写简单的示例来了解为什么事情会这样工作,并且还会研究如何避免这些(如果我们真的可以的话 🙂)。
那我们开始吧!
在 Python 中,可变的默认值常常是个棘手的问题。每当你定义一个带有可变对象(如列表或字典)作为默认参数的函数时,你会遇到意想不到的行为。
默认值仅在函数定义时评估一次,而不是每次调用函数时。如果你在函数内更改默认参数,这可能会导致意想不到的行为。
让我们来看一个例子:
def add_to_cart(item, cart=[]):
cart.append(item)
return cart
在这个例子中,add_to_cart
是一个函数,它接受一个项目并将其追加到一个列表 cart
中。cart
的默认值是一个空列表。也就是说,如果调用函数时没有添加项目,它会返回一个空购物车。
下面是几个函数调用:
# User 1 adds items to their cart
user1_cart = add_to_cart("Apple")
print("User 1 Cart:", user1_cart)
Output >>> ['Apple']
这按预期工作。但现在会发生什么呢?
# User 2 adds items to their cart
user2_cart = add_to_cart("Cookies")
print("User 2 Cart:", user2_cart)
Output >>>
['Apple', 'Cookies'] # User 2 never added apples to their cart!
因为默认参数是一个列表——一个可变对象——它在函数调用之间保持其状态。所以每次调用 add_to_cart
时,它都会将值追加到函数定义时创建的同一个列表对象中。在这个例子中,就像所有用户共享同一个购物车一样。
作为解决方案,你可以将 cart
设置为 None
,然后在函数内部初始化购物车,如下所示:
def add_to_cart(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
所以每个用户现在都有一个单独的购物车。🙂
如果你需要复习 Python 函数和函数参数,可以阅读 Python 函数参数:终极指南。
Python 的作用域怪癖需要一个专门的教程。不过我们将在这里看看其中一个怪癖。
看一下以下代码片段:
x = 10
squares = []
for x in range(5):
squares.append(x ** 2)
print("Squares list:", squares)
# x is accessible here and is the last value of the looping var
print("x after for loop:", x)
变量 x
被设置为 10。但是 x
也是循环变量。我们假设循环变量的作用范围仅限于 for 循环块,对吗?
让我们看看输出:
Output >>>
Squares list: [0, 1, 4, 9, 16]
x after for loop: 4
我们看到 x
现在是 4,这个值是它在循环中取到的最终值,而不是我们设置的初始值 10。
现在让我们看看如果我们将 for 循环替换为列表解析表达式会发生什么:
x = 10
squares = [x ** 2 for x in range(5)]
print("Squares list:", squares)
# x is 10 here
print("x after list comprehension:", x)
在这里,x
是 10,这是我们在列表解析表达式之前设置的值:
Output >>>
Squares list: [0, 1, 4, 9, 16]
x after list comprehension: 10
为了避免意外行为:如果你在使用循环时,确保不要将循环变量命名为你稍后想要访问的其他变量相同的名称。
在 Python 中,我们使用 is
关键字来检查对象身份。这意味着它检查两个变量是否引用了内存中的同一对象。要检查相等性,我们使用 ==
操作符。对吗?
现在,启动一个 Python REPL 并运行以下代码:
>>> a = 7
>>> b = 7
>>> a == 7
True
>>> a is b
True
现在运行这个:
>>> x = 280
>>> y = 280
>>> x == y
True
>>> x is y
False
等等,为什么会发生这种情况? 好吧,这是由于 CPython 中的“整数缓存”或“驻留”。
CPython 缓存整数对象 在 -5 到 256 的范围内。这意味着每次你使用这个范围内的整数时,Python 将使用内存中的相同对象。 因此,当你使用 is
关键字比较这范围内的两个整数时,结果是 True
,因为它们指向内存中的相同对象。
这就是为什么 a is b
返回 True
。你也可以通过打印 id(a)
和 id(b)
来验证这一点。
但是,这个范围之外的整数不会被缓存。每次出现这样的整数都会在内存中创建一个新对象。
所以,当你使用 is
关键字比较两个范围之外的整数时(是的,在我们的示例中 x
和 y
都设置为 280),结果是 False
,因为它们确实是内存中的两个不同对象。
除非你尝试使用 is
来比较两个对象的相等性,否则这种行为不应成为问题。因此,总是使用 ==
操作符来检查任何两个 Python 对象是否具有相同的值。
如果你熟悉 Python 的内置数据结构,你会知道元组是 不可变 的。所以你 不能 就地修改它们。另一方面,像列表和字典这样的数据结构是 可变 的。这意味着你 可以 就地更改它们。
但是包含一个或多个可变对象的元组怎么办?
启动一个 Python REPL 并运行这个简单的示例是很有帮助的:
>>> my_tuple = ([1,2],3,4)
>>> my_tuple[0].append(3)
>>> my_tuple
([1, 2, 3], 3, 4)
这里,元组的第一个元素是一个包含两个元素的列表。我们尝试向第一个列表中添加 3,它工作得很好!好吧,我们刚刚就地修改了一个元组吗?
现在让我们尝试向列表中添加两个元素,这次使用 += 操作符:
>>> my_tuple[0] += [4,5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment</module></stdin>
是的,你会得到一个 TypeError,表示元组对象不支持项赋值。这是预期的。但让我们检查一下元组:
>>> my_tuple
([1, 2, 3, 4, 5], 3, 4)
我们看到元素 4 和 5 已经被添加到列表中!程序是否在同时抛出错误并成功?
+= 操作符在内部通过调用 __iadd__()
方法来执行就地加法,并在原地修改列表。赋值会引发 TypeError 异常,但将元素添加到列表末尾已经成功。 += 也许是最尖锐的棱角!
为了避免程序中的这种怪癖,尽量 仅 使用元组来处理不可变集合。尽可能避免将可变对象作为元组元素。
可变性一直是我们讨论中的一个反复出现的话题。所以这是另一个总结本教程的内容。
有时你可能需要创建列表的独立副本。但是,当你使用类似 list2 = list1
的语法创建副本时,其中 list1
是原始列表,会发生什么?
这是一个浅拷贝。因此,它仅复制了对原始列表元素的引用。通过浅拷贝修改元素会影响 原始列表 和 浅拷贝。
让我们看这个例子:
original_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Shallow copy of the original list
shallow_copy = original_list
# Modify the shallow copy
shallow_copy[0][0] = 100
# Print both the lists
print("Original List:", original_list)
print("Shallow Copy:", shallow_copy)
我们看到对浅拷贝的修改也影响了原始列表:
Output >>>
Original List: [[100, 2, 3], [4, 5, 6], [7, 8, 9]]
Shallow Copy: [[100, 2, 3], [4, 5, 6], [7, 8, 9]]
在这里,我们修改了浅拷贝中第一个嵌套列表的第一个元素:shallow_copy[0][0] = 100
。但我们看到修改影响了原始列表和浅拷贝。
为了避免这种情况,你可以像这样创建深拷贝:
import copy
original_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Deep copy of the original list
deep_copy = copy.deepcopy(original_list)
# Modify an element of the deep copy
deep_copy[0][0] = 100
# Print both lists
print("Original List:", original_list)
print("Deep Copy:", deep_copy)
现在,对深拷贝的任何修改都不会改变原始列表。
Output >>>
Original List: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Deep Copy: [[100, 2, 3], [4, 5, 6], [7, 8, 9]]
这就结束了!在本教程中,我们探索了 Python 中的一些奇特现象:从可变默认值的意外行为到浅拷贝列表的细微差别。这只是 Python 奇特现象的一个介绍,绝不是详尽无遗的列表。你可以在 GitHub 上找到所有的代码示例。
随着你在 Python 中编程时间的增加——并更好地理解语言——你可能会遇到更多这样的情况。所以,继续编程,继续探索!
哦,如果你想读这个教程的续集,请在评论中告诉我们。
Bala Priya C**** 是来自印度的开发人员和技术作家。她喜欢在数学、编程、数据科学和内容创作的交汇点上工作。她的兴趣和专长领域包括 DevOps、数据科学和自然语言处理。她喜欢阅读、写作、编程和喝咖啡!目前,她通过编写教程、操作指南、观点文章等方式,学习并与开发者社区分享她的知识。Bala 还创建了引人入胜的资源概述和编程教程。