Python 是一种计算机编程语言。计算机编程语言和我们日常使用的自然语言有所不同,最大的区别就是,自然语言在不同的语境下有不同的理解,而计算机要根据编程语言执行任务,就必须保证编程语言写出的程序决不能有歧义,所以,任何一种编程语言都有自己的一套语法,编译器或者解释器就是负责把符合语法的程序代码转换成 CPU 能够执行的机器码,然后执行。Python 也不例外。


基础语法简介

Python的语法比较简单,采用缩进方式,写出来的代码就像下面的样子:

# print absolute value of an integer:
a = 100
if a >= 0:
    print(a)
else:
    print(-a)

# 开头的语句是注释,注释是给人看的,可以是任意内容,解释器会忽略掉注释。其他每一行都是一个语句,当语句以冒号 : 结尾时,缩进的语句视为代码块。

这种用缩进来组织代码结构的方式有利有弊。

好处是强迫你写出格式化的代码,但没有规定缩进是几个空格还是 Tab。按照约定俗成的惯例,应该始终坚持使用 4 个空格的缩进。缩进的另一个好处是强迫你写出缩进较少的代码,你会倾向于把一段很长的代码拆分成若干函数,从而得到缩进较少的代码。缩进的坏处就是 “复制-粘贴” 功能失效了,这是最坑爹的地方。当你重构代码时,粘贴过去的代码必须重新检查缩进是否正确。此外,IDE 很难像格式化 Java 代码那样格式化 Python 代码。

最后,务必注意,Python 程序是大小写敏感的,如果写错了大小写,程序会报错。


数据类型和变量

计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值。但是,计算机能处理的远不止数值,还可以处理文本、图形、音频、视频、网页等各种各样的数据,不同的数据,需要定义不同的数据类型。在 Python 中,能够直接处理的数据类型有以下几种:整数(Int)、浮点数(Float)、字符串(String)、布尔值(Boolean)、空对象(None)


基本数据类型

Int - 整数

python 可以处理任意大小的整数,当然包括负整数。(这里的任意大小指的是没有大小限制,某些语言的整数根据其存储长度是有大小限制的。例如 Java 对 32 位整数的范围限制在「-2147483648」~「2147483647」)。整数在程序中的表示方法和数学上的写法一模一样,例如:1100-80800,等等。由于计算机使用二进制,有时候用十六进制表示整数比较方便 (例如:0xff000xa5b4c3d2 等等)。

对于很大的数,例如 10000000000,python 允许在数字中间以 _ 分隔,因此,写成 10_000_000_00010000000000 是完全一样的。这种记录方式不限制数字的具体类型,十六进制数也可以写成 0xa1b2_c3d4

Float - 浮点数

浮点数也就是小数,如 1.233.14-9.01 等等。之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是浮动可变的,比如,1.23×10⁹12.3×10⁸ 是完全相等的。

对于很大或很小的浮点数,就必须用科学计数法表示,把科学计数法中的 10e 替代,1.23×10⁹ 就是 1.23e9(或者 12.3e8),0.000012可以写成 1.2e-5,等等。整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的(除法难道也是精确的?是的!),而浮点数运算则可能会有四舍五入的误差。

另外,python 的浮点数没有大小限制,但是超出一定范围就直接表示为 inf (无限大)。

String - 字符串

字符串是以单引号 ' 或双引号 " 括起来的任意文本,比如 'abc'"xyz" 等等。如果 ' 本身也是一个字符,那就可以用 "" 括起来,比如 "I'm OK"。也可以使用转义符号 (\),比如 "I\'m OK"。再或者使用 r 表示原始字符串,比如 r"I'm OK"

还有一种特殊的字符串类型,使用三引号 (Triple Quotes) 进行赋值。主要用于定义多行字符串。它们有两种形式:单引号包围的三引号(''')和双引号包围的三引号(""")。这两种形式都可以用来创建跨越多行的字符串,而且它们之间在 python 中是完全等价的,你可以根据个人偏好或代码风格选择使用哪一种。

Bool - 布尔值

布尔值和布尔代数的表示完全一致,一个布尔值只有TrueFalse两种值,要么是True,要么是False,在Python中,可以直接用TrueFalse表示布尔值(请注意大小写)。布尔值经常用在条件判断中。

None - 空值

空值是 Python 的一个特殊的值,表示 “无” 的概念。用 None 表示。None 不能理解为 0,因为 0 是有意义的 (并且 0 是整数类型常量),而 None 是一个特殊的空值。复杂数据类型也有其对应的空值。例如 [ ] 是数组的空值,( ) 是元组的空值,{ } 是字典的空值。


组合数据类型

分为有序和无序。

list - 数组

list 是一种有序的集合,可以随时添加和删除其中的元素。比如,列出班里所有同学的名字,就可以用一个 list 表示:

>>> classmates = ["Michael", "Bob", "Tracy"]
>>> classmates
["Michael", "Bob", "Tracy"]

要取出某个具体的元素,可以在数组变量的后面附上序号 (序号从 0 开始数):

>>> classmates[0]
"Michael"

tuple - 元组

tuple 和 list 非常类似,但是 tuple 一旦初始化就不能修改。比如同样是列出同学的名字:

>>> classmates = ('Michael', 'Bob', 'Tracy')

因为是 tuple 类型,现在 Classmates 这个 tuple 不能变了。与 list 相同,可以用 Classmates[0] 来获取 tuple 的第一个元素,但不能对其进行赋值。因为 tuple 是不可变的,所以 相较 list 类型而言,使用 tuple 类型要更加的安全。时刻记住:当定义一个 tuple 之时,tuple 的元素就必须被确定下来

另外,只有 1 个元素的 tuple 在定义时必须加一个逗号 ,,来消除歧义:

>>> (1)  # 不加 `,` 的返回为整数 1
1
>>> (1,)  # 加了 `,` 才会被识别为 tuple
(1,)

dict- 字典

dict 全称 dictionary,在其他语言中也称为 map,使用键-值 (key-value) 存储,具有极快的查找速度。创建字典时也需要以键值对的方式进行初始化,取出字典内元素的方式则是在字典变量名之后附上需要查询的 key:

>>> scores = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> scores['Michael']
95

dict 可以用在很多需要高速查找的地方,在 python 代码中几乎无处不在。

Set - 集合

Set 和 dict 类似,也是一组 key 的集合,但不存储 value。且由于 key 不能重复,所以,在 set 中不会存在重复的元素。要创建一个 set,需要提供一个 list 作为输入集合:

>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

重复元素在 set 中自动被过滤:

>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}

set 可以看成数学意义上的无序和无重复元素的集合,因此,两个 set 可以做数学意义上的交集、并集等操作:

>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

变量与常量

(* 赋值语句 *)
assignment_stmt ::=  target_list ("=" expression_list)+
                   | target augassign expression
                   | target_list ":=" expression  (* 海象运算符 *)

(* 赋值目标 *)
target_list     ::=  target ("," target)* [","]

target          ::=  identifier
                   | "(" target_list ")"
                   | "[" target_list "]"
                   | attributeref
                   | subscription
                   | slicing
                   | "*" target  (* 解包 *)

(* 增强赋值运算符 *)
augassign       ::=  "+=" | "-=" | "*=" | "@=" | "/=" | "//=" | "%=" 
                   | "**=" | ">>=" | "<<=" | "&=" | "^=" | "|="

(* 表达式列表 *)
expression_list ::=  expression ("," expression)* [","]

(* 属性引用、下标、切片 *)
attributeref    ::=  primary "." identifier
subscription    ::=  primary "[" expression_list "]"
slicing         ::=  primary "[" slice_list "]"
primary         ::=  atom | attributeref | subscription | slicing | call

变量

变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。对变量的命名有两个要求:(1)必须是大小写英文、数字和 _ 的组合;(2)不能用数字开头。

a = 1           # 变量 a 是一个整数
t_007 = "0007"  # 变量 t_007 是一个字符串
Answer = True   # 变量 Answer 是一个布尔值(True)

在 Python 中,等号 = 是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量,例如:

>>> a = 123 # a是整数
>>> print(a)
123
>>> a = "ABC" # a变为字符串
print(a)
ABC

这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。和静态语言相比,动态语言更灵活,静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如 Java 是静态语言,赋值语句如下:

int a = 123; // a是整数类型变量
a = "ABC"; // 错误:不能把字符串赋给整型变量

最后,理解变量在计算机内存中的表示也非常重要。当我们写 a = "ABC" 时,python解释器干了两件事情:

  1. 在内存中创建了一个 "ABC" 的字符串;
  2. 在内存中创建了一个名为 a 的变量,并把它指向 "ABC"

也可以把一个变量 a 赋值给另一个变量 b,这个操作实际上是把变量 b 指向变量 a 所指向的数据,例如下面的代码:

>>> a = "ABC"  # 创建 a 对象和 "ABC" 字符串对象,并将 a 指向 "ABC"
>>> b = a      # 创建 b 对象,将 b 指向 a 所指的对象("ABC")
>>> a = "XYZ"  # 创建字符串对象 "XYZ",并将 a 重新指向 "XYZ"
>>> print(b)
ABC

常量

所谓常量就是不能变的变量,比如常用的数学常数 π 就是一个常量。python 通常使用全大写的变量名表示常量:

PI = 3.14159265359

但事实上 PI 仍然是一个变量,python 根本没有任何机制保证 PI 不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量 PI 的值,也没人能拦住你。(请不要这么做,少写点垃圾,利人利己,求求了)。


运算符

算数运算符

运算符 名称 示例 结果 说明
+ 加法 10 + 5 15 两数相加
- 减法 10 - 5 5 两数相减
* 乘法 10 * 5 50 两数相乘
/ 除法 10 / 5 2.0 浮点数除法(结果始终为 float)
// 整除 10 // 5 2 整数除法(向下取整)
% 取余 10 % 5 0 取模运算(余数)
** 幂运算 10 ** 5 100000 10 的 5 次方

比较运算符 (==!=><)

运算符 名称 示例 结果 说明
== 等于 10 == 5 False 判断两值是否相等
!= 不等于 10 != 5 True 判断两值是否不相等
> 大于 10 > 5 True 判断左值是否大于右值
< 小于 10 < 5 False 判断左值是否小于右值
>= 大于等于 10 >= 5 True 判断左值是否大于或等于右值
<= 小于等于 10 <= 5 False 判断左值是否小于或等于右值
is 对象标识
is not 否定的对象标识

布尔运算符

运算 结果: 备注
x or y 如果 x 为真值,则 x,否则 y 短路运算符,第一个参数为 True 时短路
x and y 如果 x 为假值,则返回 x,否则返回 y 短路运算符,第一个参数为 False 时短路
not x if x is false, then True, else False not 的优先级比非布尔运算符低,
因此 not a == b 会被解读为 not (a == b)
a == not b 会引发语法错误。

表达式和语句

表达式 (Expression) 是"值",可以参与运算或赋值,语句 (Statement) 是"动作",执行具体操作,本身没有值。表达式可以放在任何需要值的地方,语句用来控制程序流程。

所有的表达式都会返回一个值:

# 字面量表达式
42          # 值: 42
"hello"     # 值: "hello"
[1, 2, 3]   # 值: [1, 2, 3]

# 运算表达式
1 + 2 * 3   # 值: 7
a > b       # 值: True 或 False
x or y      # 值: x 或 y 的真值

# 调用表达式
len("abc")  # 值: 3
max(1, 2)   # 值: 2

# 条件表达式(三元运算符)
x if x > 0 else 0  # 值: 根据条件返回 x 或 0

# 推导式(也是表达式!)
[x*2 for x in range(3)]  # 值: [0, 2, 4]

所有的语句都会执行对应的 “动作”:

# 赋值语句(动作:绑定名字)
x = 5                       # 没有值!不能写成 y = (x = 5)

# 控制流语句
if x > 0:                   # 动作:条件判断
    pass
for i in range(3):          # 动作:循环
    pass
while True:                 # 动作:循环
    break

# 函数/类定义语句
def foo():                  # 动作:创建函数对象并绑定名字
    pass
class Bar:                  # 动作:创建类
    pass

# 返回语句
return 42                   # 动作:从函数返回(本身不是值)

# 导入语句
import os                   # 动作:导入模块

# 异常处理
try:                        # 动作:捕获异常
    pass
except:
    pass

控制流工具

控制流工具是程序执行过程中根据特定条件、状态变化或外部事件来动态改变代码执行顺序和路径的核心机制,它是实现程序逻辑复杂性、响应性和适应性的基础架构。这些工具主要包括以下几个关键类别:

  1. 条件分支语句允许程序基于布尔表达式的真假值或模式匹配结果,在不同的代码块之间做出选择性执行决策,实现多路径的逻辑分流;
  2. 循环结构则通过迭代遍历可迭代对象或持续检测条件状态,使代码块能够重复执行,从而高效处理批量数据或等待特定事件的发生;
  3. 跳转控制关键字提供了对循环和函数执行流程的精细干预能力,包括立即终止循环、跳过当前迭代提前进入下一轮、以及从函数中返回计算结果等操作;
  4. 异常处理机制构建了一套应对运行时错误和非预期状况的容错体系,通过捕获、抛出和清理操作确保程序在遇到问题时能够优雅降级或恢复;
  5. 上下文管理协议则通过自动化的资源获取与释放模式,保证了文件、网络连接、锁等关键资源在使用过程中的正确生命周期管理;
  6. 生成器协程引入了惰性计算和异步编程模型,使得程序能够以更节省内存的方式处理大规模数据流,或在等待I/O操作时不阻塞主线程。

这些控制流工具相互配合,共同构成了现代编程语言表达复杂算法、构建可靠系统、实现高效并发的基础语法框架。在这里仅介绍条件分支循环结构的流控制语句:


if-else 语句

if_statement ::= "if" expression ":" suite
               ("elif" expression ":" suite)*
               ["else" ":" suite]

suite        ::= simple_statement | NEWLINE INDENT statement+ DEDENT
expression   ::= 条件表达式(返回布尔值或可转换为布尔值的对象)

在 Python 程序中,用 if 语句实现逻辑选择分支:

if number >= 0:
    print("number 是非负数")
else:
    print("number 是负数")

如果需要更细致的判断,可以在 ifelse 之间引入 elifelifelse if 的缩写,完全可以有多个 elif

if number > 0:
    print("number 是正数")
elif number == 0:
    print("number 等于 0")
elif number < 0:
    print("number 是负数")
else:
    print("number 类型非法")

if 判断条件还可以简写:

if x:
    print('True')

只要 x 不为空值,即可执行对应的语句块。


条件表达式

条件表达式也叫做 “三元运算符”,可以视作为将 if...else... 结构简单压缩在一行显示。

value = a if condition else b

condition 成立时,条件表达式返回 a,不成立时则返回 b


match 语句

match_stmt      ::=  "match" subject_expr ":" NEWLINE INDENT case_block+ DEDENT
subject_expr    ::=  expression
case_block      ::=  "case" patterns [guard] ":" block
patterns        ::=  open_sequence_pattern | pattern
pattern         ::=  as_pattern | or_pattern
as_pattern      ::=  or_pattern "as" capture_pattern
or_pattern      ::=  closed_pattern ("|" closed_pattern)*
closed_pattern  ::=  literal_pattern | capture_pattern | wildcard_pattern
                   | value_pattern | group_pattern | sequence_pattern
                   | mapping_pattern | class_pattern

guard           ::=  "if" named_expression

Python 3.10 引入的 match 语句(结构模式匹配)是一个强大的功能,它提供了更优雅的方式来处理复杂的数据结构匹配。可以理解为一个更轻便的 if...elif...else...语法:

def http_status(status):
    match status:
        case 200 | 201 | 202:
            return "OK"
        case 404:
            return "Not Found"
        case code if code.startswith("5") and code.isdigit():  # 守卫子句:模糊匹配所有5xx
            return "Server Error"
        case _:
            return "Unknown status"

根据 case 后的语句进行匹配,执行所命中 case 下缩进的语句块。


for 循环

for x in ... 循环,可依次把可迭代对象 (Iterable) 中的每个元素迭代出来,每个元素都会执行缩进块内的语句。看例子:

>>> names = ['Michael', 'Bob', 'Tracy']
>>> for name in names:
...     print(name)
...
Michael
Bob
Tracy

至于是否为可迭代对象,可以使用 isinstance(obj, Iterable),或看是否有 __iter__ 方法。

再比如我们想计算 1-10 的整数之和,可以用一个 sum 变量做累加:

>>> sum = 0
>>> for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
...    sum = sum + x
>>> print(sum)
55

Python提供一个 range() 函数,用于生成一个整数序列。通过 list () 函数可以将整数序列转换为 list 。(例如 list (range(5)))


while 循环

第二种循环是 while 循环,只要条件满足,就不断循环,条件不满足时退出循环。python 本质上只有 while 是真正的条件循环for 是语法糖包裹的迭代器遍历。

while (condition):
    loopBody

在不知道具体循环次数(上限)的情况下,只能使用 while 循环。下面是 “无限猴子理论” 的代码示例:

while monkey_typing[-19:] is "William Shakespeare":
    monkey_typing = keep_typing()

一只猴子随机敲击键盘,时间无限的情况下,必定会敲出 “莎士比亚”。


break 和 continue

在循环中,break语句可以提前退出循环。例如,本来要循环打印 1~100 之间的数字:

for i in range(1, 101):
    print(i)
print('END')

如果要提前结束循环,可以用 break 语句:

for i in range(1, 101):
    if i > 10: # 当 i=11 时,条件满足,执行 break 语句
        break  # break 语句会结束当前循环 --+
    print(i)   #                            |
print('END')   # <--------------------------+

执行上面的代码,在打印出 1~10 之后,紧接着会打印 “END”,程序结束。可见 break 的作用是提前结束循环。

在循环过程中,也可以通过 continue 语句,跳过当前的这次循环,直接开始下一次循环。依然沿用上面的例子,但这次只打印奇数,使用 continue 语句跳过奇数循环:

for i in range(1, 101):  # <-------------------------+
    if i % 2 == 0:       #                           |
        continue  # 使用 continue 语句跳过奇数循环 --+
    print(i)
print('END')

执行上面的代码可以看到,打印的不再是 1~100,而是 1,3,5,7,9。可见 continue 的作用是提前结束本轮循环,并直接开始下一轮循环。


函数定义与调用

python 内置了很多有用的函数,我们可以直接调用。要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数 abs,只有一个参数。可以直接从 python 的官方网站查看 Built-in Functions 文档,也可以在交互式命令行通过 help(abs) 查看 abs 函数的帮助信息。

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”

>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

在 python 中定义一个函数要使用 def 语句,依次写出函数名、括号、括号中的参数和冒号 :,然后在缩进块中编写函数体。函数的返回值用 return 语句返回。我们以自定义一个求绝对值的 my_abs 函数为例:

def my_abs(x):
    return x if x >= 0 else -x

函数体内部的语句在执行时,一旦执行到 return 时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。return 语句可以省略,省略的 return 相当于在函数执行完毕后默认执行 return None。而 return None 又可以简写为 return。所以,以下三种写法的返回值是相同的:

def hello1():
    print("hello!")
    return None

def hello2():
    print("hello!")
    return

def hello3():
    print("hello!")

空函数

如果想定义一个什么事也不做的空函数,可以用 pass 语句:

def nop():
    pass

pass 语句什么都不做,那有什么用?实际上 pass 可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个 pass,让代码能运行起来。pass 还可以用在其他语句里,比如:

if age >= 18:
    pass

缺少了 pass,代码运行就会有语法错误。


返回多个值

python 的函数能以 tuple 类型一次返回多个值,且在语法可以省略 ( )

def second_to_minute(second):
    minute = second // 60
    second = second % 60
    return minute, second  # 不用写为 (minute, second)

调用 second_to_minute() 参数解包 (Unpacking) 也可以省略 ( )

>>> minute, second = second_to_minute(145)  # 不用写括号
>>> print(minute, second)
2 25

本质上只是将函数的返回值装在了一个 tuple 中了而已:

>>> time = second_to_minute(145)
>>> print(time)
(2, 25)
>>> print(type(time))
<class 'tuple'>

总之,python 的函数返回多值其实就是返回一个 tuple。在语法上,返回一个 tuple 可以省略括号,而多个变量可以同时接收一个 tuple,按位置赋给对应的值。


函数类型注解(typing)

进行函数类型注解主要是为了规范代码编写人员的习惯,做到明确函数的输入输出,被注解的函数可被类型检查器、IDE、语法检查器等第三方工具审查。但 python 程序在运行时并不强制要求函数与变量类型标注。也就是说类型注解并不能对函数调用人员进行有效的约束。若要对函数调用过程进行约束,就要使用 “参数类型检查” 相关技术,在后面会讲到。

简单类型注解

# 基本类型
def greet(name: str) -> str:
    return f"Hello, {name}"

# 多个参数
def add(x: int, y: int) -> int:
    return x + y

# 默认参数值
def greet_with_default(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}"

容器类型注解

from typing import List, Dict, Set, Tuple

# 列表:指定元素类型
def sum_numbers(numbers: List[int]) -> int:
    return sum(numbers)

# 字典:指定键值类型
def get_user_info(users: Dict[str, int]) -> List[str]:
    return list(users.keys())

# 集合
def unique_items(items: List[int]) -> Set[int]:
    return set(items)

# 元组:固定长度,指定每个位置类型
def get_point() -> Tuple[float, float]:
    return (1.0, 2.0)

# 可变长度元组
def get_coordinates() -> Tuple[int, ...]:
    return (1, 2, 3, 4)

Optional (可选/可能为 None)

from typing import Optional

# 参数可能为 None
def find_user(user_id: int) -> Optional[str]:
    if user_id > 0:
        return f"User {user_id}"
    return None

# 默认值为 None 的参数
def greet_optional(name: Optional[str] = None) -> str:
    if name is None:
        return "Hello, Stranger"
    return f"Hello, {name}"

# Python 3.10+ 可用 | 语法
def find_user_new(user_id: int) -> str | None:
    pass

这部分的内容点到为止,不再展开讲解。


递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。举个例子,我们来计算阶乘 n! = 1 * 2 * 3 * ... * n,用函数 fact(n) 表示,可以看出:

$$ fact(n)=n!=1\times2\times3\times\cdot\cdot\cdot\times(n-1)\times n=(n-1)!\times n=fact(n-1)\times n $$ 所以 fact(n) 可以表示为 n * fact(n-1),仅当 n=1 时需要特殊处理。于是,fact(n) 用递归的方式写出来就是:

>>> def fact(n):
...     match n:
...         case n if n < 0:
...             print("Input cannot be negative.")
...             return None
...         case 0 | 1:
...             return 1
...         case _:
...             return n * fact(n - 1)
...
>>> fact(-100)
Input cannot be negative.
None
>>> fact(0)
1
>>> fact(1)
1
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

如果我们计算fact(5),可以根据函数定义看到计算过程如下:

fact(5)
== 5 * fact(4)
== 5 * (4 * fact(3))
== 5 * (4 * (3 * fact(2)))
== 5 * (4 * (3 * (2 * fact(1))))
== 5 * (4 * (3 * (2 * 1)))
== 5 * (4 * (3 * 2))
== 5 * (4 * 6)
== 5 * 24
== 120

使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。python 标准的解释器没有针对尾递归做优化,因此 python 的任何递归函数都存在栈溢出的隐患。


函数的参数

python 支持多种参数类型。最基础的是位置参数 (Positional Arguments),定义时只需用变量名占位,无需额外修饰。例如计算平方的函数:

def power(x):
    return x ** 2

这里 x 就是一个位置参数。调用时必须传入有且仅有的一个参数:

>>> power(5)
25
>>> power(15)
225

若需计算任意次方,可定义包含两个位置参数的函数:

def power(x, n):
    return x ** n

调用时按位置顺序依次传参,第一个值赋给 x,第二个赋给 n

>>> power(5, 2)   # x=5, n=2
25
>>> power(5, 3)   # x=5, n=3
125

可变参数

当参数个数不确定时,可将多个参数打包为元组传入。例如计算平方和的函数:

def square_and_sum(numbers):
    """接收一个可迭代对象,计算各元素的平方和"""
    return sum(n ** 2 for n in numbers)

调用时需显式传入序列:

>>> square_and_sum((1, 2, 3))      # 传入元组
14
>>> square_and_sum([1, 25, 7])     # 传入列表,函数对任意可迭代对象生效
675

更优雅的写法:*args 捕获。使用星号表达式 * 定义可变参数 (Variadic Arguments),可将多余的位置参数自动捕获为元组:

def square_and_sum(*numbers):
    """接收任意个数,计算平方和"""
    print(f"type: {type(numbers)}, value: {numbers}")
    return sum(n ** 2 for n in numbers)

此时调用无需手动打包,直接传参即可:

>>> square_and_sum(1, 25, 7)
type: <class 'tuple'>, value: (1, 25, 7)
675
>>> square_and_sum()           # 不传参,numbers 为空元组 ()
type: <class 'tuple'>, value: ()
0
>>> square_and_sum(5)          # 单参数
type: <class 'tuple'>, value: (5,)
25

关键特性:

  1. *numbers 在函数内部永远是元组 tuple 类型;
  2. 命名习惯用 *args,但可自定义;
  3. 支持与其他参数组合使用,如 def func(a, b, *args)

参数的默认值

在上面的例子中,power(x) 只需要一个位置参数,而 power(x, n) 则需要两个位置参数。我想让函数在只有一个位置参数的时候直接返回 x 的平方,在有两个位置参数的时候按照 power(x, n) 的逻辑进行计算,这样的效果如何实现呢?这个时候,可以为 n 的值指定一个默认值,即将 n 的默认值设定为 2:

def power(x, n=2):
    return x ** n

这样,当我们调用 power(5) 时,相当于调用 power(5, 2)。而对于 n > 2 的其他情况,就必须明确地传入 n,比如 power(5, 3)

>>> power(5)
25
>>> power(5, 2)
25
>>> power(5, 3)
125

从上面的例子可以看出,默认参数可以简化函数的调用。使用默认参数,甚至可以实现零传参的场景:

def power(x=0, n=2):
    return x ** n

上面的函数可以直接调用为 power(),此时返回为 0。

设置默认参数时,有几点要注意:

首先是 “必选参数在前,默认参数在后”,否则 python 的解释器会报错;

>>> def power(x, n=2):
...     return x ** n
...
>>> power(2, 5)
32
>>> power(n=2, 5)
SyntaxError: positional argument follows keyword argument

其次是 “默认参数必须指向不变对象!”,这是默认参数用法中最大的坑。由于默认参数仅在函数定义时创建一次,因此当函数重复被调用时,上一次的默认参数并不会被 “覆盖”:

>>> def add_end(L=[]):
...     L.append('END')
...     return L
...
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

这个坑如何解决的问题,留到本章最后一个小节再进行说明。但这类问题也给了我们一个启示:我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象


关键字参数

可变参数允许你传入 0 个或任意个参数,这些可变参数在函数调用时自动组装为一个 tuple。而关键字参数允许你传入 0 个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict。请看示例:

def person(name, age, **kw):
    print("name:", name, "age:", age, "other:", kw)

函数 person 除了必选参数 nameage 外,还接受关键字参数 kw。在调用该函数时,可以只传入必选参数:

>>> person("Michael", 30)
name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

>>> person("Bob", 35, city="Beijing")
name: Bob age: 35 other: {"city": "Beijing"}
>>> person("Adam", 45, gender="M", job="Engineer")
name: Adam age: 45 other: {"gender": "M", "job": "Engineer"}

关键字参数有什么用?它可以扩展函数的功能。比如,在 person 函数里,我们保证能接收到 nameage 这两个参数,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个 dict,然后,把该 dict 转换为关键字参数传进去:

>>> extra = {"city": "Beijing", "job": "Engineer"}
>>> person("Jack", 24, **extra)
name: Jack age: 24 other: {"city": "Beijing", "job": "Engineer"}

**extra 表示把 extra 这个 dic 的所有 key-value 用关键字参数传入到函数的 **kw 参数,kw 将获得一个 dict。


命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过 kw 检查。以 person() 函数为例,我们希望检查是否有 cityjob 参数:

def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)

但是调用者仍可以传入不受限制的关键字参数:

>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收 cityjob 作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job):
    print(name, age, city, job)

和关键字参数 **kw 不同,命名关键字参数需要一个特殊分隔符 ** 后面的参数被视为命名关键字参数。调用方式如下:

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符 * 了:

def person(name, age, *args, city, job):
    print(name, age, args, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。(由于调用时缺少参数名 cityjob,python 解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错):

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'

命名关键字参数可以有缺省值,从而简化调用:

def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)

由于命名关键字参数 city 具有默认值,调用时,可不传入city参数:

>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个 * 作为特殊分隔符。如果缺少 *,python 解释器将无法识别位置参数和命名关键字参数:

def person(name, age, city, job):
    # 缺少 *,city和job被视为位置参数
    pass

参数组合

在 python 中定义函数,可以用必选参数默认参数可变参数关键字参数命名关键字参数,这 5 种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

比如定义一个函数,包含上述若干种参数:

def f1(a, b, c=0, *args, **kw):
    print(f'a={a}, b={b}, c={c}, args={args}, kw={kw}')

def f2(a, b, c=0, *, d, **kw):
    print(f'a={a}, b={b}, c={c}, d={d}, kw={kw}')

在函数调用的时候,python 解释器自动按照参数位置和参数名把对应的参数传进去。

>>> f1(1, 2)
a=1, b=2, c=0, args=(), kw={}
>>> f1(1, 2, c=3)
a=1, b=2, c=3, args=(), kw={}
>>> f1(1, 2, 3, 'a', 'b')
a=1, b=2, c=3, args=('a', 'b'), kw={}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a=1, b=2, c=3, args=('a', 'b'), kw={'x': 99}
>>> f2(1, 2, d=99, ext=None)
a=1, b=2, c=0, d=99, kw={'ext': None}

最神奇的是通过一个 tuple 和 dict,你也可以调用上述函数:

>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a=1, b=2, c=3, args=(4,), kw={'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a=1, b=2, c=3, args=(), kw={'d': 88, 'x': '#'}

所以,对于任意函数,都可以通过类似 func(*args, **kw) 的形式调用它,无论它的参数是如何定义的。尽管可以组合多达 5 种类型的参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。


参数类型检查

def my_abs(x):
    return x if x >= 0 else -x

调用函数时,如果传入参数的个数不对,python 解释器会自动检查出来,并抛出 TypeError

>>> my_abs(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: my_abs() takes 1 positional argument but 2 were given

但是如果参数类型不对,解释器就无法帮我们检查。试试 my_abs 和内置函数 abs 的差别:

>>> my_abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in my_abs
TypeError: unorderable types: str() >= int()
>>> abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'

当传入了不恰当的参数时,内置函数 abs 会检查出参数错误,而我们定义的 my_abs 没有参数检查,会导致 if 语句出错,出错信息和 abs 不一样。所以,这个函数定义不够完善。让我们修改一下 my_abs 的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数 isinstance() 实现:

def my_abs(x):
    if not isinstance(x, (int, float)):
        raise TypeError(f"bad operand type for my_abs(): '{type(x).__name__}'")
    return x if x >= 0 else -x

添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:

>>> my_abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in my_abs
TypeError: bad operand type for my_abs(): 'str'

错误和异常处理将在后续讲到。


函数传参小结

默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误。

python 的默认参数仅在函数定义时只被创建一次,而不是每次调用时重新创建。如果默认参数是可变对象 (如 list, dict, set),所有调用会共享同一个对象。调用 add_item 可发现,如果第二个位置函数使用默认参数的情况下,item_list 并不会重复创建:

>>> def add_item(item, item_list=[]):  # 危险!使用了可变对象 list 作为默认值
...     item_list.append(item)
...     return id(item_list), item_list
...
>>> add_item(1)
2252960611520 [1]
>>> add_item(2)
2252960611520 [1, 2]
>>> add_item(3)
2252960611520 [1, 2, 3]
>>> add_item(3, [0])
2252973655680 [0, 3]

正确的解决方式是,使用 None 作为默认值。每次使用默认参数,会导致 item_list 被赋为空值 [ ],避免了逻辑错误:

>>> def add_item(item, item_list=None):
...     # 每次调用创建新对象
...     if item_list is None: 
...         item_list = []
...     item_list.append(item)
...     return item_list
...
>>> add_item(1)
[1]
>>> add_item(2)
[2]
>>> add_item(3)
[3]
>>> add_item(3, [0])
[0, 3]

命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值

定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符 *,否则定义的将是位置参数。下面是错误示例 (没有 *,变成位置参数):

>>> def greet(name, age=18):
...     print(f"{name} is {age} years old.")
...
>>> greet("Alice", 20)  # 位置传参
Alice is 20 years old.
>>> greet("Bob", age=25)  # 关键字传参
Bob is 25 years old.

正确的命名关键字参数定义方式:

>>> def greet(*, name, age=18):
...     print(f"{name} is {age} years old.")
...
>>> greet("Alice", 20)  # 位置传参
TypeError: greet() takes 0 positional arguments but 2 were given
>>> greet("Bob", age=25)  # 关键字传参
TypeError: greet() takes 0 positional arguments but 1 were given
>>> greet(name="Carol", age=30)
Carol is 30 years old.

当函数已有 *args 时,* 可以省略,因为 *args 已经起到了分隔作用。


*args 是可变参数,接收一个 tuple;**kw 是关键字参数,接收一个 dict。使用 *args**kw 是 python 的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

>>> def get_info(*args, **kw):
...     return args, kw
...
>>> get_info("Alice", 25, "Female", city="York", qual="master")
(('Alice', 25, 'Female'), {'city': 'York', 'qual': 'master'})

kw 获得的 dict 是 extra 的一份引用拷贝,对 kw 的改动不会影响到函数外的 extra

修改 kw 本身 (重新赋值) 不会影响外部的 extra

>>> def test_kwargs(**kw):
...     print(f"id(kw): {id(kw)}, value: {kw}")
...         # 重新赋值 kw,不影响外部
...         kw = {"new": "value"}
...         print(f"id(kw): {id(kw)}, value: {kw}")
...
>>> test_kwargs(type="Cat", name="Dog")
id(kw): 2154372319680, value: {'type': 'Cat', 'name': 'Dog'}
id(kw): 2154372275584, value: {'new': 'value'}

但修改 kw 内部的可变对象会影响外部的 extra

>>> def test_kwargs(**kw):
...     print(f"id(kw): {id(kw)}, value: {kw}")
...         # 修改 kw 内的可变对象,会影响外部 extra
...         kw["type"] = "Fox"
...         print(f"id(kw): {id(kw)}, value: {kw}")
...
>>> test_kwargs(type="Cat", name="Dog")
id(kw): 1832511392256, value: {'type': 'Cat', 'name': 'Dog'}
id(kw): 1832511392256, value: {'type': 'Fox', 'name': 'Dog'}

最安全的做法是,创建深拷贝副本:

>>> from copy import deepcopy
>>> extra = {"type": "Cat", "name": "Dog"}
>>> def test_kwargs(**kw):
...     # 创建完全独立的副本
...     kw = deepcopy(kw)  # 深拷贝,彻底隔离
...     # 现在随意修改都不会影响外部
...     kw["type"] = 999
...     return kw
...
>>> result = test_kwargs(**extra)
{'type': 999, 'name': 'Dog'}
>>> print(f"id: {id(result)}, value: {result}")
id: 1637442070272, value: {'type': 999, 'name': 'Dog'}
>>> print(f"id: {id(extra)}, value: {extra}")
id: 1637442069376, value: {'type': 'Cat', 'name': 'Dog'}

参考文章