Courses
近年来,对 Python 编码技能的需求不断上升。为帮助您提升 Python 编程能力,我们精选了 30 个酷炫的 Python 技巧,助您改进代码。接下来 30 天里每天学一个,并查看我们的Python 最佳实践,确保您的代码达到一流水平。
如果您的 Python 基础还不够扎实,也可以通过我们的 Python 技能路径来强化。
序列与数据结构技巧
#1 切片
a = "Hello World!"
print(a[::-1])
"""
!dlroW olleH
"""
切片是 Python 中基于索引以访问序列子集的特性。索引即元素在序列中的位置。如果序列类型是可变的,您可以用切片来提取并修改数据。
注意:我们也可以对不可变序列使用切片,但尝试修改切片会引发 TypeError。
切片的实现格式为:sequence[start:stop:step]。如果 start、stop 和 step 未指定,则采用默认值。默认值为:
- “start” 默认为 0
- “stop” 默认为序列长度
- “step” 默认为 1
当提供 sequence[start:stop] 时,返回的元素从起始索引到 stop - 1(不包含 stop 索引)。
我们还可以传入负索引,用于反转序列。例如,在一个包含 4 个元素的列表中,索引 0 也可写作 -4,最后一个索引也可写作 -1。在上面的示例代码中,这一知识点被用于序列的 step 参数,因此字符串被从序列末尾到索引 0 反向打印。
#2 原地交换 / 同时赋值
a = 10
b = 5
print(f"First: {a, b}")
"""
First: (10, 5)
"""
a, b = b, a + 2
print(f"Second: {a, b}")
"""
Second: (5, 12)
"""
如果您最初认为 b 的值会是 7 而不是 12,那就落入了原地交换的“陷阱”。
在 Python 中,我们可以使用自动解包在一次赋值中将可迭代对象解包到多个变量。例如:
a, b, c = [1, 2, 3]
print(a)
print(b)
print(c)
"""
1
2
3
"""
我们也可以用 * 收集多个值到一个变量中——这种技巧称为打包。下面是打包的示例。
a, *b = 1, 2, 3
print(a, b)
"""
1 [2, 3]
"""
结合自动打包与解包,就产生了同时赋值(simultaneous assignment)。我们可以用它将一系列值赋给一系列变量。
#3 列表 vs. 元组
import sys
a = [1, 2, 3, 4, 5]
b = (1, 2, 3, 4, 5)
print(f"List size: {sys.getsizeof(a)} bytes")
print(f"Tuple size: {sys.getsizeof(b)} bytes")
"""
List size: 52 bytes
Tuple size: 40 bytes
"""
大多数 Python 程序员都熟悉列表数据结构,但对元组未必那么了解。二者都是可迭代的,支持索引,并允许存储不同数据类型。但在某些场景下,优先使用元组会更好。
首先,列表是可变的,也就是说我们可以随意修改:
a = [1,2,3,4,5]
a[2] = 8
print(a)
"""
[1,2,8,4,5]
"""
另一方面,元组是不可变的,尝试修改会引发 TypeError。
因此,元组更省内存,因为 Python 可以为数据分配恰当的内存块;而列表为了可能的扩展需要预留额外内存——这称为动态内存分配。
总结:当您不希望数据被更改时,出于内存方面的考虑,元组应优先于列表。元组也比列表更快。
在本教程中进一步了解 Python 数据结构。
#4 生成器
a = [x * 2 for x in range(10)]
b = (x * 2 for x in range(10))
print(a)
print(b)
"""
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
<generator object <genexpr> at 0x7f61f8808b50>
"""
列表推导式是 Pythonic 的从另一个可迭代对象创建列表的方式——比 for 循环快得多。但如果您不小心把方括号 [] 换成了圆括号 () 会怎样?您会得到一个生成器对象。
在 Python 中,带有推导逻辑的圆括号会创建生成器对象。生成器是一种特殊的可迭代对象。不同于列表,它们不存储元素,而是存储按顺序生成每个元素的指令以及当前迭代状态。
每个元素仅在请求时生成,这称为惰性求值。使用生成器的主要好处是占用更少内存,因为不会一次性构建整个序列。
#5 别名
a = [1, 2, 3, 4 ,5]
b = a
# Change the 4th index in b
b[4] = 7
print(id(a))
print(id(b))
print(a) # Remember we did not explicitly make changes to a.
"""
15136008
15136008
[1, 2, 3, 4, 7]
"""
Python 是面向对象的编程语言——万物皆对象。因此,将对象赋给标识符就是创建对该对象的引用。
当我们把一个标识符赋给另一个标识符时,会得到两个引用同一对象的标识符。这称为别名(aliasing)。对一个别名的修改会影响另一个。有时这种行为是需要的,但它也常常让人猝不及防。
一种规避方式是在使用可变对象时避免别名。另一种方案是创建原始对象的克隆而非引用。
最简单的克隆方法是利用切片:
b = a[:]
这会在标识符 b 中创建一个指向新列表对象的引用。
您还可以使用很多其他办法,比如在赋值时调用 list(a),或使用 copy() 方法。
#6 ‘not’ 运算符
a = []
print(not a)
"""
True
"""
下一个 Python 小技巧是使用 not 运算符来检查数据结构是否为空,这是最简单的方式。Python 内置的 not 是逻辑运算符,如果表达式不为真则返回 True,否则返回 False——它会反转布尔表达式和对象的真值。
另一种常见用法是在 if 语句中:
if not a:
# do something...
当 a 为 True 时,not 运算符会返回 False,反之亦然。
刚开始可能有点绕,试着动手试试吧。
字符串与输出技巧
#7 F-strings
first_name = "John"
age = 19
print(f"Hi, I'm {first_name} and I'm {age} years old!")
"""
Hi, I'm John and I'm 19 years old!
"""
有时我们需要格式化字符串对象;Python 3.6 引入了一个很酷的特性 f-string,使这一过程更简单。若先了解新特性之前的格式化方式,会更能体会这种方法的优势。
过去字符串的格式化方式如下:
first_name = "John"
age = 19
print("Hi, I'm {} and I'm {} years old!".format(first_name, age))
"""
Hi, I'm John and I'm 19 years old!
"""
本质上,新方法更快、更易读、更简洁,也更不容易出错。
f-string 的另一个用法是在打印时同时显示标识符名和值。此功能在 Python 3.8 引入。
x = 10
y = 20
print(f"{x = }, {y = }")
"""
x = 10, y = 20
"""
查看这篇教程以进一步了解 Python 中的 f-string 格式化。
#8 print() 函数的 ‘end’ 参数
languages = ["english", "french", "spanish", "german", "twi"]
print(' '.join(languages))
"""
english french spanish german twi
"""
我们经常在不设置任何可选参数的情况下使用 print 语句。因此,很多 Python 用户并不知道可以在一定程度上控制输出。
我们可以更改的一个可选参数是 end。end 参数指定在一次 print 调用的末尾应显示什么。
end 的默认值是 "\n",告诉 Python 另起一行。在上述代码中,我们把它改成了空格。因此,列表的所有元素会在同一行打印。
#9 向元组“追加”
a = (1, 2, [1, 2, 3])
a[2].append(4)
print(a)
"""
(1, 2, [1, 2, 3, 4])
"""
我们已经知道元组是不可变的——参见 Python 技巧 #3 列表 vs. 元组。尝试改变元组的状态会抛出 TypeError。但是,如果把元组对象看作是一个名称序列,这些名称与对象的绑定不可更改,您可能会有不同的认识。
我们元组的前两个元素是整数——不可变。最后一个元素是列表,列表在 Python 中是可变对象。
如果我们把这个列表视为序列中的另一个名称,与某个对象绑定但绑定本身不可变,那么就会意识到列表仍可在元组内部被修改。
我们会建议您在实践中这么做吗?大概不会,但这是一个值得知道的小知识!
#10 合并字典
a = {"a": 1, "b": 2}
b = {"c": 3, "d": 4}
a_and_b = a | b
print(a_and_b)
"""
{"a": 1, "b": 2, "c": 3, "d": 4}
"""
在 Python 3.9 及以上版本,可以使用 |(按位或)来合并字典。对此技巧也没太多可说的——就是更易读!
代码风格与语法技巧
#11 三元运算符 / 条件表达式
condition = True
name = "John" if condition else "Doe"
print(name)
"""
John
"""
上面的代码展示了所谓的三元运算符——也称条件表达式。我们用它来根据条件是 True 还是 False 来进行判断。
另一种写法如下:
condition = True
if condition:
name = "John"
else:
name = "Doe"
print(name)
"""
John
"""
虽然两段代码输出相同,但请注意三元条件允许我们写出更短、更清晰的代码。这就是 Pythonistas 口中的更“Pythonic”的写法。
#12 去除列表重复项
a = [1, 1, 2, 3, 4, 5, 5, 5, 6, 7, 2, 2]
print(list(set(a)))
"""
[1, 2, 3, 4, 5, 6, 7]
"""
去除列表中重复元素最简单的方式是将列表转换为集合(需要的话再转回列表)。
从可变性角度看,集合与列表在 Python 中很相似。我们都可以随意添加和移除元素,但二者仍有很大不同。
列表是有序的、从零开始索引且可变。集合是无序且无索引的。集合中的元素必须是不可变类型,尽管集合本身是可变的——尝试通过索引检索或修改元素会引发错误。
集合与列表的另一个关键区别是集合不允许重复。这正是我们能移除重复元素的原因。
#13 独立下划线
>>> print(_)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
>>> 1 + 2
3
>>> print(_)
3
下划线(_)在 Python 中是合法标识符,因此可以用来引用对象。但它还有另一项职责:存储上一次求值的结果。
文档指出:“交互式解释器会将上一次求值的结果存放在变量 _ 中。(它存放在 builtins 模块中,与内置函数 print 并列。)”
由于我们首次调用时并未给下划线赋值,因此报错。然而,当我们计算 1 + 2 的结果后,交互式解释器就把结果存进了标识符 _ 。
#14 用下划线忽略值
for _ in range(100):
print("The index doesn't matter")
"""
The index doesn't matter
The index doesn't matter
...
"""
在技巧 #13 中,我们了解到交互式解释器会把上一次求值的结果放进下划线(_)标识符,但这并非它唯一的用处。
我们也可以用它来表示不关心或稍后不会使用的对象。这很重要,因为如果使用了普通标识符而不是下划线(_),在对程序进行 lint 时会触发 F841 错误。F841 错误表示为局部变量赋了值却未使用,这是一种不良实践。
#15 结尾下划线
list_ = [0, 1, 2, 3, 4]
global_ = "Hi there"
延续前两个关于下划线(_)的技巧,另一个用途是避免与 Python 关键字冲突。
PEP 8 提到,结尾下划线(_)应“按照约定用于避免与 Python 关键字冲突”。它还指出,“通常最好在末尾添加单一下划线,而不是使用缩写或拼写变体。因此 list_ 优于 lst。”
#16 前置下划线
class Example:
def __init__(self):
self._internal = 2
self.external = 20
您会经常看到资深 Python 程序员在标识符或方法名前加下划线——这有充分理由。
给标识符或方法加前置下划线有隐藏含义:该变量或方法仅用于内部使用。基本上,这是一种对其他程序员的提示,写入在 PEP 8 中,但不会被 Python 强制执行。因此,前置下划线只是弱指示。
不同于 Java,Python 并没有强烈区分私有与公共变量。换言之,它的意义来自社区共识,而非语言强制。它们的存在不会影响程序行为。
#17 下划线的可视化用法
这是关于下划线的最后一个提示;到目前为止,我们已覆盖了三种用法,您也可以查看我们的教程,进一步了解 Python 中下划线(_)的作用。
number = 1_500_000
print(number)
"""
15000000
"""
我们还可以把下划线作为整型、浮点型和复数字面量中的可视化分隔符用于分组显示——此特性在 Python 3.6 引入。
这一想法旨在提高长数字面量或应被清晰分段的字面量的可读性——您可以在 PEP 515 中了解更多。
代码风格与语法技巧
#18 __name__ == “__main__”
if __name__ == "__main__":
print("Read on to understand what is going on when you do this.")
"""
print("Read on to understand what is going on when you do this.")
"""
您很可能在很多 Python 程序中见过这种语法;Python 使用一个特殊的名称 "__main__",当当前运行的 Python 文件是主程序时,会把它赋给标识符 __name__。
如果我们决定将截图中的模块(Python 文件)导入到另一个模块并运行该文件,那么代码中表达式的真假将变为假。这是因为当我们从另一个模块导入时,__name__ 会被设置为模块(Python 文件)的名称。
#19 ‘setdefault’ 方法
import pprint
text = "It's the first of April. It's still cold in the UK. But I'm going to the museum so it should be a wonderful day"
counts = {}
for word in text.split():
counts.setdefault(word, 0)
counts[word] += 1
pprint.pprint(counts)
"""
{'April.': 1,
'But': 1,
"I'm": 1,
"It's": 2,
'UK.': 1,
'a': 1,
'be': 1,
'cold': 1,
'day': 1,
'first': 1,
'going': 1,
'in': 1,
'it': 1,
'museum': 1,
'of': 1,
'should': 1,
'so': 1,
'still': 1,
'the': 3,
'to': 1,
'wonderful': 1}
"""
您可能希望为字典中的多个键设置值,例如在统计语料中的词频时。常见做法如下:
- 检查键是否存在于字典中
- 若存在,将值加 1。
- 若不存在,添加该键并将值设为 1。
对应代码如下:
counts = {}
for word in text.split():
if word in counts:
counts[word] += 1
else:
counts[word] = 1
更简洁的方式是对字典对象使用 setdefault() 方法。
传入该方法的第一个参数是要检查的键。第二个参数是在键不存在时要设置的默认值——如果键已存在,则方法会返回键的当前值,因此不会改变它。
程序结构技巧
#20 匹配正则
import re
number = re.compile(r"(0)?(\+44)?\d{10}")
num_1 = number.search("My number is +447999999999")
num_2 = number.search("My number is 07999999999")
print(num_1.group())
print(num_2.group())
"""
'+447999999999'
'07999999999'
"""
正则表达式允许您指定要搜索的文本模式;大多数人知道可以用 CTRL + F(Windows)来搜索,但如果您并不确定要找的具体内容,如何找到它?答案是搜索模式。
例如,英国号码遵循相似的模式:它们要么以 0 开头后跟十位数字,要么以 +44 代替 0 后跟十位数字——第二种情况表示国际格式。
正则表达式能大幅节省时间。如果不用正则,而是写规则去捕捉图中的这些情况,可能需要 10 多行代码。
即使您不写代码,了解正则表达式的工作原理也很重要。大多数现代文本编辑器和文字处理器都允许在“查找替换”中使用正则。
#21 正则管道符
import re
heros = re.compile(r"Super(man|woman|human)")
h1 = heros.search("This will find Superman")
h2 = heros.search("This will find Superwoman")
h3 = heros.search("This will find Superhuman")
print(h1.group())
print(h2.group())
print(h3.group())
"""
Superman
Superwoman
Superhuman
"""
正则表达式有一个特殊字符叫管道符(|),用于匹配多个表达式中的其一,且可用于任意位置。这在存在多个相似模式时非常有用。
例如,“Superman”“Superwoman”“Superhuman”拥有相同前缀。因此,您可以利用管道符保留重复的部分,仅替换不同的部分。再次为您节省宝贵时间。
注意坑点:如果您要匹配的所有表达式都出现在同一段文本中,将返回第一个匹配到的片段——例如 “An example text containing Superwoman, Superman, Superhuman” 会返回 Superwoman。
#22 print() 函数的 ‘sep’ 参数
day = "04"
month = "10"
year = "2022"
print(day, month, year)
print(day, month, year, sep = "")
print(day, month, year, sep = ".")
"""
04 10 2022
04/10/2022
04.10.2022
"""
对 print() 函数的全部能力不了解的 Python 程序员数量之多令人惊讶;如果“Hello World”是您的第一个程序,那么 print() 可能是您学习 Python 时接触的第一个内置函数。我们用 print() 在屏幕上显示格式化消息,但它的功能远不止于此。
在上面的代码中,我们展示了不同的格式化输出方式。sep 参数是 print() 的可选参数,用于指定当传入多个对象时它们之间如何分隔。
默认是用空格分隔,但我们通过不同的 print 语句改变了这一行为——一个将 sep 设为 "",另一个将 sep 设为 "."。
#23 Lambda 函数
def square(num:int) -> int:
return num ** 2
print(f"Function call: {square(4)}")
"""
Function call: 16
"""
square_lambda = lambda x: x**2
print(f"Lambda function: {square_lambda(4)}")
"""
Lambda functional: 16
"""
Lambda 函数会把您带到 Python 更偏中高级的玩法——通过这门课程学习中级 Python。它们初看复杂,其实很简单。
在示例代码中我们只用了一个参数,但若需要也可以使用多个:
square = lambda a, b: a ** b
print(f"Lambda function: {square(4, 2)}")
"""
16
"""
本质上,lambda 关键字允许我们用一行代码创建小型、受限、匿名的函数。它们的行为与用 def 声明的常规函数一致,只是这些函数没有名字。
#24 ‘swapcase’ 方法
string = "SoMe RaNDoM sTriNg"
print(string.swapcase())
"""
sOmE rAndOm StRInG
"""
swapcase() 方法可用于字符串对象,将大写字母转小写、将小写字母转大写,且一行代码即可。此方法用例不多,但值得了解。
#25 ‘isalnum’ 方法
password = "ABCabc123"
print(password.isalnum())
"""
True
"""
假设我们要写一个程序,要求用户输入的密码必须包含字母和数字的组合。我们可以通过在字符串实例上调用 isalnum() 一行实现。
该方法会检查所有字符是否均为字母(A-Za-z)或数字(0-9)。若包含空格或符号(!#%$&? 等),则返回 False。
#26 异常处理
def get_ration(x:int, y:int) -> int:
try:
ratio = x/y
except ZeroDivisionError:
y = y + 1
ratio = x/y
return ratio
print(get_ration(x=400, y=0))
"""
400.0
"""
Python 程序在遇到错误时会终止。
有时我们并不希望出现这种行为,比如当有终端用户与我们的代码交互时。如果在这种情况下程序过早终止,那会很糟糕。
关于如何处理异常情况,有几种思路。多数 Python 程序员通常秉持“请求原谅比事先获得许可更容易”的理念。这意味着他们更倾向于通过提供可处理异常的上下文来捕获已引发的错误。背后的思路是:没必要浪费时间试图防范所有可能的异常情况。
但这只在出现问题后有应对机制时才成立。
#27 识别列表差异
list_1 = [1, 3, 5, 7, 8]
list_2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
solution_1 = list(set(list_2) - set(list_1))
solution_2 = list(set(list_1) ^ set(list_2))
solution_3 = list(set(list_1).symmetric_difference(set(list_2)))
print(f"Solution 1: {solution_1}")
print(f"Solution 2: {solution_2}")
print(f"Solution 3: {solution_3}")
"""
Solution 1: [9, 2, 4, 6]
Solution 2: [2, 4, 6, 9]
Solution 3: [2, 4, 6, 9]
"""
这里展示了在 Python 中比较两个列表差异的三种方法。
注意:除非您确定 list_1 是 list_2 的子集,否则解法 1 与另外两个解法并不相同。
#28 Args 与 kwargs
def some_function(*args, **kwargs):
print(f"Args: {args}")
print(f"Kwargs: {kwargs}")
some_function(1, 2, 3, a=4, b=5, c=6)
"""
Args: (1, 2, 3)
Kwargs: {'a': 4, 'b': 5, 'c': 6}
"""
当我们不确定函数应接收多少个参数时,会在函数参数中使用 *args 和 **kwargs。
*args 允许在参数是非关键字形式(即无需关联名称)时向函数传入可变数量的参数。另一方面,**kwargs 允许向函数传入任意数量的关键字参数。
实际上,*args 与 **kwargs 这两个词本身并不神奇:真正的魔力在星号(*)。这意味着星号后可以使用任意单词,但使用 args 和 kwargs 是通行惯例,并被 Python 开发者广泛遵循。
#29 省略号
print(...)
"""
Ellipsis
"""
def some_function():
...
# Alternative solution
def another_function():
pass
省略号(Ellipsis)是一个 Python 对象,可以通过三个点(...)或直接调用对象(Ellipsis)得到。
它最著名的用途是在 NumPy 中访问和切片多维数组,例如:
import numpy as np
arr = np.array([[2,3], [1,2], [9,8]])
print(arr[...,0])
"""
[2 1 9]
"""
print(arr[...])
"""
[[2 3]
[1 2]
[9 8]]
"""
省略号的另一用法是作为未实现函数中的占位符。
这意味着您可以使用 Ellipsis、... 或 pass,它们都有效。
#30 列表推导式
even_numbers = [x for x in range(10) if x % 2 == 0 and x != 0]
print(even_numbers)
"""
[2, 4, 6, 8]
"""
最后一个 Python 技巧是列表推导式,这是一种优雅地从另一个序列创建列表的方式。它允许您执行复杂的逻辑与过滤,就像我们在上面的代码中所做的那样。
也有其他方式实现同样的目标;例如,我们可以像下面这样使用 lambda 函数:
even_numbers = list(filter(lambda x: x % 2 ==0 and x != 0, range(10)))
print(even_numbers)
"""
[0, 2, 4, 6, 8]
"""
但许多 Pythonistas 会认为这种方案的可读性明显不如列表推导式。
FAQs
对初学者来说,最有用的 Python 技巧是什么?
对初学者而言,F-string 可能是最立竿见影的。与旧方法如.format()相比,它让字符串格式化更快、更易读,也更不易出错。
在 Python 中,交换两个变量的最快方法是什么?
使用同时赋值:a, b = b, a。不需要临时变量——Python 会在赋值前先完整求值右侧。
如何用一行代码合并两个字典?
在 Python 3.9+ 中,使用 | 运算符:merged = dict_a | dict_b。在更早的版本中,使用 {**dict_a, **dict_b}。
*args 与 **kwargs 有何区别?
*args 将额外的位置参数收集为一个元组。**kwargs 将额外的关键字参数收集为一个字典。关键在于 * 和 ** 运算符,而不是名称本身——它们可以被命名为任意词。
如何在 Python 中检查列表是否为空?
使用 not 运算符:if not my_list:。这比检查 len(my_list) == 0 更 Pythonic。
列表推导式与生成器有什么区别?
列表推导式(方括号)会一次性在内存中构建整个列表。生成器(圆括号)按需一次产生一个值,对于大型序列会大幅节省内存。
何时应使用三元运算符?
用于简单的一行条件语句,且两个分支都简短易读时:name = "John" if condition else "Doe"。更复杂的逻辑则用常规的 if/else 更清晰。
去除列表重复项的最简单方法是什么?
用 set() 包一层再转回:list(set(my_list))。注意集合是无序的,原有顺序不会被保留。