语法基础
本文介绍 Python 的语法基础,即不 import 任何包的情况下涉及的内容。
数据类型¶
Python 是动态类型语言,定义变量时不需要声明类型,赋值时会自动确定。但也支持在定义变量时写上预期的数据类型,不过解释器不会主动检查数据类型不匹配的问题,可以借助第三方类型检查工具(例如 ty)来避免此类错误。
Python 的数据类型还分「可变」与「不可变」两类,不同类型意味着不同的数据操作逻辑。
不可变类型¶
bool。布尔型,表示二元逻辑。示例用法:
int。整型,表示整数数值。示例用法:
float。浮点数型,表示带有小数部分的数值。示例用法:
str。字符串,表示由可编码字符组成的数据。示例用法:
# 基本用法
info: str = "Alice"
# 字符转义
info = "hello\tworld" # hello world
# 取消字符转义,输出原始内容(r 即 raw)
info = r"hello\tworld" # hello\tworld
# 字符模板 (f-string)
age = 18.88
info = (f"My age is {age:.1f}, "
f"and you?") # My age is 18.9, and you?
# 跨行字符串可以使用小括号包裹
# :.1f 表示给浮点数四舍五入保留 1 位小数
# 字符串拼接
# 使用 str 的 join 方法,将迭代器中的 str 拼接为一个完整版的字符串
# 相较于使用 += 省去了存储中间对象的内存开销,提升了程序的性能
num = [1314, 520, 601]
splice_str = "-".join(str(x) for x in num) # 1314-520-601
tuple。元组,底层数据结构为 顺序表。示例用法:
可变类型¶
dict。字典,底层数据结构为 哈希表。示例用法:
# 字典创建(可哈希对象才能作为键,不可变数据类型都可以作为字典的键)
person: dict[str, str | int] = {"name": "Alice", "age": 25}
# 值修改
person["age"] = 26
# 创建新的键值对
person["city"] = "New York"
# 根据键访问值
print(person["name"])
print(person.get("name", "")) 更安全的访问方法,当字典不存在时,返回第二个参数,这里是 ""
# 查询是否存在某个键 O(1)
if "bob" in person:
pass
# 等价于(基本不这么写)
if "bob" in person.keys():
pass
# 查询是否存在某个值 O(n)
if "bob" in person.values():
pass
# 查询是否存在某个键值对 O(1)
if "bob" in person.items():
pass
list。列表,底层数据结构为 顺序表。示例用法:
# 初始化
fruits: list[str] = ["apple", "banana", "cherry"]
# 初始化(列表推导式)
squares = [x**2 for x in range(5)] # 生成 0 到 4 的平方
# 尾插入 O(1)
fruits.append("orange")
# 尾删除 O(1)
fruits.pop()
# 按值删除 O(n),删除第一个匹配到的元素
fruits.remove("banana")
# 按索引删除 O(n)
del fruits[1]
# 索引(超出最大索引值会报错 list index out of range)
print(fruits[0])
# 切片(超出最大索引不会报错)
print(fruits[1:3])
set。集合,底层数据结构为 哈希表,可以理解为只保留 key 的 dict。示例用法:
colors: set[str] = {"red", "green", "blue"}
colors.add("yellow") # 添加元素
colors.remove("green") # 删除元素
不同类型数据的内存机制¶
Python 中可变与不可变数据类型的内存机制是不一样的,对变量的修改就可以体现出这一点。
对于不可变类型,修改操作本质是「重新赋值」,会开辟新内存,原来的数据保持不变。例如下面的程序。另一个引用变量 \(t\) 在进行拼接操作后,对应的内存地址发生了改变,也就是说申请了新的内存空间:
s = "hello"
t = s
print(id(s)) # 2193178293488
print(id(t)) # 2193178293488
t += 'world'
print(id(s)) # 2193178293488
print(id(t)) # 2193178282864
对于可变类型,修改操作为原地修改,不会新开内存,因此所有引用同时生效。例如下面的程序。修改了其中一个变量的值以后,三个可变数据类型变量 \(a,b,c\) 指向的内存空间没有发生改变,同时其余所有变量也都跟着改变:
a = [1, 2, 3]
b = a
c = b
print(id(a)) # 1441782879424
print(id(b)) # 1441782879424
print(id(c)) # 1441782879424
# a[0] = -1 和 b[0] = -1 效果都是一样的
c[0] = -1
print(id(a)) # 1441782879424
print(id(b)) # 1441782879424
print(id(c)) # 1441782879424
print(a) # [-1, 2, 3]
print(b) # [-1, 2, 3]
print(c) # [-1, 2, 3]
运算符¶
Python 有以下 运算符:
Python 的 运算符优先级(越往下等级越低):
| 运算符 | 描述 |
|---|---|
(expressions...) |
绑定或加圆括号的表达式 |
[expressions...] |
列表显示 |
{key: value...} |
字典显示 |
{expressions...} |
集合显示 |
x[index] |
抽取 |
x[index:index] |
切片 |
x(arguments...) |
调用 |
x.attribute |
属性引用 |
await x |
await 表达式 |
** |
乘方 |
+x, -x, ~x |
正,负,按位非 NOT |
*, @, /, //, % |
乘,矩阵乘,除,整除,取余 |
+, - |
加和减 |
<<, >> |
移位 |
& |
按位与 AND |
^ |
按位异或 XOR |
| |
按位或 OR |
in, not in, is, is not, <, <=, >, >=, !=, == |
比较运算,包括成员检测和标识号检测 |
not x |
布尔逻辑非 NOT |
and |
布尔逻辑与 AND |
or |
布尔逻辑或 OR |
if -- else |
条件表达式 |
lambda |
lambda 表达式 |
:= |
赋值表达式 |
流程控制¶
条件语句 if、elif、else 用于分支控制。例如:
循环语句 for 和 while 用于重复控制。例如:
# for 循环
for i in range(5): # 输出 0 到 4
print(i)
# while 循环
count = 0
while count < 3:
print("count:", count)
count += 1 # 增加 count 的值
函数¶
Python 使用 def 关键字定义函数,可以添加入参和返回值的类型提示,带有默认值的入参要定义在没有默认值入参的后面。例如:
def greet(name: str, school: str = "MIT") -> str:
return f"Hello {name}, welcome to {school}!"
message = greet("Alice")
print(message) # Hello Alice, welcome to MIT!
匿名函数¶
lambda 是 Python 用于定义匿名函数的关键字,适合编写非常简单、一次性的函数逻辑。例如:
不过 PEP 8 并不推荐在真实项目中将 lambda 赋值给变量,而应当使用 def 定义函数。例如:
因为使用 def 会让函数名出现在 traceback 中,更有利于调试。lambda 的核心价值在于内联使用,作为参数直接传递给高阶函数。例如:
# 示例一
nums = [1, 2, 3]
result = map(lambda x: x * 2, nums)
# 示例二
students.sort(key=lambda s: s.score)
参数的收集与解包¶
Python 函数的参数收集与解包机制让参数传递变得更灵活。
Python 函数的参数有两种类型:
-
位置参数:只有参数值,不同的位置对应不同的参数。
-
函数定义处:使用元组
*args收集位置参数。 -
函数调用处:使用
*展开可迭代对象,将其作为多个位置参数传入。
-
-
关键字参数:给定参数名和参数值。
-
函数定义处:使用字典
**kwargs收集关键字参数。 -
函数调用处:使用
**展开字典,将其作为多个关键字参数传入。
-
位置参数收集与解包的示例程序:
# 函数定义处,*args 用于接收任意数量的位置参数,并将它们收集为一个元组
def add_all(x, *args):
print(type(args)) # <class 'tuple'>
total = 0
for value in args:
total += value
print(total) # 6
# 函数调用处,* 用于把一个可迭代对象展开为多个位置参数
nums = [1, 2, 3] # 或 set、tuple 等可迭代对象
add_all(*nums) # 等价于 add_all(1, 2, 3)
关键字参数收集与解包的示例程序:
# 函数定义处,**kwargs 用于接收任意数量的关键字参数,并将它们组织为一个字典
def describe(**kwargs):
print(type(kwargs)) # <class 'dict'>
print(kwargs) # {'name': 'Bob', 'city': Singapore}
# 函数调用处,** 用于把一个字典展开为多个关键字参数
info = {"name": "Bob", "city": "Singapore"}
describe(**info) # 等价于 describe(name="Bob", city="Singapore")
类¶
Python 使用 class 关键字定义类,用于面向对象编程。
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
# 强私有方法(前置双下划线)
def __calculate_dog_years(self):
"""将狗的年龄转换为人类年龄"""
return self.age * 7
# 公有方法
def get_human_age(self):
# 在类内部可以正常调用
human_age = self.__calculate_dog_years()
return f"{self.name} is {human_age} years old in human years."
dog = Dog("Buddy", 3)
# 调用公有方法
print(dog.get_human_age()) # Buddy is 21 years old in human years
# 调用私有方法(报错)
print(dog.__calculate_dog_years()) # AttributeError
# 强制调用私有方法(不推荐)
print(dog._Dog__calculate_dog_years()) # 21
作用域¶
Python 查找变量的顺序遵循 LEGB 规则:
- Local:先查找当前函数内的变量;
- Enclosing:再查找外层函数的变量;
- Global:然后查找全局变量;
- Built-in:最后查找内置变量和其他导入的包中的变量。
当且仅当需要修改外层变量时,才需要显示声明变量的位置。一共有两种:
global var1, var2, ...:显式声明全局变量。nonlocal var1, var2, ...:显式声明往外一层函数的变量。
例如下面的程序:
global_count = 0
def outer():
nonlocal_count = 10
def inner():
global global_count # 显式声明全局变量
nonlocal nonlocal_count # 显式声明外层函数的变量
global_count += 1
nonlocal_count += 1
inner()
print("nonlocal_count =", nonlocal_count) # 11
outer()
print("global_count =", global_count) # 1
Python 的 class 变量也完全遵守上述规则,只不过增加了两种变量:实例变量和类变量(私有变量和保护变量的作用域涉及到面向对象,和本节讨论的内容无关,不予讨论)。具体地:
- 实例变量:通过
self.var定义,每个实例独立; - 类变量:直接定义在类中,所有实例共享。
例如下面的程序。不同的类中,类变量的地址是相同的,实例变量的地址不同:
class Dog:
category = "animal" # 类变量
def __init__(self, kind):
self.category = kind # 实例变量
dog1 = Dog("x")
dog2 = Dog("y")
print(dog1.__class__.category) # animal
print(dog2.__class__.category) # animal
print(dog1.category) # x
print(dog2.category) # y
print(id(dog1)) # 2398902522512
print(id(dog2)) # 2398902520016
print(id(dog1.__class__.category)) # 2398902443632
print(id(dog2.__class__.category)) # 2398902443632
print(id(dog1.category)) # 140733327476928
print(id(dog2.category)) # 140733327729768
异常¶
现实场景下,程序的输入或运行几乎不可能始终正确,为了避免程序在出现异常时直接宕机,开发人员需要主动编写代码,来应对可能的异常,以确保系统的鲁棒性。
基本异常处理逻辑主要分两步:
- 抛出异常:Python 使用
raise关键字来抛出异常。 - 捕获和处理异常:Python 使用
try、except和finally关键字捕获和处理异常。
抛出异常¶
基本语法:
在发生 raise ExceptionObject() 后,Python 会做三件事:
- 构造异常对象;
- 停止当前函数的执行;
- 基于函数调用栈「逐层向上」查找异常处理器
try。
常见异常:
# 模块没找到
raise ModuleNotFoundError("module not found")
# 文件没找到
raise FileNotFoundError("file not found")
# 除零
raise ZeroDivisionError("division by zero")
捕获和处理异常¶
原则
能在当前层级处理的异常就立刻处理,无法处理的异常就抛出让上层处理。
实际编码过程中,往往需要配合标准库中的 logging 模块,达到最佳实践:
import logging
logger = logging.getLogger(__name__)
try:
# 正常业务逻辑
...
except (
TimeoutError,
ConnectionError,
) as e:
# 可预见的、可解决的异常,直接重试
retry(e)
except ModuleNotFoundError as e:
# 可预见的、无法解决的异常,向上抛出
return error_response(e)
except Exception:
# 不可预见的异常,先记录,再抛出
logger.exception("unexpected error") # 记录异常
raise # 向上抛出
finally:
# 无论上述结果如何,都会执行这段逻辑,例如释放资源等
...
断言¶
在代码编写的过程中,为了快速验证逻辑,我们一般会使用断言语句,即 assert。其基本用法是:
这里 statement 即为需要验证的逻辑,当 statement 为 True 时程序才会继续执行下面的语句;assertion 即为 statement 为 False 时抛出的 AssertionError 的内容。本质上相当于:
不过 Python 在 -O 模式下会移除所有 assert,所以在生产环境下还是建议使用 raise 抛出异常,而不是 assert 一个断言:
迭代器¶
迭代器 (Iterator) 是一种支持逐元素取出的对象。它的核心接口是 __next__(),可以通过内置函数 next() 调用;当没有元素时,解释器会抛出 StopIteration 异常。
迭代器的特点是惰性计算,即不提前生成全部结果,而是在调用 next() 时才计算下一个元素。
可迭代对象¶
可迭代对象 (Iterable Object) 可以通过 iter() 转换为迭代器。常见的可迭代对象有 list、tuple、dict、set、str 等。
nums = [1, 2, 3]
iterator = iter(nums)
print(next(iterator)) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
print(next(iterator)) # StopIteration
for 循环本质上就是在帮我们自动调用 iter() 和 next()。例如:
上述代码大致等价于:
nums = [1, 2, 3]
iterator = iter(nums)
while True:
try:
num = next(iterator)
print(num)
except StopIteration:
break
迭代器函数¶
常见的内置迭代器函数有 map()、filter()、zip() 和 enumerate()。
map() 用于把一个函数依次应用到可迭代对象的每个元素上,返回一个迭代器。例如:
# 示例一:接收单个可迭代对象
nums = [1, 2, 3]
result = map(lambda x: x * 2, nums)
print(result) # <map object at 0x...>
print(list(result)) # [2, 4, 6]
# 示例二:接收多个可迭代对象
xs = [1, 2, 3]
ys = [10, 20, 30]
result = map(lambda x, y: x + y, xs, ys)
print(result) # <map object at 0x...>
print(list(result)) # [11, 22, 33]
filter() 用于按条件过滤元素,返回条件为 True 的元素组成的迭代器。例如:
nums = [1, 2, 3, 4, 5]
result = filter(lambda x: x % 2 == 0, nums)
print(result) # <filter object ax 0x...>
print(list(result)) # [2, 4]
zip() 用于把多个可迭代对象按位置配对。例如:
# 可迭代对象长度相等
names = ["Alice", "Bob"]
scores = [95, 88]
result = zip(names, scores)
print(result) # <zip object at 0x...>
print(list(result)) # [('Alice', 95), ('Bob', 88)]
# 可迭代对象长度不等,zip() 会在最短的对象耗尽时停止
xs = [1, 2, 3]
ys = [10, 20]
result = zip(xs, ys)
print(result) # <zip object at 0x...>
print(list(result)) # [(1, 10), (2, 20)]
enumerate() 用于在遍历时同时获取索引和值。例如:
names = ["Alice", "Bob", "Carol"]
for index, name in enumerate(names):
print(index, name)
# 0 Alice
# 1 Bob
# 2 Carol
迭代器只能被消费一次
如果已经被 list()、for 或 next() 消费过,再次遍历就不会得到已经取出的元素。例如:
result = map(lambda x: x * 2, [1, 2, 3])
print(next(result)) # 2
print(list(result)) # [4, 6]
print(list(result)) # []
因此,如果后续需要多次使用结果,应该主动转换为 list 或 tuple 保存下来。
生成器¶
生成器 (Generator) 是一种特殊的迭代器,主要用于编写惰性计算逻辑。生成器不会一次性返回所有结果,而是每次执行到 yield 时返回一个值,并暂停函数状态;下一次调用 next() 时,再从暂停的位置继续执行。
生成器基本用法¶
手动调用 next():
def count_up_to(limit: int):
count = 1
while count <= limit:
yield count
count += 1
nums = count_up_to(3)
print(next(nums)) # 1
print(next(nums)) # 2
print(next(nums)) # 3
print(next(nums)) # StopIteration
配合 for 循环:
def count_up_to(limit: int):
count = 1
while count <= limit:
yield count
count += 1
for num in count_up_to(3):
print(num)
# 1
# 2
# 3
生成器适用场景¶
生成器适合处理数据流场景,尤其是数据量较大、没有必要一次性全部放入内存的情况。例如清洗、过滤、转换日志或表格数据时,可以让数据按需流动,而不是先构造一个完整的中间列表。
对于简单的数据映射或过滤,可以使用生成器表达式(使用 () 包裹推导式)。例如下面的程序会从原始用户名中去掉空白项,并统一转成小写:
raw_names = [" Alice ", "", "Bob", " Carol "]
clean_names = (name.strip().lower() for name in raw_names if name.strip())
print(list(clean_names)) # ['alice', 'bob', 'carol']
对于步骤较多的数据处理逻辑,可以使用生成器函数(使用 yield 返回内容)。例如下面的程序会逐行解析销售记录,跳过空行和非正数记录,并把金额字段转换为整数:
def parse_sales(lines: list[str]):
for line in lines:
line = line.strip()
if not line:
continue
date, product, amount = line.split(",")
amount = int(amount)
if amount <= 0:
continue
yield {"date": date, "product": product, "amount": amount}
lines = [
"2026-07-01,book,3",
"",
"2026-07-02,pen,0",
"2026-07-03,paper,5",
]
print(list(parse_sales(lines)))
# [{'date': '2026-07-01', 'product': 'book', 'amount': 3}, {'date': '2026-07-03', 'product': 'paper', 'amount': 5}]