「学习笔记」Python 进阶(一):语言特质 — 写出地道 Python
文章目录
此篇为 Python 进阶的第一篇——语言特质篇,聚焦 Python 区别于 Java/Go 的独特语言特性。当你已经能用 Python 写基本程序后,这些是让你写出"地道 Python 代码"的关键。
前置:Python基础入门 · 后续:Python抽象设计 → Python工程实战
第一章:类型提示 — 让动态语言也有"类型契约"
Python 是动态类型语言,但通过类型提示(Type Hints)可以为变量、函数标注类型。类型提示不影响运行时行为,但能被 IDE 和静态检查工具(如 mypy、pyright)利用,大幅提升代码可读性和可维护性。
实际场景:大型 Python 项目中,类型提示是团队协作的必需品——它相当于一份"自动检查的文档",让你在编写代码时就能发现类型不匹配的问题,而不是等到运行时崩溃。
1.1 基本类型标注
# 变量标注
name: str = "HelloWorld"
count: int = 0
scores: list[float] = [90.5, 85.0, 92.3]
# 函数标注
def greet(name: str, times: int = 1) -> str:
return f"Hello {name}! " * times
# 多返回值标注
def split_path(path: str) -> tuple[str, str]:
import os
return os.path.split(path)
1.2 typing 模块核心类型
from typing import Any, Optional, Callable, TypeVar, Generic
# Any — 任意类型(等于"不标注",但语义更明确)
def parse(data: Any) -> Any: ...
# Optional — 可选类型,等价于 X | None
def find_user(user_id: int) -> Optional[str]: ...
# Callable — 可调用对象类型
# Callable[[参数类型...], 返回类型]
callback: Callable[[int, str], bool] = lambda i, s: i > len(s)
# TypeVar + Generic — 泛型(对标 Java <T>)
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
1.3 Python 3.10+ 新语法
# 联合类型:X | Y 替代 Union[X, Y]
def process(data: str | bytes | dict) -> str: ...
# 内置泛型:list[str] 替代 List[str]
def count_words(text: str) -> dict[str, int]: ...
# 元组类型
def min_max(nums: list[int]) -> tuple[int, int]:
return min(nums), max(nums)
# 模块级类型别名
Vector = list | np.ndarray # 用 numpy 数组或普通列表表示向量
1.4 TypeAlias — 语义化类型
TypeAlias(Python 3.10+)用于给现有类型提供一个更可读的别名。与普通变量赋值不同,TypeAlias 让类型检查器明确知道你是在定义类型别名,而不是运行时变量。
from typing import TypeAlias
# 语义化基本类型:让代码自解释
UserId: TypeAlias = int # 明确"这是一个用户ID",而不只是 int
UserName: TypeAlias = str # 明确"这是一个用户名",而不只是 str
JsonDict: TypeAlias = dict[str, "JsonDict"] # 递归类型别名:嵌套 JSON
# 使用:类型清晰,一目了然
def get_user(user_id: UserId) -> UserName:
# 虽然本质还是 int → str,但调用方一看就懂
...
# 简化复杂类型:避免重复写一长串
from typing import Callable, Coroutine
TimeoutException = Type[BaseException] | BaseException
OnTimeoutCallback = Callable[..., Any] | Coroutine[Any, Any, Any]
# 现在 OnTimeoutCallback 就是一个简洁的类型名,可以在多处复用
为什么用 TypeAlias 而不直接赋值?
TypeAlias让类型检查器(如 mypy)将赋值识别为"类型别名定义"而非"普通变量",从而在错误信息中展示别名名称而非底层类型,让报错更可读。例如报错 “Expected UserId, got str” 比 “Expected int, got str” 有意义得多。
第二章:生成器 — Python 的"流式管道"
生成器是一种惰性计算的迭代器,用 yield 产出值,不一次性加载全部数据到内存,非常适合处理大型数据集或复杂的计算。
核心价值:处理大数据流时,生成器让你用"对流写代码"的方式替代"对全集写代码"——内存占用从 O(n) 降到 O(1),尤其适合 LLM 流式输出、大文件逐行读取、数据库分页查询等场景。
2.1 生成器函数
def fibonacci(n):
"""生成前 n 个斐波那契数"""
a, b = 0, 1
for _ in range(n):
yield a # 产出值,暂停执行,保存状态
a, b = b, a + b # 下次调用时从此处恢复
gen = fibonacci(5)
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3(协议终止)
# 再次使用生成器
for num in fibonacci(10):
print(num, end=' ') # 输出: 0 1 1 2 3 5 8 13 21 34
2.2 生成器表达式 vs 列表推导式
最常见的创建方式是生成器表达式,语法与列表推导式类似,但使用()而不是[]。
# 列表推导式:立即计算所有值,占用全部内存
squares_list = [x ** 2 for x in range(1_000_000)] # ~8MB
# 生成器表达式:惰性计算,几乎不占内存
squares_gen = (x ** 2 for x in range(1_000_000)) # ~120 字节
# 可以传给 sum/max/min/any/all 等,不创建中间列表
total = sum(x ** 2 for x in range(1_000_000))
2.3 yield from — 委托子生成器
def flatten(nested):
"""展平嵌套列表"""
for item in nested:
if isinstance(item, (list, tuple)):
yield from flatten(item) # 递归委托:逐层展开
else:
yield item
list(flatten([1, [2, [3, 4], 5], 6]))
# [1, 2, 3, 4, 5, 6]
2.4 send() — 生成器的双向通信
# 生成器不仅能产出值,还能从外部接收值
def accumulator():
total = 0
while True:
value = yield total # yield 右侧 = 产出的值
total += value # yield 左侧 = 接收 send() 的值
gen = accumulator()
next(gen) # 0(启动生成器)
gen.send(10) # 10(接收 10,产出累计值)
gen.send(20) # 30
gen.send(5) # 35
2.5 实战:LLM 流式输出的本质
几乎所有大语言模型 API(OpenAI、DeepSeek 等)的流式调用都依赖生成器模式:
"""流式聊天:逐块产出 LLM 响应"""
response = self.client.chat.completions.create(
model=self.model_name,
messages=history,
stream=True, # 关键:开启流式
**gen_conf
)
ans = ""
for resp in response:
if not resp.choices:
continue
delta = resp.choices[0].delta.content or ""
ans += delta
yield ans, len(ans) # 每次产出一段,外部逐字渲染
# 调用方:
for answer, length in model.chat_streamly(history, config):
print(f"\r{answer}", end="") # 打字机效果
2.6 掌握自测
生成器与
return一个 list 在内存和时序上有什么本质区别?yield from替代两层for循环的好处是什么?
第三章:装饰器 — 代码的"横切面加工"
Python 提供了多种装饰器,用于增强函数和方法的功能,简单来说,装饰器@的作用是在不改变其它函数的情况下,为其它函数增加额外的功能,常用于插入日志、性能测试、事务处理等等。
@property装饰器用于将类的方法转换为属性,使得可以像访问属性一样访问方法。@classmethod装饰器用于定义类方法。类方法的第一个参数必须是表示类本身的cls,而不是实例。@staticmethod装饰器用于定义静态方法。静态方法不依赖于类或实例,不需要传递self或cls参数。@abstractmethod装饰器用于定义抽象方法,这些方法必须在子类中实现。这个装饰器通常与abc模块中的ABC类一起使用。@functools.wraps装饰器用于编写装饰器时,保留被装饰函数的元数据(如函数名、文档字符串、注解等)。@dataclass装饰器用于自动生成类的初始化方法、比较方法等(init、repr、eq),使得定义数据类更加简单和高效。
装饰器本质是一个接收函数、返回函数的高阶函数。下面深入自定义装饰器。
实际场景:Web 框架中鉴权/限流/缓存/日志等横切关注点(cross-cutting concerns)几乎都是通过装饰器实现的。不理解装饰器就无法写出简洁的 Web 后端代码。
3.1 装饰器的本质
# 原理等价于:func = decorator(func)
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用前: {func.__name__}")
result = func(*args, **kwargs) # 调用原函数
print(f"调用后: {func.__name__}")
return result # 可以修改返回值
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("World")
# 调用前: say_hello
# Hello, World!
# 调用后: say_hello
3.2 @wraps — 保留函数"身份证"
问题:装饰后,函数的 __name__、__doc__ 等信息会丢失。
from functools import wraps
def my_decorator(func):
@wraps(func) # 必须加!把 func 的元信息复制到 wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello(name):
"""打招呼"""...
print(say_hello.__name__) # 有 @wraps → "say_hello",没有 → "wrapper"
print(say_hello.__doc__) # 有 @wraps → "打招呼",没有 → None
3.3 带参数的装饰器(三层嵌套)
from functools import wraps
import time
def retry(max_retries=3, delay=1.0):
"""带参数的装饰器 → 三层嵌套"""
def decorator(func): # 第二层:接收函数
@wraps(func)
def wrapper(*args, **kwargs): # 第三层:执行逻辑
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt < max_retries:
time.sleep(delay * (2 ** attempt)) # 指数退避
continue
raise
return wrapper
return decorator # 第一层:返回装饰器
@retry(max_retries=5, delay=0.5)
def call_api(url): ...
# 等价于: call_api = retry(max_retries=5, delay=0.5)(call_api)
记忆诀窍:带 () 的装饰器就是三层,不带 () 的就是两层。
3.4 装饰器组合:执行顺序
多个装饰器从下往上应用,从上往下执行:
@decorator_a # 第二步应用(最外层),执行时先进
@decorator_b # 第一步应用(最内层),执行时后进
def func(): ...
# 等价于: func = decorator_a(decorator_b(func))
# Web 框架中的典型组合:
@manager.route("/conversation/set", methods=["POST"]) # 路由注册
@login_required # 登录鉴权
@validate_request("name", "dialog_id") # 参数验证
def set_conversation():
...
3.5 实战:参数验证装饰器
这是 Web 框架中极常见的模式——装饰器在目标函数执行前拦截请求,验证参数:
from functools import wraps
from flask import request
def validate_request(*required_args):
"""验证请求中是否包含必需参数"""
def wrapper(func):
@wraps(func)
def decorated_function(*_args, **_kwargs):
input_arguments = request.json or request.form.to_dict()
missing = [arg for arg in required_args if arg not in input_arguments]
if missing:
return {"error": f"Missing: {missing}"}, 400
return func(*_args, **_kwargs)
return decorated_function
return wrapper
@manager.route("/site", methods=["POST"])
@validate_request("name", "url")
def create_site():
req = request.json
# 此时 name 和 url 一定存在
...
第四章:上下文管理器 — 资源的"自动门"
上下文管理器通过 with 语句实现资源的自动获取和释放,对应 Java 的 try-with-resources。核心价值是:无论代码正常执行还是抛出异常,资源都会被安全释放。
实际场景:数据库连接、文件操作、分布式锁、网络 socket——任何需要"打开/关闭"配对操作的资源都适合用上下文管理器。
4.1 __enter__ / __exit__ 协议
class DatabaseConnection:
def __init__(self, db_url):
self.db_url = db_url
self.conn = None
def __enter__(self):
"""进入 with 块时调用,返回值赋给 as 变量"""
self.conn = connect(self.db_url)
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
"""离开 with 块时调用(无论是否异常)"""
if self.conn:
self.conn.close() # 确保资源释放
return False # False/None → 异常继续传播
# True → 吞掉异常(慎用)
with DatabaseConnection("mysql://localhost/db") as conn:
conn.execute("SELECT 1")
# 离开 with 块后,__exit__ 自动调用
4.2 contextlib.contextmanager(推荐写法)
用生成器实现上下文管理器,比写类更简洁:
from contextlib import contextmanager
@contextmanager
def timer(label):
import time
start = time.time()
try:
yield # yield 之前 = __enter__
finally: # yield 之后 = __exit__
elapsed = time.time() - start
print(f"{label}: {elapsed:.2f}s")
with timer("数据处理"):
data = [x ** 2 for x in range(1_000_000)]
# 数据处理: 0.15s
4.3 contextlib 实用工具
from contextlib import suppress, ExitStack
# suppress — 忽略指定异常
with suppress(FileNotFoundError):
os.remove("temp.txt") # 文件不存在也不报错
# ExitStack — 动态管理多个上下文管理器
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in file_paths]
# 离开 with 块时,所有文件自动关闭
第五章:functools — 函数式编程工具箱
functools 提供高阶函数工具,让你以函数式风格写出更优雅的代码。
5.1 @lru_cache — 自动缓存
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 瞬间返回(有缓存时 O(1))
print(fibonacci.cache_info()) # CacheInfo(hits=98, misses=101, ...)
# 实际使用:缓存 API 调用结果(如 Slack workspace URL)
@lru_cache()
def get_base_url(token: str) -> str:
client = WebClient(token=token)
return client.auth_test()["url"]
5.2 partial — 函数部分应用
from functools import partial
# 预绑定部分参数,创建新函数
dumps_utf8 = partial(json.dumps, ensure_ascii=False)
dumps_pretty = partial(json.dumps, ensure_ascii=False, indent=2)
# 实际使用:全局替换 requests 的 JSON 编码器
requests.models.complexjson.dumps = functools.partial(
json.dumps, cls=CustomJSONEncoder
)
5.3 reduce — 归约操作
from functools import reduce
product = reduce(lambda x, y: x * y, [1, 2, 3, 4, 5]) # 120
# 等价于: ((((1 * 2) * 3) * 4) * 5)
# 拼接路径
path = reduce(lambda a, b: f"{a}/{b}", ["home", "user", "docs", "file.txt"])
# "home/user/docs/file.txt"
第六章:魔法方法 — 让对象"像内置类型一样工作"
基础篇已介绍 __init__、__str__,这里覆盖实际项目中最常用的魔法方法。每个魔法方法让你的类获得对应的内置语法糖——这就是 Python 的"duck typing"在对象层面的体现。
6.1 速查表(含 Java 对照)
| 方法 | 触发场景 | 对应 Java 概念 |
|---|---|---|
__call__ |
obj(...) 调用 |
@FunctionalInterface |
__getitem__ |
obj[key] |
Map.get() |
__setitem__ |
obj[key] = value |
Map.put() |
__contains__ |
item in obj |
contains() |
__iter__ / __next__ |
for x in obj |
Iterable / Iterator |
__len__ |
len(obj) |
size() |
__eq__ / __hash__ |
== / hash() |
equals() / hashCode() |
__enter__ / __exit__ |
with obj |
AutoCloseable |
6.2 __call__ — 让对象可像函数一样调用
# 图像处理管道:每个步骤都是可调用对象
# 这是一种优雅的"责任链模式"实现
class ResizeImage:
def __init__(self, target_size=(640, 640)):
self.target_size = target_size
def __call__(self, data):
"""使实例可像函数一样调用"""
data['image'] = self._resize(data['image'])
return data
class Pipeline:
def __init__(self, *steps):
self.steps = steps
def __call__(self, data):
for step in self.steps:
data = step(data) # 每个 step 都是可调用对象
return data
pipe = Pipeline(
ResizeImage((640, 640)),
NormalizeImage(mean=[0.485], std=[0.229]),
)
result = pipe(raw_data)
6.3 __getitem__ / __setitem__ — 索引访问
class SparseVector:
def __init__(self):
self._data = {}
def __getitem__(self, index: int) -> float:
return self._data.get(index, 0.0) # 不存在返回默认值
def __setitem__(self, index: int, value: float):
self._data[index] = value
def __contains__(self, index: int) -> bool:
return index in self._data
vec = SparseVector()
vec[0] = 0.5
vec[3] = 0.8
print(vec[0]) # 0.5
print(vec[1]) # 0.0(默认值)
print(3 in vec) # True
6.4 __iter__ / __next__ — 自定义迭代器
class PageIterator:
"""分页迭代器:逐页加载数据,对外像普通迭代器一样使用"""
def __init__(self, fetch_func, page_size=100):
self.fetch = fetch_func
self.page_size = page_size
self.current_page = 0
self.buffer = []
self.exhausted = False
def __iter__(self):
return self
def __next__(self):
if not self.buffer and not self.exhausted:
self.current_page += 1
self.buffer = self.fetch(self.current_page, self.page_size)
if not self.buffer:
self.exhausted = True
if not self.buffer:
raise StopIteration
return self.buffer.pop(0)
for item in PageIterator(fetch_from_db, page_size=50):
process(item)
第七章:pathlib — 面向对象的路径操作
pathlib 比传统的 os.path 更优雅,是 Python 3.4+ 推荐的路径操作方式。它以面向对象的 Path 类封装所有路径操作,替代了分散在各处的 os.path.xxx 函数。
实际场景:任何需要处理文件路径的项目(API 路由发现、配置文件定位、临时文件管理、文件批量处理)都应该统一使用 pathlib,避免字符串拼接路径带来的跨平台问题。
7.1 基本操作
from pathlib import Path
# 创建路径对象
p = Path.home() # 用户主目录
p = Path.cwd() # 当前工作目录
# 路径拼接(用 / 运算符,跨平台安全)
config = Path("conf") / "service_conf.yaml"
# Windows → conf\service_conf.yaml
# Linux → conf/service_conf.yaml
# 路径属性
p = Path("/home/user/docs/file.txt")
print(p.name) # file.txt
print(p.stem) # file(不含扩展名)
print(p.suffix) # .txt
print(p.parent) # /home/user/docs
print(p.parts) # ('/', 'home', 'user', 'docs', 'file.txt')
# 判断
p.exists() / p.is_file() / p.is_dir()
7.2 文件读写与目录遍历
# Path 对象自带读写方法
p = Path("output.txt")
p.write_text("Hello", encoding="utf-8")
content = p.read_text(encoding="utf-8")
# 创建目录
Path("data/cache").mkdir(parents=True, exist_ok=True)
# glob 模式匹配
for f in Path(".").glob("*.py"): # 当前目录所有 .py 文件
print(f)
for f in Path(".").rglob("*.py"): # 递归搜索所有子目录
print(f)
# 实际使用:自动发现所有 API 路由文件
pages_dir = Path(__file__).parent
for path in pages_dir.glob("*_app.py"):
if not path.name.startswith("."):
print(f"Found route: {path.stem}")
第八章:海象运算符 :=
Python 3.8+ 引入的赋值表达式,在表达式内部赋值,减少重复代码。
# 传统写法:先赋值再判断
line = input()
while line != "quit":
process(line)
line = input()
# 海象运算符:在条件中赋值
while (line := input()) != "quit":
process(line)
# 在 if 语句中:避免重复 .get()
if (value := req.get("name")):
use(value)
# 在列表推导式中:避免重复计算
results = [
email for owner in file.get("owners", [])
if (email := owner.get("emailAddress"))
and "@" in email
]
# 组合使用:一次遍历,避免重复调用
upper_words = [
wu for w in text.split()
if len(w) > 3 and (wu := w.upper())
]
适用场景:所有需要"先取一个值,然后判断这个值"的地方都可以用海象运算符简化。注意:不要滥用——如果赋值和使用的距离很远,老老实实分开写更清晰。
本篇自检清单
学完本篇后确认以下问题能答上来:
- 类型提示:
Optional[str]vsstr | None区别?TypeVar+Generic怎么配合?Optional[str]与str | None:两者完全等价,都表示该变量可以是字符串或None。区别仅在于语法版本:Optional是 Python 3.5+ 的写法,str | None是 Python 3.10+ 引入的更简洁的联合类型语法。TypeVar+Generic配合:TypeVar用于定义一个泛型类型占位符(如T),Generic[T]用于让类继承该占位符,从而创建参数化容器。例如class Stack(Generic[T])允许在实例化时指定具体类型(如Stack[int]),保证容器内数据类型的统一。
- 生成器:
yieldvsreturn的本质区别?生成器如何实现流式输出?yieldvsreturn:return会彻底终止函数并返回一个值;yield则是暂停函数执行并返回一个值,保留函数的内部状态,下次调用时从暂停处继续执行。- 流式输出:生成器采用惰性计算(按需生成),不会一次性将所有数据加载到内存中。通过
for循环或next()逐个获取数据,非常适合处理大文件或无限数据流。
- 装饰器:带参数装饰器为什么三层嵌套?
@wraps不加会出什么问题?- 三层嵌套的原因:当装饰器自身需要接收参数时,最外层接收装饰器参数,中间层接收被装饰的函数,最内层(wrapper)接收被装饰函数的参数并执行实际逻辑。
- 不加
@wraps的后果:被装饰函数的元数据(如__name__、__doc__)会被替换为 wrapper 函数的元数据,导致调试困难、文档丢失,以及某些依赖函数名的框架失效。
- 上下文管理器:
__enter__返回值去哪了?__exit__返回True会怎样?__enter__返回值去向:它的返回值会被赋给with ... as <变量>中的<变量>。__exit__返回True:表示抑制(吞掉)异常。即使with代码块内发生了异常,只要__exit__返回True,异常就不会向外抛出,程序会继续执行with块之后的代码。
- 魔法方法:
__call__和__getitem__分别让对象支持什么语法?__call__:让对象实例支持函数调用语法(即obj()),使其表现得像一个可调用对象(Callable)。__getitem__:让对象支持索引和切片语法(即obj[key]或obj[start:end]),使其表现得像一个容器或序列。
- functools:
@lru_cache的本质是什么?partial和柯里化有什么关系?@lru_cache的本质:它是一个基于字典的记忆化(Memoization)缓存装饰器。通过记录函数的参数和返回值,在下次遇到相同参数时直接返回缓存结果,避免重复计算,常用于加速递归或耗时函数。partial与柯里化:partial是柯里化(Currying)在 Python 中的一种部分实现。它通过固定函数的部分参数来生成一个新函数。区别在于:标准的柯里化是每次只固定一个参数并返回新函数,而partial可以一次性固定任意数量的参数。
相关系列文章
- 「学习笔记」Python 进阶(三):工程实战 — 从脚本到项目
- 「学习笔记」Python 进阶(二):抽象设计 — 构建优雅系统
- 「学习笔记」Python 进阶(一):语言特质 — 写出地道 Python
- 「学习笔记」Python基础入门