服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 06月2日 23:06

TypeORM 实体关系怎么定义?一对一、一对多、多对多装饰器详解

TypeORM 用装饰器定义实体间的关系:一对一、一对多、多对多。关键是搞清谁是"拥有方"(存外键的一方),谁是"反方"(只声明关系不存外键)。一对一(OneToOne)一个人只有一个档案,一个档案只属于一个人:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @OneToOne(() => Profile, profile => profile.user) @JoinColumn() // 外键加在这一侧(拥有方) profile: Profile;}@Entity()export class Profile { @PrimaryGeneratedColumn() id: number; @OneToOne(() => User, user => user.profile) user: User; // 反方,不加 @JoinColumn}@JoinColumn() 标记拥有方——数据库会在 User 表加一个 profileId 外键列。一对一关系只有一方能加 @JoinColumn,另一方是反向引用。一对多 / 多对一(OneToMany / ManyToOne)一个用户有多篇文章,一篇文章属于一个用户:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @OneToMany(() => Post, post => post.author) posts: Post[];}@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @ManyToOne(() => User, user => user.posts) @JoinColumn({ name: 'authorId' }) // 外键在 Post 表 author: User;}ManyToOne 总是拥有方——外键存在"多"的一侧的表里。OneToMany 只是反向引用,不能单独存在,必须配对 ManyToOne。数据库里 Post 表有一个 authorId 列指向 User.id。多对多(ManyToMany)一个文章有多个标签,一个标签属于多篇文章:@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @ManyToMany(() => Tag, tag => tag.posts) @JoinTable() // 拥有方加 @JoinTable,自动创建中间表 tags: Tag[];}@Entity()export class Tag { @PrimaryGeneratedColumn() id: number; @ManyToMany(() => Post, post => post.tags) posts: Post[]; // 反方,不加 @JoinTable}@JoinTable() 让 TypeORM 自动创建中间表 post_tags_tags(postid + tagid)。只有一方加 @JoinTable,另一方是反向引用。加载关联数据定义了关系不代表查询时自动加载。必须显式指定:// 方式一:find 时指定 relationsconst user = await userRepo.findOne({ where: { id: 1 }, relations: ['posts', 'posts.tags'] // 支持嵌套});// 方式二:QueryBuilder joinconst user = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.tags', 'tag') .where('user.id = :id', { id: 1 }) .getOne();不加 relations 或 leftJoinAndSelect,返回的 user.posts 就是 undefined。这是 TypeORM 的设计选择——避免无意义的 JOIN 查询。常见坑循环引用:User 引用 Post,Post 引用 User。TypeScript 用 () => Post 延迟求值避免循环依赖。不要直接写 Post,必须用箭头函数。级联操作:cascade: true 让保存 User 时自动保存关联的 Post。小心使用——可能在你不知情时写入大量数据。建议只在明确的父子关系上使用。删除行为:onDelete: 'CASCADE' 在数据库层面级联删除。删除 User 时自动删除其所有 Post。比应用层级联更可靠。
服务端阅读 06月2日 23:04

TypeORM Active Record 和 Data Mapper 有什么区别?怎么选?

TypeORM 支持两种数据访问模式:Active Record(实体自带 CRUD 方法)和 Data Mapper(通过 Repository 操作实体)。选哪种影响代码组织方式,但不影响功能。Active Record 模式实体继承 BaseEntity,直接调用静态方法操作数据:@Entity()export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string;}// 使用:实体自带 find/save/removeconst users = await User.find();const user = await User.findOne({ where: { email: 'test@test.com' } });user.name = 'Updated';await user.save();await user.remove();优点:代码简洁,一个对象既承载数据又负责持久化。适合简单 CRUD 和小项目。缺点:实体和数据库操作耦合——同一个类既定义数据结构又包含查询逻辑,业务复杂后类会膨胀。Data Mapper 模式实体是纯数据对象,通过 Repository 操作:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string;}// 使用:通过 Repository 操作const userRepo = dataSource.getRepository(User);const users = await userRepo.find();const user = await userRepo.findOne({ where: { email: 'test@test.com' } });user.name = 'Updated';await userRepo.save(user);await userRepo.remove(user);优点:关注点分离——实体只管数据结构,Repository 负责持久化逻辑。业务代码不依赖数据库 API,方便单元测试(mock Repository)。缺点:多一层间接,简单操作代码量稍多。怎么选Active Record 适合:小项目,实体少,查询逻辑简单快速原型,不想写太多样板代码Rails/Django 背景,习惯模型自带操作Data Mapper 适合:中大型项目,业务逻辑复杂需要单元测试,要 mock 数据层领域驱动设计(DDD)风格,实体只表达业务概念NestJS 项目推荐 Data Mapper——NestJS 的 @InjectRepository() 天然就是 Data Mapper 风格,依赖注入也让测试更方便。两种模式可以混用TypeORM 不强制二选一。同一个实体可以同时用两种方式:@Entity()export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; // Active Record 方式 static findActive() { return this.find({ where: { active: true } }); }}// Data Mapper 方式也可以const repo = dataSource.getRepository(User);const users = await repo.find({ where: { active: true } });但不建议混用——团队应该统一风格,避免同一项目里两种模式交叉出现。自定义查询放哪里Active Record 模式的常见问题:复杂查询方法堆积在实体类上。// 不推荐:实体类越来越胖export class User extends BaseEntity { static findWithStats() { ... } static findActiveUsers() { ... } static findByDepartment() { ... } static searchByName() { ... }}// 推荐:抽取到单独的 Service 或自定义 Repositoryexport class UserService { constructor(@InjectRepository(User) private repo: Repository<User>) {} async findWithStats() { return this.repo.createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .loadRelationCountAndMap('user.postCount', 'user.posts') .getMany(); }}Data Mapper 模式自然避免这个问题——查询逻辑在 Service 里,实体保持干净。
服务端阅读 06月2日 01:50

TypeORM 查询缓存怎么配?Redis 缓存和缓存一致性实战

数据库查询是后端应用最常见的性能瓶颈。缓存是第一道防线——能从缓存拿的数据就不查数据库。TypeORM 支持查询级缓存和 Redis 缓存,配置简单但有几个细节要注意。TypeORM 查询缓存开启缓存后,相同的 SQL 查询结果会被缓存,指定时间内不重复查数据库:TypeOrmModule.forRoot({ type: 'postgres', cache: { type: 'redis', options: { host: 'localhost', port: 6379, }, duration: 30000, // 缓存 30 秒 },}),在查询时指定缓存:const users = await userRepo.find({ cache: true, // 使用全局 duration(30 秒)});// 或自定义缓存时间const users = await userRepo.find({ cache: 60000, // 缓存 60 秒});// QueryBuilder 也可以const users = await createQueryBuilder('user') .cache(true) .getMany();缓存 key 基于 SQL 语句自动生成——相同 SQL 共享缓存,不同 SQL 各自缓存。什么时候用缓存适合缓存的查询:配置数据(很少变)用户信息(变更频率低)热门文章列表(可以接受短暂不一致)不适合缓存的查询:实时数据(库存、余额)分页偏移大的查询(缓存命中率低)写多读少的数据(缓存频繁失效)缓存一致性缓存的经典问题:数据库更新了但缓存还是旧数据。TypeORM 不会自动在数据变更时清除缓存——需要手动处理。方案一:写入后清除缓存async function updateUser(id: number, data: Partial<User>) { await userRepo.update(id, data); // 清除与 User 相关的所有缓存 await getConnection().queryResultCache.clear();}queryResultCache.clear() 清除所有缓存,比较粗暴但简单。精细清除需要知道具体的缓存 key,TypeORM 没有直接提供按表清除的 API。方案二:短过期时间 + 接受短暂不一致const users = await userRepo.find({ cache: 5000, // 5 秒过期,最多延迟 5 秒});简单有效,大部分场景够用。5 秒的不一致对用户体验几乎没有影响。方案三:手动管理缓存(Redis 直接操作)绕过 TypeORM 的缓存机制,用 ioredis 自己管理:import Redis from 'ioredis';const redis = new Redis();async function getUser(id: string) { const cached = await redis.get(`user:${id}`); if (cached) return JSON.parse(cached); const user = await userRepo.findOne(id); await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 60); return user;}async function updateUser(id: string, data: Partial<User>) { await userRepo.update(id, data); await redis.del(`user:${id}`); // 精确清除}自己管理更灵活——可以按 key 精确清除、设置不同过期时间、做缓存预热。但需要更多代码。缓存命中率监控Redis 缓存命中率反映了缓存是否有效:redis-cli info stats | grep keyspace_hitsredis-cli info stats | grep keyspace_misses命中率低于 50% 说明缓存策略有问题——要么过期时间太短,要么查询太分散没有复用。TypeORM 缓存的局限缓存粒度是 SQL 级别,不是实体级别。同一个 User 的不同查询(列表查询 vs 详情查询)有各自独立的缓存没有 Cache Aside 模式的内置支持(先查缓存再查数据库)分布式环境下需要用 Redis 共享缓存,内存缓存(默认)会导致各实例缓存不一致如果缓存需求复杂,建议绕过 TypeORM 缓存,直接用 Redis + 自定义缓存层。
服务端阅读 06月2日 01:49

TypeORM 软删除怎么用?@DeleteDateColumn 配置和唯一约束冲突解决

软删除不是真的删除数据,而是标记 deletedAt 字段,查询时自动过滤已删除的记录。TypeORM 内置支持软删除,一行配置开启。开启软删除在实体上加 @DeleteDateColumn():@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date; // null = 未删除,有值 = 已删除}delete 操作变成 UPDATE:await userRepo.softDelete(1);// SQL: UPDATE user SET "deletedAt" = NOW() WHERE id = 1await userRepo.remove(user);// 同样效果,设置 deletedAt 而不是 DELETEfind 自动排除已删除记录:const users = await userRepo.find();// SQL: SELECT * FROM user WHERE "deletedAt" IS NULL查询包含已删除的记录// 包含已删除的记录const allUsers = await userRepo.find({ withDeleted: true});// 只查已删除的记录const deletedUsers = await userRepo.find({ where: { deletedAt: Not(IsNull()) }, withDeleted: true});withDeleted: true 告诉 TypeORM 不要加 WHERE "deletedAt" IS NULL 条件。恢复已删除的记录await userRepo.restore(1);// SQL: UPDATE user SET "deletedAt" = NULL WHERE id = 1restore 把 deletedAt 设回 NULL,记录恢复正常。软删除的坑1. 唯一约束冲突软删除后,唯一字段(如 email)仍然占用唯一约束。删除 user@email.com 后,新建同名用户会报唯一冲突。解决方案:唯一约束包含 deletedAt。@Entity()@Unique(['email', 'deletedAt'])export class User { @Column() email: string; @DeleteDateColumn() deletedAt: Date;}但这样未删除记录的 deletedAt 是 NULL,多条 NULL 在 PostgreSQL 的唯一约束里不冲突(符合 SQL 标准),MySQL 则需要用条件索引或去掉唯一约束。2. 关联数据不会级联软删除@OneToMany(() => Post, post => post.author)posts: Post[];软删除 User 时,Post 不会被自动软删除。需要手动处理:async function softDeleteUser(id: number) { await postRepo.softDelete({ authorId: id }); await userRepo.softDelete(id);}3. 物理删除仍然是需要的软删除的数据积累会膨胀表。定期清理:// 物理删除 30 天前软删除的记录await userRepo .createQueryBuilder() .delete() .where("deletedAt < :date", { date: new Date(Date.now() - 30 * 86400000) }) .execute();不用软删除的替代方案事件溯源:不删除数据,而是追加"已删除"事件。查询时通过事件回放构建当前状态归档表:DELETE 前把数据 INSERT 到归档表,然后物理删除。查询只看主表,归档表做审计软删除适合"需要恢复、需要审计追踪"的场景。如果只是为了安全,备份 + 物理删除更干净。
服务端阅读 06月2日 01:49

Node.js ORM 怎么选?TypeORM、Prisma、Sequelize、MikroORM 对比

Node.js 的 ORM 主要有四个:TypeORM、Prisma、Sequelize、MikroORM。2025 年的格局:Prisma 增长最快,TypeORM 用量最大但有退潮趋势,Sequelize 已经老化,MikroORM 是小众但口碑好。四个 ORM 对比| 维度 | TypeORM | Prisma | Sequelize | MikroORM ||------|---------|--------|-----------|----------|| 类型安全 | 弱(装饰器 + any) | 强(自动生成类型) | 弱 | 强 || 查询方式 | 装饰器 + QueryBuilder | 链式 API | 链式 API | 链式 API + QB || 迁移工具 | 内置,体验一般 | 最好 | 内置 | 内置 || 关联查询 | 容易 N+1 | include 清晰 | include | 自动 JOIN || 社区 | 最大 | 增长最快 | 缩小中 | 小但活跃 || 学习曲线 | 中 | 低 | 高 | 中 || NestJS 集成 | 官方推荐 | 社区包 | 社区包 | 社区包 |Prisma:类型安全的赢家Prisma 的核心优势是类型推导——schema 定义一次,查询自动补全、返回类型自动推导,几乎不需要手写类型。model User { id String @id @default(uuid()) email String @unique posts Post[]}const user = await prisma.user.findUnique({ where: { email: 'test@test.com' }, include: { posts: true }});// user 的类型自动包含 posts,不需要手动标注迁移体验也是最好的:npx prisma migrate dev --name init 自动生成迁移 SQL,prisma migrate deploy 在生产环境执行。Prisma 的缺点:schema 用自己的 DSL(不是 TypeScript),复杂查询要写 raw SQL,N+1 问题用 include 解决但不如 DataLoader 灵活。TypeORM:装饰器风格的老牌TypeORM 用装饰器定义实体,和 NestJS 风格统一:@Entity()export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) email: string; @OneToMany(() => Post, post => post.author) posts: Post[];}优势:和 NestJS 深度集成,装饰器风格统一。QueryBuilder 灵活,复杂查询不依赖 raw SQL。劣势:类型安全弱——findOne 返回的是 User | undefined,关联字段需要手动标注类型。N+1 问题严重,relations 的加载策略容易踩坑。迁移工具粗糙,自动生成的迁移经常需要手动修改。Sequelize:该退休了Sequelize 是最老的 Node.js ORM,v5 及以前用 callback 风格,v6 改成了 Promise 但 API 设计仍然笨重。类型安全最差(TypeScript 支持是后加的)。不建议新项目使用。MikroORM:小众精品MikroORM 的设计理念更接近 Doctrine(PHP),强调 Unit of Work 模式——所有修改先在内存中记录,flush() 时一次性写入数据库。好处是自动处理关联关系,不需要手动 save 每个实体。const user = em.create(User, { email: 'test@test.com' });await em.flush(); // 一次性写入类型安全比 TypeORM 好很多,自动推导关联类型。但社区小,遇到问题不好查。适合对 ORM 设计有洁癖的开发者。怎么选新项目:Prisma(类型安全 + 迁移体验),或 MikroORM(如果你喜欢 Unit of Work)NestJS 项目:TypeORM 仍是官方默认,但 Prisma 在 NestJS 社区的采用率快速上升已有 TypeORM 项目:不必迁移。TypeORM 能正常工作,迁移成本 > 收益不要选 Sequelize:除非你在维护老项目
服务端阅读 06月2日 01:46

TypeORM 查询太慢怎么优化?N+1、索引和批量插入实战

TypeORM 性能问题主要来自三个地方:N+1 查询、缺少索引、大量数据操作用了逐条插入。逐一解决后,大部分应用的数据库性能就够了。1. 解决 N+1 查询最常见也最致命的性能问题。查询用户列表后逐个查用户的文章:// N+1:1 次查用户 + N 次查文章const users = await userRepo.find();for (const user of users) { user.posts = await postRepo.find({ where: { authorId: user.id } });}100 个用户 = 101 条 SQL。解决方案:用 relations 或 join 一次性查出。// 方案一:relations(发 2 条 SQL,但比 N+1 好很多)const users = await userRepo.find({ relations: ['posts'] });// 方案二:QueryBuilder join(1 条 SQL)const users = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();leftJoinAndSelect 同时查出关联数据。leftJoin 只 join 不 select——用于过滤条件但不返回关联数据。2. 只查需要的字段find() 默认 SELECT 所有列。大表里有 TEXT/BLOB 列时,查全列浪费网络带宽和内存:// 查全列const users = await userRepo.find();// 只查需要的列const users = await userRepo.find({ select: ['id', 'name', 'email']});// QueryBuilder 方式const users = await userRepo .createQueryBuilder('user') .select(['user.id', 'user.name', 'user.email']) .getMany();列表页只需要 id 和 name,详情页才需要所有字段。按场景选择查询范围。3. 分页不要用 skip/take 处理大偏移// 大偏移时很慢:MySQL 要扫描前 10000 行然后丢掉const posts = await postRepo.find({ skip: 10000, take: 20});改用游标分页(cursor-based pagination):// 基于 ID 的游标分页const posts = await postRepo.find({ where: { id: LessThan(lastId) }, order: { id: 'DESC' }, take: 20});游标分页不依赖偏移量,性能与页码无关。缺点是不能跳转到指定页码。4. 批量插入逐条插入 1000 条数据 = 1000 条 INSERT 语句,极慢:// 慢:逐条插入for (const item of data) { await repo.save(item);}// 快:批量插入await repo.save(data); // TypeORM 自动合并成批量 INSERTsave 传入数组时,TypeORM 会合并成一条 INSERT 语句(部分数据库支持)。更大数据量用 createQueryBuilder 的 INSERT:await createQueryBuilder() .insert() .into(User) .values(data) .execute();极大数据量(10 万+)分批插入,每批 1000 条,避免单条 SQL 过长。5. 索引优化TypeORM 的 @Index 装饰器声明索引:@Entity()@Index(['email']) // 单列索引@Index(['firstName', 'lastName']) // 复合索引export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() firstName: string; @Column() lastName: string;}索引设计原则:WHERE 条件里的列加索引复合索引把区分度高的列放前面(@Index(['status', 'createdAt']) 如果 status 只有 3 个值就不如 @Index(['createdAt', 'status']))不要过度索引——每个索引增加写入开销查看慢查询:EXPLAIN ANALYZE + SQL 日志。TypeORM 开启 SQL 日志:TypeOrmModule.forRoot({ logging: true, // 打印所有 SQL})6. 连接池配置默认连接池大小可能不够用:TypeOrmModule.forRoot({ type: 'postgres', extra: { max: 20, // 最大连接数 idleTimeoutMillis: 30000, },})max 设为 CPU 核心数 * 2 + 磁盘数是常见公式。太多连接反而增加数据库锁争用。
服务端阅读 06月2日 01:46

npm 是什么?包管理器核心概念和 package.json 详解

npm(Node Package Manager)是 Node.js 的默认包管理器,负责安装、管理和发布 JavaScript 包。npm 由三部分组成:命令行工具、在线仓库(registry)和网站(npmjs.com)。npm 做什么安装别人写的包:npm install react 从 registry 下载 react 及其依赖到 node_modules 目录。一个项目通常有几十到上百个依赖,手动下载不现实。管理项目依赖:package.json 记录项目需要哪些包和版本范围,package-lock.json 锁定精确版本。任何人 clone 项目后 npm install 就能还原相同的依赖。运行脚本:package.json 的 scripts 字段定义常用命令:{ "scripts": { "dev": "vite", "build": "vite build", "test": "jest" }}npm run dev 执行 vite 命令,比手动敲完整命令方便。npm 自动把 node_modules/.bin 加到 PATH,不用全局安装工具。发布自己的包:npm publish 把代码上传到 registry,其他人就能 npm install 使用。package.json 核心字段{ "name": "my-app", "version": "1.0.0", "dependencies": { "react": "^18.2.0" }, "devDependencies": { "jest": "^29.0.0" }, "scripts": { "start": "node index.js" }}dependencies:生产环境需要的包(React、Express)devDependencies:开发环境需要的包(测试工具、构建工具)安装时 npm install -D 写入 devDependencies,npm install 写入 dependenciesnode_modules 的结构npm install 把所有包下载到 nodemodules 目录。包之间可以互相依赖,形成依赖树。npm 3+ 用扁平结构——尽量把依赖提升到顶层 nodemodules,减少嵌套。node_modules 通常很大(几百 MB),不要提交到 Git。.gitignore 里加上 node_modules/。npm vs yarn vs pnpmnpm:Node.js 自带,零配置,生态兼容性最好Yarn:Facebook 出品,早期比 npm 快,现在差距不大。Yarn 1 已停止维护pnpm:用硬链接共享包文件,磁盘占用少 3-5 倍,安装速度最快新项目推荐 pnpm 或 npm。已有项目没必要迁移。初始化项目mkdir my-project && cd my-projectnpm init -y # 生成默认 package.jsonnpm install express # 安装第一个依赖npm init -y 跳过交互式提问,直接用默认值生成 package.json。
服务端阅读 06月2日 01:45

npm 版本号 ^ 和 ~ 有什么区别?SemVer 和 package-lock 详解

npm 用语义化版本(SemVer)管理包版本,package.json 声明版本范围,package-lock.json 锁定精确版本。理解版本范围符号能避免"昨天还好好的今天就挂了"的问题。语义化版本号版本号格式:主版本.次版本.补丁版本(Major.Minor.Patch)Patch(1.0.x):Bug 修复,不改变 APIMinor(1.x.0):新增功能,向后兼容Major(x.0.0):破坏性变更,不向后兼容npm install 默认安装最新版本,但 package.json 里记录的是版本范围,不是精确版本。版本范围符号精确版本"react": "18.2.0" // 只安装 18.2.0,不安装任何其他版本插入号 ^(Caret)"react": "^18.2.0" // >=18.2.0 <19.0.0"lodash": "^4.17.0" // >=4.17.0 <5.0.0允许 Minor 和 Patch 更新,锁定主版本。最常用的范围符号——npm install react 默认就用 ^。规则:左边第一个非零数字锁定。^1.2.3 → >=1.2.3 <2.0.0^0.2.3 → >=0.2.3 <0.3.0(0.x 视为开发阶段,次版本也可能破坏兼容)^0.0.3 → >=0.0.3 <0.0.4(0.0.x 视为实验阶段,几乎锁定)波浪号 ~(Tilde)"react": "~18.2.0" // >=18.2.0 <18.3.0只允许 Patch 更新,锁定主版本和次版本。比 ^ 更保守,适合需要稳定但偶尔接受 bug 修复的场景。其他范围"react": ">=18.0.0" // 大于等于 18"react": "18.0.0 - 18.2.0" // 闭区间"react": "*" // 任意版本(危险,不要用)"react": "latest" // 最新版本(同上,危险)"react": "file:../local-pkg" // 本地路径package-lock.json 的作用package.json 声明范围,package-lock.json 锁定精确版本。// package.json"react": "^18.2.0" // 范围:18.2.0 到 18.x.x// package-lock.json"react": "18.2.0" // 实际安装的精确版本没有 lock 文件时,npm install 每次可能安装不同版本(18.2.0 → 18.3.1),导致团队成员或 CI 环境的依赖不一致。lock 文件保证所有人安装完全相同的版本。必须把 package-lock.json 提交到 Git。不要把它加入 .gitignore。版本冲突排查npm ls react # 查看项目中安装的 react 版本npm outdated # 列出所有过时的包npm view react versions # 查看包的所有已发布版本重复依赖问题(同一个包安装了多个版本):npm dedupe 尝试去重。save-exact:精确安装不想用 ^ 范围?配置 npm 默认用精确版本:npm config set save-exact true# 或在 .npmrc 里加save-exact=true之后 npm install react 会写 "react": "18.2.0" 而不是 "react": "^18.2.0"。适合对版本控制严格的团队。
服务端阅读 06月2日 01:43

npm、Yarn 和 pnpm 怎么选?2025 年包管理器对比

npm 和 Yarn 都是 JavaScript 包管理器,做的事情一样(安装依赖、管理版本、运行脚本),区别在于速度、稳定性和锁文件机制。2025 年的实际情况:npm 够用,pnpm 值得切换,Yarn 1 已过时。核心差异| 维度 | npm | Yarn 1 (Classic) | Yarn 2+ (Berry) | pnpm ||------|-----|-------------------|-----------------|------|| 锁文件 | package-lock.json | yarn.lock | yarn.lock | pnpm-lock.yaml || 安装速度 | 中 | 快 | 快 | 最快 || 离线安装 | 缓存但需网络 | 支持 | 支持 | 支持 || Monorepo | workspaces (npm 7+) | workspaces | workspaces | workspaces || 磁盘占用 | 高 | 高 | 高 | 低(硬链接) || Plug'n'Play | 不支持 | 不支持 | 支持 | 不支持 |Yarn 曾经的优势,npm 已经追平Yarn 1 在 2016 年发布时碾压 npm:确定性安装(yarn.lock)、并行下载、离线缓存。但 npm 5+ 引入了 package-lock.json,npm 7+ 加了 workspaces,核心差距已经很小。2025 年不建议新项目用 Yarn 1。它已经停止维护(最后版本 1.22.x),安全漏洞不会修复。Yarn 2+ (Berry):激进但有代价Yarn Berry 引入了 Plug'n'Play(PnP)——不生成 nodemodules,用 .pnp.cjs 文件映射包路径。好处是安装快、磁盘占用小。代价是很多依赖 nodemodules 的工具不兼容,需要额外配置。零安装(Zero-Install)是 Berry 的另一个特性——把 .yarn/cache 提交到 Git,clone 后不用 npm install 直接开发。对小团队方便,但 cache 目录会让 Git 仓库膨胀。实际采用率不高——PnP 的兼容性问题导致迁移成本大,很多团队试了又切回 npm。pnpm:当前最值得切换的方案pnpm 用硬链接替代复制——所有项目共享同一份包文件,磁盘占用只有 npm 的 1/3 到 1/5。安装速度也最快(比 npm 快 2-3 倍)。npm install -g pnpmpnpm install # 替代 npm installpnpm add react # 替代 npm install reactpnpm run dev # 替代 npm run devpnpm 的严格模式(非扁平的 node_modules)避免了幽灵依赖——你只能 import 声明过的包,不会意外引用到间接依赖。这对项目长期维护是好事,但迁移老项目时可能暴露隐藏的依赖问题。怎么选新项目:pnpm(磁盘省、速度快、严格依赖)或 npm(零配置、生态兼容性最好)已有项目用 npm:没必要迁移。npm 够用,迁移收益不大已有项目用 Yarn 1:建议迁移到 pnpm 或 npm。Yarn 1 不再维护已有项目用 Yarn Berry:如果 PnP 工作正常就继续用,没有问题就不要换一句话:没有强需求就不要换包管理器。迁移成本 > 收益的情况很常见。
服务端阅读 06月2日 01:42

.npmrc 怎么配?registry 镜像、私有包和常用配置项详解

.npmrc 是 npm 的配置文件,控制 registry 源、代理、认证信息等。分三层:全局、项目、用户级,优先级从高到低。三层 .npmrc| 文件位置 | 作用范围 | 优先级 ||----------|----------|--------|| 项目根目录/.npmrc | 只对当前项目生效 | 最高 || ~/.npmrc | 对当前用户所有项目生效 | 中 || $PREFIX/etc/npmrc | 全局,对所有用户生效 | 最低 |项目级配置覆盖用户级,用户级覆盖全局。大多数配置写在项目级或用户级就够了。最常用的配置1. 切换 registry(国内开发者必备)registry=https://registry.npmmirror.com默认的 npmjs.org 在国内经常超时。淘宝镜像 npmmirror.com 速度快且稳定。项目级 .npmrc 加这一行,团队成员都能用。2. 私有包的 scoped registry@mycompany:registry=https://npm.mycompany.com@mycompany scope 的包从公司私有 registry 拉取,其他包走公共 registry。不需要配置 VPN 或全局代理。3. 认证 token//npm.mycompany.com/:_authToken=${NPM_TOKEN}用环境变量避免把 token 写死在文件里。CI/CD 里设 NPM_TOKEN 环境变量即可。4. 代理配置proxy=http://127.0.0.1:7890https-proxy=http://127.0.0.1:7890公司内网需要通过代理访问外网时设置。其他实用配置save-exact=true # npm install 默认用精确版本号而非 ^ 前缀package-lock=false # 不生成 package-lock.json(不推荐)audit=false # 关闭 npm audit 检查fund=false # 关闭 npm fund 提示legacy-peer-deps=true # 忽略 peerDependencies 冲突(npm 7+ 经常需要)legacy-peer-deps=true 是 npm 7+ 升级后最常见的配置——npm 7 默认严格检查 peerDeps,很多老包会报冲突。加上这行回到 npm 6 的宽松模式。查看当前生效的配置npm config list # 显示所有配置(包括来源)npm config get registry # 查看某个配置项的值npm config edit # 直接编辑用户级 .npmrc.npmrc 要提交到 Git 吗?项目级 .npmrc:应该提交,确保团队成员用相同的 registry 和配置。但不要包含 auth token——用环境变量代替。用户级 .npmrc:不提交,是个人偏好(代理、token 等)。# .gitignore.npmrc如果项目需要共享 registry 配置,把不含敏感信息的部分提交,token 用 .npmrc + .gitignore 或环境变量处理。
服务端阅读 06月2日 01:41

npm link 怎么用?本地包开发链接和常见坑

npm link 让你在本地开发时把一个包"链接"到另一个项目,改了代码立即生效,不用反复 npm publish + npm install。工作原理npm link 分两步:第一步:在要开发的包目录里执行 npm link,把当前包注册到全局 node_modules。cd ~/projects/my-ui-libnpm link# 把 my-ui-lib 链接到 /usr/local/lib/node_modules/my-ui-lib第二步:在使用这个包的项目里执行 npm link my-ui-lib,创建一个符号链接。cd ~/projects/my-appnpm link my-ui-lib# node_modules/my-ui-lib -> /usr/local/lib/node_modules/my-ui-lib -> ~/projects/my-ui-lib本质就是创建符号链接(symlink)。修改 my-ui-lib 的代码,my-app 里立即生效,不用重新安装。实际使用场景1. 开发组件库你在开发一个 UI 组件库,同时在业务项目里使用它。用 npm link 把组件库链接到业务项目,改组件代码后业务项目自动更新。2. 开发 CLI 工具CLI 工具通常全局安装测试。npm link 在全局注册你的 CLI,修改代码后直接运行最新版本。3. 修复第三方包的 bugfork 一个包,本地修改后 link 到项目里验证修复。确认无误后再提 PR。常见问题1. 多个包互相依赖monorepo 里 A 依赖 B,B 也依赖 A?用 npm link 双向链接:先在 B 目录 npm link,再去 A 目录 npm link B;然后在 A 目录 npm link,再去 B 目录 npm link A。更好的方案:用 workspace(npm 7+ 的 workspaces)替代 npm link,自动处理内部依赖。2. React 双实例问题组件库和业务项目各有一份 React 实例,导致 Hooks 报错 "Invalid hook call"。解决:在业务项目里 link React 到组件库。cd ~/projects/my-app/node_modules/reactnpm linkcd ~/projects/my-ui-libnpm link react这样两个项目共用同一份 React。3. link 后 publish 会把符号链接打包npm link 创建的是符号链接,npm publish 时可能把链接路径打包进去。发布前务必 npm unlink 确保包内容正确。4. npm unlink 清除链接cd ~/projects/my-appnpm unlink my-ui-lib # 在项目里取消链接npm install my-ui-lib # 重新安装正式版本cd ~/projects/my-ui-libnpm unlink # 取消全局注册替代方案npm workspaces:monorepo 场景首选,不需要 link,npm 自动处理内部包的符号链接yalc:把包发布到本地仓库(不是全局 node_modules),比 link 更稳定,不受全局污染影响pnpm link:pnpm 的 link 命令,行为类似但更严格
服务端阅读 06月2日 01:40

npm 常用命令速查:安装、版本管理、脚本和高效技巧

日常开发用到的 npm 命令其实不多,核心就 10 个左右。记住这些够用 90% 的场景,剩下的需要时再查。安装和卸载npm install # 根据 package.json 安装所有依赖npm install react # 安装到 dependenciesnpm install -D jest # 安装到 devDependenciesnpm install -g typescript # 全局安装npm uninstall react # 从 dependencies 移除npm i 是 install 的缩写,npm un 是 uninstall 的缩写。版本管理npm outdated # 查看过时的包npm update # 更新到 semver 允许的最新版本npm install react@18.2.0 # 安装特定版本npm install react@^18 # 18.x.x 最新版本号语义:^18.2.0 允许 18.x.x(主版本不变),~18.2.0 允许 18.2.x(次版本不变)。运行脚本npm run dev # 运行 scripts.devnpm run build # 运行 scripts.buildnpm start # 等价于 npm run startnpm test # 等价于 npm run teststart 和 test 可以省略 run,其他脚本必须加。查看所有可用脚本:npm run。发布npm login # 登录npm publish # 发布当前包npm version patch # +0.0.1 并自动 git commit + tagnpm version minor # +0.1.0npm version major # +1.0.0查看包信息npm view react version # 最新版本号npm view react versions # 所有已发布版本npm ls # 当前项目依赖树npm ls react # 某个包的安装版本高效技巧npx 执行一次性命令:不用全局安装,npx create-react-app my-app 用完即弃。npx 先找项目本地,找不到再下载临时执行。npm ci 代替 npm install:CI/CD 环境必须用 npm ci。根据 package-lock.json 精确安装,比 install 快且版本完全一致。清理缓存:安装报错"Unexpected end of JSON input"时,npm cache clean --force 清理重试。
服务端阅读 06月2日 01:38

Python 装饰器是怎么工作的?@ 语法糖、执行时机和 wraps 详解

装饰器的本质是一个接收函数作为参数并返回新函数的高阶函数。@decorator 语法糖等价于 func = decorator(func)。理解装饰器的关键:它只是函数替换,在定义时执行,不是调用时。装饰器做了什么def log(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper@logdef hello(name): print(f"Hello, {name}")# 等价于hello = log(hello)@log 发生在函数定义时,不是调用时。Python 解释器看到 @log 后,把 hello 传给 log(),返回的 wrapper 替换掉原来的 hello。之后所有对 hello() 的调用实际上调的是 wrapper()。执行时机def log(func): print("装饰器执行了") # 模块加载时就执行 def wrapper(*args, **kwargs): print("wrapper 执行了") # 每次调用函数时执行 return func(*args, **kwargs) return wrapper@logdef hello(): print("hello")# 输出: 装饰器执行了(定义时立即执行)hello()# 输出: wrapper 执行了 / hello装饰器外层的代码在模块加载时执行(一次),wrapper 内的代码在每次函数调用时执行。多个装饰器的叠加@a@bdef f(): pass# 等价于f = a(b(f))从下到上装饰。调用 f() 时,执行顺序是 a -> b -> f -> b 的后处理 -> a 的后处理。带参数的装饰器需要三层嵌套——最外层接收装饰器参数,中间层接收被装饰函数,最内层是 wrapper:def retry(max_attempts=3): def decorator(func): def wrapper(*args, **kwargs): for i in range(max_attempts): try: return func(*args, **kwargs) except Exception: if i == max_attempts - 1: raise return wrapper return decorator@retry(max_attempts=5)def call_api(): ...# 等价于call_api = retry(max_attempts=5)(call_api)@retry(5) 先调用 retry(5) 返回 decorator,再 decorator(call_api) 返回 wrapper。三层嵌套是固定模式。functools.wraps装饰器替换了原函数,导致 __name__、__doc__ 等元信息丢失。@wraps(func) 复制原函数的元信息到 wrapper:from functools import wrapsdef log(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper@logdef hello(): passprint(hello.__name__) # hello(没有 @wraps 会是 wrapper)装饰器 vs 闭包装饰器是基于闭包的。wrapper 闭包了 func 变量——每次调用 wrapper 时都能访问到被装饰的原始函数。装饰器就是"创建闭包的工厂函数"。追问装饰器能装饰类吗?能。装饰器接收的参数不一定是函数,也可以是类:def add_repr(cls): def __repr__(self): return f"{cls.__name__}({self.__dict__})" cls.__repr__ = __repr__ return cls@add_reprclass User: def __init__(self, name): self.name = namedataclass 的 @dataclass 就是类装饰器。装饰器有性能开销吗?有一层函数调用的开销,通常可以忽略。但在极高性能场景(百万次/秒调用),装饰器的额外调用栈可能成为瓶颈。可以用 functools.lru_cache 缓存结果避免重复执行。
服务端阅读 06月2日 01:37

Python GIL 是什么?为什么多线程不能利用多核?怎么绕过?

GIL(Global Interpreter Lock)是 CPython 的一把全局互斥锁,同一时刻只允许一个线程执行 Python 字节码。这意味着 Python 多线程无法利用多核 CPU 做计算密集型任务。但 IO 密集型任务不受影响——线程等待 IO 时会释放 GIL。GIL 为什么存在CPython 的内存管理(引用计数)不是线程安全的。如果多个线程同时修改 ob_refcnt,可能导致内存泄漏或提前释放。GIL 是最简单的解决方案——一个线程执行 Python 代码时,其他线程不能运行。为什么不去掉?Python 核心开发者试过多次,去掉 GIL 会导致单线程性能下降 10-30%(因为要加细粒度锁替代全局锁),C 扩展也需要大量改写。社区不愿意接受这个代价。Python 3.13 引入了实验性的 free-threaded 模式(PEP 703),允许禁用 GIL,但目前还是实验阶段,性能和兼容性都不成熟。GIL 的影响范围| 任务类型 | 多线程表现 | 原因 ||----------|-----------|------|| CPU 密集 | 比单线程还慢 | 线程争抢 GIL,切换开销大 || IO 密集 | 有效加速 | 等 IO 时释放 GIL,其他线程可运行 || 混合型 | 部分有效 | 计算部分受 GIL 限制 |CPU 密集比单线程还慢是因为 GIL 切换本身有开销,多线程反而增加了竞争。绕过 GIL 的三种方式1. 多进程(最常用)from multiprocessing import Pooldef heavy_compute(n): return sum(i * i for i in range(n))with Pool(4) as p: results = p.map(heavy_compute, [10**7] * 4) # 4 个进程并行每个进程有独立的 GIL,真正并行。缺点:进程间通信成本高(数据要序列化),启动慢。2. C 扩展释放 GIL在 C 扩展中做计算密集型操作时,可以手动释放 GIL:Py_BEGIN_ALLOW_THREADS// 这里做纯 C 计算,不操作 Python 对象result = heavy_computation(data);Py_END_ALLOW_THREADSNumPy、Pandas、hashlib 等库在底层 C 代码中释放了 GIL,所以它们在多线程中可以并行运行。3. 用 asyncio 替代多线程IO 密集型任务不需要多线程,用协程就够了:import asyncioimport aiohttpasync def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text()async def main(): tasks = [fetch(url) for url in urls] results = await asyncio.gather(*tasks)单线程 + 事件循环,没有 GIL 问题,也没有线程切换开销。GIL 的释放时机CPython 在两种情况下释放 GIL:IO 操作:网络请求、文件读写、time.sleep() 等C 扩展主动释放:NumPy 运算、hashlib 哈希等纯 Python 代码(循环、计算、字符串操作)永远不会释放 GIL。追问Python 3.13 的 no-GIL 模式能用吗?实验阶段,不推荐生产使用。性能比有 GIL 模式慢约 10-15%,很多 C 扩展还不兼容。等 Python 3.14/3.15 稳定后再考虑。多线程在 Python 里完全没用吗?不是。IO 密集型场景(网络爬虫、API 调用、数据库查询)多线程比单线程快得多。只是 CPU 密集型场景多线程没有加速效果。
服务端阅读 06月2日 01:36

Python 迭代器和生成器有什么区别?yield 和迭代器协议详解

迭代器是实现了 __iter__ 和 __next__ 方法的对象,生成器是用 yield 关键字自动创建的迭代器。生成器是迭代器的子集——所有生成器都是迭代器,但迭代器不一定是生成器。核心区别:生成器更简洁,且天然支持惰性求值。迭代器协议迭代器必须实现两个方法:__iter__:返回 self(让迭代器本身也可迭代)__next__:返回下一个值,没有值时抛出 StopIterationclass Countdown: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self): if self.current <= 0: raise StopIteration self.current -= 1 return self.current + 1for n in Countdown(3): print(n) # 3, 2, 1手动实现迭代器要写 __iter__、__next__、维护状态、处理 StopIteration。代码量多,容易写错。生成器:迭代器的语法糖生成器用 yield 关键字,Python 自动实现迭代器协议:def countdown(start): current = start while current > 0: yield current current -= 1for n in countdown(3): print(n) # 3, 2, 1调用 countdown(3) 不会执行函数体,而是返回一个生成器对象。每次 next() 执行到 yield 暂停并返回值,下次 next() 从暂停处继续。3 行代码 vs 10 行代码,效果完全一样。这就是为什么 Python 社区几乎总是用生成器而不是手写迭代器。生成器表达式类似列表推导式,但用圆括号,惰性求值:# 列表推导式 — 一次性生成所有数据squares = [x**2 for x in range(1000000)] # 占用大量内存# 生成器表达式 — 按需生成squares = (x**2 for x in range(1000000)) # 几乎不占内存# 在 sum/max/min 等函数里直接用total = sum(x**2 for x in range(1000000))处理大数据时,生成器表达式是列表推导式的直接替代。yield from:委托给子生成器yield from 把迭代委托给另一个生成器,避免手写 for 循环:def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item) # 递归展开 else: yield itemlist(flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]yield from 不只是语法糖——它还正确处理了 send()、throw()、close() 等生成器方法的传递。生成器做协程yield 不只能返回值,还能接收值(通过 send()),这让生成器可以用来实现协程:def accumulator(): total = 0 while True: value = yield total if value is None: break total += valuegen = accumulator()next(gen) # 启动生成器,返回 0gen.send(10) # 返回 10gen.send(20) # 返回 30Python 3.5+ 推荐用 async/await 替代这种用法,但理解 yield 的双向通信有助于理解协程原理。追问迭代器只能遍历一次吗?是的。迭代器是有状态的,遍历完就空了。要重新遍历,需要创建新的迭代器。可迭代对象(如列表)每次调用 iter() 都返回新的迭代器。生成器的内存优势有多大?读 10GB 日志文件,for line in open("log.txt") 只占几 KB 内存(每次读一行)。readlines() 会把整个文件加载到内存。差距在数据量大时非常显著。
服务端阅读 06月2日 01:36

Python 内存管理是怎样的?引用计数、分代 GC 和内存池原理

Python 内存管理分三层:引用计数(主要)、垃圾回收(处理循环引用)、内存池(减少 malloc 开销)。日常开发不需要手动管理内存,但理解机制能帮你排查内存泄漏。引用计数:核心机制每个对象维护一个引用计数 ob_refcnt。引用增加时 +1,引用减少时 -1,归零时立即释放。import sysa = [1, 2, 3] # 引用计数 1b = a # 引用计数 2c = a # 引用计数 3print(sys.getrefcount(a)) # 4(多 1 是因为 getrefcount 参数本身也是引用)del b # 引用计数 2c = None # 引用计数 1# a 离开作用域后引用计数归零,内存释放引用计数的优势:实时释放,不需要暂停程序做垃圾回收。劣势:无法处理循环引用。循环引用问题a = []b = []a.append(b) # a 引用 bb.append(a) # b 引用 adel a, b # 引用计数各剩 1(互相引用),永远不会归零引用计数对循环引用无能为力。Python 用分代垃圾回收(GC)处理这种情况。分代垃圾回收GC 把对象分成三代:第 0 代(新对象)、第 1 代、第 2 代(长寿对象)。新创建的对象在第 0 代经过一次 GC 存活的对象晋升到下一代第 0 代 GC 最频繁(阈值约 700 个对象),第 2 代最少分代回收的理论依据:大部分对象很快变成垃圾(如函数内的临时变量),长寿对象倾向于一直活着。只频繁检查年轻对象,减少 GC 开销。import gcprint(gc.get_threshold()) # (700, 10, 10) — 第0代阈值700,每10次第0代GC触发1次第1代print(gc.get_count()) # 当前各代对象计数手动触发 GC:gc.collect()。通常不需要手动调用,但在处理大量循环引用对象后可以主动回收。内存池:pymallocPython 不直接用系统的 malloc/free 管理小对象,而是用自己实现的 pymalloc 内存池:小对象(:由 pymalloc 从大块内存中分配,减少系统调用大对象(>=512 字节):直接用系统 malloc内存池按 256 KB 的 arena 分块,arena 内按 4 KB 的 pool 分块,pool 内按固定大小的 block 分配。相同大小的 block 共享 pool,减少碎片。这就是为什么 Python 进程的 RSS(常驻内存)不会随着对象释放而下降——pymalloc 保留空闲 arena 供后续使用,不归还给操作系统。内存泄漏排查Python 的内存泄漏通常是"对象被意外引用导致无法释放"而非真正的泄漏。1. 用 objgraph 找到引用链import objgraphobjgraph.show_backrefs(objgraph.by_type("dict")[0], max_depth=5)生成引用关系图,找到谁在持有不该持有的引用。2. 用 tracemalloc 定位分配位置import tracemalloctracemalloc.start()# ... 运行代码 ...snapshot = tracemalloc.take_snapshot()top_stats = snapshot.statistics("lineno")for stat in top_stats[:10]: print(stat)显示内存分配最多的代码行。3. 常见泄漏原因全局列表/字典不断追加但从不清理闭包或回调持有大对象引用__del__ 方法导致对象无法被 GC 回收(Python 3.4 已改善)缓存没有上限(用 functools.lru_cache 替代手动缓存)追问为什么 Python 进程的内存只增不减?pymalloc 保留空闲 arena 不归还操作系统。如果你看到 RSS 很高但对象不多,可能是内存碎片。malloc_trim(0) 可以强制归还(Linux),但不保证生效。引用计数和 GC 哪个先执行?引用计数是实时的,每次赋值/删除都更新。GC 是周期性运行的,只处理循环引用。两者配合工作。
服务端阅读 06月2日 01:35

Python 异常处理怎么写?try/except/else/finally 和自定义异常详解

Python 用 try/except 捕获异常,else 放无异常时执行的代码,finally 放无论如何都执行的清理逻辑。自定义异常继承 Exception,让错误类型可区分。基本结构try: result = 10 / 0except ZeroDivisionError: print("除零错误")except (TypeError, ValueError) as e: print(f"类型或值错误: {e}")else: print("没有异常时执行")finally: print("无论如何都执行")else 在 try 块没有抛异常时执行,适合放"成功后才做的事"finally 总是执行,即使 try 里有 return 或 except 里有 raise。用于释放资源(关闭文件、断开连接)异常继承体系所有异常继承自 BaseException,日常使用继承 Exception:BaseException +-- SystemExit # sys.exit() +-- KeyboardInterrupt # Ctrl+C +-- Exception # 所有业务异常的基类 +-- ValueError +-- TypeError +-- KeyError +-- FileNotFoundError不要捕获 BaseException——except: 或 except BaseException: 会把 KeyboardInterrupt 和 SystemExit 也吞掉,程序无法正常退出。永远用 except Exception: 或更具体的异常类型。自定义异常class BusinessError(Exception): """业务逻辑错误基类""" passclass InsufficientBalance(BusinessError): def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__(f"余额不足: 余额 {balance}, 需要 {amount}")try: withdraw(account, amount)except InsufficientBalance as e: print(e) print(e.balance) # 100自定义异常比直接 raise ValueError("余额不足") 好在:调用方可以按类型捕获(except InsufficientBalance),而不是解析错误消息字符串。异常链:raise from捕获一个异常后抛出另一个,保留原始异常的堆栈:try: data = json.loads(raw)except json.JSONDecodeError as e: raise DataFormatError("数据格式错误") from efrom e 让新异常的 __cause__ 指向原始异常。调试时能看到完整的异常链。不加 from e,原始异常的堆栈信息会丢失。常见反模式1. 空 except 吞掉所有错误try: do_something()except: # 错误!吞掉 KeyboardInterrupt pass# 正确做法except Exception as e: log.error(f"操作失败: {e}")2. except 块太宽try: value = data["key"] result = int(value)except Exception: # 太宽,KeyError 和 ValueError 混在一起 ...# 正确:分别处理except KeyError: result = 0except ValueError: result = -13. 用异常做流程控制# 错误:用异常判断 key 是否存在try: value = d[key]except KeyError: value = default# 正确:用 get 或 invalue = d.get(key, default)异常比条件判断慢 10-100 倍。只对真正的"异常"情况用异常,正常流程用条件判断。追问finally 里的 return 会覆盖 try 里的 return 吗?会。永远不要在 finally 里写 return。def foo(): try: return 1 finally: return 2 # 覆盖 try 里的print(foo()) # 2
服务端阅读 06月2日 01:33

Python 装饰器高级用法有哪些?带参数装饰器、类装饰器和 functools.wraps 详解

装饰器的高级用法围绕三个问题:怎么传参数、怎么保持被装饰函数的元信息、什么时候用类而不是函数写装饰器。带参数的装饰器普通装饰器只能装饰函数,不能接收额外参数。需要参数时,加一层嵌套:def retry(max_attempts=3, delay=1): def decorator(func): def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts - 1: raise time.sleep(delay) return wrapper return decorator@retry(max_attempts=5, delay=2)def call_api(): ...@retry(5, 2) 先调用 retry(5, 2) 返回 decorator,再 decorator(call_api) 返回 wrapper。三层嵌套是带参数装饰器的固定模式。functools.wraps:保持函数身份装饰器替换了原函数,导致 __name__、__doc__ 等元信息丢失:def log(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper@logdef hello(): passprint(hello.__name__) # 'wrapper' 而不是 'hello'加 @wraps(func) 把原函数的元信息复制到 wrapper:from functools import wrapsdef log(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapperprint(hello.__name__) # 'hello'@wraps 必须加——调试时看到 wrapper 而不是实际函数名,排查问题会非常痛苦。类装饰器用类写装饰器,适合需要维护状态的场景:class CountCalls: def __init__(self, func): self.func = func self.count = 0 wraps(func)(self) # 保持元信息 def __call__(self, *args, **kwargs): self.count += 1 print(f"{self.func.__name__} called {self.count} times") return self.func(*args, **kwargs)@CountCallsdef say_hello(): print("Hello")say_hello() # say_hello called 1 times; Hellosay_hello() # say_hello called 2 times; Hello__init__ 接收被装饰的函数,__call__ 每次调用时执行。self.count 跨调用持久化——函数写装饰器用闭包变量存状态,类装饰器用实例属性,语义更清晰。装饰类装饰器不只能装饰函数,还能装饰整个类:def add_repr(cls): def __repr__(self): attrs = ', '.join(f'{k}={v!r}' for k, v in self.__dict__.items()) return f'{cls.__name__}({attrs})' cls.__repr__ = __repr__ return cls@add_reprclass User: def __init__(self, name, age): self.name = name self.age = ageprint(User('Alice', 30)) # User(name='Alice', age=30)dataclass 的 @dataclass 就是类装饰器——自动生成 __init__、__repr__、__eq__。常见的高级装饰器模式1. 缓存/记忆化from functools import lru_cache@lru_cache(maxsize=128)def expensive(n): return sum(i ** 2 for i in range(n))标准库自带,不用自己写。maxsize=None 无限缓存。2. 类型检查def validate(**types): def decorator(func): @wraps(func) def wrapper(**kwargs): for name, expected in types.items(): if name in kwargs and not isinstance(kwargs[name], expected): raise TypeError(f'{name} must be {expected}') return func(**kwargs) return wrapper return decorator@validate(age=int, name=str)def create_user(name, age): ...3. 超时控制import signaldef timeout(seconds): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): def handler(signum, frame): raise TimeoutError(f'{func.__name__} timed out') old = signal.signal(signal.SIGALRM, handler) signal.alarm(seconds) try: return func(*args, **kwargs) finally: signal.alarm(0) signal.signal(signal.SIGALRM, old) return wrapper return decorator追问多个装饰器的执行顺序?从下到上装饰,从上到下执行。@a @b def f() 等价于 a(b(f)),调用 f() 时先执行 a 的逻辑,再执行 b 的逻辑,最后执行 f。装饰器和 AOP 是什么关系?装饰器是 Python 实现 AOP(面向切面编程)的方式。日志、权限、缓存这些"横切关注点"用装饰器统一处理,不侵入业务代码。
服务端阅读 06月2日 01:32

Python 元类是什么?type 怎么创建类?元类什么时候该用?

元类是创建类的类。普通类创建实例,元类创建类。Python 里 class 语句本质上是调用 type() 来创建类对象,元类让你拦截这个过程,在类创建时自动修改类的属性、方法、继承关系。Python 类的创建过程class Foo: x = 1这行代码执行时,Python 做了这件事:Foo = type('Foo', (object,), {'x': 1})type(类名, 父类元组, 属性字典) 就是创建类的底层调用。type 本身就是一个元类——所有类都是 type 的实例。print(type(Foo)) # <class 'type'>print(type(Foo())) # <class '__main__.Foo'>type(Foo) 是 type,说明 Foo 类是 type 的实例。type(Foo()) 是 Foo,说明 Foo() 实例是 Foo 的实例。自定义元类继承 type,重写 __new__ 或 __init__:class Meta(type): def __new__(cls, name, bases, dct): # 在类创建前修改属性字典 dct['created_by_meta'] = True return super().__new__(cls, name, bases, dct)class Foo(metaclass=Meta): x = 1print(Foo.created_by_meta) # True__new__ 在类对象创建之前调用,可以修改 dct(属性字典)。__init__ 在类对象创建之后调用,可以修改已创建的类。大多数场景用 __new__ 就够了。实际用途1. 自动注册子类ORM、插件系统常用——每定义一个子类就自动注册到一个全局字典里:class RegistryMeta(type): _registry = {} def __new__(cls, name, bases, dct): klass = super().__new__(cls, name, bases, dct) if name != 'Base': # 跳过基类本身 cls._registry[name] = klass return klassclass Base(metaclass=RegistryMeta): passclass User(Base): passclass Order(Base): passprint(RegistryMeta._registry) # {'User': <class 'User'>, 'Order': <class 'Order'>}Django 的 ModelBase 就是这种模式——每个 Model 子类自动注册到 Django 的 app registry。2. 接口检查强制子类实现特定方法:class InterfaceMeta(type): def __new__(cls, name, bases, dct): if bases: # 不是基类本身 required = getattr(bases[0], '_required_methods', []) for method in required: if method not in dct: raise TypeError(f'{name} 必须实现 {method}') return super().__new__(cls, name, bases, dct)class Animal(metaclass=InterfaceMeta): _required_methods = ['speak']class Dog(Animal): def speak(self): return 'Woof!'class Cat(Animal): pass # TypeError: Cat 必须实现 speak3. 单例模式class SingletonMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]class Database(metaclass=SingletonMeta): pass重写 __call__ 拦截实例化过程——第一次创建实例,后续返回同一个。追问元类和类装饰器有什么区别?类装饰器在类创建之后修改,元类在类创建过程中修改。类装饰器更简单透明,优先用装饰器。元类能控制 __new__ 和 __init__,能做的事更多但也更难调试。什么时候该用元类?几乎不需要。Python 社区有一句话:"如果你不确定是否需要元类,你就不需要。"95% 的场景用类装饰器、__init_subclass__、或普通继承就能解决。元类适合框架作者(Django ORM、SQLAlchemy),应用层开发者很少需要。
服务端阅读 06月2日 01:31

Python 闭包是什么?变量怎么被记住的?

闭包是一个函数记住了自己被创建时的作用域里的变量,即使那个作用域已经执行完毕。Python 里闭包最常见的用途:工厂函数、延迟计算、替代简单类。闭包怎么产生的def make_counter(start=0): count = start def counter(): nonlocal count count += 1 return count return counterc = make_counter(10)print(c()) # 11print(c()) # 12make_counter 执行完了,count 变量按理应该被销毁。但 counter 函数内部引用了 count,Python 会把 count 和 counter 绑在一起——这就是闭包。counter 闭包了 count 变量。nonlocal 声明告诉 Python count 不是局部变量,而是外层作用域的变量。不加 nonlocal,count += 1 会在 counter 内部创建一个新的局部变量 count,而不是修改外层的。闭包存储在哪里闭包变量存在函数的 __closure__ 属性里:print(c.__closure__) # (<cell at 0x...: int object at ...>,)print(c.__closure__[0].cell_contents) # 12每个被闭包的变量是一个 cell 对象。这就是 Python 实现闭包的底层机制。常见用途1. 工厂函数根据参数生成不同的函数:def power(exp): def f(base): return base ** exp return fsquare = power(2)cube = power(3)print(square(5)) # 25print(cube(5)) # 1252. 缓存/记忆化def memoize(fn): cache = {} def wrapper(*args): if args not in cache: cache[args] = fn(*args) return cache[args] return wrapper@memoizedef fib(n): if n < 2: return n return fib(n-1) + fib(n-2)print(fib(100)) # 瞬间出结果cache 被 wrapper 闭包,不需要全局变量。这就是装饰器能工作的基础——装饰器本质上就是闭包。3. 替代简单类只有状态 + 一个方法的场景,闭包比类轻量:# 闭包方式def make_accumulator(initial=0): total = initial def add(value): nonlocal total total += value return total return addacc = make_accumulator()print(acc(10)) # 10print(acc(20)) # 30追问闭包和类怎么选?有多个方法或复杂状态管理用类。只有一个操作 + 简单状态用闭包。闭包更函数式,类更面向对象。闭包会导致内存泄漏吗?会。被闭包的变量不会在函数返回后释放,只要闭包函数还活着,变量就一直在。长期存活的闭包(如事件监听器、回调)要注意。lambda 能形成闭包吗?能。lambda x: x + offset 里的 offset 就是被闭包的变量。但 lambda 不能用 nonlocal,所以无法修改外层变量,只能读取。