让你的 Python 代码更优雅的实用小技巧

本文分享 11 个实用的 Python 代码技巧,包括使用 enumerate、理解赋值机制、F-strings、生成器、dict.get()、zip()、列表推导式、defaultdict、列表去重和 str.join() 等,帮助你编写简洁高效的 Python 代码。

enumerate() 获取索引和值

在遍历列表时,我们经常需要同时拿到元素的索引和值。很多人会习惯性地写出

for i in range(len(my_list))
    print(i, my_list[i])

这种写法可以工作,但它不够直观。你需要通过 my_list[i] 这种间接的方式来访问元素,可读性稍差。

Python 内置的 enumerate() 函数为此提供了完美的解决方案。它会将一个可迭代对象 (如列表) 包装成一个枚举对象,在每次迭代时,同时返回索引和对应的值。

In [1]:
my_list = ["A", "B", "C"]
for index, value in enumerate(my_list):
    print(index, value)
0 A
1 B
2 C

这样做的好处很明显:代码的意图更加清晰。enumerate() 直接告诉读代码的人:“我需要索引和值”。这比 range(len(my_list)) 的方式要 Pythonic 得多。

理解 Python 的赋值:别名与副本

这是一个非常基础但极其重要的概念,很多 bug 的根源就在于此。在 Python 中,变量赋值 (特别是对可变类型如列表、字典) 实际上是“贴标签”,而不是“复制内容”。

看下面的例子:

In [2]:
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(id(a), id(b))
[1, 2, 3, 4]
4452752320 4452752320

输出结果:[1, 2, 3, 4] 以及两个相同的内存地址。id() 函数返回对象的内存地址。可以看到,ab 指向的是同一个内存地址,它们是同一个列表对象的两个名字 (别名)。因此,修改 b 就等于修改 a

如果你想创建一个独立的副本,而不是别名,可以使用切片 a[:] 或者 a.copy() 方法:

In [3]:
a = [1, 2, 3]
b = a.copy()  # 或者 b = a[:]
b.append(4)
print(a)
[1, 2, 3]

格式化字符串,F-strings 是你的首选

拼接字符串是日常操作。从 Python 3.6 开始,f-strings (格式化字符串字面量) 提供了一种非常简洁和高效的方式。

In [4]:
name = "Alice"
age = 30
print(f"Hello, my name is {name} and I'm {age} years old.")
Hello, my name is Alice and I'm 30 years old.

相比于老的 str.format() 方法或者 % 操作符,f-strings 的可读性更高,因为变量直接嵌入在字符串中,一目了然。而且,它的性能通常也是最好的。

使用生成器节省内存

当你处理大量数据时,内存占用是个不得不考虑的问题。列表推导式会一次性生成所有元素并放入内存,如果数据量巨大,可能会导致内存溢出。

生成器表达式则不同,它的语法和列表推导式类似,只是把方括号 [] 换成了圆括号 ()

In [5]:
# 列表推导式:创建完整列表,占用内存大
large_list = [x * 2 for x in range(10**6)]
print(large_list.__sizeof__())  # 输出列表占用的字节数 (很大)

# 生成器表达式:不立即生成所有元素,内存占用小
generator = (x * 2 for x in range(10**6))
print(generator.__sizeof__())  # 输出生成器对象本身占用的字节数 (很小)
8448712
184

输出结果可能类似这样 (具体数值取决于系统):8448712 (列表) 和 184 (生成器)。可以看到,生成器本身只占用极小的内存。它是一个懒加载的迭代器,只有在你向它请求下一个元素时,它才会去计算和生成这个元素。对于求和、遍历等操作,生成器和列表的行为结果一致,但内存效率天差地别。

但要注意,生成器只能被完整遍历一次。一旦耗尽,它就空了。

In [6]:
g = (x for x in range(3))
print(list(g))  # [0, 1, 2]
print(list(g))  # [] (已经耗尽)
[0, 1, 2]
[]

这是一个常见的 bug 来源。

安全地访问字典:dict.get()

直接用 d[key] 的方式访问字典,如果键 (key) 不存在,程序会立即抛出 KeyError 异常并中断。

更稳妥的做法是使用 dict.get(key, default) 方法。它允许你指定一个默认值,当键不存在时,会返回这个默认值,而不是报错。

In [7]:
person = {"name": "Alice", "age": 30}
# 安全获取 'city',不存在则返回 'Unknown'
city = person.get("city", "Unknown")
print(city)
Unknown

这让我们的代码健壮性更强,避免了不必要的 try-except 块。

zip() 并行遍历多个序列

如果你有两个或多个长度相同的列表,需要将它们的元素一一对应起来处理,zip() 函数是最佳选择。

In [8]:
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25, 35]

for name, age in zip(names, ages):
    print(f"{name} is {age} years old.")
Alice is 30 years old.
Bob is 25 years old.
Charlie is 35 years old.

zip() 会将多个列表像拉链一样合并起来,每次迭代返回一个包含各个列表对应元素的元组。这比使用索引 for i in range(len(names)): print(names[i], ages[i]) 要简洁和清晰得多。

zip() 会以最短的序列为准进行配对。

列表推导式:一行代码实现循环和判断

列表推导式是 Python 的特色之一,它能用非常简洁的一行代码来创建列表,并且可以结合条件语句,实现筛选和转换。

场景 1:筛选元素

假设我们只想保留列表中的偶数:

In [9]:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [x for x in numbers if x % 2 == 0]
print(even_numbers)
[2, 4, 6, 8, 10]

[x for x in numbers if x% 2 == 0] 这一行代码就完成了循环和判断筛选,非常紧凑。

场景 2:条件转换

假设我们想对列表进行处理:如果是偶数,就将它乘以 2;如果是奇数,保持不变。

In [10]:
numbers = [1, 2, 3, 4, 5]
processed = [x * 2 if x % 2 == 0 else x for x in numbers]
print(processed)
[1, 4, 3, 8, 5]

注意,这种带 else 的条件判断要写在 for x in numbers 循环的前面。

collections.defaultdict 简化计数和分组

在统计词频或对数据进行分组时,我们通常需要先检查字典中是否已存在某个键,如果不存在,则需要先初始化一个值 (比如 0 或空列表)。

In [11]:
words = "a b c a a b".split(" ")
In [12]:
# 普通字典的繁琐写法
word_counts = {}
for word in words:
    if word not in word_counts:
        word_counts[word] = 0  # 初始化
    word_counts[word] += 1
word_counts
Out[12]:
{'a': 3, 'b': 2, 'c': 1}

使用 collections.defaultdict 后,当你第一次访问一个不存在的键时,defaultdict 会自动为你创建一个键,并将其值初始化为 default_factory 的结果,也就是 int() (即 0)。这样你就可以直接进行 += 1 操作,代码瞬间清爽了不少。

In [13]:
from collections import defaultdict

word_counts = defaultdict(int)  # 键不存在时自动初始化为 0
for word in words:
    word_counts[word] += 1  # 无需手动初始化
print(word_counts)
# 分组示例 (初始化为空列表)
items = [("A", "Apple"), ("B", "Banana"), ("B", "Blueberry")]
groups = defaultdict(list)
for key, value in items:
    groups[key].append(value)  # 无需手动初始化列表
print(groups)
defaultdict(<class'int'>, {'a': 3, 'b': 2, 'c': 1})
defaultdict(<class'list'>, {'A': ['Apple'], 'B': ['Banana', 'Blueberry']})

列表去重的高效方法

要从列表中移除重复的元素,最简单、最快速的方法是利用 set 数据结构的特性——元素唯一。

In [14]:
numbers = [1, 2, 2, 3, 4, 4, 5]
unique_numbers = list(set(numbers))
print(unique_numbers)
[1, 2, 3, 4, 5]

需要注意的是,set 是无序的。在 Python 3.7 及以上版本,dict 的实现会保留插入顺序,但在旧版本中顺序会被打乱。如果需要保持原有的顺序,可以使用 dict.fromkeys()

In [15]:
numbers = [1, 2, 2, 3, 4, 4, 5]
unique_numbers = list(dict.fromkeys(numbers))  # 利用 dict 键的唯一性和插入顺序
print(unique_numbers)
[1, 2, 3, 4, 5]

拼接字符串的正确姿势:str.join()

当需要将一个字符串列表拼接成一个长字符串时,新手可能会用 for 循环相加。这种方式效率很低,因为字符串是不可变对象,每次 += 操作都会创建一个新的字符串对象。

In [16]:
# 低效的方式
words = ["Hello", "world", "!"]
result = ""
for word in words:
    result += word  # 每次都创建新字符串

正确且高效的做法是使用字符串的 join() 方法。

In [17]:
words = ["Hello", "world", "!"]
result = " ".join(words)  # 用空格连接
print(result)
Hello world!

join() 方法会一次性计算出最终字符串所需的总长度,然后只进行一次内存分配,效率远高于循环相加。

一行代码反转字典

有时候,我们需要将字典的键和值互换。利用字典推导式,可以一行代码搞定。

In [18]:
original = {"a": 1, "b": 2, "c": 3}
reversed_dict = {v: k for k, v in original.items()}
print(reversed_dict)
{1: 'a', 2: 'b', 3: 'c'}

这里有一个重要的前提:原始字典中的值 (value) 必须是唯一的。如果有重复的值,反转后后面的键值对会覆盖前面的,因为字典的键不能重复。

In [19]:
original = {"a": 1, "b": 1}
reversed_dict = {v: k for k, v in original.items()}
print(reversed_dict)
{1: 'b'}

相关推荐