运维常见题-Python运维开发

目录
本文所引用的核心资料与数据,均由 DeepSeek V4 Flash Vision Exp 辅助生成。若您在阅读中发现存疑或矛盾之处,欢迎反馈讨论,笔者将及时核实与修正。

🤔 为什么说 Python “一切皆为对象”?

  • Python 中“一切皆为对象”:int/str/list 等基础类型、函数、类、模块、甚至代码本身都是对象——都有类型(type)、身份(id,内存地址)、可赋值/传参/存容器。核心:“type(x) 都有返回、id(x) 都有地址、x 都能传”——没有裸的“值”,只有对象”。

    • 对象的三要素

      • 身份(id:对象在内存的唯一标识(id(x) 返回该标识——CPython 中即内存地址,is 比较身份)
      • 类型(type:决定对象的行为与可操作(type(x)type(x) is type(y)
      • :对象承载的数据(可变对象值可改,不可变对象不可改)
    • “一切都是对象”的体现

      • 基础类型也是对象int/float/str/bool 都是对象(有自己的方法,如 1 .bit_length()
      • 函数是一等公民:函数可赋值给变量、当参数传、当返回值(高阶函数/装饰器的基础)
      • 类本身也是对象:类是 type 的实例(元类可动态建类)
      • 模块/异常/None:也都是对象(NoneNoneType 的实例)
    • 实际价值

      • 统一操作:所有对象都走“传对象引用/共享传参”(pass-by-object-reference——重新绑定参数名不影响外部,只有原地修改才影响)、都能放进容器([1, 'a', len, int] 合法)
      • 动态性基础:运行时可检查/修改(type/dir/getattr),支撑反射、装饰器、元编程
  • 协助记忆

    • 口诀:“万物皆对象:有类型、有地址、可传递——数字/字符串/函数/类都一样”。
  • 进阶思考

    • 函数为什么能当参数传(装饰器的原理)?
      • 因为函数也是对象(有类型 function、有 id),可以像普通对象一样赋值/传参/返回。装饰器“接收函数返回新函数”正是把函数当一等公民用的典型(@decorator 语法糖)。
    • is== 有什么区别?
      • is身份(是否同一个对象,id 相同),==(调用 __eq__)。小整数/短字符串有缓存(is 可能恰好为真),但判断相等应该用 ==is 只用于 x is Nonex is True 这类身份判断。
  • 扩展信息

    • 对象模型:Python 一切对象基于 PyObject(引用计数 + 类型指针),CPython 实现层面也统一
    • 元类:类是 type 的实例(type(name, bases, dict) 动态建类),可自定义元类(metaclass)控制类的创建

🤔 Python 里常见可变 / 不可变对象分别有哪些?

  • 可变对象(值可原地改,id 不变):list/dict/set/bytearray/自定义类实例;不可变对象(值不可改,改了就是新对象):int/float/bool/str/tuple/frozenset/bytes/None。核心:“改可变对象原地生效(会影响引用它的所有变量),改不可变对象等于创建新对象”。

    • 可变对象(Mutable)

      • listappend/pop/sort 原地改)、dict(增删键值)、setadd/remove
      • bytearray(可变字节串)、自定义类实例(属性可改)
      • 特点:原地修改不换 id,所有引用同步看到变化
    • 不可变对象(Immutable)

      • 数字:int/float/complex/bool
      • 序列:strs += 'x' 是新建对象)、tuple(元素不可替换)、bytes
      • frozenset(不可变集合)、None
      • 特点:任何“修改”操作都返回新对象(原对象不变)
    • 可变性的实际影响

      • 函数传参:传可变对象,函数内修改会影响外部(如默认参数别用 []/{},用 None
      • 作为 dict key/set 元素:必须可哈希(通常是不可变对象),list/dict/set/bytearray 不行,str/int/frozenset 可以,tuple 仅当元素都可哈希时才可(含 listtuple 不可哈希)
      • 别名陷阱a = b 是引用同一对象,改可变对象两边都变(浅拷贝解决一层)
  • 协助记忆

    • 口诀:“可变 list/dict/set,不可变数字/str/tuple——可变改原地、不可变换新对象”。
  • 进阶思考

    • tuple 里有 list,还算不可变吗?
      • tuple 本身不可变(不能增删/替换元素),但元素是引用——若元素是可变对象(如 list),那个 list 的内容还能改。所以“不可变”指 tuple 的槽位不变,不深及元素内容(这也是它哈希可能失败的坑)。
    • 为什么默认参数别用 []
      • 默认参数在函数定义时求值一次(存到 __defaults__),所有调用共享同一个 list 对象;第一次调用往里 append,之后所有调用都带着这批数据。正确写法:默认 None,函数内 if x is None: x = []
  • 扩展信息

    • 小对象缓存:CPython 对小整数(-5~256)/短字符串做缓存,is 比较可能为真(实现细节,勿依赖)
    • 哈希与不可变:可哈希 = 实现了 __hash__ 且生命周期内哈希值不变(不可变对象天然满足)

🤔 Python 浅拷贝 & 深拷贝 有什么区别?

  • = 赋值只是引用同一对象;浅拷贝copy.copy)只复制最外层容器,内层元素仍共享引用;深拷贝copy.deepcopy)递归复制所有层级,完全独立。核心:“浅拷贝复制外壳、深拷贝复制全部——嵌套结构要独立必须深拷贝”。

    • 三种复制方式

      • b = a(赋值):不复制,a/b 是同一个对象(a is b 为真),改一个都变
      • 浅拷贝 copy.copy(a)/a.copy()/list(a):新建外层容器,内层元素是引用(第一层独立、嵌套共享)
      • 深拷贝 copy.deepcopy(a):递归复制每一层,完全独立(改任何一层都不互相影响)
    • 示例(嵌套列表)

      • a = [1, [2, 3]];浅拷贝 b = copy.copy(a)b[0] 独立(改 b[0] 不影响 a[0]),但 b[1]a[1] 是同一个 list(改 b[1].append(4) 两边都变)
      • 深拷贝 c = copy.deepcopy(a)c[1] 也是新对象,随便改都不影响 a
    • 深拷贝的特点/代价

      • 循环引用处理deepcopymemo 字典记录已复制对象,能正确处理自引用/循环引用(不会死循环)
      • 代价:递归复制开销大(大结构/深嵌套慢、耗内存);对象可能需要自定义 __deepcopy__
    • 怎么选

      • 只改第一层 → 浅拷贝;嵌套结构要完全独立 → 深拷贝;只是别名 → 赋值
  • 协助记忆

    • 比喻:“赋值 = 同一把钥匙,浅拷贝 = 复印了房子图纸但家具还是那些(内层共享),深拷贝 = 整栋楼连同家具全部重建”。
    • 口诀:“浅拷一层、深拷到底;嵌套独立必深拷”。
  • 进阶思考

    • 为什么浅拷贝后改 b[0] 不影响 a[0],改 b[1] 却影响?
      • 浅拷贝新建了外层 listb 的槽位是独立数组——给 b[0] 赋新值只是改 b 自己的槽位;但 b[1] 引用的还是原来的内层 list 对象,通过它做原地修改append)时,两边看到的是同一个对象。
    • dict.copy()deepcopy 在运维脚本里怎么选?
      • 配置字典改一层字段 → dict.copy()(快);要把模板配置深改后各自独立(嵌套字典/列表)→ deepcopy。运维场景常见坑:用 dict.copy() 复制模板后改嵌套 list,把模板也改了——嵌套必须 deepcopy
  • 扩展信息

    • 相关 APIcopy.copy/copy.deepcopylist()/dict()/set() 构造器(浅拷贝)、切片 a[:](浅拷贝)、__copy__/__deepcopy__ 自定义
    • 序列化替代json.loads(json.dumps(x)) 可实现“伪深拷贝”(要求可 JSON 化,函数/对象不行)

🤔 List(列表)和 Tuple(元组)的核心区别?

  • 核心区别是可变性:list 可变(增删改、动态数组)、tuple 不可变(定长、不可改)。衍生差异:tuple 可哈希(能做 dict key/set 元素)、内存更小且可被缓存优化、语义上 tuple 表示“固定结构”而 list 表示“同类集合”。核心:“变用 list、不变用 tuple,做 key 必须 tuple”。

    • 可变性(根本区别)

      • listappend/insert/remove/pop/sort 都可原地改(动态数组)
      • tuple:不可增删改元素(定长);“修改”只能新建对象
    • 哈希与用途

      • tuple 可哈希(元素都不可变时)→ 可作 dict key、set 元素(如坐标 (x, y) 缓存)
      • list 不可哈希 → 不能作 key(可变性会导致哈希不稳定)
    • 性能与内存

      • tuple 创建更快、内存更小(无预留扩容空间)、常量可被解释器缓存/优化
      • list 有扩容机制(预留空间),动态增删灵活
    • 语义惯例

      • list:同质元素的集合(一列数据,会增删)
      • tuple:异质固定结构(一条记录:(name, age, city)、函数多返回值)
  • 协助记忆

    • 口诀:“list 动态可变,tuple 定长不可变;做 keytuple,装数据用 list”。
  • 进阶思考

    • 单元素元组怎么写(常见的坑)?
      • (1) 是数字 1(括号被当运算符),(1,) 才是元组——必须带逗号。函数返回/解包时容易踩:x = (1) 得到 int
    • 函数返回多个值其实是返回什么?
      • 返回的是一个 tuplereturn a, b 打包成元组),调用方 x, y = f() 是序列解包。理解这点就不会困惑“返回多个值”的机制。
  • 扩展信息

    • namedtuple/dataclass:给 tuple 加字段名(Point = namedtuple('Point', 'x y')),兼顾轻量与可读
    • 列表推导 vs 元组[x for x in ...] 是 list,(x for x in ...) 是生成器(不是 tuple)

🤔 Set(集合)有哪些特性?怎么实现去重的?

  • set 特性:无序、元素唯一(自动去重)、可变、元素必须可哈希;底层是哈希表,去重靠 hash() 定位 + == 判等(同哈希且相等才算重复)。支持集合运算(交/并/差/对称差)。核心:“哈希表存储,hash 找位、eq 判重,天然去重”。

    • 核心特性

      • 无序:不保插入顺序(遍历顺序不保证与加入顺序一致、不可依赖;要保序用 dict(3.7+)或 list+去重)
      • 唯一:重复元素自动去重({1, 1, 2}{1, 2}
      • 可变add/remove/discard/pop;不可变版本 frozenset
      • 元素可哈希int/str/tuple 可以,list/dict/set 不行
    • 去重原理(哈希表)

      • 底层是哈希表:加入元素先算 hash(x) 定位桶,桶为空直接放;不空则 == 比较现有元素——相等视为重复不加入,不等则探测下一个位置
      • 所以去重要求:元素可哈希,且“哈希相同 + 相等”的对象只保留一个
      • 复杂度:平均 O(1) 判重/查找(哈希冲突退化到 O(n)
    • 集合运算

      • a & b/a.intersection(b)、并 a | b、差 a - b、对称差 a ^ b
      • 子集/超集:a.issubset(b)/a.issuperset(b)
    • 常见去重场景

      • 列表去重:list(set(lst))(不保序);保序去重:list(dict.fromkeys(lst))dict 3.7+ 保序)
  • 协助记忆

    • 口诀:“set 无序唯一去重,哈希定位、相等判重;保序去重用 dict.fromkeys”。
  • 进阶思考

    • 为什么 set 元素必须可哈希?
      • 哈希表靠 hash(x) 定位存储位置;不可变对象哈希稳定可定位,list/dict 可变(内容改了哈希就错位、找不回),所以禁止。自定义对象要进 set/当 key,需正确实现 __hash____eq__(相等对象哈希必须相同)。
    • 大日志去重怎么选?
      • 量大且元素是 IP/URL 等短字符串 → set 内存可承受就用;百万级以上且只数唯一值 → 数据库 DISTINCT/外部排序/布隆过滤器(允许误判、省内存)。
  • 扩展信息

    • 相关frozenset(不可变集合,可哈希)、collections.Counter(计数+most_common)、dict.fromkeys 保序去重
    • 布隆过滤器:概率型去重结构(可能误判存在、不会漏判不存在),超大数据量去重的空间优化方案

🤔 Dict(字典)的底层实现原理是什么?

  • dict 底层是哈希表:keyhash() 算哈希定位桶,冲突用开放寻址解决,扩容按负载因子触发(重建表再散列);Python 3.7+ 用“紧凑 + 分离”结构(entries 数组 + indices 稀疏索引),并保证插入顺序。核心:“哈希定位 + 开放寻址 + 按需扩容,3.7+ 保序”。

    • 哈希定位(存取流程)

      • 写入/查询 d[k]:先 hash(k) → 按表大小取模得到槽位 → 比对槽位上的 key(先比哈希再比 ==)→ 命中返回/写入
      • 平均 O(1),最坏 O(n)(大量冲突,实际罕见)
    • 冲突解决:开放寻址

      • 冲突(槽位被占且 key 不同)时按探测序列找下一个空槽(Python 用扰动哈希 perturb 打散,减少聚集)
      • 不同于链地址法(Java HashMap 挂链表),Python 不挂链、全靠探测
    • 扩容(rehash)

      • 负载因子超阈值(约 2/3)触发扩容:分配更大的表(通常约 2 倍),把所有条目重新散列
      • 扩容是 O(n) 但均摊后单次操作仍近 O(1)
    • 紧凑结构 + 保序(3.7+)

      • 旧版:一张大数组(稀疏、浪费);3.6 起(3.7 语言保证):entries 紧凑数组存键值 + indices 稀疏数组存下标——省内存且按插入顺序遍历
    • key 的要求

      • key 必须可哈希(不可变:str/int/tuple/frozenset);自定义对象需实现 __hash__/__eq__(相等对象哈希必须相同)
      • value 任意(可变/不可变都行)
  • 协助记忆

    • 口诀:“dict = 哈希表:hash 定位、探测解冲突、满了扩容重建;3.7 起紧凑结构还保插入序”。
  • 进阶思考

    • 为什么 dict 查找这么快(和 list 遍历比)?
      • list 查值是逐个比对(O(n));dict 用哈希直接算出存储位置(O(1))——不扫描、直接跳到桶。所以“按 key 找值”永远优先用 dict(如 IP → 信息的映射)而不是并行 list
    • key 改了会怎样(可变 key 的坑)?
      • key 存的是哈希值定位;若 key 是自定义可变对象且内容被改,哈希变了但槽位没变——再查就找不到(哈希错位)。所以 key 必须用不可变对象或保证 __hash__ 与内容一致。
  • 扩展信息

    • 相关collections.OrderedDict(3.7 前的保序方案,现多用于 move_to_endLRU 语义)、defaultdict(缺省值)、Counter(计数)
    • __slots__:给类实例省内存(禁用 __dict__),大量对象时的优化手段

🤔 *args 和 **kwargs 的作用是什么?

  • *args 把多余的位置参数收进一个 tuple**kwargs 把多余的关键字参数收进一个 dict——用于定义“参数数量不定”的函数(转发/包装/装饰器必备)。调用时 */** 反向解包:*lst 拆序列、**dct 拆字典。核心:“定义时收集(打包),调用时展开(解包)”。

    • 定义侧(收集参数)

      • def f(*args):多余位置参数打包成 tuplef(1, 2, 3)args == (1, 2, 3)
      • def f(**kwargs):多余关键字参数打包成 dictf(a=1, b=2)kwargs == {'a': 1, 'b': 2}
      • 顺序固定:def f(普通, 默认=值, *args, **kwargs)*args**kwargs 前)
      • 命名可换(*values/**options),*args/**kwargs 只是惯例名
    • 调用侧(解包传参)

      • f(*[1, 2]) 等价 f(1, 2)(序列拆成位置参数)
      • f(**{'a': 1}) 等价 f(a=1)(字典拆成关键字参数)
    • 典型应用

      • 装饰器透传def wrapper(*args, **kwargs): return func(*args, **kwargs)(不关心被包函数签名)
      • 子类转发super().__init__(*args, **kwargs) 透传构造参数
      • 通用封装:执行命令/API 封装,参数灵活透传
    • 相关语法

      • 仅限关键字参数:def f(a, *, key=None)* 后只能关键字传)
      • 仅位置参数(3.8+):def f(a, b, /)/ 前只能位置传)
  • 协助记忆

    • 口诀:“*args 收位置成 tuple**kwargs 收关键字成 dict;调用加 * 是拆包——一收一放”。
  • 进阶思考

    • 为什么装饰器必须写 (*args, **kwargs)
      • 装饰器要包任意签名的函数,不能预知参数——用 *args, **kwargs 全量接收,再原样 func(*args, **kwargs) 透传,才能“包一切”。少写 **kwargs 会导致关键字参数全部报错。
    • *args 后还能加普通参数吗?
      • def f(*args, key=None) 合法——key 变成仅限关键字参数(只能 f(1, key=2),不能 f(1, 2))。这是设计 API 时强制调用方写清楚参数名的常用手段。
  • 扩展信息

    • functools.wraps:装饰器里保留原函数元信息(__name__/__doc__),配合 *args/**kwargs 使用
    • inspect.signature:运行时检查函数签名(写通用框架/CLI 工具时用)

🤔 什么匿名函数(Lambda)?

  • Lambda 是“没有名字的一次性小函数”:lambda 参数: 表达式——单表达式、隐式 return、不能写语句(无 if 块/while/赋值语句)。适用:需要函数的地方但不值得 def(排序 key、map/filter 回调)。核心:“一行小函数,当参数用完即弃”。

    • 语法与等价

      • add = lambda x, y: x + y 等价 def add(x, y): return x + y
      • 支持默认参数/可变参数:lambda x, n=1: x*nlambda *a: sum(a)
    • 限制(重要)

      • 只能一个表达式:不能多行、不能写语句(print 是表达式可写,赋值/import/return 不行)
      • 复杂逻辑别用:超过一行就该 def(可读性优先)
      • 别赋值给变量当 defPEP8 明确不建议 f = lambda: ...(应直接 def f(): ...
    • 典型用法

      • sorted(data, key=lambda x: x[1])(按第二列排序)
      • map/filter 回调、functools.reducedefaultdict 的工厂、Tkinter 按钮回调
  • 协助记忆

    • 口诀:“lambda = 一行函数:单表达式、隐式返回、当参数用——复杂逻辑回 def”。
  • 进阶思考

    • lambdadef 除了名字还有什么区别?
      • 本质都是函数对象(类型都是 function),能力上 lambda 只是“单表达式”语法糖。区别是形式:lambda 便于内联定义(当参数);def 有名字、可多行、可 docstring。运行期两者无性能差异。
    • 循环里用 lambda 捕获变量踩过坑吗(闭包晚绑定)?
      • fs = [lambda: i for i in range(3)] 三个函数调用时都返回 2——闭包捕获的是变量本身不是当时值,循环结束 i=2。修复:默认参数绑定当前值 lambda i=i: i
  • 扩展信息

    • 可调用对象:实现 __call__ 的类实例也能当“函数”传(比 lambda 更适合带状态),如 operator.itemgetter('key')
    • 运维场景sorted(logs, key=lambda l: l.split()[0]) 按日志首字段排序、filter 筛选进程

🤔 匿名函数(Lambda)有哪些应用场景?

  • Lambda 的主战场是“把小函数当参数传”:①sorted/list.sortkey(最常见)②map/filter/reduce 回调 ③dict/defaultdict 工厂 ④min/maxkeyGUI/回调注册 ⑥运维脚本(日志排序/字段提取)。核心:“作为一次性 key 或回调,不值得 def 时用”。

    • 排序 key(最常用)

      • sorted(users, key=lambda u: u['age']) 按字典字段排序
      • 多级:key=lambda x: (x[0], -x[1])(第一列升、第二列降)
      • 运维例:sorted(ips, key=lambda ip: int(ip.split('.')[-1]))IP 末段排序
    • map/filter/reduce 回调

      • list(map(lambda x: x.strip(), lines)) 批量处理
      • list(filter(lambda x: x > 90, scores)) 过滤
      • functools.reduce(lambda a, b: a + b, nums) 聚合
    • min/max/defaultdict 工厂

      • max(servers, key=lambda s: s.load) 取负载最高节点
      • defaultdict(lambda: {'count': 0}) 复杂默认值工厂
    • 运维脚本常见

      • 日志解析:key=lambda line: line.split()[3](按时间列排序)
      • 按扩展名分组:groupby(sorted(files, key=lambda f: f.rsplit('.', 1)[-1]), key=lambda f: f.rsplit('.', 1)[-1])groupby 只合并相邻相同键,必须先按分组键排序,否则同扩展名会散成多组)
  • 协助记忆

    • 场景口诀:“排序 keymap/filter 回调、max/min 取极值、工厂函数——一次性小逻辑就 lambda”。
  • 进阶思考

    • 什么时候不该用 lambda(用替代品更好)?
      • 有现成可调用就别 lambda:取字典字段用 operator.itemgetter('k')、取属性 attrgetter('a.b')、按真值过滤用 filter(None, lst)、按字符串排序直接 key=str.lower——更快更可读。逻辑复杂(超过一行表达式)就 def
    • lambda 在类里当方法有什么坑?
      • class A: f = lambda self: ... 语法可用但可读性差(PEP8 不建议);闭包捕获 self 时注意晚绑定。定义实例行为用普通 deflambda 留给参数场景。
  • 扩展信息

    • operator 模块itemgetter/attrgetter/methodcaller 是排序 key 的更优解(性能好、语义清晰)
    • key 函数思想:Python 排序哲学“Schwartzian 变换”——先算 key 再比,lambda 是这个体系最灵活的入口

🤔 什么是高阶函数?

  • 高阶函数(Higher-Order Function)是“接收函数当参数、或返回函数”的函数——因为 Python 里函数是对象(一等公民),可以像普通值一样传递。内置高阶函数:map/filter/sorted(key)/max(key)/min(key)reducefunctools(Python 3 起非内置);自建的:装饰器(返回新函数)。核心:“函数当参数/返回值用——抽象行为的手段”。

    • 两个判定条件(满足其一)

      • 接收函数作参数sorted(data, key=func)map(f, lst)filter(f, lst)
      • 返回函数:装饰器(def deco(f): def wrapper(): ... return wrapper)、工厂函数(make_multiplier(n) 返回新函数)
    • 内置高阶函数

      • map(f, iterable):映射(惰性,返回迭代器)
      • filter(f, iterable):过滤(f 返回真值保留)
      • sorted(iterable, key=f):按 key 排序;min/max 同理
      • functools.reduce(f, iterable, init):滚动聚合(两两合并)
    • 自建高阶函数(价值所在)

      • 行为参数化def run_tasks(tasks, handler): ...(把“怎么处理”作为参数传入,框架思维)
      • 装饰器:不改原函数逻辑,动态增强(日志/计时/缓存/重试)
      • 闭包工厂def make_retry(times): def deco(f): ... return deco(参数化装饰器)
    • 为什么有用

      • 复用模式而非数据:把“变化的部分”(规则/策略/回调)抽成函数传入,主体逻辑写一次
      • 装饰器/回调/策略模式/中间件的统一底层机制
  • 协助记忆

    • 口诀:“收函数、吐函数,就是高阶——map/filter/sorted(key) 是内置代表,装饰器是自建代表”。
  • 进阶思考

    • 高阶函数和回调函数是什么关系?
      • 回调是高阶函数的一种用法:把函数 A 传给 B,B 在合适时机“回过头来调用 A”。sortedkey、按钮的 command、异步框架的 on_done 都是回调——本质都是“函数作为参数”。
    • map/filter 和列表推导式怎么选?
      • 简单映射/过滤推导式更可读([f(x) for x in lst][x for x in lst if cond]),且天然 lambda-free;map/filter 在已有现成函数(不用 lambda)或多序列并行时占优(map(add, a, b))。风格上现代 Python 更偏推导式。
  • 扩展信息

    • functools 家族partial(固定参数生成新函数)、reducewraps(装饰器元信息)、lru_cache(缓存装饰器)
    • 函数式工具itertoolschain/groupby/islice)+ 高阶函数组合,处理日志/文件流非常高效

🤔 装饰器解决了什么问题?

  • 装饰器解决“不改原函数代码,统一给一批函数增强功能”的问题:把日志/计时/缓存/重试/鉴权等横切逻辑抽出来,用 @deco 语法一键套用——避免每个函数里复制粘贴同样的样板代码。本质是“接收函数、返回增强后新函数”的高阶函数。核心:“增强与业务解耦、一处定义多处复用”。

    • 解决的问题(横切关注点)

      • 重复样板:每个函数都写一遍“记录开始/结束/异常”→ 装饰器写一次全函数复用
      • 侵入为零:原函数代码不动(只在定义处加 @),方便随时增删
      • 统一收口:鉴权/限流/监控等策略改一处即全局生效
    • 典型应用

      • 日志/计时@log_call@timer(性能排查常用)
      • 缓存@functools.lru_cache(maxsize=128)(纯函数结果缓存,递归/API 查询提速)
      • 重试@retry(times=3, delay=1)(网络请求/脚本容错,运维必备)
      • 注册/路由@app.route('/api')(Web 框架路由注册)、@celery.task(任务注册)
      • 权限/校验@login_required、参数校验
    • 标准写法(带透传 + 保留元信息)

      • def deco(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): ... return f(*args, **kwargs) + return wrapper
      • *args/**kwargs 保证任意签名可用;wraps 保留 __name__/__doc__(否则日志/调试里全叫 wrapper
    • 带参装饰器

      • 三层:def deco(参数): def inner(f): def wrapper(...): ... return wrapper; return inner; return deco——@deco(参数) 先求值得真装饰器
  • 协助记忆

    • 口诀:“装饰器 = 不改原函数、统一加料——wraps 保元信息、*args/**kwargs 透传签名”。
    • 一句话:“把每个函数都要写的横切代码(日志/缓存/重试)抽成一个 @ 复用”。
  • 进阶思考

    • 装饰器执行时机(定义时还是调用时)?
      • 定义时@deco 在函数定义处立即执行(包装一次);调用时:执行的才是 wrapper 里增强逻辑。所以装饰器里的“装饰前打印”在 import 时就会输出——理解这点可解释很多“奇怪日志顺序”问题。
    • 多个装饰器的顺序?
      • 自下而上包装:@a @b def f 等价 f = a(b(f))——b 先包、a 后包(最外层 a)。执行增强逻辑时从最外层进(awrapper 先跑)。排列时想清楚“谁在最外层生效”。
  • 扩展信息

    • 类装饰器:实现 __call__ 的类也能装饰(可维护状态,如计数器装饰器)
    • 运维实用lru_cache 缓存 API 结果、自写 @retrysubprocess/requests@timer 定位慢函数

🤔 什么是闭包?

  • 闭包(Closure)是“内层函数引用了外层函数的变量,并且外层函数返回后该变量依然存活”的现象:内层函数把外层作用域“打包带走”。价值:函数工厂(按参数生成定制函数)、带状态的函数(不暴露全局)、装饰器的实现基础。核心:“函数 + 它引用的外层环境 = 闭包”。

    • 构成条件

      • ①有嵌套函数(内层定义在外层里)②内层引用了外层的变量③在外层作用域结束后仍能使用(典型是外层返回内层函数——非必须“返回”,但要让闭包在外层调用后存活通常这么写)
      • 例:def outer(n): def inner(x): return x + n; return inner——inner 捕获了 nadd5 = outer(5) 之后 n 依然活着
    • 核心机制

      • 被捕获的变量存在内层函数的 __closure__(cell 对象)里,不随外层栈帧销毁
      • 晚绑定:捕获的是“变量”不是“当时的值”(循环里 lambda: i 全拿到最后值的原因)
      • 写回要用 nonlocal:内层想修改外层变量(不是重新绑定自己的局部名)必须 nonlocal 声明
    • 典型用途

      • 函数工厂make_multiplier(3) 生成“乘 3”的函数(配置模板生成)
      • 带状态函数:计数器(count 变量 + nonlocal),比全局变量安全(状态封装在闭包里)
      • 装饰器wrapper 捕获 func——装饰器就是闭包最成功的应用
      • 回调配置lambda: send(url) 固定部分参数
  • 协助记忆

    • 口诀:“内层用外层变量 + 外层返回内层 = 闭包;改外层变量要 nonlocal;捕获的是变量不是值”。
  • 进阶思考

    • 闭包和全局变量有什么区别(为什么要用)?
      • 闭包把状态封装在函数里:不污染全局命名空间、多次工厂调用互不干扰(outer(1)/outer(2) 各自独立的 n)、变量生命周期受控。全局变量人人可改,闭包只能通过返回的函数访问——安全性/可维护性好。
    • 循环 + 闭包的“晚绑定”坑怎么破?
      • fs = [lambda: i for i in range(3)] 全返回 2(捕获变量 i 本身)。修复三选:①默认参数固化当前值 lambda i=i: i ②用 functools.partial(f, i) ③嵌套一层工厂函数把 i 变成参数。
  • 扩展信息

    • 查看闭包f.__closure__(cell 元组)、cell.cell_contents 取值;调试闭包状态有用
    • 类 vs 闭包:带多状态/多方法用类更清晰;轻量单状态/一次性工厂用闭包(lru_cache、装饰器内部都是闭包)

🤔 Python 的 LEGB 作用域规则?

  • LEGB 是变量查找顺序:Local(当前函数内)→ Enclosing(外层嵌套函数)→ Global(模块级)→ Built-in(内建),逐层向外找、找到即停;赋值默认只在当前层创建变量(要跨层写用 global/nonlocal)。核心:“读从内向外找,写只写本层——跨层写必须声明”。

    • 四层作用域

      • L(Local):当前函数内部定义的变量/参数
      • E(Enclosing):外层嵌套函数的变量(闭包捕获层)
      • G(Global):模块级变量(global 声明后可在函数内写)
      • B(Built-in):内建名(len/print/range
    • 查找规则(读)

      • 从 L 往外逐层找,首次命中即用(所以函数内 len = 5 会遮蔽内建 len
      • 全部未命中 → NameError
    • 赋值规则(写)——最常见的坑

      • 函数内任何赋值x = 1)默认把 x 声明为局部变量(编译期决定)——哪怕上面有同名全局变量
      • 若在赋值之前读它会 UnboundLocalError(不是拿到全局值!这是高频坑)
      • 想改全局 → global x;想改外层闭包变量 → nonlocal x
    • 特例

      • 只引用不赋值的变量会按 LEGB 正常向外查(函数里 print(g_var) 能拿到全局值)
      • del x/import x/for x 也算“赋值”(同样创建局部名)
  • 协助记忆

    • 口诀:“LEGB 由内向外找;函数里一赋值,变量就变局部——跨层写 global/nonlocal,先读后赋会 UnboundLocalError”。
  • 进阶思考

    • UnboundLocalError 为什么会出现?怎么排查?
      • 函数体内有 x = ... → 编译期把 x 标记为局部 → 执行到赋值前访问 x(如 print(x))就报此错。排查:看函数里是否有对它的赋值;要么改名、要么 global/nonlocal 声明、要么先赋值再读。
    • 为什么 Python 不像 shell 那样默认“写也是全局”?
      • 默认局部是防意外污染:函数自成命名空间,内部临时变量不会覆盖全局(否则大项目互相踩名字)。代价就是要显式声明跨层写入——这是刻意的安全设计。
  • 扩展信息

    • 类作用域不在 LEGB 链:类体内定义的名字,方法里不能直接当外层访问(要 self./类名)——常见混淆点
    • globals()/locals():运行时查看作用域字典(调试/动态代码用)

🤔 生成器与迭代器有什么区别?

  • 迭代器是协议(实现 __iter__/__next__ 的对象),生成器是“写迭代器最简单的方式”(含 yield 的函数自动生成迭代器)。区别:①生成器用 yield 一行搞定,迭代器要写类 ②生成器天然惰性(现场算现场给、状态自动保存)③生成器是一次性的。核心:“生成器 = 语法糖版迭代器,惰性求值省内存”。

    • 迭代器(Iterator,协议层)

      • 实现迭代器协议__iter__() 返回自身、__next__() 返回下一个值、耗尽抛 StopIteration
      • 可迭代(Iterable)≠ 迭代器list/str 可迭代(有 __iter__),但不是迭代器(没有 __next__);iter(lst) 才得到迭代器
      • 迭代器一次性:遍历完就空了(再遍历需重新 iter()
    • 生成器(Generator,语法层)

      • 生成器函数:函数体含 yield → 调用返回生成器(不执行函数体),每次 next() 执行到下一个 yield 暂停(局部变量/执行位置自动保存)
      • 生成器表达式(x*2 for x in data)(圆括号版推导式,惰性)
      • 是迭代器的简写:自动实现协议,不用手写类
    • 核心价值:惰性 + 省内存

      • 数据不一次性载入内存——读 10GB 日志文件逐行处理(for line in f 就是文件对象这个迭代器)
      • 无限序列可行:def counter(): n = 0; while True: yield n; n += 1
      • 管道组合:sum(int(x) for x in line.split()) 各环节惰性流式处理
    • 使用注意

      • 生成器只能消费一次(读完即空;要复用得重新创建或转 list
      • len() 不可用(不知道长度);调试打印需显式消费
  • 协助记忆

    • 口诀:“迭代器是协议(__iter__/__next__),生成器是 yield 语法糖——惰性逐个给、省内存、只能读一遍”。
  • 进阶思考

    • 为什么处理大文件用生成器(不用 readlines())?
      • f.readlines() 一次把全文件装进内存(10GB 文件直接 OOM);for line in f(文件迭代器)逐行给、处理完就释放——内存恒定。运维场景:日志分析、大 CSV 导入都用逐行惰性处理。
    • yieldreturn 的区别?
      • return 结束函数并交出全部结果;yield 暂停函数交出一个值,下次 next() 从暂停处继续(局部变量保留)。生成器函数调用时不执行任何函数体,返回生成器对象——首次 next() 才开始跑。
  • 扩展信息

    • yield from:委托子生成器(简化生成器嵌套),协程的前身
    • 相关工具itertoolschain/islice/groupby 惰性组合器)、enumerate/zip(本身就是迭代器)

🤔 如何高效处理 GB 级别大文件?

  • GB 级大文件的核心是“流式处理 + 不全量加载”:逐行迭代(for line in f,文件对象就是迭代器)替代 readlines();必要时分块读二进制(read(size)/iter(lambda: f.read(n), b''));压缩文件用 gzip/bz2 流式解压;配合生成器管道逐层过滤。核心:“内存恒定、逐行/分块流过,绝不一次读入”。

    • 逐行迭代(文本,最常用)

      • with open(path) as f: for line in f: process(line)——文件对象是迭代器,逐行给、内存 O(1)
      • f.readlines()/f.read():全量进内存(10GB 文件直接 OOM
    • 分块读(二进制/无行概念)

      • while chunk := f.read(8*1024*1024): process(chunk)8MB 一块,算 MD5/上传分片常用)
      • 迭代器写法:iter(lambda: f.read(n), b'') 直到空串
    • 压缩文件流式

      • gzip.open(path, 'rt')/bz2.open/lzma.openopen 用法一致(逐行流式解压)
      • 日志按天轮转压缩(.gz)直接这样读,不用先解压落盘
    • 生成器管道(复杂处理)

      • read_lines() → parse() → filter() → aggregate() 每级都是生成器,数据流水线惰性流过
      • 好处:解耦处理逻辑、内存恒定、可组合复用
    • 配合技巧

      • mmap(内存映射,超大文件随机访问)、pandas 分块 chunksizelinecache(随机取行)
      • 统计类(求和/计数)适合流式;需要全局排序/去重则外部排序或落数据库
  • 协助记忆

    • 口诀:“大文件三不:不 read()、不 readlines()、不全量进内存——逐行/分块/生成器管道流过去”。
  • 进阶思考

    • for line in f 为什么内存恒定?
      • 文件对象实现了迭代器协议,__next__ 每次只读一行到缓冲、处理完即可回收——同一时刻内存里只有当前行(和固定缓冲)。readlines() 则是构造完整列表。
    • 怎么边读边写(原地修改大文件)?
      • 文件不能“原地插行”——安全做法:读源文件逐行处理写到临时文件,os.replace() 原子替换;或先写新文件再改名。忌“边读边写同一文件”(会覆盖未读内容)。
  • 扩展信息

    • 相关 APIopen 的 buffering/encoding、fileinput(多文件逐行)、mmapgzip.openshutil.copyfileobj(流式复制)
    • 运维场景:日志分析(grep/统计 Top N)、大 CSV 清洗、备份文件校验(分块算 MD5)、nginx 日志切割合并

🤔 什么是面向对象编程(OOP)?

  • OOP(面向对象编程)是以“对象”为中心组织代码:把数据(属性)和操作(方法)封装成类,通过封装/继承/多态三大特性建模现实问题。核心:“类是模板、对象是实例——封装藏细节、继承复用扩展、多态统一接口”。

    • 基本概念

      • 类(Class):对象的模板/图纸(定义属性和方法)
      • 对象/实例(Object/Instance):类的具体化(Server('web1') 生成实例)
      • 属性(Attribute):对象的数据(self.hostname);方法(Method):对象的行为(self.restart()
    • 三大特性

      • 封装(Encapsulation):数据+操作打包,隐藏内部细节(_private/__name 约定与改名),只暴露接口——降低耦合
      • 继承(Inheritance):子类复用/扩展父类(class CMDB(Server): ...),super() 调父类逻辑——代码复用 + 层次建模
      • 多态(Polymorphism):不同类的对象响应同一接口(都实现 deploy(),调用方不关心具体类)——duck typing:看行为不看类型
    • Python 中的 OOP 特点

      • 一切皆对象(类本身也是对象)、支持多继承(MRO 解决菱形问题)、@property 优雅封装属性
      • 魔法方法(__init__/__str__/__len__)让自定义对象接入语言协议
    • 什么时候用 OOP

      • 有明确实体和状态(服务器/主机/任务)、多实例+统一操作(批量管理主机对象)、需要继承扩展的框架
      • 纯脚本/一次性任务用函数即可——不要为 OOP 而 OOP
  • 协助记忆

    • 口诀:“类模板对象实例,封装藏、继承扩、多态换——有状态多实例才用类”。
  • 进阶思考

    • Python 的多态和 Java 有什么不同?
      • Python 是 duck typing(鸭子类型):不看继承体系,只要对象有该方法就能调(“走起来像鸭子就是鸭子”)。Java 要显式实现接口/继承父类。Python 更灵活,但错误推迟到运行时(可用 abc 抽象基类/Protocol 补约束)。
    • 组合优于继承是什么意思?
      • 继承是“是一个”(强耦合父类实现);组合是“有一个”(把功能对象作为属性复用)。继承层次深了易碎(父类改动牵连全部子类),优先组合 + 接口小继承—— composabilityinheritance 更稳。
  • 扩展信息

    • 相关机制@property/@abstractmethod__slots__、MRO(C3 线性化)、dataclass(自动生成 __init__ 等)
    • 运维应用:批量主机管理(Host 类 + deploy()/check())、Ansible 模块开发、CMDB 建模

🤔 newinit 的区别是什么?

  • __new__ 负责“创建实例”(静态方法,先执行,返回实例对象);__init__ 负责“初始化实例”(给 self 填属性,后执行,不返回值)。__new__ 没返回实例,__init__ 根本不会被调用。核心:“__new__ 生、__init__ 养——单例/不可变类型定制才动 __new__”。

    • 职责与时机

      • __new__(cls, *args):真正的构造(分配内存/返回实例)。隐式静态方法,第一个参数是cls
      • __init__(self, *args):初始化(设置属性)。第一个参数是实例 self,只能返回 None
      • 调用顺序:obj = A(x) → 先 A.__new__(A, x) 拿到实例 → 再 __init__(obj, x)
    • 关键规则

      • __new__ 必须返回实例(通常 return super().__new__(cls));返回了别的类型对象 → __init__ 不执行
      • 日常业务不需要碰 __new____init__ 足够)
    • 什么时候用 __new__

      • 单例模式:类持 _instance__new__ 里判断已存在就返回同一实例(实现“全局唯一”)
      • 继承不可变类型str/int/tuple 子类定制(实例在 __new__ 阶段已定型,__init__ 改不了)
      • 元类/对象池:控制实例创建过程
  • 协助记忆

    • 口诀:“__new__ 先造人(返回实例),__init__ 再穿衣(填属性);__new__ 不还人,__init__ 不执行”。
  • 进阶思考

    • __new__ 写一个单例(运维配置中心场景)?
      • class Config: _inst = None; + def __new__(cls): if cls._inst is None: cls._inst = super().__new__(cls); return cls._inst——多处 Config() 拿同一实例(配置只加载一次)。注意多线程下要加锁(或用模块级对象天然单例)。
    • __init__ 里为什么不能 return 值?
      • 它的返回值被解释器强制忽略(约定只返回 None);实例由 __new__ 创建后交给 __init__ 填充。若 __init__ 返回非 None 直接 TypeError——返回新对象的活是 __new__ 的。
  • 扩展信息

    • 相关__del__(析构,引用计数归零时调用,慎用)、__call__(实例可调用)、元类 __call__ 控制类实例化
    • 单例替代:模块级全局对象(config.py 里直接实例化)、functools.lru_cache 的工厂——多数场景比 __new__ 单例更 Pythonic

🤔 @classmethod 类方法、@staticmethod 静态方法区别?

  • 三者区别在“第一个参数绑定了什么”:实例方法绑 self(实例);@classmethodcls(类本身,可访问类属性/造实例);@staticmethod 什么都不绑(就是放进类命名空间的普通函数)。核心:“实例用 self、类用 cls、都不沾用 staticmethod——classmethod 最典型用途是替代构造器”。

    • 三种方法对比

      • 实例方法def m(self)——操作实例状态,最常用
      • @classmethoddef m(cls)——第一个参数是;能访问/修改类属性、能 cls(...) 创建实例
      • @staticmethoddef m()——没有隐式第一参数,纯工具函数(放类里只为归组/避免散落模块)
    • @classmethod 的杀手锏:替代构造器(factory)

      • @classmethod def from_string(cls, s): return cls(*parse(s))——从字符串造实例
      • cls(...) 而非硬编码类名 → 子类调用返回子类实例(继承友好,工厂可扩展)
      • 运维例:Server.from_json(data)HostConfig.from_yaml(path)
    • @staticmethod 的适用

      • 逻辑上属于类但不依赖实例/类状态的工具:参数校验、格式转换、is_valid_ip(ip)
      • 与模块级函数的区别:归组在类下、命名空间清晰
    • 怎么选

      • 用到实例数据 → 实例方法;需要类信息或造实例 → classmethod;都不需要 → staticmethod(或干脆模块函数)
  • 协助记忆

    • 口诀:“self 管实例、cls 管类(造实例当工厂)、staticmethod 谁也不管——工具函数归个类”。
  • 进阶思考

    • 为什么替代构造器要用 classmethod 而不是 staticmethod
      • staticmethod 里造实例只能写死类名 Server(...)——子类调用还是返回父类实例;classmethodcls(...)Sub.from_string(s) 返回 Sub 实例,工厂随继承自动适配。这就是标准库 dict.fromkeysclassmethod 的原因。
    • @property 和这三者是什么关系?
      • @property 把方法伪装成属性(obj.status 不加括号),本质是描述符——访问器语义(读起来像字段、算起来是方法)。和 classmethod/staticmethod 一样都是装饰器改变方法绑定方式,只是目标不同:property 管属性访问、classmethod 管类绑定。
  • 扩展信息

    • 相关@property/@x.setterfunctools.cached_property(首次访问缓存)、抽象类 @classmethod 工厂
    • 运维场景:配置类多来源构造(from_yaml/from_env)、Inventory.from_file()、工具校验函数收进类

🤔 hasattr() 和 getattr() 有什么作用?

  • 两者都是反射(运行时按名字访问对象成员):hasattr(obj, 'name') 判断有没有该属性/方法(存在返回 True);getattr(obj, 'name', default) 按字符串名字取属性/方法(可带默认值,取到方法还能直接调用)。核心:“把属性名从写死变成字符串参数——按配置/输入动态调用”。

    • hasattr(obj, name)

      • 判断对象是否有某属性/方法:hasattr(host, 'restart') → 决定是否调用
      • 实现:内部尝试 getattr,异常(AttributeError)返回 False
      • 注意:会触发 property 的 getter(有副作用/抛错的 property 要小心)
    • getattr(obj, name, default)

      • 按字符串取属性:getattr(server, 'ip') 等价 server.ip,但名字可以是变量
      • 带默认值:属性不存在返回 default(不抛异常)——getattr(cfg, 'timeout', 30)
      • 取方法并调用action = getattr(host, op_name); action()——命令分发器
    • 典型应用(运维向)

      • 命令/动作分发:CLI 输入 restartgetattr(module, f'do_{action}') 动态调用(替代一长串 if/elif
      • 配置容错读取:对象配置里可选字段用 getattr(obj, key, default)
      • 插件机制:模块里有没有 handle 函数就注册(hasattr + getattr
      • 批量探测[m for m in ('cpu', 'mem', 'disk') if hasattr(agent, f'get_{m}')]
    • 配套

      • setattr(obj, name, value) 动态设置、delattr(obj, name) 删除
      • dir(obj) 列出全部属性名(配合过滤探索对象)
  • 协助记忆

    • 口诀:“hasattr 问有没有,getattr 按名取(带默认不抛错),取到方法直接调——名字字符串化,分发全靠它”。
  • 进阶思考

    • 用字符串分发方法和写 if/elif 链怎么权衡?
      • 动作少且固定 → if/elif 直观;动作多/可扩展(插件)→ getattr(module, f'do_{name}') + 注册表。注意安全:名字来自用户输入时必须白名单校验(前缀限定 do_ + hasattr 确认),否则可能调到不该调的方法(如 __class__)。
    • getattr 会不会破坏代码提示/静态检查?
      • 会——IDE/mypy 看不到动态名字的调用关系。核心接口写显式调用保可读,边缘/插件场景用动态;getattr 处加注释和默认值兜底,降低维护成本。
  • 扩展信息

    • 反射家族getattr/hasattr/setattr/delattr__getattr__/__getattribute__(拦截访问)、inspect 模块(更丰富的 introspection)
    • __getattr__ 区别:类上定义 __getattr__ 是“找不到属性时的钩子”(惰性加载/代理对象常用),和内置 getattr 函数不是一回事

🤔 super() 工作原理是什么?

  • super() 返回一个“沿 MRO(方法解析顺序)查找下一个类”的代理对象:调用 super().m() 不是“父类”,而是 MRO 链上当前类的下一个类(可能是兄弟类)。这解决了多继承/菱形继承的重复调用问题,并让协作式继承(每层只写增量)成为可能。核心:“super 找的是 MRO 下一个,不是父类”。

    • 基本行为

      • super().__init__(...):调用 MRO 中当前类的下一个类的 __init__
      • 无参 super()(Python 3)自动绑定当前类与第一个参数——等价 super(当前类, self)
    • MRO(方法解析顺序)

      • 多继承时确定“先找谁”的线性顺序(Python 3 用 C3 线性化
      • 查看方式:ClassName.__mro__ClassName.mro()
      • 保证:子类在父类前、父类间保持声明顺序、同一类只出现一次
    • 菱形继承与协作式初始化

      • D(B, C)B/C 都继承 A:若 B/C 都用 super().__init__()A.__init__ 只执行一次(沿 MRO 传递)
      • 若手动写 B.__init__(self)/C.__init__(self) 反而会重复调用 A——多继承下必须用 super()
      • 每层只写自己的增量 → “协作式继承”,参数用 *args/**kwargs 往下传
    • 常见坑

      • 单继承心智误区super() ≈ 父类没错,但多继承里“下一个类”可能出乎意料(要看 MRO)
      • 参数不匹配:链上各层 __init__ 签名不同时,super().__init__() 传参会断——需要设计统一签名或 **kwargs 透传
      • 混用 super() 和显式父类调用 → 链路断裂/重复执行
  • 协助记忆

    • 口诀:“super = MRO 下一个(不是父类);菱形继承必用 super,各层只加增量、**kwargs 往下传”。
  • 进阶思考

    • 为什么说“多继承里不用 super() 会重复初始化”?
      • 手动 B.__init__(self) + C.__init__(self)B/C 内部又各自调 A.__init__A 初始化两次(状态被重置/资源重复申请)。super() 按 MRO 串成链,A 只会轮到一次——这是 super() 存在的核心意义。
    • super() 在运维代码里哪里用得多?
      • 封装主机/任务类时扩展父类初始化(super().__init__(**kwargs) 透传再加自己的字段);插件基类约定:子类重写 run() 前后统一 super().run() 保钩子执行。
  • 扩展信息

    • 查看 MROA.__mro__inspect.getmro(cls)mro() 与 C3 算法
    • 设计建议:继承链超过 2-3 层就考虑组合;框架类(unittest.TestCase/Django View)里按规范 super(),钩子才不会漏

🤔 with 语句的原理是什么?

  • with 是上下文管理器协议的语法糖:进入时调用对象的 __enter__(拿到资源),无论成功/异常退出时必调 __exit__(释放资源)——等价于 try/finally 的标准化封装,保证“打开必关闭”。核心:“__enter__ 拿资源、__exit__ 兜底清理,异常也不会漏释放”。

    • 工作流程

      • with expr as x: → ①调用 expr.__enter__(),返回值绑定给 x ②执行块内代码 ③无论正常/异常,调用 expr.__exit__(exc_type, exc_val, tb)
      • __exit__ 返回 True吞掉异常(不再向外抛);返回 False/None → 异常继续传播(默认行为是释放资源后照常抛)
    • 解决的痛点

      • 必释放:文件/锁/连接忘关、异常路径漏 closewith 结构上保证清理
      • 等价手写f = open(...) + try: ... finally: f.close() 的简洁替代
    • 常用场景(运维向)

      • 文件with open(path) as f:(读大文件逐行)
      • with lock:(临界区自动加解锁,threading.Lock 支持协议)
      • 网络/数据库连接requests.Session/cx_Oracle/redis 客户端支持时用 with(自动归还/断开)
      • 临时目录tempfile.TemporaryDirectory()(退出自动删除)
      • 事务with conn: 自动提交/回滚
    • 自定义上下文管理器

      • 实现 __enter__/__exit__ 的类;或 @contextlib.contextmanager + yield(更简洁:yield 前是 __enter__、后是 __exit__finally 里清理)
  • 协助记忆

    • 口诀:“with = 自动 finally:进走 __enter__、出走 __exit__,异常也逃不掉;想吞异常 __exit__ 返回 True”。
  • 进阶思考

    • __exit__ 返回 True 有什么风险?
      • 它会静默吞掉块内所有异常(包括 KeyboardInterrupt 之外的真实错误),问题被掩盖。除非明确做异常转换(如把库异常转为自定义异常),默认返回 None 让异常正常抛出。
    • @contextmanager 和类实现怎么选?
      • 简单的一次性清理(yield 前后逻辑)用 @contextmanager 少写一个类;需要复用状态、多方法、或非 with 场景也能用 → 类实现 __enter__/__exit__。两者最终都走同一协议。
  • 扩展信息

    • contextlib 家族contextmanagerclosing(自动 close)、suppress(Exception)(吞指定异常)、ExitStack(动态管理多个上下文)
    • 运维价值:批量文件处理/连接池/分布式锁/临时配置切换——凡“用完必须还原”的操作都套 with

🤔 多进程、多线程、协程的区别是什么?

  • 三者是不同层级的并发模型:多进程(多个 Python 解释器,真并行,绕开 GIL,吃 CPU)→ 多线程(进程内多条执行流,系统调度,受 GIL 限制但 IO 等待时可切换)→ 协程(用户态单线程内切换,最轻量,专为高并发 IO)。核心:“CPU 密集用多进程、线程池够用的 IO 用线程、海量高并发 IO 用协程”。

    • 多进程(multiprocessing)

      • 每个进程独立解释器+独立 GIL真并行,充分利用多核
      • 适合 CPU 密集(批量计算/压缩/解析);开销大(启动慢、内存多、IPC 通信/序列化成本)
      • concurrent.futures.ProcessPoolExecutor 最省心
    • 多线程(threading)

      • 同进程内共享内存,由 OS 调度;受 GIL 限制(同一时刻仅一个线程执行 Python 字节码)
      • IO 密集可用:等网络/磁盘时释放 GIL,线程切出去干活(爬虫/批量请求/文件 IO
      • 共享状态需加锁(threading.Lock);线程数不宜过多(上下文切换开销)
    • 协程(asyncio/async-await)

      • 单线程用户态切换async/await + 事件循环,遇到 await 主动让出——切换成本极低(函数级),可支撑数万并发
      • 适合高并发 IO(网关/采集上万设备/长连接服务);忌在协程里跑 CPU 密集/阻塞调用(会卡死整个循环)
      • 生态:aiohttp/asyncpg/aiomqtt——IO 库必须用异步版
    • 对比与选择

      • CPU 密集 → 多进程(绕 GIL 真并行)
      • 少量并发 IO(几十~几百)→ 线程池(简单、生态全)
      • 海量并发 IO(千~万+)→ 协程(轻量高并发)
      • 混合场景:进程池 + 每进程内线程/协程(ProcessPoolExecutor + asyncio
  • 协助记忆

    • 口诀:“CPU 密集开进程,IO 密集线程够,海量并发上协程——进程重、线程中、协程轻”。
  • 进阶思考

    • 为什么协程能扛万级并发而线程不行?
      • 线程:每个占 MB 级栈内存 + 内核调度切换成本,千级线程系统就吃紧;协程:用户态切换(保存恢复函数栈,几百字节级),单线程内 await 让出,万级并发内存/切换都可控。代价是全链路异步(一个阻塞调用毁掉整个事件循环)。
    • GIL 到底挡住了什么(多线程就没用吗)?
      • GIL 挡的是“同一时刻多线程执行 Python 字节码”——CPU 密集多线程毫无加速;但阻塞 IO(网络/磁盘/sleep)会释放 GIL,所以 IO 密集多线程依然有效。另外 C 扩展(numpy/哈希计算)可在内部释放 GIL,也能并行。
  • 扩展信息

    • 标准库concurrent.futuresThreadPool/ProcessPoolExecutor 统一接口)、multiprocessingasyncioqueue(跨线程/进程通信)
    • 运维场景:批量主机采集(线程池)、万级设备监控(asyncio + MQTT)、日志并行压缩(进程池)

🤔 GIL(全局解释器锁)有什么作用?

  • GIL 是 CPython 的“一把大锁”:保证同一时刻只有一个线程执行 Python 字节码,作用是保护解释器内部状态(引用计数/内存管理)的线程安全,简化 CPython 实现。代价:多线程无法并行执行 CPU 密集的 Python 代码。核心:“GIL 护解释器不护你的并发——CPU 密集靠多进程,IO 密集线程照样有效”。

    • GIL 是什么

      • CPython 解释器级的互斥锁:线程要执行字节码必须先拿到 GIL
      • 目的:让引用计数等内部数据结构免于复杂加锁(实现简单、单线程性能高)
      • 注意GILCPython 实现细节,不是 Python 语言规范(Jython/IronPythonGIL
    • 影响(关键结论)

      • CPU 密集 + 多线程:无加速(甚至因锁竞争变慢)→ 应对:multiprocessing 多进程/C 扩展
      • IO 密集 + 多线程有效——网络/磁盘等待时线程会释放 GIL,别的线程继续跑
      • C 扩展可绕开numpy/hashlib/压缩库在 C 层计算时会释放 GIL,照样并行
    • 工作机制(简化)

      • 线程默认执行一段就主动让出 GIL:旧版用 sys.setcheckinterval(按字节码指令数),新版用 sys.setswitchinterval(默认时间片 5ms);遇到 IO/系统调用立即释放
      • 换回需要争抢 → 多线程频繁切换带来额外开销
    • 应对策略(运维开发向)

      • CPU 密集批量任务 → ProcessPoolExecutor(进程各自有 GIL,真并行)
      • 高并发 IO → 线程池或 asyncio 协程(单线程事件循环根本绕开线程竞争)
      • 计算下沉 Cnumpy 向量化/hashlib/Pillow——扩展内部并行
      • 前瞻:PEP 703free-threadingGIL 构建)已在演进,Python 3.13+ 提供实验性无 GIL 版本
  • 协助记忆

    • 口诀:“一把锁锁字节码:CPU 密集被锁死(上进程),IO 等待会放锁(线程可用),C 扩展能绕行”。
  • 进阶思考

    • 为什么 CPython 不干脆去掉 GIL?
      • 去掉 GIL 需要给所有对象/引用计数加细粒度锁——单线程性能大幅下降、C 扩展兼容性破坏。官方路线是 free-threading 独立构建(按需选择),而非直接替换默认版——兼容性与性能的折中。
    • 批量给 100 台机器算文件 MD5,怎么用对并发模型?
      • MD5hashlibC 实现)计算,读盘是 IO、计算时释放 GIL——线程池就有不错的加速;若算完还要做大量 Python 层统计(纯 CPU),再叠加进程池。先分清“瓶颈在哪一层”再选模型。
  • 扩展信息

    • 相关sys.setswitchinterval(切换间隔)、multiprocessingconcurrent.futuresasyncio
    • PEP 703 / free-threadingPython 3.13 起官方提供无 GIL 实验构建,生态正在跟进——长期方向但生产仍在过渡期

🤔 简述 Python 垃圾回收机制工作原理?

  • CPython 回收内存以引用计数为主(对象计数归零立即释放)+ 标记-清除兜底处理循环引用 + 分代回收提升效率(0/1/2 三代,越老越少扫)。核心:“引用计数即时回收,循环引用靠标记-清除,分代降低扫描成本”。

    • 引用计数(主力,即时回收)

      • 每个对象记录“被引用次数”;a = obj +1、del a/离开作用域 -1;归零立即释放(确定性回收)
      • 查看计数:sys.getrefcount(obj)(调用本身临时 +1)
      • 局限:循环引用a.b = b; b.a = a)计数永远不为 0 → 漏回收
    • 标记-清除(解决循环引用)

      • 从根对象(当前栈/全局/模块)出发标记可达对象,清除不可达的——循环引用双方若外界不可达,一起回收
      • 只对“容器对象”(list/dict/自定义类实例等可能形成环的)做,开销较大所以不是每次都跑
    • 分代回收(性能优化)

      • 经验假设:“对象越老越不容易死”——按存活时间分 0/1/2 三代
      • 新对象进 0 代;0 代扫描后存活晋升 1 代,1 代晋升 2 代;0 代扫描频繁、2 代稀少 → 摊薄总开销
      • 触发条件:各代分配/释放数量差超过阈值(gc.get_threshold(),默认 (700, 10, 10)
    • 运维相关(实战要点)

      • 内存不降不一定泄漏gc 回收有滞后;del 后计数是否归零、有无全局容器攥着引用才是关键
      • gc.disable() 慎用:纯短生命周期服务可关(省暂停),但长跑进程会积累循环垃圾
      • 大量临时对象/长列表 → 考虑生成器、__slots__ 减压
  • 协助记忆

    • 口诀:“引用计数即时清,环引用标记-清除兜底,分三代少扫描——泄漏先查引用攥在哪”。
  • 进阶思考

    • del x 之后内存为什么没立刻还给系统?
      • del 只是减引用计数;计数归零对象被销毁,但内存归还的是 pymalloc 内存池(供后续对象复用),不一定立刻返还 OS——所以 RSS 不降不代表泄漏。判断泄漏看趋势:持续增长且不回落才是。
    • __del__ 方法有什么坑?
      • 调用时机不确定(依赖 gc)、循环引用中可能不被调用、解释器退出时全局已部分销毁易报错——资源清理应该用 with/try-finally__del__ 只做兜底且要极简。
  • 扩展信息

    • 相关 APIgc.collect()(手动触发)、gc.get_count()/get_threshold()/set_threshold()gc.freeze()fork 前冻结)
    • 内存诊断tracemalloc(分配追踪)、objgraph(引用关系/环可视化)、pympler——排查泄漏三件套

🤔 Python 如何递归遍历目录并统计总文件数量?

  • 首选 pathlib.Path.rglob('*')os.walk() 递归遍历,文件计数:os.walk 按目录产出 (dirpath, dirs, files),累加 len(files)rglob 直接筛 is_file()。核心:“os.walk 效率高(生成器逐目录给)、pathlib 更简洁,按类型过滤用 Path 的方法”。

    • 方式一:os.walk()(经典,性能好)

      • total = sum(len(files) for _, _, files in os.walk(top_dir))
      • os.walk 是生成器:逐目录产出 (当前路径, 子目录列表, 文件列表),不一次性加载树
      • 带过滤:遍历时改 dirs[:](原地筛掉要跳过的目录,如 .git/node_modules)——注意必须改 dirs[:] 本身
    • 方式二:pathlib.Path.rglob()(现代,简洁)

      • total = sum(1 for f in Path(dir).rglob('*') if f.is_file())
      • rglob('*') 递归匹配全部;Path.iterdir() 只看一层;配合 glob('*.log') 按模式
      • f.stat() 可顺便算总大小/按扩展名统计
    • 按类型/大小统计(运维常用)

      • 分组计数:Counter(f.suffix for f in Path(d).rglob('*') if f.is_file())
      • 大文件排查:sorted((f.stat().st_size, f) for f in ... ) 找最大文件
    • 性能与边界

      • 软链接:os.walk(followlinks=False) 默认不跟(防死循环);Path.rglob 不跟目录软链
      • 权限不足目录会抛 PermissionError——包 try/except 跳过
      • 海量文件(百万级)用 os.scandir/walk 更快(scandir 附带文件类型,少一次 stat
  • 协助记忆

    • 口诀:“统计用 os.walk 累加 len(files),优雅用 Path.rglob;跳目录改 dirs[:],权限异常要兜住”。
  • 进阶思考

    • os.walk 怎么跳过指定目录?
      • 遍历中 dirs[:] = [d for d in dirs if d not in ('.git', 'logs')]——必须原地切片赋值dirs = ... 只换局部名,不影响后续遍历)。这是 os.walk 最经典的坑。
    • 百万级文件统计怎么提速?
      • os.scandirDirEntry 自带 is_file 结果,省 stat 系统调用)②多进程分目录并行(ProcessPoolExecutor 每个子树一个任务)③只统计必要信息(避免全量 stat)。
  • 扩展信息

    • 相关 APIos.walk/os.scandir/os.statpathlib.Pathrglob/glob/stat/suffix)、glob.glob(recursive=True)
    • 运维场景:磁盘小文件普查、日志目录按天统计大小、inode 排查(海量小文件)、清理前预览(find 的 Python 版)

🤔 Python 如何执行 Shell 命令?

  • subprocess.run()(标准推荐):参数列表形式 run(['ls', '-l'], capture_output=True, text=True, check=True)——拿返回码/输出、可控超时;需要 shell 特性(管道/通配)才 shell=True。核心:“subprocess.run + 列表参数 + check/captureos.system 不推荐用,os.popen/commands 已淘汰”。

    • 标准姿势:subprocess.run

      • r = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=30)
      • 结果:r.returncode(0 成功)、r.stdout/r.stderrtext=True 时是 str,否则 bytes
      • check=True:返回码非 0 直接抛 CalledProcessError(脚本里快速失败)
      • timeout=30:超时抛 TimeoutExpired(防止命令挂死拖垮脚本)
    • shell=True 什么时候用

      • 需要管道/重定向/通配:run('cat access.log | awk \'{print $1}\' | sort -u', shell=True)
      • 安全警告shell=True + 拼接用户输入 = 命令注入风险——输入必须走 shlex.quote() 或尽量用列表参数
    • 进阶用法

      • 管道串联p1 = Popen([...], stdout=PIPE); p2 = Popen([...], stdin=p1.stdout, ...)
      • 流式读输出:逐行 for line in p.stdout(长任务实时处理,不憋内存)
      • 后台进程Popen([...], start_new_session=True) 脱离会话(守护化)
      • 环境变量env={**os.environ, 'EXTRA': '1'}
    • 历史方案(了解即可)

      • os.system(只返回码、无法捕获输出,未被正式废弃但不建议)、os.popen(老式管道)、commands(Python 2 已删,已被移除)——统一用 subprocess
  • 协助记忆

    • 口诀:“执行命令就 subprocess.run:列表参数防注入、capture 拿输出、check 快失败、timeout 防挂死”。
  • 进阶思考

    • 列表参数和 shell=True 的本质区别?
      • 列表参数:Python 直接 execvp,参数原样传递,不经过 shell 解释;/|/$VAR 都是普通字符)——注入面小;shell=True 起一个 /bin/sh -c,字符串被 shell 展开/解析——功能强但危险。原则:能用列表就不用 shell=True
    • 远程批量执行怎么组合?
      • subprocess + sshrun(['ssh', host, cmd]),配 BatchMode=yes 免交互)——多机并发用线程池/asyncio 包起来;更进一步用 paramiko(纯 Python SSH 库,可控 SFTP/隧道)或直接上 Ansible
  • 扩展信息

    • 相关 APIsubprocess.run/Popen/CompletedProcessshlex.split(把命令串安全拆成列表)、shlex.quote
    • 运维场景:封装系统命令(磁盘/服务/网络检查)、CI 脚本调用、采集命令输出入库、自动化巡检

🤔 Python 如何实现在 100 台服务器上执行脚本?

  • 核心是“SSH 通道 + 并发 + 结果聚合”:①简单场景 subprocess + ssh 配线程池(ThreadPoolExecutor)并发执行 ②纯 Python 用 paramiko(SSH 库,可控超时/密钥/SFTP)+ 并发 ③生产级直接用 Ansiblead-hoc/playbook,幂等+失败重试)。核心:“SSH 到 100 台 → 线程/协程并发 → 超时与失败收集 → 结果汇总报告”。

    • 方案一:subprocess + ssh + 线程池(最快落地)

      • 前提:主机互信(ssh-keygen + ssh-copy-id 分发公钥)、Host 列表文件
      • ThreadPoolExecutor(20) + run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', host, cmd])
      • BatchMode=yes 禁止交互(密码提示直接失败)、ConnectTimeout 防挂死
      • 收集 (host, returncode, stdout, stderr) 汇总——失败的重试或单独报告
    • 方案二:paramiko(纯 Python SSH)

      • SSHClient + AutoAddPolicy + 私钥/密码认证,exec_command(cmd)stdin/stdout/stderr
      • 优点:不依赖系统 ssh、可设超时、可 SFTP 传文件、异常可控
      • 并发:线程池(每主机一个连接)或 asyncio + asyncssh
    • 方案三:Ansible(生产推荐)

      • ansible all -i hosts.ini -m shell -a 'uptime' -f 20(20 并发)
      • 优势:免写代码、幂等模块、失败重试、结果格式化、playbook 固化成任务
      • Python 里可调 ansible-runner/ansible-python api 集成
    • 工程要点(100 台规模的坑)

      • 并发度:20-50 合理(太高压垮自己网络/目标机 sshd
      • 超时:连接超时 + 命令超时都要设(防个别机器卡死拖整批)
      • 失败收集:哪台成功/失败/超时分类输出,失败重试队列
      • 凭据安全:密钥而非密码、known_hosts 管控、日志脱敏
  • 协助记忆

    • 口诀:“互信打底,线程池 + ssh 并发跑,BatchMode/超时防挂死;生产上 Ansibleparamiko 补定制”。
  • 进阶思考

    • 为什么用线程池而不是进程池跑 SSH?
      • SSH 执行是 IO 密集(等网络回包),线程足够且轻量(GIL 在等待时释放);进程池启动开销大、内存翻倍,只有命令本身要吃 CPU 时才考虑。百台规模线程池(20-50 并发)几秒到几十秒能完成。
    • 执行结果怎么保证可信(有机器假成功)?
      • ①校验返回码 + 关键输出(命令 echo 特定标记)②抽查/全量比对预期结果(如版本号一致性)③失败/超时机器单独列出人工复核。批量操作后必须给“主机-结果”清单,而不是一句“执行完成”。
  • 扩展信息

    • 相关库paramiko/asyncssh/fabricparamiko 封装,部署脚本利器)、plumbumansible/ansible-runner
    • 运维进阶Fabric 写批量部署脚本很顺手;长期固化任务上 Ansible playbook + CI;万级规模考虑 SaltStack/自研 Agent

🤔 Socket 是什么?有哪些应用场景?

  • Socket(套接字)是网络通信的端点抽象:IP + 端口 + 协议 组成的“门牌”,应用通过它读写网络数据——它是 HTTP/SSH/MySQL 等一切网络应用的底层。类型:TCP(可靠流,SOCK_STREAM)、UDP(无连接报文,SOCK_DGRAM)、Unix Socket(本机进程间,性能更高)。核心:“一切网络通信都建立在 socket 之上”。

    • Socket 是什么

      • 操作系统提供的网络编程接口(API):应用 ↔ 内核网络栈的通道
      • 定位五元组:协议 + 源 IP + 源端口 + 目的 IP + 目的端口(一条连接的唯一标识)
    • 三种主要类型

      • TCP(SOCK_STREAM:面向连接、可靠有序、流量/拥塞控制——HTTP/SSH/数据库
      • UDP(SOCK_DGRAM:无连接、低开销、不保证送达——DNS/监控打点/视频流
      • Unix Domain Socket:本机进程间通信(文件路径而非 IP),不走网络栈更快——MySQL/Docker/PHP-FPM 本机场景
    • TCP 编程模型(Python)

      • 服务端:socket() → bind(地址) → listen() → accept() 循环收连接 → recv/send
      • 客户端:socket() → connect() → send/recv → close()
      • Python 示例:s = socket.socket(AF_INET, SOCK_STREAM); s.bind(('0.0.0.0', 9000)); s.listen(5)
    • 应用场景(运维向)

      • 服务探活socket.create_connection((ip, port), timeout=2) 判端口通(比 ping 准)
      • 自定义采集/Agent 通信Agent 与服务端长连接上报(自定义协议或 MQTT/HTTP
      • 端口监听检查ss/netstat 之外用脚本验证服务真实可连
      • 本机加速:应用连本机 MySQL/RedisUnix Socket(省 TCP 开销)
      • 日志/打点syslog(UDP/Unix socket)、statsd(UDP 打点)
  • 协助记忆

    • 口诀:“socket = IP+端口+协议的网络门牌;TCP 可靠、UDP 轻快、本机走 Unix socket——探活/Agent/自定义协议都靠它”。
  • 进阶思考

    • 写 TCP 服务为什么一般用 socketserver/框架而不是裸 socket?
      • socketaccept/recv 循环是单线程阻塞,一次只能服务一个客户端;生产要并发(每连接线程/asyncio)、处理半包/粘包、优雅关闭——socketserver.ThreadingTCPServerasyncio.start_server 或现成框架(gunicorn/uvicorn)都封装好了。裸 socket 适合理解原理和小工具。
    • TIME_WAIT 大量堆积和 socket 什么关系?
      • 主动关闭方进入 TIME_WAIT(等 2MSL 确保对端收到 FIN 重传)——短连接高频(压测/爬虫)会堆积几万个端口。缓解:客户端复用连接(连接池/长连接)、SO_REUSEADDR(服务端重启快)、内核参数 tw_reuse。本质是“短连接太多”,治本是长连接化。
  • 扩展信息

    • 相关库socket/socketserver/asyncioopen_connection/start_server)、select/selectorsIO 多路复用)、paramikoSSH 协议跑在 TCP 上)
    • 排查工具ss -antp(连接状态)、tcpdump(抓包看协议交互)、nc(手工 socket 测试)

🤔 你都用 Python 调用过哪些 API 及应用场景?

  • 运维开发中 Python 调 API 主线是“云厂商 + 监控 + 协作工具”:阿里云 SDKECS/OSS/SLB/RAM 管理)、K8s client(资源操作)、Prometheus/Zabbix(指标拉取/告警)、企业微信/钉钉/飞书机器人(告警通知)、GitHub/GitLabCI 集成)。核心:“requests 通用打底,官方 SDK 优先,认证凭据走配置/环境变量”。

    • 云厂商 API(资源自动化)

      • 阿里云aliyun-python-sdk-*/alibabacloud-*——ECS 启停/创建、OSS 传取、SLB 挂载、RAM 临时凭证
      • 场景:定时开关测试环境 ECS(省成本)、OSS 日志归档、自动扩缩容脚本
      • 认证:AccessKey 走环境变量/RAM 角色实例(忌硬编码
    • K8s API(容器平台)

      • kubernetes 官方 client:读 Pod/Node 状态、扩缩容 Deployment、批量清理 Evicted
      • 场景:集群巡检脚本、Pod 异常自动处理、自定义 CRD 操作
    • 监控 API(数据/告警)

      • Prometheus/api/v1/queryPromQL 查询当前/历史指标)、/api/v1/alerts
      • Zabbixzabbix-api(主机/触发器/事件管理)
      • 场景:巡检报告拉数据、容量预测、告警闭环自动化
    • 协作/通知 API(告警闭环)

      • 企业微信/钉钉/飞书机器人 webhookPOST 一个 JSON 就能把告警/日报推进群
      • 工单系统Jira/ServiceNow):告警自动建单/关联
    • 开发协作 API

      • GitHub/GitLab:拉 MR 状态、自动打标签、CI 触发
      • CMDB/内部平台:资产查询、变更单流转
    • 调用通用规范

      • requests.Session 复用连接、超时必设timeout=(3, 10))、重试用 urllib3.Retry/装饰器、限速控制(防打爆对端)、异常捕获 + 日志
  • 协助记忆

    • 口诀:“云管资源(阿里云/K8s)、监控取数(Prometheus/Zabbix)、通知闭环(企微/钉钉 webhook)——requests 打底 + 官方 SDK,凭据绝不硬编码”。
  • 进阶思考

    • 调用第三方 API 最容易踩什么坑?
      • ①没设超时(对方挂了你的脚本也挂)②没处理限流(429,要退避重试)③分页没拉全 ④凭据硬编码进代码/日志。规范:timeout + 重试(指数退避)+ 分页封装 + 凭据走环境变量/KMS + 日志脱敏。
    • 官方 SDK 和直接 requests 调 REST 怎么选?
      • 有官方 SDK 优先(签名/重试/类型模型都封装好,如阿里云 SDKV3 签名手写极易错);SDK 覆盖不到的接口、或轻量一两个接口的场景,requests + 官方签名文档直接调。关键看签名复杂度和维护成本。
  • 扩展信息

    • 相关库requests/httpx(同步/异步)、各云官方 SDKkubernetesprometheus-api-clientPyGithub/python-gitlab
    • 架构化:告警通知统一封装(级别→路由到不同群/值班)、API 调用统一中间层(重试/限速/审计一处管)

🤔 如何排查 Python 程序的内存泄漏问题?

  • 排查思路“先确认 → 找增量 → 定对象 → 查引用”:①监控确认 RSS 持续涨(排除正常缓存)②tracemalloc 对比快照找增长最多的分配点 ③objgraph 看对象数量增长和引用链 ④常见根因:全局容器只进不出、闭包/缓存无限累积、循环引用带 __del__C 扩展泄漏。核心:“快照对比 + 对象增长定位 + 引用链追凶”。

    • 第一步:确认是泄漏(不是正常增长)

      • 监控 RSS/进程内存随时间趋势(Prometheus+node_exporter/容器 memory
      • 排除合理缓存(lru_cache 有上限的没事)、连接池预热——看“持续单调上涨且 gc.collect() 后不回落”
    • 第二步:tracemalloc(定位分配点,首选)

      • tracemalloc.start() → 运行一段时间拍快照 snapshot1 → 再拍 snapshot2
      • snapshot2.compare_to(snapshot1, 'lineno') → 按代码行列出增长最多的分配——直击“哪行代码在涨”
      • 生产可采样降低开销(tracemalloc.start(5) 保留 5 帧栈)
    • 第三步:objgraph(定位对象与引用链)

      • objgraph.growth():对比两次对象类型计数增长(哪类对象暴涨)
      • objgraph.show_most_common_types():当前对象 Topdict 多?Request 多?)
      • objgraph.show_backrefs([obj]):画引用链——看“谁攥着这个对象不放”(全局列表/缓存/闭包)
      • gc.get_referrers(obj):标准库版查引用者
    • 常见根因清单

      • 全局容器只进不出:日志缓存/请求记录 append 无上限、dict 当缓存无淘汰
      • 闭包/回调持有:注册的回调/事件监听从不注销(长连接场景常见)
      • 缓存无界:自写缓存不用 lru_cache(maxsize)functools 默认无限
      • 循环引用 + __del__:带析构的循环对象旧版处理异常
      • 线程本地/会话未清理threading.local 长期线程池累积
      • C 扩展泄漏tracemalloc 看不到——换 memray(能看到 C 层)
  • 协助记忆

    • 口诀:“先看趋势确泄漏,tracemalloc 找行,objgraph 找引用;全局容器/无界缓存/注销回调是最惯犯”。
  • 进阶思考

    • gc.collect() 前后内存差异说明什么?
      • collect 后明显回落 → 存在循环引用垃圾在等分代回收(正常机制,量大才要优化对象结构);collect 后不回落 → 对象仍被活引用攥着(真泄漏,去查引用链)——这个试验是快速分流的第一刀。
    • 生产环境不能重启怎么在线排查?
      • py-spy dump(无侵入看栈)、memray attach(附加追踪)、gdb + python-gdb 扩展、或程序预留诊断端点(触发 tracemalloc 快照/objgraph 报告)。重度问题可灰度开采样后重启一次拿完整数据。
  • 扩展信息

    • 工具链tracemalloc(标准库)、objgraphmemrayC 层+火焰图,最强)、pympler(对象大小)、py-spy(无侵入栈)
    • 预防:缓存一律带上限(lru_cache)、全局容器定界清理(定时裁剪)、长连接回调注册/注销配对、CI 内存回归测试(压测看曲线)

🤔 SQLAlchemy 是什么?

  • SQLAlchemy 是 Python 最主流的数据库工具包与 ORM:①Core 层:SQL 表达式语言/连接池/事务(贴近 SQL 的工程化封装)②ORM 层:把表映射成类、行映射成对象(2.0 风格 session.execute(select(User).where(User.name=='x')),旧 session.query(...) 在 2.0 已弃用)。核心:“连接池 + SQL 抽象 + ORM 映射,Python 操作数据库的事实标准”。

    • 两套能力(分层)

      • Core(表达式语言):用 Python 表达 SQL(select(users).where(...)),拿 Result 行——贴近 SQL、性能可控
      • ORM(对象映射):定义模型类 ↔ 表,操作对象即操作行(增删改查对象化),带身份映射/关系加载(relationship
      • 典型用法:ORM 日常 + 复杂查询下沉 Core/原生 SQL
    • 核心组件

      • Engine/连接池create_engine(url, pool_size=10, pool_pre_ping=True)——连接复用、断线重连
      • Session/事务:工作单元(with Session() as s: s.add(obj); s.commit()),自动 flush/回滚
      • 模型(Declarative)class User(Base): __tablename__='users'; id = Column(...)
      • relationship:表关系对象化(一对多/多对多,user.servers 直接拿列表)
    • 运维场景

      • CMDB/资产平台:主机/服务/变更记录建模 CRUD
      • 巡检/采集入库:采集结果批量写入(bulk_insert_mappings 高效批量)
      • 工单/发布系统:Django 之外的轻量选择(配合 Flask/FastAPI
    • 常用生态

      • Alembic:数据库迁移(表结构版本化,像 git 管理 schema)
      • SQLModelFastAPI 作者把 SQLAlchemy+Pydantic 合一(现代轻量)
  • 协助记忆

    • 口诀:“SQLAlchemy = 连接池(Engine)+ 会话事务(Session)+ 表类映射(ORM);复杂 SQL 下沉 Core,结构变更交给 Alembic”。
  • 进阶思考

    • ORM 和手写 SQL 怎么权衡?
      • 常规 CRUD/业务逻辑用 ORM(类型安全、防注入、可维护);复杂报表/大查询/DBA 调优场景用 Core 或原生 text() SQL(ORM 生成的 SQL 不一定最优)。注意 N+1 查询(循环里查关系)——用 selectinload/joinedload 预加载。
    • 运维脚本里用 SQLAlchemy 要注意什么?
      • engine 全局建一次(连接池别反复建)②批量写入用 bulk/executemany 而非循环单条 ③pool_pre_ping=True 防长空闲断连 ④脚本结束 engine.dispose() 释放连接。
  • 扩展信息

    • 版本:2.0 风格在 1.4 作为过渡引入(select() 语法/Session.execute),到 SQLAlchemy 2.0 才正式定型(迁移旧 Query 风格)——新项目直接 2.0 风格
    • 相关Alembic(迁移)、SQLModelasyncmy/asyncpg + SQLAlchemy async(异步引擎)、Django ORM(对照)

🤔 Python 运维自动化脚本你都用过哪些模块?

  • 运维自动化模块按“系统→文件→网络→并发→监控→第三方”分:系统交互 subprocess/os/sys/shutil;文件 pathlib/glob/fileinput;网络 requests/paramiko/socket/dnspython;并发 concurrent.futures/threading/asyncio;解析 re/json/yaml/csv;监控 psutil;打包 argparse/logging。核心:“subprocess+psutil+paramiko+requests 是运维四大件”。

    • 系统交互

      • subprocess(执行命令,run/Popen)、os/os.path/sys(环境/参数/路径)
      • shutil(复制/移动/删树/磁盘用量 disk_usage)、signal(信号处理,优雅退出)
      • psutilCPU/内存/磁盘/网络/进程全家桶(监控采集必备)
    • 文件与文本

      • pathlib(路径对象化)、glob/fnmatch(模式匹配)、fileinput(多文件逐行)
      • re(正则解析日志)、collectionsCounter 统计)、csv/json/yaml/configparser(数据交换与配置)
    • 网络与远程

      • requests/httpx(HTTP API 调用)、paramiko/fabricSSH 批量)、socket(端口探活)
      • dnspythonDNS 查询)、netifaces(网卡信息)、netmiko(网络设备 CLI
    • 并发与任务

      • concurrent.futures(线程/进程池)、threading/multiprocessingasyncio(高并发 IO
      • queue(生产消费)、sched/APScheduler(定时任务)、celery(分布式任务)
    • 工程化(脚本质量)

      • argparse/click/fire(命令行参数)、logging(日志分级落盘)、tenacity(重试)
      • pymysql/psycopg2/redis-py/pymongo(数据库客户端)、SQLAlchemyORM
    • 打包分发

      • venv/pip/pyproject.toml(环境)、pyinstaller(打成单文件可执行,方便分发到无 Python 主机)
  • 协助记忆

    • 口诀:“系统 subprocess+psutil,远程 paramikoAPIrequests,并发 futures,配置 yaml,参数 argparse,日志 logging——四大件加工程化”。
  • 进阶思考

    • 写一个可长期维护的运维脚本,模块组合套路是什么?
      • argparse 收参数 → logging 起日志(分级+落盘)→ yaml/环境变量读配置 → tenacity 包重试 → subprocess/paramiko 干活 → 结果结构化(json/csv)输出 + 异常分类退出码。骨架统一,脚本才可复用可交接。
    • 什么时候该从脚本升级到框架(Ansible 等)?
      • 重复 SSH 批量逻辑超过几处、需要幂等与变更留痕、多人协作维护 → 上 Ansible/SaltStack;脚本留在“轻量采集/胶水/一次性修复”定位。别用 Python 重造配置管理轮子。
  • 扩展信息

    • 一大波轮子glances/psutil(监控)、supervisor(进程管理,Python 写的)、ansible(自动化,Python 写的)
    • 质量工具pytest(脚本测试)、ruff/black(规范)、pre-commit——脚本也要过基本质量关

🤔 Python 如何统计 Nginx 日志 Top 10 访问 IP?

  • 标准三步:“提取 IP → Counter 计数 → most_common(10) 输出”:逐行读取日志(大文件流式)、正则或 split 取第一列 IPcollections.Counter 累加、most_common(10)Top。核心:“生成器逐行 + Counter 计数 + most_common,内存 O(唯一IP数)”。

    • 基础实现(十几行搞定)

      • 用生成器逐行读(GB 级也不怕):for line in open(log)
      • IPline.split()[0]nginx 默认 combined 格式第一列就是 IP)或正则 re.match(r'(\d+\.\d+\.\d+\.\d+)', line)
      • Counter 累加:Counter 对象 updatec[ip] += 1
      • 输出:c.most_common(10) 直接得到 [(ip, count), ...]
    • 代码骨架

      • from collections import Counter + c = Counter() + 循环里 c[line.split()[0]] += 1 + for ip, n in c.most_common(10): print(ip, n)
      • with open(...) 管理文件句柄;异常行 try/except 跳过(脏数据容错)
    • 进阶变体

      • 按状态码过滤if ' 500 ' in line 再计 IP(找错误请求来源)
      • 多文件/压缩日志fileinput.input(files)gzip.open 流式读 .gz
      • 多维度 Top:按 URL/UA/时段统计——正则捕获组 + 多个 Counter
      • 时间窗口:记录 (时间, IP) 按分钟分桶(Counter 键用 IP+分钟)找突发 CC
    • 性能要点

      • 逐行流式(内存 O(唯一IP数),百万 IP 也就几十 MB);正则比 split 慢数倍,能用 split 不用正则
      • 超大数据量(多 GB 多文件)→ 多进程分文件统计再合并 Counter,或交给 goaccess/ELK
  • 协助记忆

    • 口诀:“逐行读 → split[0]IPCounter 计数 → most_common(10)——三行核心 + 流式读文件”。
  • 进阶思考

    • 为什么用 Counter 而不是普通 dict?
      • Counter 就是“计数 dict”的封装:c[k] += 1 不用判 key 存在(缺失默认 0)、直接给 most_common/update 合并/算术运算(c1 - c2)——统计场景少一半样板代码。defaultdict(int) 也能干,但 most_common 得自己 sorted
    • 日志里有代理/CDN,第一列不是真实 IP 怎么办?
      • nginx 配了 X-Forwarded-For/X-Real-IP 时真实 IPhttp_x_forwarded_for 字段——解析时取 XFF 链的第一段(或按可信代理剥离)。注意 XFF 可伪造,安全统计要结合可信代理列表。
  • 扩展信息

    • 相关工具goaccess(实时日志面板)、awk '{print $1}' access.log | sort | uniq -c | sort -rn | head(一行流)、ELK/Loki(平台化)
    • 延伸场景CC 攻击识别(短窗 Top IP + 阈值告警)、慢请求分析(结合 request_time 字段)、PV/UV 日报自动化

🤔 Python 如何获取 CPU、内存、硬盘、网卡指标?

  • psutil(跨平台系统监控库)一把抓:cpu_percent()/virtual_memory()/disk_usage()/net_io_counters() 分别拿 CPU/内存/磁盘/网卡;进程级 psutil.Process() 全覆盖。核心:“psutil 采集 + 换算百分比 + 定时采样,替代手拼 top/free 输出”。

    • CPU

      • psutil.cpu_percent(interval=1):整体使用率(阻塞 1 秒采样);percpu=True 每核
      • cpu_count() 核数、cpu_times() 用户/系统/空闲细分、load_avg() 负载(Unix
    • 内存

      • psutil.virtual_memory()total/available/percentavailable 算使用率,比 100-used 准——含可回收缓存)
      • swap_memory():交换分区
    • 磁盘

      • psutil.disk_usage('/'):单分区 total/used/percent
      • disk_partitions():枚举所有挂载点(遍历逐个 usage
      • disk_io_counters():读写 IO(次数/字节/耗时)
    • 网卡

      • psutil.net_io_counters(pernic=True):每网卡收发字节/包/错包——两次采样差值÷时间 = 速率
      • net_if_addrs()IP/MACnet_if_stats(): up/down、速率、MTU
    • 进程级

      • psutil.Process(pid).cpu_percent()/.memory_percent()/.open_files()/.connections()(查端口占用)
      • process_iter() 遍历所有进程(找 CPU 最高的进程)
    • 监控落地

      • 定时采样脚本(while True + sleep)→ 推 Prometheusprometheus_client)/Zabbixsender)/写 InfluxDB
      • 阈值告警:mem.percent > 90 触发通知;配合历史趋势看异常
  • 协助记忆

    • 口诀:“psutil 一把抓:cpu_percentvirtual_memory(用 available)、disk_usagenet_io_counters(采样差值算速率)”。
  • 进阶思考

    • 内存使用率为什么用 available 而不是 100 - used
      • Linux 把空闲内存拿去做页缓存(buffers/cached),used 看着很高但缓存可回收;available 是“不给新进程添乱的前提下还能分配多少”——才是真实可用量。这也是 free 命令新口径的含义。
    • 网卡速率为什么要两次采样?
      • net_io_counters 给的是累计字节数(开机以来),不是速率——取 t1/t2 两次差值除以间隔才是 B/scpu_percent(interval=1) 内部就是帮你做了这个两次采样,而网卡要自己算。
  • 扩展信息

    • 相关库psutil(跨平台 Windows/Linux)、prometheus_client(暴露指标端点)、netifacespy3nvmlGPU
    • 对照命令top/free -h/df -h/ifconfig——psutil 即它们的编程化(且跨平台统一接口)

🤔 Python 有哪些 Web 框架及各自特点?

  • Python Web 框架按“全能 vs 轻量 vs 高性能”分:Django(全家桶,大而全)、Flask(微框架,灵活组装)、FastAPI(现代异步,类型驱动+自动文档)、Tornado/Sanic(高性能异步)、aiohttp(异步客户端+服务端)。核心:“企业级全能用 Django、轻量脚本用 FlaskAPI 高并发用 FastAPI”。

    • Django(全能型)

      • 自带 ORM/Admin 后台/认证/中间件/模板/迁移(migrations)—— batteries included
      • 适合:完整业务系统(CMS/管理平台/CMDB),开发效率高、生态成熟
      • 代价:重、学习曲线陡、灵活性低(约定大于配置)
    • Flask(微框架)

      • 核心极小(路由+请求对象),其他靠扩展拼装(Flask-SQLAlchemy/Flask-Login
      • 适合:中小服务、内部工具、运维脚本 Web 化——自由度高
      • 同步为主(gevent 补异步),大并发需额外架构
    • FastAPI(现代 API 框架)

      • 基于 StarletteASGI 框架)构建、运行于 ASGI 服务器(如 uvicorn)原生异步;Pydantic 类型校验;自动生成交互式文档Swagger/ReDoc
      • 适合:高性能 API 服务、微服务、OpenAPI 驱动的平台接口
      • 当前新项目 API 服务的热门首选
    • 其他

      • Tornado:老牌异步(长连接/WebSocket 先驱);Sanicasync 高性能;aiohttp:异步 client+server 一体
      • StarletteFastAPI 的底层(轻量 ASGI 工具箱)
    • 选型

      • 完整平台带后台 → Django;内部小工具/脚本服务 → Flask;纯 API 高并发/要文档 → FastAPI
  • 协助记忆

    • 口诀:“Django 全家桶、Flask 拼积木、FastAPI 异步快——按项目体量和接口形态选”。
  • 进阶思考

    • 运维开发大多用哪个?
      • 内部平台(CMDB/工单/发布系统)常用 DjangoAdmin 白捡管理后台);对接 API/采集上报用 FastAPI(异步高并发+文档);一次性小服务 Flask 最快。三者共存的团队很常见,按场景不是二选一。
    • WSGIASGI 是什么关系?
      • WSGI:同步网关协议(Flask/Django 传统部署,gunicorn+nginx);ASGI:异步扩展(FastAPI/Starlette,支持 WebSocket/长连接,uvicorn/daphne)。Django 自 3.0 起支持 ASGI(现主流 5.x;3.0-3.2 已 EOL)——协议决定并发模型。
  • 扩展信息

    • 部署标配gunicorn/uvicorn(应用服务器)+ nginx(反代/静态)——框架只管应用层
    • 相关Pydantic(数据校验)、OpenAPI(接口规范)、celery(异步任务,各框架通用)

🤔 简述 Django MVT 架构?

  • DjangoMTV 模式(官方称谓);常被写作 MVTModel(模型,管数据与业务规则,映射数据库)、Template(模板,管展示 HTML)、View(视图,管请求处理逻辑,连接 MT)+ URLconf 路由分发。与传统 MVC 对应:DjangoViewMVCControllerTemplateView。核心:“M 管数据、V 管逻辑、T 管展示,URL 把请求引到 V”。

    • 三个角色

      • Model(模型)class Host(models.Model): hostname = models.CharField(...)——定义表结构、校验、业务方法;ORM 负责 SQL
      • View(视图)def host_list(request): ...——接收请求、查 Model、组装上下文、返回 HttpResponse/渲染模板
      • Template(模板){{ host.hostname }} {% for %}——Django 模板语言渲染 HTML(展示与逻辑分离)
    • 请求完整流程(面试常问)

      • 浏览器请求 → WSGI/ASGI 服务器(gunicorn/uvicorn)→ Django 中间件链 → URLconfurls.py)匹配路由 → 对应 View 执行 → ViewModel(数据库)→ 数据传给 Template 渲染 → HttpResponse 原路返回
    • 与 MVC 的对应(易混淆点)

      • Django MVTModelMViewC(控制器角色)、TemplateV(视图角色)
      • MTV 更准确(官方文档用词)——考察的是“ViewDjango 里不是展示层”
    • 各文件归属(工程结构)

      • models.py(M)、views.py(V)、templates/(T)、urls.py(路由)、forms.py(表单校验)、admin.py(后台)
  • 协助记忆

    • 口诀:“MVTModel 管库、View 管逻辑、Template 管页面,urls 做引路人——DjangoView 是控制器不是视图”。
  • 进阶思考

    • 前后端分离后 MVT 还有意义吗?
      • 有变化:Template 层被前端框架(Vue/React)替代,Django 退为“M+V”提供 APIDRFJSON)——Model/View/路由/中间件/ORM 全部照用。MVT 的分层思想(数据/逻辑/展示解耦)不变,只是展示层外移。
    • View 里的业务逻辑要不要塞进 Model?
      • 数据相关的业务规则(校验、状态流转)放 Model 方法/Manager(可复用、事务安全);纯请求编排(解析参数、选模板、多模型组合)放 View。别把业务都堆 View——fat models, thin views 是 Django 社区主流建议。
  • 扩展信息

    • 配套机制forms/serializers(输入校验)、middleware(横切处理)、signals(解耦事件)、admin(自动后台)
    • 运维应用CMDB/工单/发布平台用 Django 开发,Admin 快速生成管理界面

🤔 Django ORM 是什么?

  • Django ORMDjango 的对象关系映射层:把数据库表映射成 Python 类(Model)、行映射成对象、字段映射成属性——用 Host.objects.filter(rack='A1') 这样的 Python 代码代替手写 SQL。核心:“表↔类、行↔对象、查询↔方法链,附带防注入/迁移/多数据库支持”。

    • 核心概念

      • Modelclass Host(models.Model): hostname = CharField(max_length=64, unique=True) → 自动建表 app_host
      • QuerySet:查询结果集(惰性、可链式)——Host.objects.filter().exclude().order_by() 生成 SQL 后才执行
      • ManagerHost.objects——查询入口(可自定义 Manager 加常用查询方法)
    • 常用能力

      • CRUDsave()/delete()objects.get()/create()/update()
      • 过滤:filter(rack='A1', status='on')(AND)、Q(rack='A1') | Q(rack='A2')(OR)、__ 跨关系/字段查找(hostname__containscreated__gte
      • 聚合:annotate/aggregate + Count/Sum(统计报表)
      • 关联:ForeignKey/ManyToMany + related_name 反向查询
    • 优点与代价

      • 优点:免写 SQL 防注入、数据库可切换(MySQL/PG)、migrations 管理表结构版本、模型即文档
      • 代价:复杂查询生成 SQL 可能低效(要看 query/日志)、魔法多黑盒、N+1 查询坑
    • 配套

      • migrationsmakemigrations + migrate——表结构变更版本化(像 git 管理 schema)
      • Django 外可用 SQLAlchemy(更通用);ORM 理念相通
  • 协助记忆

    • 口诀:“ORM = 表变类、行变对象、filter 代替 WHERE;惰性 QuerySet__ 穿关系、迁移管结构”。
  • 进阶思考

    • 什么是 N+1 查询问题,怎么解决?
      • 遍历 100 个 Host 再逐个取 host.owner.name → 1 次查列表 + 100 次查外键 = N+1。解决:Host.objects.select_related('owner')(外键 JOIN)或 prefetch_related('tags')(多对多二次查询)——一次批量取,列表页性能天壤之别。
    • QuerySet 是惰性的意味着什么?
      • qs = Host.objects.filter(...) 只构造不执行;迭代/list()/len()/切片带 step 才触发 SQL。利用惰性可自由拼条件(不同分支追加 filter);QuerySet 一旦求值会缓存结果——重复迭代同一个对象不重查;只有新 filter(产生新 QuerySet)才重查——注意底层数据变更不会自动刷新已缓存结果(stale 旧值),要拿新数据得重建查询。
  • 扩展信息

    • 性能手段select_related/prefetch_relatedonly/defer(字段裁剪)、bulk_create/bulk_update(批量)、iterator()(大结果集流式)
    • 运维场景CMDB 资产模型(机房/机架/主机关系)、工单流转、发布记录审计——Admin 后台白捡管理界面

🤔 Django ORM 有哪些常用查询方法?

  • 常用查询分五类:取数据(all/get/filter/exclude/first/last)、字段条件(__ 系列:__contains/__in/__gte/__isnull)、排序截断(order_by/[切片]/distinct)、聚合统计(count/exists/aggregate/annotate)、关系查询(select_related/prefetch_related/__ 跨表)。核心:“filter 链式组合 + __ 双下划线穿字段/关系 + 聚合函数收尾”。

    • 基础取数

      • Host.objects.all() 全部(惰性);.filter(rack='A1') 条件筛选(返回 QuerySet)
      • .get(pk=1) 唯一一条(多/无都抛异常);.first()/.last() 取一(无则 None)
      • .exclude(status='off') 反向排除;.exists() 是否存在(比 count()>0 快)
    • 字段条件(__ 查找后缀)

      • hostname__contains='web'(含)/__icontains(忽略大小写)/__startswith/__endswith
      • id__in=[1,2,3]created__gte=start/__lte=end(范围)、owner__isnull=True
      • 跨关系:host__rack__name='A1'(双下划线一路穿到底)
    • 排序与截断

      • .order_by('-created', 'hostname')- 倒序);.distinct() 去重
      • 切片:qs[:10](LIMIT 10)、qs[10:20](分页);注意负索引不支持
    • 聚合与统计

      • .count() 总数;.aggregate(total=Sum('size'), avg=Avg('cpu')) 整体聚合(返回 dict)
      • .values('rack').annotate(n=Count('servers'))分组(如机架)附加聚合值——annotate 单独用时按模型主键逐行附列,配合 values() 才做分组聚合
      • values('rack')/values_list('hostname', flat=True)——要字段不要对象(轻量)
    • 关联优化(性能关键)

      • .select_related('owner'):外键 JOIN 一次取(一对一/多对一)
      • .prefetch_related('tags'):多对多/反向外键分两步批量取
      • 批量:bulk_create([Host(...), ...])update(rack='B2')(批量改不逐个 save)
    • 逻辑组合

      • Q 对象:filter(Q(rack='A1') | Q(rack='A2'), status='on')| 或、& 与、~ 非)
      • F 表达式:update(count=F('count') + 1) 数据库端自增(防并发丢更新)
  • 协助记忆

    • 口诀:“取数 all/filter/get,条件靠 __(contains/in/gte),排序 order_by 截断用切片,统计 count/aggregate/annotate,关联 select_related/prefetch_relatedN+1”。
  • 进阶思考

    • get()filter().first() 怎么选?
      • get() 语义是“必须恰好一条”——多条抛 MultipleObjectsReturned、没有抛 DoesNotExist(显式失败,适合主键/唯一键);first() 宽容返回 None(适合“可能没有”的业务查询)。乱用 get() 会让脚本被脏数据炸掉。
    • annotateaggregate 的区别?
      • aggregate 整表聚合一行结果({'total': 100});annotate每个分组行附加计算列(每个机房多少台主机:values('rack').annotate(n=Count('id')))。一个是汇总值,一个是分组标签。
  • 扩展信息

    • 进阶Q/F/Subquery/Case-When(条件表达式)、自定义 Managerraw() 原生 SQL 逃生口
    • 调试print(qs.query) 看生成 SQLdjango-debug-toolbar/日志 sql ——优化前先看它到底发了什么 SQL

🤔 Django REST Framework(DRF)是什么?

  • DRF 是基于 Django 的 REST API 开发框架:把模型/业务快速暴露成规范的 JSON API——Serializer(序列化/校验)、ViewSet+Router(一套代码自动生成 CRUD 路由)、认证/权限/限流/分页/版本 内置可插拔、自带 Browsable API 调试页。核心:“DjangoWebDRFAPI——序列化 + 通用视图 + 可插拔组件”。

    • 核心组件

      • Serializer(序列化器):对象 ↔ JSON 转换 + 输入校验(类似 FormAPI 版)——ModelSerializer 从模型自动生成字段
      • View/ViewSetAPIView(最灵活)→ GenericAPIView(泛型基类)→ ListCreateAPIView 等(GenericAPIView+Mixin 组合)→ ModelViewSet(一套搞定增删改查)
      • RouterDefaultRouter 注册 ViewSet 自动生成 RESTful 路由(/hosts//hosts/1/
    • 内置可插拔机制(面试重点)

      • 认证(Authentication)Session/Token/JWTdjangorestframework-simplejwt)——我是谁
      • 权限(Permission)IsAuthenticated/IsAdminUser/自定义——能干什么
      • 限流(Throttling)AnonRateThrottle/UserRateThrottle——防刷
      • 分页(Pagination)PageNumberPagination/LimitOffset过滤/搜索django-filter + SearchFilter排序:DRF 内置 OrderingFilter
      • 版本化URLPathVersioning 等;渲染器JSON + Browsable API(浏览器直接调试)
    • 典型用法(三件套起步)

      • 定义 ModelSerializer → 写 ModelViewSetqueryset + serializer_class)→ router.register('hosts', HostViewSet)——5 行代码一套完整 CRUD API
    • 运维场景

      • CMDB/工单/发布平台对外 API、自动化平台与脚本的接口层、对接监控/告警回调
  • 协助记忆

    • 口诀:“DRF = 序列化(Serializer)+ 通用视图(ViewSet+Router)+ 插件(认证/权限/限流/分页)——Django 数据能力转 REST 接口”。
  • 进阶思考

    • DRF 的 ViewSet 和普通 View 怎么选?
      • 标准 CRUD 资源 → ModelViewSet + Router(最少代码、规范路由);非标准动作 → @action(detail=True) 加自定义端点;完全自定义逻辑 → APIViewDjango 原生 View。先 ViewSet,特殊再下沉。
    • JWT 认证在 DRF 里怎么落地(运维平台场景)?
      • simplejwt 提供获取/刷新 token 端点;脚本端先拿 token、后续 Authorization: Bearer 头携带;配合权限类控制只读/读写。相比 SessionJWT 更适合脚本/多服务调用(无状态、跨域友好);注意 token 过期与刷新策略。
  • 扩展信息

    • 配套库django-filter(过滤)、simplejwtJWT)、drf-spectacular(自动生成 OpenAPI 文档)、drf-extensions
    • 对比FastAPI(原生 Python 类型驱动,异步)vs DRFDjango 生态,Admin/ORM 复用)——已有 Django 项目加 API 首选 DRF

🤔 DRF ModelViewSet 的父类有哪些?

  • ModelViewSet = GenericViewSetViewSetMixin + GenericAPIView 骨架,提供查询集/序列化器/分页过滤)+ Mixin 五件套(ListModelMixin/CreateModelMixin/RetrieveModelMixin/UpdateModelMixin/DestroyModelMixin,分别提供 list/create/retrieve/update/destroy 动作)。核心:“GenericAPIView 定骨架 + Mixin 按需混入动作——ReadOnlyModelViewSet 只读、ModelViewSet 全有”。

    • 继承结构(自底向上)

      • APIViewDRF 根视图(request/response/认证权限限流/异常处理)
      • GenericAPIView:+ queryset/serializer_class/get_object()/分页过滤钩子(通用骨架,无具体动作)
      • 五个 MixinListModelMixin.list()CreateModelMixin.create()RetrieveModelMixin.retrieve()UpdateModelMixin.update()/partial_update()DestroyModelMixin.destroy()——各只实现一个动作
      • ModelViewSet = GenericViewSet(内含 GenericAPIView + ViewSetMixin)+ 全部五个 Mixin → 完整 CRUD
    • 组合产物(常用)

      • ReadOnlyModelViewSet:只 list+retrieve(只读接口)
      • ModelViewSet:全 CRUD
      • 泛型具体视图:ListAPIView/CreateAPIView/ListCreateAPIView/RetrieveUpdateDestroyAPIView(单动作版,不用 Router 时直接绑 URL
    • 定制方式

      • 重写 get_queryset()(按用户/参数过滤数据——数据隔离的关键)
      • 重写 perform_create(ser)/perform_update(ser)(保存前后加逻辑:记录操作人/发事件)
      • @action(detail=True/False) 添加自定义端点(如 /hosts/{id}/restart/
    • 路由

      • router = DefaultRouter(); router.register(r'hosts', HostViewSet) → 自动映射 GET/POST /hosts/GET/PUT/PATCH/DELETE /hosts/{pk}/
  • 协助记忆

    • 口诀:“APIView 打底、GenericAPIView 加骨架、五个 Mixin 各管一个动作——ModelViewSet 全家桶、ReadOnly 只读;定制就重写 get_querysetperform_*”。
  • 进阶思考

    • 为什么用 Mixin 组合而不是一个大基类?
      • 按需组装:只读接口继承 ReadOnlyModelViewSet(不会意外暴露写操作)、只要列表+创建就混对应两个 Mixin——动作粒度可控、权限更容易收窄。大基类要么全给要么裁剪,容易“多出不该有的接口”。
    • ViewSet 里的数据权限怎么实现(用户只看自己的主机)?
      • 重写 get_queryset()return Host.objects.filter(owner=self.request.user)——列表/详情/修改删除全部自动收窄;再配权限类兜底。这是 DRF 数据隔离的标准姿势(比在 serializer 里过滤干净得多)。
  • 扩展信息

    • 相关@action(自定义路由)、DefaultRouter/SimpleRouterget_serializer_class()(不同动作不同序列化器)
    • 调试Browsable API 页面直接点接口、drf-spectacular 生成 OpenAPI 给前端/脚本对接

🤔 Django 中间件的作用是什么?

  • 中间件(Middleware)是 Django 请求/响应的全局钩子链:所有请求进入 View 前和响应返回后都会依次穿过它们——用于处理“横切所有请求”的事:认证会话、CSRFGZip 压缩、日志审计、限流、异常兜底。核心:“请求进、响应出各过一遍中间件——全局横切逻辑的唯一标准位置”。

    • 执行模型(洋葱模型)

      • 请求 → M1.process_requestM2.process_requestViewM2.process_responseM1.process_response → 返回(先进后出)
      • MIDDLEWARE 列表顺序即层级(越靠前越外层)——顺序错了认证/CSRF 会失效
    • 五个钩子方法

      • process_request(request):进 View 前(记录开始时间/黑白名单)
      • process_view(request, view, args, kwargs):路由确定后、View 执行前
      • process_exception(request, exception):View 抛异常时(统一异常处理/告警)
      • process_template_response():模板响应时
      • process_response(request, response):响应返回时(加响应头/审计落库)
    • Django 内置中间件(必知)

      • SecurityMiddleware(安全头/SSL 重定向)、SessionMiddleware(会话)、CommonMiddleware(通用头/APPEND_SLASH
      • CsrfViewMiddlewareCSRF 防护)、AuthenticationMiddlewarerequest.user)、GZipMiddleware(压缩,Django 5.0 已弃用、计划 6.0 移除——压缩多由 Web 服务器承担)
    • 运维场景(自定义中间件典型用途)

      • 审计日志:记录谁在什么时间动了什么接口(process_requestprocess_response 落库)
      • 接口耗时监控:记录慢请求、上报指标
      • 维护模式/黑白名单IP 拦截、token 校验统一收口
  • 协助记忆

    • 口诀:“中间件 = 请求/响应的洋葱圈——先进后出,五钩子(request/view/exception/template_response/response);顺序即安全(CSRF 在认证前,会话在认证前)”。
  • 进阶思考

    • 中间件顺序错会出什么问题?
      • 经典例:AuthenticationMiddleware 必须在 SessionMiddleware 之后(它依赖 request.session)——放前面启动直接报错。SecurityMiddleware 要在最前(尽早拦)。自加的日志中间件放最外层才能记录到完整往返。
    • 中间件里能做耗时操作(查库/调接口)吗?
      • 慎重——它在每个请求上执行,一次 200ms 的外部调用会拖垮全站 P99。审计落库用异步(celery/缓冲队列)、监控用轻量内存计数;确需同步的要加超时和降级。
  • 扩展信息

    • 相关process_request 里返回非 None 会短路(直接当响应返回,不再进 View)——限流/封禁的实现点
    • 对照DRF 也有自己的 throttle/permission 层(视图级);全局横切还是中间件,视图内的策略放 DRF 组件——两层配合

🤔 简述 Django 中间件自定义流程?

  • 自定义中间件三步:写类(按需实现 process_request/process_response/process_exception 等钩子,__init__get_response)→ 注册到 settings.MIDDLEWARE 列表(注意位置)→ 验证请求/响应路径生效。轻量场景可用 MiddlewareMixin。核心:“类 + 钩子 + 注册,顺序决定层次”。

    • 第一步:编写中间件类(新版风格)

      • class AuditMiddleware: + def __init__(self, get_response): self.get_response = get_response
      • def __call__(self, request): 里前置逻辑 → response = self.get_response(request) → 后置逻辑 → return response(新式单一入口,覆盖 request/response 两段)
      • 兼容写法(MiddlewareMixin):继承后只写 process_request/process_response 等独立钩子
    • 第二步:注册(settings.py)

      • MIDDLEWARE = [..., 'common.middleware.AuditMiddleware']——列表位置决定外内层
      • 要全局最先/最后处理就放到列表头/尾
    • 第三步:验证

      • 前置逻辑打日志/设 request.start_time;响应段算耗时加响应头(X-Process-Time
      • 单元测试:django.test.Client 发请求断言副作用;中间件只作用于请求-响应周期(manage.py 管理命令不走请求、不经中间件;DEBUG 下静态文件由 StaticFilesHandler 拦截也绕过)
    • 典型自定义示例(运维向)

      • 耗时监控__call__ 前记 t0,响应段 time.time()-t0 超 1s 告警日志
      • IP 黑名单process_request 里命中直接 HttpResponseForbidden(短路,不进 View)
      • 审计:响应段把“用户+路径+状态码+耗时”异步落库(celery 派发)
    • 注意点

      • 钩子里异常要兜住(中间件抛异常会 500 全站)
      • 只做横切通用逻辑——业务判断放 View/DRF 权限
  • 协助记忆

    • 口诀:“自定义中间件 = 类 + __call__(前处理 → get_response → 后处理)→ 注册 MIDDLEWARE → 调位置验证;抛错要兜底,短路能拦截”。
  • 进阶思考

    • process_request 返回响应会发生什么?
      • 短路:Django 不再调用后续 View(后续中间件的 process_request 也跳过),该响应沿外层中间件的 process_response 链返回——这就是 IP 封禁/维护页/限流的标准实现点。
    • 新旧风格(MiddlewareMixin vs __call__)怎么选?
      • 新项目直接 __call__ 单入口(逻辑集中、层次清晰);需要兼容老钩子风格或只插某一环(只管异常)用 Mixin 钩子。两者可共存于一个类,但别重复处理同一段逻辑。
  • 扩展信息

    • 测试钩子override_settings(MIDDLEWARE=[...]) 隔离测试;request.META 取真实 IP(注意 X-Forwarded-For 代理链)
    • 运维建议:审计/监控中间件与业务解耦(配置开关 + 异常静默降级),避免运维组件反过来影响业务可用性

🤔 DRF 限流是怎么实现的?

  • DRF 限流(Throttling)基于“速率类 + 访问标识 + 计数存储”:内置 AnonRateThrottle(匿名按 IP)/UserRateThrottle(按用户 ID,匿名回退 IP)/ScopedRateThrottle(按视图分组),规则用 '3/min' 这类频率字符串,计数存缓存(Redis/Memcached),超限返回 429。核心:“节流类判定身份 → 按频率窗口计数 → 超了 429”。

    • 内置节流类(选身份维度)

      • AnonRateThrottle:未认证用户按 IP 限(防爬/防扫)
      • UserRateThrottle:认证用户按用户 ID(request.user.pk)限,匿名回退按 IP(防单用户滥用)
      • ScopedRateThrottle:按视图 throttle_scope 分组限(不同接口不同配额,如登录接口单独收紧)
      • 自定义:继承 SimpleRateThrottle 重写 get_cache_key()(如按 API Key/租户限)
    • 配置(settings)

      • DEFAULT_THROTTLE_CLASSES:全局默认类;DEFAULT_THROTTLE_RATES{'anon': '100/hour', 'user': '1000/hour', 'login': '5/min'}
      • 视图级覆盖:throttle_classes = [...]throttle_scope = 'login'
      • 频率语法:N/secondN/minN/hourN/day
    • 实现机制

      • allow_request(request, view):算身份键(IP/用户)→ 从缓存读窗口内计数 → 判断是否超 rate → 超限返回 False → DRF 抛 429 Too Many Requests(响应头带 Retry-After
      • 计数存储走 Django 缓存框架——生产必须配 Redis/Memcached(默认 LocMemCache 多进程不共享,限流形同虚设)
    • 运维场景

      • 登录/验证码接口 5/min(防爆破)、开放 API 每用户每小时配额、CMDB 查询接口防爬
      • 配合监控:429 激增告警(有攻击或客户端 bug)
  • 协助记忆

    • 口诀:“限流三要素:节流类定身份(anon/user/scope)、频率字符串定配额、缓存存计数——超限 429,生产计数必须上 Redis”。
  • 进阶思考

    • 为什么默认本地缓存会让限流失效?
      • gunicorn 多 worker/多机部署时,每个进程各有一份 LocMemCache——限流阈值被进程数放大 N 倍且互不可见。必须用共享存储(Redis):CACHESdjango-redis,计数才能全局一致。
    • DRF 限流和 nginx 层限流怎么分工?
      • nginxlimit_req)做粗粒度兜底(按 IP 挡洪水/扫描,保护后端);DRF 做业务粒度(按用户/接口/租户的配额与计费逻辑)。两层都要:边缘挡量、应用管额度;只靠 DRF 限流,恶意流量已经打进了应用。
  • 扩展信息

    • 算法对照DRF 内置是“时间窗+历史时间戳”(近似滑动窗);更严格可用自定义 token bucket;nginx/网关层是漏桶
    • 响应细节:429 带 Retry-After/X-RateLimit-* 头——客户端按头退避,比裸报错友好

🤔 DRF 认证和权限怎么实现的?

  • DRF 把“你是谁”和“你能干什么”分成两层:认证(Authentication)在 request.user 上落实身份(Session/Token/JWT,多个认证类按序尝试,全失败则匿名);权限(Permission)对认证结果做策略判断(AllowAny/IsAuthenticated/IsAdminUser/自定义,返回 403/401)。核心:“认证定身份、权限定策略,视图级 + 全局级可插拔配置”。

    • 认证(Authentication → 谁在访问)

      • 流程:请求进来逐个尝试 authentication_classes 里的类 → 每个类 authenticate(request) 返回 (user, auth) 则该认证生效;返回 None 继续试下一个;抛 AuthenticationFailed 立即中断整条认证链
      • 内置:SessionAuthentication(浏览器会话)、TokenAuthentication(请求头 Authorization: Token <token>)、BasicAuthentication
      • JWTdjangorestframework-simplejwtAuthorization: Bearer <token>,支持刷新)——脚本/前后端分离首选
      • 全失败 → request.user 为匿名(AnonymousUser);凭据错误可抛 AuthenticationFailed(401)
    • 权限(Permission → 能否访问)

      • permission_classes 逐个 has_permission(),全部通过才放行;对象级 has_object_permission() 控单条资源
      • 内置:AllowAny(全放)/IsAuthenticated(登录才行)/IsAdminUser(staff)/IsAuthenticatedOrReadOnly
      • 自定义:继承 BasePermission 实现 has_permission(如“运维组才可写”)/has_object_permission(“只能改自己的主机”)
    • 配置层级(就近覆盖)

      • 全局:settings.DEFAULT_AUTHENTICATION_CLASSES/DEFAULT_PERMISSION_CLASSES
      • 视图级:authentication_classes/permission_classes 属性覆盖(如登录接口 AllowAny
      • 语义:401=未认证、403=权限不足;实际返回码取决于认证类是否设 WWW-Authenticate 头(设置了通常 401,否则未认证的权限拒绝可能降级为 403)——语义区分而非绝对对应
    • 运维平台典型组合

      • 脚本/平台间调用:JWT 认证 + IsAuthenticated + 自定义角色权限(只读组/操作组)
      • 数据隔离:get_queryset() 按用户过滤(权限管“能不能进”,queryset 管“能看见哪些数据”)
  • 协助记忆

    • 口诀:“认证答‘你是谁’(Session/Token/JWT 逐个试),权限答‘能不能’(IsAuthenticated/自定义 has_permission)——401 没身份、403 没资格,数据范围交给 get_queryset”。
  • 进阶思考

    • 认证和权限为什么必须分开成两层?
      • 身份与授权解耦:同一套 JWT 认证可搭配不同权限策略(只读/读写/管理员);同一权限策略可换认证方式(Session 换 JWT 不动权限代码)。混在一起写,接口变更时两边都要动,也难做统一审计。
    • has_object_permission 什么时候会被调用(常见坑)?
      • 只在视图显式调用 self.check_object_permissions() 时生效(DRF 通用视图对 retrieve/update/destroy 会调,list 不会)——列表数据泄露要在 get_queryset() 里过滤。两个机制配合才完整:queryset 管列表范围、对象权限管单条操作。
  • 扩展信息

    • 相关simplejwt(JWT + 黑名单/刷新)、django-guardian(对象级权限落地存储)、throttling(与权限同层)
    • 安全建议:token 短有效期 + 刷新机制;敏感操作二次校验(操作密码/MFA);权限变更留审计日志

🤔 Celery 是什么?什么情况下用?

  • Celery 是 Python 的分布式异步任务队列:业务代码把任务丢进消息队列(Redis/RabbitMQ),worker 进程集群异步取出执行——实现异步化(不阻塞请求)、解耦、定时调度(celery beat)与重试。核心:“生产者投递 → 队列缓冲 → worker 消费,API 秒回、重活后台跑”。

    • 架构三件套

      • Producer(业务应用)task.delay(args) 把任务发到队列(秒级返回 task_id)
      • Broker(消息中间件)Redis/RabbitMQ——存队列、缓冲削峰
      • Worker(执行者)celery -A app worker 起多进程消费执行;Result Backend:存结果(Redis/数据库)供查询
    • 什么时候用(判断标准)

      • 耗时:任务超过几百毫秒就该异步(报表生成/批量扫描/视频转码)
      • 不需同步结果:发通知/邮件/webhook——用户不必等它完成
      • 定时调度celery beat 周期任务(替代 crontab,统一管控+重试+监控)
      • 削峰/解耦:高并发下把重操作排队慢慢消化(发布流水线、巡检集群)
      • 不该用:必须同步返回结果的小查询(直接算更快,队列入栈出栈反成开销)
    • 运维价值(高频场景)

      • 批量主机巡检/采集(worker 并发跑)、发布流水线各阶段任务化、告警通知异步发、定时备份校验、CMDB 自动发现
    • 工程要点

      • 幂等 + 重试task(bind=True, max_retries=3, autoretry_for=(ConnectionError, TimeoutError), retry_backoff=True, retry_backoff_max=60)(用具体异常而非裸 Exception——后者会把编程 bug 也当可重试;retry_backoff_max 封顶指数退避)
      • 超时soft_time_limit/time_limit 防 worker 被卡死任务占满
      • 监控flower 看队列/worker/任务状态;队列积压告警(worker 扩容)
      • 序列化:参数可 JSON 化(传 ID 不传大对象);结果别存大块数据(存引用)
  • 协助记忆

    • 口诀:“Celery = delay 投递 + Broker 排队 + Worker 消费;耗时/免等/定时/削峰才用——配幂等、超时、监控三件套”。
  • 进阶思考

    • 用 Celery 和直接起个后台线程有什么区别?
      • 线程在应用进程内:应用重启任务丢失、无持久化、无法横向扩;Celery 任务在队列里持久——取决于 broker 配置(Redis 默认不落盘需开 RDB/AOF,要稳选 RabbitMQ durable 队列+持久化消息)、worker 独立部署可扩容、自带重试/路由/定时。一次性轻量异步线程够用,业务关键任务必须队列化。
    • 任务积压了怎么处理?
      • worker 数/并发度(flower),扩 worker;看是否有慢任务堵队列(加 time_limit、拆分任务);按优先级/队列拆分(routes 把轻重任务分到不同 worker);积压清空需谨慎。预防靠容量评估 + 积压告警。
  • 扩展信息

    • 相关kombu(broker 抽象)、flower(监控面板)、django-celery-beat(定时任务入库管理)
    • 替代品对照RQ(轻量 Redis 队列)、Dramatiqarqasyncio)——复杂度不高时 RQ 足够;生态/功能完整性 Celery 最强

🤔 简述 Django CSRF 工作原理?

  • CSRF(跨站请求伪造)防护原理是“请求携带只有本站知道的随机令牌”:Django 给每个会话生成 csrf_token,表单里嵌隐藏字段(或 AJAXX-CSRFToken 头),CsrfViewMiddleware 收到 POST 等不安全方法时做双重提交校验(cookie 里 + 提交内容里的令牌必须一致),第三方站点拿不到令牌所以伪造请求被拒。核心:“发令牌 + 回验令牌——验证请求真出自本站页面”。

    • 攻击原理(为什么需要防护)

      • 用户已登录 A 站(浏览器存 cookie)→ 访问恶意 B 站 → B 站页面偷偷向 A 发起请求 → 浏览器自动带上 A 站 cookie → A 以为是用户本人操作(转账/改密码)
      • 要害:cookie 自动携带 + 请求可被第三方页面发起
    • Django 防护机制(双重提交)

      • 发:{% csrf_token %} 在表单里渲染隐藏字段;AJAX 从名为 csrftoken 的 cookie(CSRF_COOKIE_NAME 配置)取值放进 X-CSRFToken
      • 验:POST/PUT/PATCH/DELETE(除 GET/HEAD/OPTIONS/TRACE 外的不安全方法)时,中间件比对“提交的令牌”与“cookie/session 中的令牌”是否匹配——不匹配返回 403
      • 双重提交 + 掩码:默认令牌存 csrftoken cookie(非会话绑定),提交时对 cookie 与表单/头里的令牌做掩码去敏后比对;攻击者跨站拿不到 cookie 内容,伪造请求缺令牌必被拒(CSRF_USE_SESSIONS=True 才移入 session)
    • 相关配置

      • MIDDLEWARE 里的 CsrfViewMiddleware(删掉即全局关——勿动)
      • CSRF_TRUSTED_ORIGINS:跨域可信来源(前后端分离域名不同时必配,含协议)
      • @csrf_exempt:单视图豁免(如接收外部 webhook——改用其他鉴权),@csrf_protect 强制开启
    • AJAX/前后端分离注意

      • 前端从名为 csrftoken 的 cookie(CSRF_COOKIE_NAME 配置)读 token 放请求头(axios 拦截器统一加);跨域时 CSRF_TRUSTED_ORIGINS + cookie 的 SameSite/CORS 凭据配置要一起对齐
  • 协助记忆

    • 口诀:“CSRF 防的是‘借刀杀人’:登录态被第三方借用——Django 发随机令牌(表单/头),提交时双提交校验,外人拿不到令牌就伪造不了”。
  • 进阶思考

    • CSRF TokenJWT 有什么区别(都能防 CSRF 吗)?
      • CSRF token 专防“借 cookie 的伪造请求”;JWT 放在 Authorization 头(浏览器不自动携带)时天然免疫 CSRF——所以纯 JWT 的前后端分离可少管 CSRF。但 JWT 存 cookie 就又回到 CSRF 风险。规律:凭据自动携带(cookie)就需要 CSRF 防护,手动携带(header)就不需要。
    • SameSite cookie 和 CSRF 是什么关系?
      • SameSite=Lax/Strict 让浏览器跨站请求不带 cookie,从源头大幅缓解 CSRF(现代浏览器默认 Lax)。但仍有兼容性/绕过面(顶级 GET 导航、老浏览器),Django 的 token 校验仍应保留——纵深防御,不把宝押在单一机制上。
  • 扩展信息

    • 相关设置CSRF_COOKIE_SECURE(仅 HTTPS)、CSRF_COOKIE_HTTPONLY(JS 可读性与安全权衡)、CSRF_FAILURE_VIEW(自定义 403 页)
    • 运维提醒webhook/对外回调接口用 csrf_exempt 时必须自备签名校验(如钉钉/GitHub 的 sign 头验证),否则等于裸奔

🤔 Django 如何编写测试用例?

  • django.test.TestCase(自带测试数据库与事务回滚)继承编写:类继承 TestCase、方法以 test_ 开头,用 self.client 模拟请求、assertEqual/assertTrue 断言;pytest-django 是现代增强(fixture/参数化)。运行 python manage.py test。核心:“TestCase 建事务沙箱 + self.client 发请求 + assert 断言”。

    • 基础结构(Django 原生)

      • from django.test import TestCaseclass HostViewTest(TestCase):def setUp(self) 准备公共数据 → def test_list_hosts(self): 具体用例
      • 运行:python manage.py test app.tests -v 2(自动建测试库、每个用例包在事务里执行完回滚)
    • 测试视图/API(self.client

      • resp = self.client.get('/api/hosts/');断言 self.assertEqual(resp.status_code, 200)self.assertJSONEqual(resp.content, ...)
      • POST:self.client.post(url, data, content_type='application/json')
      • 模拟登录:self.client.force_login(user)(跳过认证流程直接种会话)
      • DRFfrom rest_framework.test import APIClient(用法类似,多 format='json'
    • 数据与隔离

      • setUp 造数据 / 工厂库(factory_boy)生成测试对象;每个测试方法独立(事务回滚隔离);多方法共用数据用 setUpTestData(类级只建一次,更省时)
      • 外部依赖隔离:unittest.mock.patch 打桩(requests/celery 任务不真发)——@patch('app.views.run_cmd')
      • pytest-django 增强:db fixture、client fixture、参数化更灵活(现代项目主流)
    • 测什么(运维平台建议)

      • API 状态码与响应结构(回归保护)、权限(未登录 401/越权 403)、模型方法与校验、URL 路由覆盖
      • 断言行为而非实现细节(换实现不换行为时测试不该挂)
  • 协助记忆

    • 口诀:“继承 TestCasetest_ 开头写用例;self.client 发请求、assert* 验结果;外部依赖 mock 掉,每例事务自动滚”。
  • 进阶思考

    • TestCaseSimpleTestCase/TransactionTestCase 区别?
      • TestCase:每个用例包事务回滚(快,但不能测事务本身);TransactionTestCase:真提交(可测事务/多连接,慢,需清理);SimpleTestCase:不碰数据库(默认查询报错,可 databases 显式放行)。按需选择,默认 TestCase
    • CI 里跑 Django 测试要注意什么?
      • 用独立测试数据库(容器化临时库,--parallel 需数据库用户有建库权限、为各进程建独立库)、覆盖率 coverage run manage.py test 出报告;外部服务(Redis/MQ)用容器起真实例或 mock,保证 CI 稳定可重复。
  • 扩展信息

    • 工具链pytest + pytest-django(现代主流)、factory_boy(造数据)、mock/freezegun(时间冻结)、coverage/pytest-cov(覆盖率)
    • 运维价值CMDB/发布系统改动用测试兜底回归;脚本类逻辑抽成函数后也能直接 pytest(不必是 Django 项目才能测)

🤔 Websocket 与 HTTP 有什么区别?

  • HTTP 是“请求-响应”的单向短交互协议(客户端问一次、答一次,连接可复用但服务端不能主动推);WebSocket 是升级后的全双工长连接(一次握手建立后,双方随时互发,服务端可主动推送),适合实时场景(聊天/行情/终端/告警推送)。核心:“HTTP 一问一答,WebSocket 建一条常开管道双向随时说”。

    • 核心区别

      • 通信模型:HTTP 请求→响应(客户端发起);WebSocket 全双工(服务端可主动推)
      • 连接生命周期:HTTP 短交互(keep-alive 复用但仍一问一答);WebSocket 握手一次后长连接持续
      • 协议与开销WebSocket 帧头极小(2-14 字节),HTTP 每次带完整头;实时高频场景开销差数量级
      • 协议标识ws:///wss://TLS
    • 握手过程(WebSocket 从 HTTP 升级而来)

      • 客户端发 HTTP GET + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key
      • 服务端回 101 Switching Protocols → 之后同一 TCP 连接切换为 WebSocket 帧协议
    • 适用场景

      • WebSocket:实时推送(告警/行情/监控大盘)、聊天/协同、在线终端(WebShell/K8s exec)、多人编辑
      • HTTP:常规 CRUD/页面/API(请求-响应语义天然匹配)
      • 轮询/SSE 对比:HTTP 轮询浪费请求;SSE 单向推送(服务端→客户端)够用且简单;真双向才上 WebSocket
    • 运维要点

      • 反代支持:nginxproxy_set_header 透传 Upgrade/Connection 升级头;超时参数(proxy_read_timeout)要拉长
      • 连接保持:心跳(ping/pong 帧)防中间设备断链;扩容用粘性会话或 Redis pub/sub 跨实例广播
  • 协助记忆

    • 口诀:“HTTP 一问一答短平快,WebSocket 握手一次双向长聊——实时推送选 WS,普通接口选 HTTP;SSE 是单向的折中”。
  • 进阶思考

    • 轮询、长轮询、SSE、WebSocket 怎么演进与选择?
      • 轮询(定时全量问,浪费)→ 长轮询(有更新才答,挂住连接)→ SSE(服务端单向流,HTTP 原生)→ WebSocket(全双工)。选择:只要服务端推且不需要客户端高频上行 → SSE 足够(简单可靠);双向交互(终端/聊天)才上 WebSocket
    • 运维平台里 WebSocket 的典型坑?
      • nginx 忘透传 Upgrade 头(握手 200 而非 101);长连接被负载均衡空闲超时掐掉(要心跳);多实例部署消息只到一台(广播要走 Redis pub/sub/MQ);连接数即资源(fd 上限、内存),要监控与上限控制。
  • 扩展信息

    • 实现库websockets/aiohttp(异步)、channels(Django WebSocket,走 ASGI)、socket.io(带降级回退的封装)
    • 相关协议:SSE(EventSource,单向)、MQTT over WebSocket(IoT web 端接入常用)

🤔 Python 怎么使用正则?

  • 用标准库 re 模块,标准三步:re.compile(模式) 编译 → match/search/findall 匹配 → group()/groups() 取结果。常用函数:match(从头)、search(任意位置)、findall(所有匹配)、sub(替换)、split(切分);r'' 原始字符串防转义坑。核心:“compile 预编译 + 按‘找一个/找全部/替换’选函数 + 命名分组取值”。

    • 常用函数(按需求选)

      • re.match(p, s):从开头匹配(开头不合就 None)
      • re.search(p, s):全文找第一处(日志解析最常用)
      • re.findall(p, s):所有匹配列表(无分组返回整匹配,有分组返回分组元组)
      • re.finditer(p, s):迭代器版(大文本流式处理)
      • re.sub(p, repl, s):替换(repl 可用 \1 反向引用或函数);re.split(p, s):按模式切分
      • re.fullmatch:整串必须全匹配(校验格式用,如 IP/域名)
    • 标准用法模板

      • p = re.compile(r'(?P<ip>\d+\.\d+\.\d+\.\d+) .* (?P<status>\d{3})') 编译一次
      • m = p.search(line)m.group('ip')/m.group('status')(命名分组比数字可读)
      • 多行日志:for m in p.finditer(text): ...
    • 工程要点(运维高频坑)

      • r'' 原始字符串:r'\d+' 而非 '\\d+'(否则反斜杠双重转义)
      • . 不匹配换行:跨行用 re.S(DOTALL);^$ 逐行语义用 re.M(MULTILINE)
      • 预编译:re 内部有编译缓存(相同模式串命中缓存,非每次都编译),但缓存有上限易被剔除——循环里显式 compile 仍是更可控/更易读的实践
      • 贪婪 vs 非贪婪:.* 贪到头,.*? 最短匹配——取两标签之间内容用 .*?
      • 复杂日志解析先试 split/字符串方法(更快更可读),正则留给真模式匹配
  • 协助记忆

    • 口诀:“compile 编译一次,search 找一个、findall 找一堆、sub 替换;r'' 写模式、命名分组取值、跨行加 S 多行加 M、贪婪改 ?”。
  • 进阶思考

    • matchsearchfullmatch 的区别场景?
      • match 锚定开头(前缀判断);search 任意位置(日志里抠字段);fullmatch 整串校验(格式合规:IP/手机号——更接近 \A...\Z,比 ^...$ 严格——后者 $ 在串尾换行前也匹配、re.M 下按行)。用错锚定语义是误匹配的常见原因。
    • 解析 Nginx 日志用正则还是 split
      • 固定格式且字段无空格 → split() 索引取(快数倍);字段可能含空格(UA/referer 带引号)或要容错 → 正则+命名分组((?P<ip>\S+).*(?P<method>\w+) (?P<url>\S+))。生产里常见“split 提列 + 正则兜底”混合。
  • 扩展信息

    • 进阶特性(?P<name>...) 命名分组、(?:...) 非捕获组、(?=...)/(?<=...) 环视、re.VERBOSE 写可读正则(加注释)
    • 调试工具regex101.com(在线解释正则)、re.DEBUG 打印解析树——复杂表达式先可视化再上线

🤔 Python 正则中有哪些常用的字符及作用?

  • 常用分四组:元字符(. 任意字符 / ^ $ 行首尾 / * + ? 数量 / | 或 / () 分组 / [] 字符集)、预定义类(\d 数字 / \w 字母数字下划线 / \s 空白,大写取反)、数量词({m,n} 区间 / *? +? 非贪婪)、分组扩展((?P<name>) 命名 / (?:) 非捕获)。核心:“.\d\w\s + *+?{} + 分组,覆盖日志解析九成场景”。

    • 元字符(结构)

      • .:任意单字符(不含换行,re.S 才含)
      • ^ / $:串/行首、尾(re.M 下按行)
      • |:或(cat|dog
      • ():分组捕获(group(1));(?:...) 只分组不捕获
      • [abc]:字符集任一;[a-z0-9] 范围;[^abc] 取反
      • \:转义(. 本身要写 \.——匹配 IP 点号必须转义)
    • 预定义字符类

      • \d 数字 / \D 非数字(默认 Unicode 模式还会匹配全角等非 ASCII 数字,要纯 ASCII 加 re.A/(?a),或显式用 [0-9]
      • \w 字母数字下划线 / \W 反之
      • \s 空白(空格/制表/换行)/ \S 非空白
      • \b 单词边界(\berror\b 精确匹配单词)
    • 数量词(重复次数)

      • * ≥0 次、+ ≥1 次、? 0 或 1 次
      • {m} 恰好 m、{m,} 至少 m、{m,n} 区间
      • 非贪婪*? +? ?? {m,n}?——最短匹配(取两个引号间内容用 (.*?),若确定非空才用 (.+?)——+ 至少一字符,空串匹配不上)
    • 分组扩展((?...)

      • (?P<name>...) 命名分组(group('name'));(?P=name) 反向引用
      • (?=...)/(?!...) 正/负向前瞻、(?<=...)/(?<!...) 后顾——位置判断不消费字符
      • (?i) 忽略大小写(或 re.I
    • 运维高频组合示例

      • IP:\d{1,3}(?:\.\d{1,3}){3}(点必须转义)
      • 时间戳:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}
      • 日志状态码:\s(\d{3})\s、请求行:(?:GET|POST) (\S+)
  • 协助记忆

    • 口诀:“点任意、星加问量、尖括号定首尾、方括号挑字符;d 数字 ws 空白、大写全反;数量加问变懒惰、点要转义别忘”。
  • 进阶思考

    • 为什么匹配 IP 的 . 必须转义?
      • 未转义的 . 是“任意字符”——192.168.1.1 写成未转义正则会把点当通配,192x168y1z1 也匹配(误匹配/安全漏洞)。字符类内 [.] 或转义 \. 才是字面点。
    • .*.*? 在日志解析里怎么选?
      • .* 贪婪会“吃满一行再回溯”(嵌套引号/多字段时错位);.*? 最短匹配在第一个结束符停。解析带引号字段(UAreferer)必须 .*?;注意两者都可能回溯暴增——能精确字符类(排除引号的 [^"xxx] 思路)就别用 .*
  • 扩展信息

    • re.VERBOSE:允许正则里写空白与注释(复杂表达式必备可读性)
    • 性能与安全:嵌套量词((a+)+)可致回溯爆炸(ReDoS,正则 DoS)——对外输入的正则要避免嵌套不确定长度组

🤔 都做过哪些运维开发项目?

  • 运维开发项目按“平台→自动化→工具→集成”组织回答:平台类(CMDB 资产管理、发布部署系统、工单审批流)、自动化类(批量巡检、日志采集分析、监控告警闭环)、工具类(CLI 运维工具、日志 Top 分析、备份校验脚本)、集成类(云 API 管控、K8s 平台、钉钉/企微机器人)。回答框架:背景痛点 → 方案与角色 → 技术栈 → 量化效果。核心:“讲清痛点-方案-技术栈-收益,突出自己在项目中的角色”。

    • 平台类项目(系统建设)

      • CMDB 资产管理:主机/机架/应用关系建模(Django+DRF),自动发现(agent/SSH 采集)+ 变更审计——解决“资产靠表格、变更没记录”
      • 发布部署系统:Web 发单 → 审核 → 多机灰度发布(Ansible/paramiko)→ 回滚;解决手工发布事故多
      • 工单/审批流:权限申请/变更审批线上化(状态机 + 通知集成)
    • 自动化项目(提质提效)

      • 批量巡检平台:Python + 线程池/Celery 并发采集 CPU/内存/磁盘/证书到期,结果入库 + 日报推送
      • 日志分析:Nginx 日志 Top IP/错误率统计(Counter/生成器流式),CC 攻击自动识别
      • 告警闭环Zabbix/Prometheus 告警 webhook → 自动诊断脚本(看日志/重启策略)→ 钉钉通知 + 处理记录
    • 工具类项目(脚本利器)

      • 批量执行工具(subprocess+ssh+并发)、证书到期检查、OSS/备份校验、数据库慢查询采集
      • 特点:argparse 参数化、logging、可定时跑
    • 集成类项目(云原生)

      • 云管脚本/平台ECS 定时开关(降成本)、OSS 生命周期治理、RAM(阿里云资源访问控制)权限审计(云 SDK)
      • K8s 运维:集群巡检(kubernetes client)、Pod 异常自动处置、发布平台对接 K8s API
      • 机器人集成:告警/发布/审批通知到钉钉/企微群(webhook
    • 回答模板(每项目 1 分钟版)

      • 痛点:之前怎么做、什么问题(人工/慢/易错)→ 方案:我设计的架构与流程 → 技术栈:语言/框架/组件 → 效果:量化(工时节省/故障率下降/覆盖台数)→ 角色:独立开发/主导设计/参与模块
  • 协助记忆

    • 口诀:“平台(CMDB/发布/工单)+ 自动化(巡检/日志/告警)+ 工具(批量/校验)+ 集成(云/K8s/机器人)——每项目讲痛点、方案、技术栈、量化收益”。
  • 进阶思考

    • 没有大项目经验怎么回答(包装原则)?
      • 从“重复劳动”找素材:手动巡检 → 写成自动化脚本就是项目(讲采集维度/并发量/报告形式);手工备份 → 备份校验工具(覆盖率/失败告警)。真实小项目 + 深度讲透远胜虚构大项目——追问三层就露馅,诚实 + 量化是底线。
    • 面试官追问“系统架构怎么设计的”怎么应对?
      • 按层拆:入口(Web/CLI)→ 逻辑层(任务调度/权限/状态机)→ 执行层(并发框架/Agent)→ 存储(MySQL/Redis)→ 集成(MQ/通知)。同时主动讲踩过的坑(并发控制/失败重试/幂等)——坑讲得具体,可信度立刻上来。
  • 扩展信息

    • 加分项:开源贡献/内部工具开源化、IaCTerraform/Ansible 代码化)、监控体系(Prometheus 栈)建设经验、CI/CD 流水线治理
    • 能力映射:平台项目考工程能力(Django/DRF/数据库),自动化考语言功底(并发/异常/日志),集成考云与 K8s 视野——三线并举是运维开发的核心画像

目录