说明

# 主题
python 语言特性

# 介绍
python 的个人使用经验;
总结了 python 核心特性,按照概念、总结、示例的顺序逐步说明;
不涉及 python 源码实现,均为 python 代码示例;

对象

  • 概念
# 对象
python 中的一切都是对象:
- 所有数据都是对象
- 所有类型本身也是对象
- 函数、类、模块甚至代码块也是对象

python 中的任何东西都可以被赋值给变量、作为参数传递、或者作为返回值返回;

# 引用
所有的变量都是对象的引用(类似别名),不直接存储对象(不会涉及内存操作);
  • 总结
在做大型项目的开发时,需要考虑对象的生命周期以及读写控制;

* 生命周期
只要引用存在,对象就不会回收(循环引用除外);
【建议】尽量避免使用全局变量;
  
* 读写控制
所有变量引用的都是同一个对象,容易修改;
【建议】尽量提供接口控制,合理规划读写场景;
  • 示例
# 示例:内置方法

a = 1
assert type(a) is int # 查看实例的类
assert a is a  # 判断是否为同一个对象
print(dir(a))  # 查看实例上定义的属性和方法
print(id(a))  # 进程内唯一标识(虚拟地址)
# 示例:将函数作为参数传递,并作为返回值返回

def a():
    pass

def b(arg):
    return arg

assert b(a) is a  # 同一个对象

# 示例:将函数赋给变量
c = b
assert b is c # 同一个对象

assert c(a) is a # 等价于 b
# 示例:返回函数内的对象,在函数外使用

id1 = None

def c():
    global id1
    s = 'a_string'
    id1 = id(s)
    return s 

assert id(c()) == id1  # 同一个对象

多态

  • 概念
# 多态
多态是一种编程语言能力,将算法与类型解耦,提高代码复用性;

通用分类:
- 函数重载
- 继承/接口
- 泛型(编译时类型参数化)与模板(编译时生成代码)

python 实现:
通过鸭子类型实现多态,不需要类型定义,只需要运行时行为;
具体而言,不在编译阶段检查对象类型,而是在运行时,判断对象是否具备执行的能力;

# 类型注释
背景:由于 python 的多态能力,编写 python 代码时对象是不需要定义类型的,增加了理解代码的难度;

通过类型注释可以标注类型,类型注释不影响代码执行逻辑;
  • 总结
python 的多态不需要提前定义接口或抽象类,实现起来语法简单;

类型注释可以触发静态语法检查,加强 ide 功能,包括:
- 校验参数类型,例如,对象属性及方法的访问和使用
- 编码过程自动触发语法提示
  • 示例
# 示例:多态实现

# 1. 定义出处理逻辑
def func(obj):
    res = obj + 1
    res.double()
    return str(res)

# 2. 实现满足该处理逻辑/行为的对象
class A:

    def __init__(self, data):
        self.data = data

    def __add__(self, other):
        # 重载运算符
        self.data += 1
        return self  # 返回同一个对象

    def double(self):
        self.data *= 2

    def __str__(self):
        # 重载 str 函数
        return f'data: {self.data}'

a = A(1)
assert func(a) == 'data: 4'
# 示例:类型注释

import typing

class A:
    
    def __init__(self, data):
        self.data = data

# 无类型注释
# 编写过程中没有任何提示,编写完成后能理解部分含义;
def func1(mapping):
    for k, v in mapping.items():
        print(k, v.data)

# 有类型注释
# 编写过程提供语法提示;编写完成后提供静态语法检查;代码阅读时可以跳转;
def func2(mapping: typing.Dict[str, A]) -> None:
    for k, v in mapping.items():
        print(k, v.data)

m = {
    'x': A(1),
    'y': A(2),
}
# 静态语法检查会检查传参类型
func2(m)

内存管理

  • 概念
# 内存分配
python 中一切皆为对象,创建对象以及回收对象都是自动管理的,不需要显式处理;

# 垃圾回收
引用计数:对象有新引用时加一,引用它的对象被删除时减一,引用计算为 0 时回收;
循环引用:通过遍历所有对象,不可达对象为循环引用,回收;
分代回收:根据对象存活时间,触发回收逻辑(存活越久,回收频率越低);

# 生命周期
一般回收对象主要考虑引用计数为 0 的场景,包括:
- 函数结束后,销毁局部变量
- 变量不再指向原对象
- 删除引用(del)
- 程序结束
  • 总结
简单来说,对象的生命周期,就是从创建该对象开始,到删除对它的所有引用;
  • 示例
# 示例:生命周期

def func1():
    # 该字典对象持续到函数结束 
    a = dict()
    a['x'] = 1
    print(a)

def func2():
    a = dict()
    a['x'] = 1
    
    # 替换变量引用的对象,导致上一个对象的引用计数清零,被回收
    a = set()
    print(a)

模块

  • 概念
# 代码组织方式
module:单个 py 文件
package:包含一个或多个 py 文件的文件夹(可嵌套)

# 启动 python 程序
执行 python main.py 时:
1. 操作系统启动 python 解释器进程
2. 创建一个主线程和一个解释器实例
3. 解释器读取并编译 main.py
4. 从上到下执行该模块 main 的所有顶层代码

顶层代码包括:
import、类定义、函数定义、全局变量、其它代码

# 模块
文件层面:一个 py 文件;
运行层面:执行 import 流程的结果,是一个 module 类型的实例;

# import 流程
import 会加载模块,执行模块顶层代码,生成 module 对象,并绑定到当前命名空间; 
- 导入文件会执行/编译文件内容;
- 导入文件夹会执行/编译 __init__.py 文件内容;

本地模块的通用导入流程:
> 根据导入名称查找内存,如果有缓存的模块对象,则直接返回(在当前命名空间创建变量引用该模块对象)
> 根据搜索路径(文件夹)查找模块(文件/文件夹)
> 如果没有模块则拋出导入异常,中断导入流程
> 如果模块有字节码缓存文件,直接加载并返回
> 执行加载过程,将模块对应的 py 文件编译为字节码,生成字节码缓存文件,创建模块对象写入内存,返回
  • 总结
# 模块及模块全局变量
模块导入后会缓存在内存中,重复 import 会尝试从缓存中读取已加载的模块,所以只会执行一次导入流程;
模块中创建的对象(全局变量),生命周期是从导入模块开始,到程序结束的;

# 线程安全
模块是天然的单例模式(顶层代码只会执行一次且缓存在内存中),在多线程中使用模块级别的变量,需要考虑并发问题(线程安全);
跨线程使用的模块,需要确认模块是否线程安全;
  • 示例
# 示例,模块级别定义的变量

# -----------------
# code1.py 文件内容如下:
# -----------------

# 导入模块会执行从上到下执行所有顶层代码

# 顶层代码:全局变量
x = {}

# 顶层代码:类定义
class A(object):

    # 类定义包括类变量,所以类变量也是全局变量
    data = {}
    
    def log(self):
        print(self.__class__.data)

# 顶层代码:函数定义
def func():
    pass

# 顶层代码:其它代码;
# 由于模块的缓存机制,重复导入该模块也只会执行一次
print('import one')
# ---------------------
# code2.py 文件内容如下:
# ---------------------
import code1

# 模块可以重复导入,但是只会执行一次;
import code1

print(code1.x)
print(code1.A.data)
# ---------------------
# code3.py 文件内容如下:
# ---------------------

# 同一个进程中导入相同的模块,访问的是相同的模块对象;
import code1
import code2

# code2 访问的对象和 code3 访问的是同一个 code1 模块
print(id(code1))
print(id(code2.code1))
# 示例,线程安全的模块,logging

import logging

# logging 模块中全局的顶层 logger
print(logging.root)

# logging 模块中全局的 manager
print(logging.root.manager)

# logging 对共享资源加锁
# logging/__init__.py:l217

# logging 模块中提供的配置函数,可以配置全局默认的日志处理器
# 加锁,配置 root logger 的 handler,最后释放锁
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(module)s.%(funcName)s] %(message)s',
)

# [2026-03-07 15:10:01,800] [INFO] [root] [test.<module>] info 2
logging.info('info')

进程与线程

  • 概念
# 进程与线程
通用概念:
- 进程是资源分配的基本单位;
- 线程是调度执行的基本单位;

# 性能
gil(global interpreter lock),全局解释器锁是进程持有的,线程执行代码前需要先获取 gil;
一个进程可以有多个线程,由于 gil,同一时刻只允许一个线程执行 python 字节码,只能有效利用一个 cpu 核心;

io 操作会释放 gil 锁,所以线程能够处理 io 密集型任务,不适合 cpu 密集任务; 
现在 io 密集型任务一般使用异步 io 来实现;

# 资源
进程中的资源是线程共享的,如果是多线程服务,则需要考虑并发问题(对共享资源加锁);
多进程的资源是互相独立的,如果创建多进程服务,需要考虑资源共享和通信问题;

# 线程安全
线程安全:
多线程并发访问同一份数据时,程序能够正确运行,不会产生错误结果;

基本概念:
- 临界区:多个线程并发访问,并且至少有一个线程会修改的代码区域
- 竞态条件:依赖于执行顺序,会产生不同的结果

实现线程安全的办法,核心原理是破坏并发读写条件

* 限制并发:对象不跨线程访问
- 加锁,限制临界区的访问
- thread local
- (不访问/引用全局变量的)局部变量,例如,工具包
- 通过线程安全的对象传递,限制同一时刻只有一个线程在访问,例如,生产消费模型中的队列

* 限制读写:对象只读
- 代码中约定只有读操作,例如,在多线程服务启动前准备好对象,不再修改
- 对象本身不可修改,限制了只读,例如,元组、字符串等等
  • 总结
python 中的多进程在不同平台(windows/linux)上实现不同,非必要不使用多进程并发;
即使是多进程服务,一般也是框架实现,自己编写多进程服务的应用场景比较少;

python 进程默认只有一个主线程,如果需要启动新的线程,就需要考虑并发问题(线程安全等等);
python 模块不一定都是线程安全的,也不一定都要求线程安全;

现有数据中心接触的业务,基本都是 io 密集型,优先选用多线程实现并发:
  • 示例
# 示例,多线程应用中的配置管理:约定对象只读,不需要加锁

import threading

def load_configure():
    # 模拟配置加载过程
    return {0: {}, 1: {}, 2: {}}

def handler(cf, i):
    # 只读
    print(cf.get(i))

def main():
    # 提前加载配置
    configure = load_configure()

    ts = []
    for i in range(3):
        ts.append(threading.Thread(target=handler, args=(configure, i)))

    for t in ts:
        t.start()

    for t in ts:
        t.join()
# 示例,定时任务框架 apscheduler 配置,使用线程池执行器

# apscheduler 同时支持线程池与进程池执行器,当前使用线程池执行器;

gconfig = {
    'apscheduler.jobstores.default': {
        'type': 'memory',  # 无持久化
    },
    'apscheduler.executors.default': {
        'class': 'apscheduler.executors.pool:ThreadPoolExecutor',  # 线程池
        'max_workers': '20',  # 定时任务最大并发
    },
    'apscheduler.job_defaults.max_instances': '1',  # 同一个任务最大并发执行数量
    'apscheduler.timezone': 'Asia/Shanghai',  # 时区
}

生成器

  • 概念
# 生成器
生成器是 python 中的一种惰性迭代机制,用于按需生成数据,而不是一次性生成全部数据。
生成器也是迭代器(实现了迭代器协议);
生成器基于 yield 语法;
  • 总结
基于生成器可以实现一些内存友好的代码,例如流式处理大数据;
  • 示例
# 示例,生成器的逐步执行机制

# 方式一:通过 next 方法单步执行

def gen():
    yield 1
    yield 2
    yield 3

g = gen()
assert next(g) == 1  # 执行到 yield value 后暂停
assert next(g) == 2
assert next(g) == 3

# 生成器执行完成后,拋出异常
try:
    next(g)
except StopIteration:
    pass
# 示例,生成器的逐步执行机制

# 方式二:通过调用生成器的 send 方法单步执行

def gen():
    val = yield 1
    print(val)  # first
    val = yield 2
    print(val)  # second
    val = yield 3
    print(val)  # third

# next(g) 方法相当于 g.send(None)
g = gen()
assert g.send(None) == 1  # 启动生成器只能传参 None,并且忽略 None 值
assert g.send('first') == 2
assert g.send('second') == 3
try:
    g.send('third')
except StopIteration:
    pass
# 示例,生成器也是迭代器

def iterator(n):
    # [0, 1, ..., n-1]
    return list(range(n))

def generator(n):
    for i in range(n):
        yield i

def run():
    n = 3
    
    # 生成器也实现了迭代器协议
    for i in generator(n):
        print(i)
    
    for i in iterator(n):  
        print(i)
# 示例:从大文件中读取数据,流式处理

def load1(file):
    """读取文件:逐行读取文件,通过列表返回"""
    # open 读取文件提供了逐行读取的方式,避免一次性加载整个文件到内存中
    with open(file, 'r', encoding='utf-8') as f:
        data = []
        for line in f:
            data.append(line)
    return data

def load2(file):
    """读取文件:逐行读取文件,并通过生成器返回"""
    with open(file, 'r', encoding='utf-8') as f:
        for line in f:
            yield line

def stats(file):
    """统计文件中单词的出现频率"""
    
    # 生成器也实现了迭代器协议,可以用 for 遍历
    # iterator = load1(file)
    iterator = load2(file)
    
    res = {}
    for line in iterator:
        words = line.strip().split(' ')
        for word in words:
            res[word] = res.get(word, 0) + 1
    return res

# 使用列表,该程序最大占用内存为,文件大小加上统计字典大小
# 使用生成器,忽略单行字符串的大小,该程序最大占用内存为,统计字典大小
# 示例,从数据库中流式读取数据,并流式处理

# pymysql:mysql 数据库驱动
import pymysql.cursors

def iter_select():
    # 流式获取数据,并且逐行返回,内存最大占用为单次批量数据大小
    conn = pymysql.connect(
        host='localhost',
        user='root',
        password='123456',
        database='test',
        cursorclass=pymysql.cursors.SSCursor,  # 指定流式游标
    )

    try:
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM big_table")

            while True:
                # 流式游标:每次实时从数据库获取批量数据
                # 默认游标:一次性从数据库获取所有数据,在内存中分批后再逐批返回
                rows = cursor.fetchmany(1000)  # 每次拉 1000 条
                if not rows:
                    break

                for row in rows:
                    yield row  # 每次返回 1 条
    finally:
        conn.close()

def run():
    # 对于主程序而言,不用关系底层是如何获取数据的,支持遍历的方式
    for row in iter_select():
        print(row)  # 模拟处理函数 

异步

  • 概念
# 协程
一般概念:
比线程更轻量的并发单位,在同一个线程内执行,可以中途暂停和恢复执行;
协程是协作式调度(区别于抢占式),自己决定什么时候挂起,让出执行权;

在 python 中:
协程基于生成器实现,提供暂停与恢复的功能;
协程本身不包含调度逻辑;

# 异步 io
* 异步 io
python 中的异步一般是指的异步 io;
单个线程内,基于协程和事件循环的并发模型,大部分代码是用户态的代码(非系统调用);

* 协程对象
所有的函数被封装成一个协程对象,可以单步执行,提供挂起和恢复机制;

* 事件循环
io 事件循环中注册了所有的 io 读写事件,阻塞至事件发生,然后取出所有就绪的事件,逐个处理;

# 异步 io 处理逻辑
忽略定时调度、非 io 事件任务;

* 事件循环调度
step 1: 
- 逐个处理就绪队列中的任务
step 2: 
- 如果有 io 事件,阻塞至事件发生
- 如果没有 io 事件,结束事件循环
step 3:
- 某个事件发生,将所有就绪的事件对应的回调(可能有多个),放入就绪队列

* 处理流程
事件循环启动,主协程放入就绪队列,执行事件循环调度;

* 注意
所有的调度都在单个线程内,同一时刻只会有一个协程在执行;
io 操作由操作系统执行,不会影响当前线程执行;

# 异步 io 中的定时器
事件循环与定时器是分离实现的;
定时器中记录了未来时间点要触发的回调;

* 定时器与事件循环协作
事件循环会阻塞至下一个定时任务开始(计算 timeout)
  • 总结
异步 io 执行过程是单线程的,不适合处理 cpu 密集的任务;

协程中的 io 操作必须使用支持事件循环的异步接口(awaitable),否则会阻塞事件循环,失去异步并发能力;
例如,异步 http 请求包 aiohttp、异步 mysql 驱动包 aiomysql 等等;

在异步代码中要注意避免 cpu 密集的任务,如果必须,可以尝试使用额外的线程池来处理;
# 示例,使用异步接口
import time
import asyncio

async def func1():
    """非异步 sleep"""
    time.sleep(1)  # 系统实现,不占用 cpu,但会阻塞线程

async def func2():
    """异步 sleep"""
    await asyncio.sleep(1)  # 用户代码实现,会挂起协程,不阻塞线程(被调度)

async def main():
    # await func1()
    await func2()

# 底层会启动一个事件循环,并执行主协程
asyncio.run(main())
# 示例,同步与异步 http 请求

# 同步请求:requests
import requests

def fetch(url):
    resp = requests.get(url)
    return resp.text

def main():
    html = fetch('https://httpbin.org./get')
    print(html)

main()
# 示例,同步与异步 http 请求

# 异步请求:aiohttp
import aiohttp
import asyncio

async def fetch(url):
    # 异步 with 上下文
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    html = await fetch('https://httpbin.org/get')
    print(html)

asyncio.run(main())

更多推荐