Python 的魅力,很多时候藏匿于那些不经意间,不那么显眼,但一旦发现,便会让人会心一笑的小细节里。不像某些语言那么喜欢张扬自己的新特性,Python 更像是位老友,用一种润物细无声的方式,让你的编程生活变得更舒适、更高效。
这里有几个我私藏已久的、不那么广为人知,但却相当有趣的 Python 小秘密,希望能让你也感受到这份“Pythonic”的乐趣。
1. 对象的“自毁”与垃圾回收:生命周期的优雅落幕
我们写 Python 代码,经常会创建各种对象:列表、字典、字符串,甚至我们自己定义的类实例。大多数时候,我们并不需要手动去“销毁”它们。这是因为 Python 有一套内置的垃圾回收机制。
但你有没有想过,当一个对象不再被任何变量引用时,它到底是怎么“消失”的?这里有一个更深层次的细节:当一个对象的引用计数降为零时,它就会被 Python 的垃圾回收器(主要是基于引用计数)标记为“可回收”。更进一步,如果你的对象所在的类定义了一个 `__del__` 方法,那么在对象被真正回收之前,Python 会尝试调用这个 `__del__` 方法。
这个 `__del__` 方法,就像是对象在告别这个世界前最后的“遗言”。你可以用它来释放一些外部资源,比如关闭文件句柄、释放网络连接,或者做一些清理工作。
举个例子:
```python
class MyResource:
def __init__(self, name):
self.name = name
print(f"资源 '{self.name}' 被创建了。")
def __del__(self):
print(f"资源 '{self.name}' 即将消失... 正在进行清理。")
创建一个资源对象
resource1 = MyResource("数据库连接")
此时,resource1 变量引用着 MyResource 对象,引用计数为 1
移除引用
del resource1
现在,MyResource 对象没有任何变量引用了,引用计数变为 0。
Python 的垃圾回收器会在某个时刻,在你不知道的时候,调用 MyResource 对象的 __del__ 方法。
你可能会在程序结束时,或者在内存压力较大的时候看到这个输出。
另一个例子,对象可能因为作用域结束而被回收
def create_temp_resource():
temp = MyResource("临时文件")
print("临时资源已创建,即将离开作用域。")
调用函数
create_temp_resource()
当 create_temp_resource 函数执行完毕,'temp' 变量会出作用域,
它的引用计数归零,MyResource("临时文件") 对象也会被清理。
```
为什么这不显眼?
大多数时候,我们创建的对象生命周期很短,或者程序很快就结束了,我们根本不会注意到 `__del__` 的执行。而且,过度依赖 `__del__` 来管理资源是不推荐的,因为它执行的时机是不确定的,可能导致资源泄露。Python 推荐使用 `with` 语句(上下文管理器)来更可靠地管理资源。但了解 `__del__` 的存在,能让你更深入地理解 Python 对象是如何被管理和回收的,这是一种底层的美。
2. 序列的“切片”:不止步于列表和字符串
我们都知道列表和字符串可以用切片来获取一部分内容,比如 `my_list[1:5]`。但你知道吗,几乎所有 Python 的序列类型,甚至包括一些自定义的序列类型,都支持切片操作。
更妙的是,切片操作本身会返回一个新的对象,而不是修改原有的序列。而且,切片操作可以接受三个参数:`start:stop:step`。`step` 可以是负数,用来反向切片。
但隐藏在切片背后更强大的功能是:切片赋值。
你不仅可以用切片来获取数据,还可以用它来替换序列中的一部分。
```python
my_list = [1, 2, 3, 4, 5, 6, 7]
替换一部分元素
my_list[1:4] = [10, 20, 30]
print(my_list) 输出: [1, 10, 20, 30, 5, 6, 7]
插入元素,通过替换一个空切片
my_list[2:2] = [100, 200]
print(my_list) 输出: [1, 10, 100, 200, 20, 30, 5, 6, 7]
删除元素,通过替换一个空切片
my_list[3:5] = []
print(my_list) 输出: [1, 10, 100, 20, 30, 5, 6, 7]
还可以替换不同长度的切片
my_list[1:3] = [99, 88, 77, 66]
print(my_list) 输出: [1, 99, 88, 77, 66, 20, 30, 5, 6, 7]
```
为什么这不显眼?
我们通常只用切片来“读”数据,对切片赋值这个“写”的功能了解不多。它提供了一种非常 Pythonic 的方式来高效地修改序列,远比一个个元素去修改要简洁得多。尤其是在处理大型序列时,切片赋值的效率优势就更加明显了。
3. `__slots__` 的秘密:节省内存的小能手
Python 的类,默认情况下,为每个实例都维护一个 `__dict__` 属性,这个属性是一个字典,用来存储实例的属性。这非常灵活,你可以动态地给实例添加或删除属性。
但是,维护一个 `__dict__` 需要额外的内存开销。对于大量创建的、结构相对固定的对象来说,这可能会累积成不小的内存浪费。
这时候,`__slots__` 就派上用场了。当你定义了一个类的 `__slots__` 属性,并提供一个包含所有实例属性名称的元组时,Python 就不会为该类的实例创建 `__dict__`。相反,它会为每个属性分配固定的内存空间。
```python
class NoSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y') 注意这里是个元组
def __init__(self, x, y):
self.x = x
self.y = y
简单对比一下它们的大小(虽然这个不是绝对精确的内存占用,但能说明问题)
import sys
obj1 = NoSlots(1, 2)
obj2 = WithSlots(1, 2)
print(f"NoSlots 实例大小: {sys.getsizeof(obj1)}")
print(f"WithSlots 实例大小: {sys.getsizeof(obj2)}")
你会发现 WithSlots 的实例要小得多
尝试给 WithSlots 实例添加一个不在 __slots__ 中的属性,会报错
try:
obj2.z = 3
except AttributeError as e:
print(f"尝试给WithSlots添加新属性失败: {e}")
```
为什么这不显眼?
`__slots__` 是一个比较底层的优化手段。大多数开发者在刚开始学习 Python 时,不太会去关注实例的内存占用,更关注代码的易读性和功能实现。而且,使用 `__slots__` 会牺牲掉一些灵活性(比如不能动态添加新属性),所以并非所有类都适合使用它。但对于需要创建大量对象的场景,比如数据处理、模拟仿真等,`__slots__` 是一个非常有用的技巧,可以显著降低内存开销。
4. `collections.defaultdict`:懒惰初始化的小魔法
在很多时候,我们会需要一个字典,当访问一个不存在的键时,它能自动创建一个默认值,而不是抛出 `KeyError`。例如,统计词频的时候,如果一个词第一次出现,我们希望它的计数是0,然后加1。
通常我们会这样做:
```python
word_counts = {}
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
for word in words:
if word not in word_counts:
word_counts[word] = 0
word_counts[word] += 1
print(word_counts) {'apple': 3, 'banana': 2, 'orange': 1}
```
这个 `if word not in word_counts:` 的检查,虽然很常见,但写起来有点冗余。
这时候,`collections.defaultdict` 就闪亮登场了:
```python
from collections import defaultdict
word_counts_dd = defaultdict(int) int() 会返回 0,所以默认值是 0
或者 defaultdict(list) 会在键不存在时,返回一个空列表 []
for word in words:
word_counts_dd[word] += 1
print(word_counts_dd) defaultdict(, {'apple': 3, 'banana': 2, 'orange': 1})
```
你看,`defaultdict` 替我们完成了那个“如果不存在就创建默认值”的操作。当你访问 `word_counts_dd["grape"]` 时(如果 "grape" 不存在),它会先调用 `int()`,得到 `0`,然后赋值给 `word_counts_dd["grape"]`,最后 `+= 1` 才会执行,所以结果是 `1`。
为什么这不显眼?
`defaultdict` 包含在 `collections` 模块里,这个模块本身就有很多实用的数据结构,容易被一些更核心的特性(如列表、字典、集合)的光芒所掩盖。而且,对于一些简单场景,直接用 `if` 检查也能实现功能。但一旦你习惯了 `defaultdict`,你会发现它能极大地简化很多计数、分组、集合化等数据处理的代码,让你的代码更简洁、更易读。
这些小细节,就像是 Python 语言里那些埋藏在深处的宝藏,不时地给你带来惊喜。它们不是那些能够写出华丽特效的“大招”,而是让你在日常使用中,感受到一种顺畅、优雅和高效,一种“哦,原来是这样”的会心一笑。