运维常见题-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:也都是对象(
None是NoneType的实例)
- 基础类型也是对象:
实际价值
- 统一操作:所有对象都走“传对象引用/共享传参”(
pass-by-object-reference——重新绑定参数名不影响外部,只有原地修改才影响)、都能放进容器([1, 'a', len, int]合法) - 动态性基础:运行时可检查/修改(
type/dir/getattr),支撑反射、装饰器、元编程
- 统一操作:所有对象都走“传对象引用/共享传参”(
协助记忆
- 口诀:“万物皆对象:有类型、有地址、可传递——数字/字符串/函数/类都一样”。
进阶思考
- 函数为什么能当参数传(装饰器的原理)?
- 因为函数也是对象(有类型
function、有id),可以像普通对象一样赋值/传参/返回。装饰器“接收函数返回新函数”正是把函数当一等公民用的典型(@decorator语法糖)。
- 因为函数也是对象(有类型
is和==有什么区别?is比身份(是否同一个对象,id相同),==比值(调用__eq__)。小整数/短字符串有缓存(is可能恰好为真),但判断相等应该用==;is只用于x is None、x is True这类身份判断。
- 函数为什么能当参数传(装饰器的原理)?
扩展信息
- 对象模型:Python 一切对象基于
PyObject(引用计数 + 类型指针),CPython 实现层面也统一 - 元类:类是
type的实例(type(name, bases, dict)动态建类),可自定义元类(metaclass)控制类的创建
- 对象模型:Python 一切对象基于
🤔 Python 里常见可变 / 不可变对象分别有哪些?
可变对象(值可原地改,
id不变):list/dict/set/bytearray/自定义类实例;不可变对象(值不可改,改了就是新对象):int/float/bool/str/tuple/frozenset/bytes/None。核心:“改可变对象原地生效(会影响引用它的所有变量),改不可变对象等于创建新对象”。可变对象(Mutable)
list(append/pop/sort原地改)、dict(增删键值)、set(add/remove)bytearray(可变字节串)、自定义类实例(属性可改)- 特点:原地修改不换
id,所有引用同步看到变化
不可变对象(Immutable)
- 数字:
int/float/complex/bool - 序列:
str(s += 'x'是新建对象)、tuple(元素不可替换)、bytes frozenset(不可变集合)、None- 特点:任何“修改”操作都返回新对象(原对象不变)
- 数字:
可变性的实际影响
- 函数传参:传可变对象,函数内修改会影响外部(如默认参数别用
[]/{},用None) - 作为
dictkey/set元素:必须可哈希(通常是不可变对象),list/dict/set/bytearray不行,str/int/frozenset可以,tuple仅当元素都可哈希时才可(含list的tuple不可哈希) - 别名陷阱:
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__且生命周期内哈希值不变(不可变对象天然满足)
- 小对象缓存:CPython 对小整数(-5~256)/短字符串做缓存,
🤔 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
深拷贝的特点/代价
- 循环引用处理:
deepcopy用memo字典记录已复制对象,能正确处理自引用/循环引用(不会死循环) - 代价:递归复制开销大(大结构/深嵌套慢、耗内存);对象可能需要自定义
__deepcopy__
- 循环引用处理:
怎么选
- 只改第一层 → 浅拷贝;嵌套结构要完全独立 → 深拷贝;只是别名 → 赋值
协助记忆
- 比喻:“赋值 = 同一把钥匙,浅拷贝 = 复印了房子图纸但家具还是那些(内层共享),深拷贝 = 整栋楼连同家具全部重建”。
- 口诀:“浅拷一层、深拷到底;嵌套独立必深拷”。
进阶思考
- 为什么浅拷贝后改
b[0]不影响a[0],改b[1]却影响?- 浅拷贝新建了外层
list,b的槽位是独立数组——给b[0]赋新值只是改b自己的槽位;但b[1]引用的还是原来的内层list对象,通过它做原地修改(append)时,两边看到的是同一个对象。
- 浅拷贝新建了外层
dict.copy()和deepcopy在运维脚本里怎么选?- 配置字典改一层字段 →
dict.copy()(快);要把模板配置深改后各自独立(嵌套字典/列表)→deepcopy。运维场景常见坑:用dict.copy()复制模板后改嵌套list,把模板也改了——嵌套必须deepcopy。
- 配置字典改一层字段 →
- 为什么浅拷贝后改
扩展信息
- 相关 API:
copy.copy/copy.deepcopy、list()/dict()/set()构造器(浅拷贝)、切片a[:](浅拷贝)、__copy__/__deepcopy__自定义 - 序列化替代:
json.loads(json.dumps(x))可实现“伪深拷贝”(要求可 JSON 化,函数/对象不行)
- 相关 API:
🤔 List(列表)和 Tuple(元组)的核心区别?
核心区别是可变性:
list可变(增删改、动态数组)、tuple不可变(定长、不可改)。衍生差异:tuple可哈希(能做dictkey/set元素)、内存更小且可被缓存优化、语义上tuple表示“固定结构”而list表示“同类集合”。核心:“变用list、不变用tuple,做key必须tuple”。可变性(根本区别)
list:append/insert/remove/pop/sort都可原地改(动态数组)tuple:不可增删改元素(定长);“修改”只能新建对象
哈希与用途
tuple可哈希(元素都不可变时)→ 可作dictkey、set元素(如坐标(x, y)缓存)list不可哈希 → 不能作key(可变性会导致哈希不稳定)
性能与内存
tuple创建更快、内存更小(无预留扩容空间)、常量可被解释器缓存/优化list有扩容机制(预留空间),动态增删灵活
语义惯例
list:同质元素的集合(一列数据,会增删)tuple:异质固定结构(一条记录:(name, age, city)、函数多返回值)
协助记忆
- 口诀:“
list动态可变,tuple定长不可变;做key用tuple,装数据用list”。
- 口诀:“
进阶思考
- 单元素元组怎么写(常见的坑)?
(1)是数字 1(括号被当运算符),(1,)才是元组——必须带逗号。函数返回/解包时容易踩:x = (1)得到int。
- 函数返回多个值其实是返回什么?
- 返回的是一个 tuple(
return a, b打包成元组),调用方x, y = f()是序列解包。理解这点就不会困惑“返回多个值”的机制。
- 返回的是一个 tuple(
- 单元素元组怎么写(常见的坑)?
扩展信息
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))(dict3.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底层是哈希表:key经hash()算哈希定位桶,冲突用开放寻址解决,扩容按负载因子触发(重建表再散列);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稀疏数组存下标——省内存且按插入顺序遍历
- 旧版:一张大数组(稀疏、浪费);3.6 起(3.7 语言保证):
对
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_end等LRU语义)、defaultdict(缺省值)、Counter(计数) __slots__:给类实例省内存(禁用__dict__),大量对象时的优化手段
- 相关:
🤔 *args 和 **kwargs 的作用是什么?
*args把多余的位置参数收进一个tuple,**kwargs把多余的关键字参数收进一个dict——用于定义“参数数量不定”的函数(转发/包装/装饰器必备)。调用时*/**反向解包:*lst拆序列、**dct拆字典。核心:“定义时收集(打包),调用时展开(解包)”。定义侧(收集参数)
def f(*args):多余位置参数打包成tuple(f(1, 2, 3)→args == (1, 2, 3))def f(**kwargs):多余关键字参数打包成dict(f(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*n、lambda *a: sum(a)
限制(重要)
- 只能一个表达式:不能多行、不能写语句(
print是表达式可写,赋值/import/return不行) - 复杂逻辑别用:超过一行就该
def(可读性优先) - 别赋值给变量当
def用:PEP8明确不建议f = lambda: ...(应直接def f(): ...)
- 只能一个表达式:不能多行、不能写语句(
典型用法
sorted(data, key=lambda x: x[1])(按第二列排序)map/filter回调、functools.reduce、defaultdict的工厂、Tkinter按钮回调
协助记忆
- 口诀:“
lambda= 一行函数:单表达式、隐式返回、当参数用——复杂逻辑回def”。
- 口诀:“
进阶思考
lambda和def除了名字还有什么区别?- 本质都是函数对象(类型都是
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.sort的key(最常见)②map/filter/reduce回调 ③dict/defaultdict工厂 ④min/max的key⑤GUI/回调注册 ⑥运维脚本(日志排序/字段提取)。核心:“作为一次性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只合并相邻相同键,必须先按分组键排序,否则同扩展名会散成多组)
- 日志解析:
协助记忆
- 场景口诀:“排序
key、map/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时注意晚绑定。定义实例行为用普通def,lambda留给参数场景。
- 什么时候不该用
扩展信息
operator模块:itemgetter/attrgetter/methodcaller是排序key的更优解(性能好、语义清晰)key函数思想:Python 排序哲学“Schwartzian 变换”——先算key再比,lambda是这个体系最灵活的入口
🤔 什么是高阶函数?
高阶函数(Higher-Order Function)是“接收函数当参数、或返回函数”的函数——因为 Python 里函数是对象(一等公民),可以像普通值一样传递。内置高阶函数:
map/filter/sorted(key)/max(key)/min(key);reduce在functools(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”。
sorted的key、按钮的command、异步框架的on_done都是回调——本质都是“函数作为参数”。
- 回调是高阶函数的一种用法:把函数 A 传给 B,B 在合适时机“回过头来调用 A”。
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(固定参数生成新函数)、reduce、wraps(装饰器元信息)、lru_cache(缓存装饰器)- 函数式工具:
itertools(chain/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@bdef f等价f = a(b(f))——b先包、a后包(最外层a)。执行增强逻辑时从最外层进(a的wrapper先跑)。排列时想清楚“谁在最外层生效”。
- 自下而上包装:
- 装饰器执行时机(定义时还是调用时)?
扩展信息
- 类装饰器:实现
__call__的类也能装饰(可维护状态,如计数器装饰器) - 运维实用:
lru_cache缓存API结果、自写@retry包subprocess/requests、@timer定位慢函数
- 类装饰器:实现
🤔 什么是闭包?
闭包(Closure)是“内层函数引用了外层函数的变量,并且外层函数返回后该变量依然存活”的现象:内层函数把外层作用域“打包带走”。价值:函数工厂(按参数生成定制函数)、带状态的函数(不暴露全局)、装饰器的实现基础。核心:“函数 + 它引用的外层环境 = 闭包”。
构成条件
- ①有嵌套函数(内层定义在外层里)②内层引用了外层的变量③在外层作用域结束后仍能使用(典型是外层返回内层函数——非必须“返回”,但要让闭包在外层调用后存活通常这么写)
- 例:
def outer(n): def inner(x): return x + n; return inner——inner捕获了n;add5 = 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
- 从 L 往外逐层找,首次命中即用(所以函数内
赋值规则(写)——最常见的坑
- 函数内任何赋值(
x = 1)默认把x声明为局部变量(编译期决定)——哪怕上面有同名全局变量 - 若在赋值之前读它会
UnboundLocalError(不是拿到全局值!这是高频坑) - 想改全局 →
global x;想改外层闭包变量 →nonlocal x
- 函数内任何赋值(
特例
- 只引用不赋值的变量会按 LEGB 正常向外查(函数里
print(g_var)能拿到全局值) del x/import x/for x也算“赋值”(同样创建局部名)
- 只引用不赋值的变量会按 LEGB 正常向外查(函数里
协助记忆
- 口诀:“LEGB 由内向外找;函数里一赋值,变量就变局部——跨层写
global/nonlocal,先读后赋会UnboundLocalError”。
- 口诀:“LEGB 由内向外找;函数里一赋值,变量就变局部——跨层写
进阶思考
UnboundLocalError为什么会出现?怎么排查?- 函数体内有
x = ...→ 编译期把x标记为局部 → 执行到赋值前访问x(如print(x))就报此错。排查:看函数里是否有对它的赋值;要么改名、要么global/nonlocal声明、要么先赋值再读。
- 函数体内有
- 为什么 Python 不像 shell 那样默认“写也是全局”?
- 默认局部是防意外污染:函数自成命名空间,内部临时变量不会覆盖全局(否则大项目互相踩名字)。代价就是要显式声明跨层写入——这是刻意的安全设计。
扩展信息
- 类作用域不在 LEGB 链:类体内定义的名字,方法里不能直接当外层访问(要
self./类名)——常见混淆点 globals()/locals():运行时查看作用域字典(调试/动态代码用)
- 类作用域不在 LEGB 链:类体内定义的名字,方法里不能直接当外层访问(要
🤔 生成器与迭代器有什么区别?
迭代器是协议(实现
__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导入都用逐行惰性处理。
yield和return的区别?return结束函数并交出全部结果;yield暂停函数交出一个值,下次next()从暂停处继续(局部变量保留)。生成器函数调用时不执行任何函数体,返回生成器对象——首次next()才开始跑。
- 为什么处理大文件用生成器(不用
扩展信息
yield from:委托子生成器(简化生成器嵌套),协程的前身- 相关工具:
itertools(chain/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.open与open用法一致(逐行流式解压)- 日志按天轮转压缩(
.gz)直接这样读,不用先解压落盘
生成器管道(复杂处理)
read_lines() → parse() → filter() → aggregate()每级都是生成器,数据流水线惰性流过- 好处:解耦处理逻辑、内存恒定、可组合复用
配合技巧
mmap(内存映射,超大文件随机访问)、pandas分块chunksize、linecache(随机取行)- 统计类(求和/计数)适合流式;需要全局排序/去重则外部排序或落数据库
协助记忆
- 口诀:“大文件三不:不
read()、不readlines()、不全量进内存——逐行/分块/生成器管道流过去”。
- 口诀:“大文件三不:不
进阶思考
for line in f为什么内存恒定?- 文件对象实现了迭代器协议,
__next__每次只读一行到缓冲、处理完即可回收——同一时刻内存里只有当前行(和固定缓冲)。readlines()则是构造完整列表。
- 文件对象实现了迭代器协议,
- 怎么边读边写(原地修改大文件)?
- 文件不能“原地插行”——安全做法:读源文件逐行处理写到临时文件,
os.replace()原子替换;或先写新文件再改名。忌“边读边写同一文件”(会覆盖未读内容)。
- 文件不能“原地插行”——安全做法:读源文件逐行处理写到临时文件,
扩展信息
- 相关 API:
open的 buffering/encoding、fileinput(多文件逐行)、mmap、gzip.open、shutil.copyfileobj(流式复制) - 运维场景:日志分析(
grep/统计Top N)、大CSV清洗、备份文件校验(分块算MD5)、nginx日志切割合并
- 相关 API:
🤔 什么是面向对象编程(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:看行为不看类型
- 封装(Encapsulation):数据+操作打包,隐藏内部细节(
Python 中的 OOP 特点
- 一切皆对象(类本身也是对象)、支持多继承(MRO 解决菱形问题)、
@property优雅封装属性 - 魔法方法(
__init__/__str__/__len__)让自定义对象接入语言协议
- 一切皆对象(类本身也是对象)、支持多继承(MRO 解决菱形问题)、
什么时候用 OOP
- 有明确实体和状态(服务器/主机/任务)、多实例+统一操作(批量管理主机对象)、需要继承扩展的框架
- 纯脚本/一次性任务用函数即可——不要为 OOP 而 OOP
协助记忆
- 口诀:“类模板对象实例,封装藏、继承扩、多态换——有状态多实例才用类”。
进阶思考
- Python 的多态和 Java 有什么不同?
- Python 是 duck typing(鸭子类型):不看继承体系,只要对象有该方法就能调(“走起来像鸭子就是鸭子”)。Java 要显式实现接口/继承父类。Python 更灵活,但错误推迟到运行时(可用
abc抽象基类/Protocol补约束)。
- Python 是 duck typing(鸭子类型):不看继承体系,只要对象有该方法就能调(“走起来像鸭子就是鸭子”)。Java 要显式实现接口/继承父类。Python 更灵活,但错误推迟到运行时(可用
- 组合优于继承是什么意思?
- 继承是“是一个”(强耦合父类实现);组合是“有一个”(把功能对象作为属性复用)。继承层次深了易碎(父类改动牵连全部子类),优先组合 + 接口小继承——
composability比inheritance更稳。
- 继承是“是一个”(强耦合父类实现);组合是“有一个”(把功能对象作为属性复用)。继承层次深了易碎(父类改动牵连全部子类),优先组合 + 接口小继承——
- Python 的多态和 Java 有什么不同?
扩展信息
- 相关机制:
@property/@abstractmethod、__slots__、MRO(C3线性化)、dataclass(自动生成__init__等) - 运维应用:批量主机管理(
Host类 +deploy()/check())、Ansible模块开发、CMDB建模
- 相关机制:
🤔 new 与 init 的区别是什么?
__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(实例);@classmethod绑cls(类本身,可访问类属性/造实例);@staticmethod什么都不绑(就是放进类命名空间的普通函数)。核心:“实例用self、类用cls、都不沾用staticmethod——classmethod最典型用途是替代构造器”。三种方法对比
- 实例方法:
def m(self)——操作实例状态,最常用 @classmethod:def m(cls)——第一个参数是类;能访问/修改类属性、能cls(...)创建实例@staticmethod:def 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(...)——子类调用还是返回父类实例;classmethod用cls(...),Sub.from_string(s)返回Sub实例,工厂随继承自动适配。这就是标准库dict.fromkeys用classmethod的原因。
@property和这三者是什么关系?@property把方法伪装成属性(obj.status不加括号),本质是描述符——访问器语义(读起来像字段、算起来是方法)。和classmethod/staticmethod一样都是装饰器改变方法绑定方式,只是目标不同:property管属性访问、classmethod管类绑定。
- 为什么替代构造器要用
扩展信息
- 相关:
@property/@x.setter、functools.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 输入
restart→getattr(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}')]
- 命令/动作分发:CLI 输入
配套
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处加注释和默认值兜底,降低维护成本。
- 会——IDE/
- 用字符串分发方法和写
扩展信息
- 反射家族:
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()保钩子执行。
- 封装主机/任务类时扩展父类初始化(
- 为什么说“多继承里不用 super() 会重复初始化”?
扩展信息
- 查看 MRO:
A.__mro__、inspect.getmro(cls);mro()与 C3 算法 - 设计建议:继承链超过 2-3 层就考虑组合;框架类(
unittest.TestCase/Django View)里按规范super(),钩子才不会漏
- 查看 MRO:
🤔 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→ 异常继续传播(默认行为是释放资源后照常抛)
解决的痛点
- 必释放:文件/锁/连接忘关、异常路径漏
close→with结构上保证清理 - 等价手写:
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家族:contextmanager、closing(自动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.futures(ThreadPool/ProcessPoolExecutor统一接口)、multiprocessing、asyncio、queue(跨线程/进程通信) - 运维场景:批量主机采集(线程池)、万级设备监控(
asyncio+MQTT)、日志并行压缩(进程池)
- 标准库:
🤔 GIL(全局解释器锁)有什么作用?
GIL是 CPython 的“一把大锁”:保证同一时刻只有一个线程执行 Python 字节码,作用是保护解释器内部状态(引用计数/内存管理)的线程安全,简化 CPython 实现。代价:多线程无法并行执行 CPU 密集的 Python 代码。核心:“GIL护解释器不护你的并发——CPU密集靠多进程,IO密集线程照样有效”。GIL 是什么
- CPython 解释器级的互斥锁:线程要执行字节码必须先拿到
GIL - 目的:让引用计数等内部数据结构免于复杂加锁(实现简单、单线程性能高)
- 注意:
GIL是 CPython 实现细节,不是 Python 语言规范(Jython/IronPython无GIL)
- CPython 解释器级的互斥锁:线程要执行字节码必须先拿到
影响(关键结论)
CPU密集 + 多线程:无加速(甚至因锁竞争变慢)→ 应对:multiprocessing多进程/C扩展IO密集 + 多线程:有效——网络/磁盘等待时线程会释放GIL,别的线程继续跑C扩展可绕开:numpy/hashlib/压缩库在C层计算时会释放GIL,照样并行
工作机制(简化)
- 线程默认执行一段就主动让出
GIL:旧版用sys.setcheckinterval(按字节码指令数),新版用sys.setswitchinterval(默认时间片 5ms);遇到IO/系统调用立即释放 - 换回需要争抢 → 多线程频繁切换带来额外开销
- 线程默认执行一段就主动让出
应对策略(运维开发向)
CPU密集批量任务 →ProcessPoolExecutor(进程各自有GIL,真并行)- 高并发
IO→ 线程池或asyncio协程(单线程事件循环根本绕开线程竞争) - 计算下沉
C:numpy向量化/hashlib/Pillow——扩展内部并行 - 前瞻:
PEP 703(free-threading无GIL构建)已在演进,Python 3.13+ 提供实验性无GIL版本
协助记忆
- 口诀:“一把锁锁字节码:
CPU密集被锁死(上进程),IO等待会放锁(线程可用),C扩展能绕行”。
- 口诀:“一把锁锁字节码:
进阶思考
- 为什么 CPython 不干脆去掉 GIL?
- 去掉
GIL需要给所有对象/引用计数加细粒度锁——单线程性能大幅下降、C扩展兼容性破坏。官方路线是free-threading独立构建(按需选择),而非直接替换默认版——兼容性与性能的折中。
- 去掉
- 批量给 100 台机器算文件
MD5,怎么用对并发模型?MD5由hashlib(C实现)计算,读盘是IO、计算时释放GIL——线程池就有不错的加速;若算完还要做大量 Python 层统计(纯CPU),再叠加进程池。先分清“瓶颈在哪一层”再选模型。
- 为什么 CPython 不干脆去掉 GIL?
扩展信息
- 相关:
sys.setswitchinterval(切换间隔)、multiprocessing、concurrent.futures、asyncio - PEP 703 / free-threading:
Python 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__只做兜底且要极简。
- 调用时机不确定(依赖
扩展信息
- 相关 API:
gc.collect()(手动触发)、gc.get_count()/get_threshold()/set_threshold()、gc.freeze()(fork前冻结) - 内存诊断:
tracemalloc(分配追踪)、objgraph(引用关系/环可视化)、pympler——排查泄漏三件套
- 相关 API:
🤔 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.scandir(DirEntry自带is_file结果,省stat系统调用)②多进程分目录并行(ProcessPoolExecutor每个子树一个任务)③只统计必要信息(避免全量stat)。
- ①
扩展信息
- 相关 API:
os.walk/os.scandir/os.stat、pathlib.Path(rglob/glob/stat/suffix)、glob.glob(recursive=True) - 运维场景:磁盘小文件普查、日志目录按天统计大小、
inode排查(海量小文件)、清理前预览(find的 Python 版)
- 相关 API:
🤔 Python 如何执行 Shell 命令?
用
subprocess.run()(标准推荐):参数列表形式run(['ls', '-l'], capture_output=True, text=True, check=True)——拿返回码/输出、可控超时;需要 shell 特性(管道/通配)才shell=True。核心:“subprocess.run+ 列表参数 +check/capture;os.system不推荐用,os.popen/commands已淘汰”。标准姿势:
subprocess.runr = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=30)- 结果:
r.returncode(0 成功)、r.stdout/r.stderr(text=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。
- 列表参数:Python 直接
- 远程批量执行怎么组合?
subprocess+ssh(run(['ssh', host, cmd]),配BatchMode=yes免交互)——多机并发用线程池/asyncio包起来;更进一步用paramiko(纯 Python SSH 库,可控SFTP/隧道)或直接上Ansible。
- 列表参数和
扩展信息
- 相关 API:
subprocess.run/Popen/CompletedProcess、shlex.split(把命令串安全拆成列表)、shlex.quote - 运维场景:封装系统命令(磁盘/服务/网络检查)、
CI脚本调用、采集命令输出入库、自动化巡检
- 相关 API:
🤔 Python 如何实现在 100 台服务器上执行脚本?
核心是“SSH 通道 + 并发 + 结果聚合”:①简单场景
subprocess+ssh配线程池(ThreadPoolExecutor)并发执行 ②纯 Python 用paramiko(SSH 库,可控超时/密钥/SFTP)+ 并发 ③生产级直接用Ansible(ad-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管控、日志脱敏
- 并发度:20-50 合理(太高压垮自己网络/目标机
协助记忆
- 口诀:“互信打底,线程池 +
ssh并发跑,BatchMode/超时防挂死;生产上Ansible,paramiko补定制”。
- 口诀:“互信打底,线程池 +
进阶思考
- 为什么用线程池而不是进程池跑 SSH?
- SSH 执行是
IO密集(等网络回包),线程足够且轻量(GIL在等待时释放);进程池启动开销大、内存翻倍,只有命令本身要吃CPU时才考虑。百台规模线程池(20-50 并发)几秒到几十秒能完成。
- SSH 执行是
- 执行结果怎么保证可信(有机器假成功)?
- ①校验返回码 + 关键输出(命令
echo特定标记)②抽查/全量比对预期结果(如版本号一致性)③失败/超时机器单独列出人工复核。批量操作后必须给“主机-结果”清单,而不是一句“执行完成”。
- ①校验返回码 + 关键输出(命令
- 为什么用线程池而不是进程池跑 SSH?
扩展信息
- 相关库:
paramiko/asyncssh/fabric(paramiko封装,部署脚本利器)、plumbum、ansible/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(
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/Redis走Unix Socket(省TCP开销) - 日志/打点:
syslog(UDP/Unix socket)、statsd(UDP 打点)
- 服务探活:
协助记忆
- 口诀:“socket =
IP+端口+协议的网络门牌;TCP可靠、UDP轻快、本机走 Unix socket——探活/Agent/自定义协议都靠它”。
- 口诀:“socket =
进阶思考
- 写 TCP 服务为什么一般用
socketserver/框架而不是裸 socket?- 裸
socket的accept/recv循环是单线程阻塞,一次只能服务一个客户端;生产要并发(每连接线程/asyncio)、处理半包/粘包、优雅关闭——socketserver.ThreadingTCPServer、asyncio.start_server或现成框架(gunicorn/uvicorn)都封装好了。裸 socket 适合理解原理和小工具。
- 裸
TIME_WAIT大量堆积和 socket 什么关系?- 主动关闭方进入
TIME_WAIT(等 2MSL 确保对端收到FIN重传)——短连接高频(压测/爬虫)会堆积几万个端口。缓解:客户端复用连接(连接池/长连接)、SO_REUSEADDR(服务端重启快)、内核参数tw_reuse。本质是“短连接太多”,治本是长连接化。
- 主动关闭方进入
- 写 TCP 服务为什么一般用
扩展信息
- 相关库:
socket/socketserver/asyncio(open_connection/start_server)、select/selectors(IO多路复用)、paramiko(SSH协议跑在 TCP 上) - 排查工具:
ss -antp(连接状态)、tcpdump(抓包看协议交互)、nc(手工 socket 测试)
- 相关库:
🤔 你都用 Python 调用过哪些 API 及应用场景?
运维开发中 Python 调 API 主线是“云厂商 + 监控 + 协作工具”:阿里云
SDK(ECS/OSS/SLB/RAM管理)、K8sclient(资源操作)、Prometheus/Zabbix(指标拉取/告警)、企业微信/钉钉/飞书机器人(告警通知)、GitHub/GitLab(CI集成)。核心:“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/query(PromQL查询当前/历史指标)、/api/v1/alerts - Zabbix:
zabbix-api(主机/触发器/事件管理) - 场景:巡检报告拉数据、容量预测、告警闭环自动化
- Prometheus:
协作/通知 API(告警闭环)
- 企业微信/钉钉/飞书机器人 webhook:
POST一个JSON就能把告警/日报推进群 - 工单系统(
Jira/ServiceNow):告警自动建单/关联
- 企业微信/钉钉/飞书机器人 webhook:
开发协作 API
- GitHub/GitLab:拉
MR状态、自动打标签、CI触发 - CMDB/内部平台:资产查询、变更单流转
- GitHub/GitLab:拉
调用通用规范
requests.Session复用连接、超时必设(timeout=(3, 10))、重试用urllib3.Retry/装饰器、限速控制(防打爆对端)、异常捕获 + 日志
协助记忆
- 口诀:“云管资源(阿里云/
K8s)、监控取数(Prometheus/Zabbix)、通知闭环(企微/钉钉 webhook)——requests打底 + 官方SDK,凭据绝不硬编码”。
- 口诀:“云管资源(阿里云/
进阶思考
- 调用第三方 API 最容易踩什么坑?
- ①没设超时(对方挂了你的脚本也挂)②没处理限流(
429,要退避重试)③分页没拉全 ④凭据硬编码进代码/日志。规范:timeout+ 重试(指数退避)+ 分页封装 + 凭据走环境变量/KMS+ 日志脱敏。
- ①没设超时(对方挂了你的脚本也挂)②没处理限流(
- 官方 SDK 和直接 requests 调 REST 怎么选?
- 有官方
SDK优先(签名/重试/类型模型都封装好,如阿里云SDK的V3签名手写极易错);SDK覆盖不到的接口、或轻量一两个接口的场景,requests+ 官方签名文档直接调。关键看签名复杂度和维护成本。
- 有官方
- 调用第三方 API 最容易踩什么坑?
扩展信息
- 相关库:
requests/httpx(同步/异步)、各云官方SDK、kubernetes、prometheus-api-client、PyGithub/python-gitlab - 架构化:告警通知统一封装(级别→路由到不同群/值班)、
API调用统一中间层(重试/限速/审计一处管)
- 相关库:
🤔 如何排查 Python 程序的内存泄漏问题?
排查思路“先确认 → 找增量 → 定对象 → 查引用”:①监控确认
RSS持续涨(排除正常缓存)②tracemalloc对比快照找增长最多的分配点 ③objgraph看对象数量增长和引用链 ④常见根因:全局容器只进不出、闭包/缓存无限累积、循环引用带__del__、C扩展泄漏。核心:“快照对比 + 对象增长定位 + 引用链追凶”。第一步:确认是泄漏(不是正常增长)
- 监控
RSS/进程内存随时间趋势(Prometheus+node_exporter/容器memory) - 排除合理缓存(
lru_cache有上限的没事)、连接池预热——看“持续单调上涨且gc.collect()后不回落”
- 监控
第二步:
tracemalloc(定位分配点,首选)tracemalloc.start()→ 运行一段时间拍快照snapshot1→ 再拍snapshot2snapshot2.compare_to(snapshot1, 'lineno')→ 按代码行列出增长最多的分配——直击“哪行代码在涨”- 生产可采样降低开销(
tracemalloc.start(5)保留 5 帧栈)
第三步:
objgraph(定位对象与引用链)objgraph.growth():对比两次对象类型计数增长(哪类对象暴涨)objgraph.show_most_common_types():当前对象Top(dict多?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(标准库)、objgraph、memray(C层+火焰图,最强)、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
- Core(表达式语言):用 Python 表达 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直接拿列表)
- Engine/连接池:
运维场景
- CMDB/资产平台:主机/服务/变更记录建模 CRUD
- 巡检/采集入库:采集结果批量写入(
bulk_insert_mappings高效批量) - 工单/发布系统:Django 之外的轻量选择(配合
Flask/FastAPI)
常用生态
- Alembic:数据库迁移(表结构版本化,像 git 管理 schema)
- SQLModel:
FastAPI作者把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()释放连接。
- ①
- ORM 和手写 SQL 怎么权衡?
扩展信息
- 版本:2.0 风格在 1.4 作为过渡引入(
select()语法/Session.execute),到 SQLAlchemy 2.0 才正式定型(迁移旧Query风格)——新项目直接 2.0 风格 - 相关:
Alembic(迁移)、SQLModel、asyncmy/asyncpg+SQLAlchemy async(异步引擎)、Django ORM(对照)
- 版本:2.0 风格在 1.4 作为过渡引入(
🤔 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(信号处理,优雅退出)psutil:CPU/内存/磁盘/网络/进程全家桶(监控采集必备)
文件与文本
pathlib(路径对象化)、glob/fnmatch(模式匹配)、fileinput(多文件逐行)re(正则解析日志)、collections(Counter统计)、csv/json/yaml/configparser(数据交换与配置)
网络与远程
requests/httpx(HTTPAPI调用)、paramiko/fabric(SSH批量)、socket(端口探活)dnspython(DNS查询)、netifaces(网卡信息)、netmiko(网络设备CLI)
并发与任务
concurrent.futures(线程/进程池)、threading/multiprocessing、asyncio(高并发IO)queue(生产消费)、sched/APScheduler(定时任务)、celery(分布式任务)
工程化(脚本质量)
argparse/click/fire(命令行参数)、logging(日志分级落盘)、tenacity(重试)pymysql/psycopg2/redis-py/pymongo(数据库客户端)、SQLAlchemy(ORM)
打包分发
venv/pip/pyproject.toml(环境)、pyinstaller(打成单文件可执行,方便分发到无 Python 主机)
协助记忆
- 口诀:“系统
subprocess+psutil,远程paramiko,API用requests,并发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取第一列IP、collections.Counter累加、most_common(10)拿Top。核心:“生成器逐行 +Counter计数 +most_common,内存O(唯一IP数)”。基础实现(十几行搞定)
- 用生成器逐行读(
GB级也不怕):for line in open(log) - 取
IP:line.split()[0](nginx默认combined格式第一列就是IP)或正则re.match(r'(\d+\.\d+\.\d+\.\d+)', line) Counter累加:Counter对象update或c[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]取IP→Counter计数 →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时真实IP在http_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/percent(用available算使用率,比100-used准——含可回收缓存)swap_memory():交换分区
磁盘
psutil.disk_usage('/'):单分区total/used/percentdisk_partitions():枚举所有挂载点(遍历逐个usage)disk_io_counters():读写IO(次数/字节/耗时)
网卡
psutil.net_io_counters(pernic=True):每网卡收发字节/包/错包——两次采样差值÷时间 = 速率net_if_addrs():IP/MAC;net_if_stats(): up/down、速率、MTU
进程级
psutil.Process(pid):.cpu_percent()/.memory_percent()/.open_files()/.connections()(查端口占用)process_iter()遍历所有进程(找CPU最高的进程)
监控落地
- 定时采样脚本(
while True + sleep)→ 推Prometheus(prometheus_client)/Zabbix(sender)/写InfluxDB - 阈值告警:
mem.percent > 90触发通知;配合历史趋势看异常
- 定时采样脚本(
协助记忆
- 口诀:“
psutil一把抓:cpu_percent、virtual_memory(用 available)、disk_usage、net_io_counters(采样差值算速率)”。
- 口诀:“
进阶思考
- 内存使用率为什么用
available而不是100 - used?Linux把空闲内存拿去做页缓存(buffers/cached),used看着很高但缓存可回收;available是“不给新进程添乱的前提下还能分配多少”——才是真实可用量。这也是free命令新口径的含义。
- 网卡速率为什么要两次采样?
net_io_counters给的是累计字节数(开机以来),不是速率——取t1/t2两次差值除以间隔才是B/s。cpu_percent(interval=1)内部就是帮你做了这个两次采样,而网卡要自己算。
- 内存使用率为什么用
扩展信息
- 相关库:
psutil(跨平台Windows/Linux)、prometheus_client(暴露指标端点)、netifaces、py3nvml(GPU) - 对照命令:
top/free -h/df -h/ifconfig——psutil即它们的编程化(且跨平台统一接口)
- 相关库:
🤔 Python 有哪些 Web 框架及各自特点?
Python Web 框架按“全能 vs 轻量 vs 高性能”分:
Django(全家桶,大而全)、Flask(微框架,灵活组装)、FastAPI(现代异步,类型驱动+自动文档)、Tornado/Sanic(高性能异步)、aiohttp(异步客户端+服务端)。核心:“企业级全能用Django、轻量脚本用Flask、API高并发用FastAPI”。Django(全能型)- 自带
ORM/Admin后台/认证/中间件/模板/迁移(migrations)—— batteries included - 适合:完整业务系统(
CMS/管理平台/CMDB),开发效率高、生态成熟 - 代价:重、学习曲线陡、灵活性低(约定大于配置)
- 自带
Flask(微框架)- 核心极小(路由+请求对象),其他靠扩展拼装(
Flask-SQLAlchemy/Flask-Login) - 适合:中小服务、内部工具、运维脚本
Web化——自由度高 - 同步为主(
gevent补异步),大并发需额外架构
- 核心极小(路由+请求对象),其他靠扩展拼装(
FastAPI(现代 API 框架)- 基于
Starlette(ASGI框架)构建、运行于ASGI服务器(如uvicorn)原生异步;Pydantic类型校验;自动生成交互式文档(Swagger/ReDoc) - 适合:高性能
API服务、微服务、OpenAPI驱动的平台接口 - 当前新项目
API服务的热门首选
- 基于
其他
Tornado:老牌异步(长连接/WebSocket先驱);Sanic:async高性能;aiohttp:异步client+server一体Starlette:FastAPI的底层(轻量ASGI工具箱)
选型
- 完整平台带后台 →
Django;内部小工具/脚本服务 →Flask;纯API高并发/要文档 →FastAPI
- 完整平台带后台 →
协助记忆
- 口诀:“
Django全家桶、Flask拼积木、FastAPI异步快——按项目体量和接口形态选”。
- 口诀:“
进阶思考
- 运维开发大多用哪个?
- 内部平台(
CMDB/工单/发布系统)常用Django(Admin白捡管理后台);对接API/采集上报用FastAPI(异步高并发+文档);一次性小服务Flask最快。三者共存的团队很常见,按场景不是二选一。
- 内部平台(
WSGI和ASGI是什么关系?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 架构?
Django用MTV模式(官方称谓);常被写作MVT:Model(模型,管数据与业务规则,映射数据库)、Template(模板,管展示HTML)、View(视图,管请求处理逻辑,连接M和T)+URLconf路由分发。与传统MVC对应:Django的View≈MVC的Controller、Template≈View。核心:“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(展示与逻辑分离)
- Model(模型):
请求完整流程(面试常问)
- 浏览器请求 →
WSGI/ASGI服务器(gunicorn/uvicorn)→ Django 中间件链 →URLconf(urls.py)匹配路由 → 对应View执行 →View查Model(数据库)→ 数据传给Template渲染 →HttpResponse原路返回
- 浏览器请求 →
与 MVC 的对应(易混淆点)
Django MVT:Model≈M、View≈C(控制器角色)、Template≈V(视图角色)- 叫
MTV更准确(官方文档用词)——考察的是“View在Django里不是展示层”
各文件归属(工程结构)
models.py(M)、views.py(V)、templates/(T)、urls.py(路由)、forms.py(表单校验)、admin.py(后台)
协助记忆
- 口诀:“
MVT:Model管库、View管逻辑、Template管页面,urls做引路人——Django的View是控制器不是视图”。
- 口诀:“
进阶思考
- 前后端分离后 MVT 还有意义吗?
- 有变化:
Template层被前端框架(Vue/React)替代,Django退为“M+V”提供API(DRF出JSON)——Model/View/路由/中间件/ORM全部照用。MVT的分层思想(数据/逻辑/展示解耦)不变,只是展示层外移。
- 有变化:
View里的业务逻辑要不要塞进 Model?- 数据相关的业务规则(校验、状态流转)放
Model方法/Manager(可复用、事务安全);纯请求编排(解析参数、选模板、多模型组合)放View。别把业务都堆View——fat models, thin views是 Django 社区主流建议。
- 数据相关的业务规则(校验、状态流转)放
- 前后端分离后 MVT 还有意义吗?
扩展信息
- 配套机制:
forms/serializers(输入校验)、middleware(横切处理)、signals(解耦事件)、admin(自动后台) - 运维应用:
CMDB/工单/发布平台用Django开发,Admin快速生成管理界面
- 配套机制:
🤔 Django ORM 是什么?
Django ORM是Django的对象关系映射层:把数据库表映射成 Python 类(Model)、行映射成对象、字段映射成属性——用Host.objects.filter(rack='A1')这样的 Python 代码代替手写SQL。核心:“表↔类、行↔对象、查询↔方法链,附带防注入/迁移/多数据库支持”。核心概念
- Model:
class Host(models.Model): hostname = CharField(max_length=64, unique=True)→ 自动建表app_host - QuerySet:查询结果集(惰性、可链式)——
Host.objects.filter().exclude().order_by()生成SQL后才执行 - Manager:
Host.objects——查询入口(可自定义Manager加常用查询方法)
- Model:
常用能力
CRUD:save()/delete()、objects.get()/create()/update()- 过滤:
filter(rack='A1', status='on')(AND)、Q(rack='A1') | Q(rack='A2')(OR)、__跨关系/字段查找(hostname__contains、created__gte) - 聚合:
annotate/aggregate+Count/Sum(统计报表) - 关联:
ForeignKey/ManyToMany+related_name反向查询
优点与代价
- 优点:免写
SQL防注入、数据库可切换(MySQL/PG)、migrations管理表结构版本、模型即文档 - 代价:复杂查询生成
SQL可能低效(要看query/日志)、魔法多黑盒、N+1查询坑
- 优点:免写
配套
migrations:makemigrations+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')(多对多二次查询)——一次批量取,列表页性能天壤之别。
- 遍历 100 个
- QuerySet 是惰性的意味着什么?
qs = Host.objects.filter(...)只构造不执行;迭代/list()/len()/切片带step才触发SQL。利用惰性可自由拼条件(不同分支追加filter);QuerySet 一旦求值会缓存结果——重复迭代同一个对象不重查;只有新filter(产生新 QuerySet)才重查——注意底层数据变更不会自动刷新已缓存结果(stale 旧值),要拿新数据得重建查询。
- 什么是 N+1 查询问题,怎么解决?
扩展信息
- 性能手段:
select_related/prefetch_related、only/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/__endswithid__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_related防N+1”。
- 口诀:“取数
进阶思考
get()和filter().first()怎么选?get()语义是“必须恰好一条”——多条抛MultipleObjectsReturned、没有抛DoesNotExist(显式失败,适合主键/唯一键);first()宽容返回None(适合“可能没有”的业务查询)。乱用get()会让脚本被脏数据炸掉。
annotate和aggregate的区别?aggregate整表聚合一行结果({'total': 100});annotate给每个分组行附加计算列(每个机房多少台主机:values('rack').annotate(n=Count('id')))。一个是汇总值,一个是分组标签。
扩展信息
- 进阶:
Q/F/Subquery/Case-When(条件表达式)、自定义Manager、raw()原生SQL逃生口 - 调试:
print(qs.query)看生成SQL、django-debug-toolbar/日志sql——优化前先看它到底发了什么SQL
- 进阶:
🤔 Django REST Framework(DRF)是什么?
DRF是基于Django的 REST API 开发框架:把模型/业务快速暴露成规范的JSON API——Serializer(序列化/校验)、ViewSet+Router(一套代码自动生成CRUD路由)、认证/权限/限流/分页/版本内置可插拔、自带Browsable API调试页。核心:“Django管Web,DRF管API——序列化 + 通用视图 + 可插拔组件”。核心组件
- Serializer(序列化器):对象 ↔
JSON转换 + 输入校验(类似Form的API版)——ModelSerializer从模型自动生成字段 - View/ViewSet:
APIView(最灵活)→GenericAPIView(泛型基类)→ListCreateAPIView等(GenericAPIView+Mixin组合)→ModelViewSet(一套搞定增删改查) - Router:
DefaultRouter注册ViewSet自动生成RESTful路由(/hosts/、/hosts/1/)
- Serializer(序列化器):对象 ↔
内置可插拔机制(面试重点)
- 认证(Authentication):
Session/Token/JWT(djangorestframework-simplejwt)——我是谁 - 权限(Permission):
IsAuthenticated/IsAdminUser/自定义——能干什么 - 限流(Throttling):
AnonRateThrottle/UserRateThrottle——防刷 - 分页(Pagination):
PageNumberPagination/LimitOffset;过滤/搜索:django-filter+SearchFilter;排序:DRF 内置OrderingFilter - 版本化:
URLPathVersioning等;渲染器:JSON+Browsable API(浏览器直接调试)
- 认证(Authentication):
典型用法(三件套起步)
- 定义
ModelSerializer→ 写ModelViewSet(queryset+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)加自定义端点;完全自定义逻辑 →APIView或Django原生View。先ViewSet,特殊再下沉。
- 标准
- JWT 认证在 DRF 里怎么落地(运维平台场景)?
simplejwt提供获取/刷新token端点;脚本端先拿token、后续Authorization: Bearer头携带;配合权限类控制只读/读写。相比Session,JWT更适合脚本/多服务调用(无状态、跨域友好);注意token过期与刷新策略。
- DRF 的 ViewSet 和普通 View 怎么选?
扩展信息
- 配套库:
django-filter(过滤)、simplejwt(JWT)、drf-spectacular(自动生成OpenAPI文档)、drf-extensions - 对比:
FastAPI(原生Python类型驱动,异步)vsDRF(Django生态,Admin/ORM复用)——已有Django项目加API首选DRF
- 配套库:
🤔 DRF ModelViewSet 的父类有哪些?
ModelViewSet=GenericViewSet(ViewSetMixin+GenericAPIView骨架,提供查询集/序列化器/分页过滤)+Mixin五件套(ListModelMixin/CreateModelMixin/RetrieveModelMixin/UpdateModelMixin/DestroyModelMixin,分别提供list/create/retrieve/update/destroy动作)。核心:“GenericAPIView定骨架 +Mixin按需混入动作——ReadOnlyModelViewSet只读、ModelViewSet全有”。继承结构(自底向上)
APIView:DRF根视图(request/response/认证权限限流/异常处理)GenericAPIView:+queryset/serializer_class/get_object()/分页过滤钩子(通用骨架,无具体动作)- 五个
Mixin:ListModelMixin.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_queryset和perform_*”。
- 口诀:“
进阶思考
- 为什么用 Mixin 组合而不是一个大基类?
- 按需组装:只读接口继承
ReadOnlyModelViewSet(不会意外暴露写操作)、只要列表+创建就混对应两个Mixin——动作粒度可控、权限更容易收窄。大基类要么全给要么裁剪,容易“多出不该有的接口”。
- 按需组装:只读接口继承
- ViewSet 里的数据权限怎么实现(用户只看自己的主机)?
- 重写
get_queryset():return Host.objects.filter(owner=self.request.user)——列表/详情/修改删除全部自动收窄;再配权限类兜底。这是DRF数据隔离的标准姿势(比在serializer里过滤干净得多)。
- 重写
- 为什么用 Mixin 组合而不是一个大基类?
扩展信息
- 相关:
@action(自定义路由)、DefaultRouter/SimpleRouter、get_serializer_class()(不同动作不同序列化器) - 调试:
Browsable API页面直接点接口、drf-spectacular生成OpenAPI给前端/脚本对接
- 相关:
🤔 Django 中间件的作用是什么?
中间件(Middleware)是 Django 请求/响应的全局钩子链:所有请求进入 View 前和响应返回后都会依次穿过它们——用于处理“横切所有请求”的事:认证会话、
CSRF、GZip压缩、日志审计、限流、异常兜底。核心:“请求进、响应出各过一遍中间件——全局横切逻辑的唯一标准位置”。执行模型(洋葱模型)
- 请求 →
M1.process_request→M2.process_request→View→M2.process_response→M1.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)CsrfViewMiddleware(CSRF防护)、AuthenticationMiddleware(request.user)、GZipMiddleware(压缩,Django 5.0已弃用、计划6.0移除——压缩多由Web服务器承担)
运维场景(自定义中间件典型用途)
- 审计日志:记录谁在什么时间动了什么接口(
process_request起process_response落库) - 接口耗时监控:记录慢请求、上报指标
- 维护模式/黑白名单:
IP拦截、token校验统一收口
- 审计日志:记录谁在什么时间动了什么接口(
协助记忆
- 口诀:“中间件 = 请求/响应的洋葱圈——先进后出,五钩子(
request/view/exception/template_response/response);顺序即安全(CSRF在认证前,会话在认证前)”。
- 口诀:“中间件 = 请求/响应的洋葱圈——先进后出,五钩子(
进阶思考
- 中间件顺序错会出什么问题?
- 经典例:
AuthenticationMiddleware必须在SessionMiddleware之后(它依赖request.session)——放前面启动直接报错。SecurityMiddleware要在最前(尽早拦)。自加的日志中间件放最外层才能记录到完整往返。
- 经典例:
- 中间件里能做耗时操作(查库/调接口)吗?
- 慎重——它在每个请求上执行,一次 200ms 的外部调用会拖垮全站 P99。审计落库用异步(
celery/缓冲队列)、监控用轻量内存计数;确需同步的要加超时和降级。
- 慎重——它在每个请求上执行,一次 200ms 的外部调用会拖垮全站 P99。审计落库用异步(
- 中间件顺序错会出什么问题?
扩展信息
- 相关:
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_responsedef __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 封禁/维护页/限流的标准实现点。
- 短路:Django 不再调用后续 View(后续中间件的
- 新旧风格(
MiddlewareMixinvs__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/second、N/min、N/hour、N/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):CACHES配django-redis,计数才能全局一致。
DRF限流和nginx层限流怎么分工?nginx(limit_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 JWT:djangorestframework-simplejwt(Authorization: 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管“能看见哪些数据”)
- 脚本/平台间调用:JWT 认证 +
协助记忆
- 口诀:“认证答‘你是谁’(
Session/Token/JWT逐个试),权限答‘能不能’(IsAuthenticated/自定义has_permission)——401 没身份、403 没资格,数据范围交给get_queryset”。
- 口诀:“认证答‘你是谁’(
进阶思考
- 认证和权限为什么必须分开成两层?
- 身份与授权解耦:同一套 JWT 认证可搭配不同权限策略(只读/读写/管理员);同一权限策略可换认证方式(
Session换 JWT 不动权限代码)。混在一起写,接口变更时两边都要动,也难做统一审计。
- 身份与授权解耦:同一套 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/数据库)供查询
- Producer(业务应用):
什么时候用(判断标准)
- 耗时:任务超过几百毫秒就该异步(报表生成/批量扫描/视频转码)
- 不需同步结果:发通知/邮件/
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,要稳选RabbitMQdurable队列+持久化消息)、worker独立部署可扩容、自带重试/路由/定时。一次性轻量异步线程够用,业务关键任务必须队列化。
- 线程在应用进程内:应用重启任务丢失、无持久化、无法横向扩;
- 任务积压了怎么处理?
- 查
worker数/并发度(flower),扩worker;看是否有慢任务堵队列(加time_limit、拆分任务);按优先级/队列拆分(routes把轻重任务分到不同 worker);积压清空需谨慎。预防靠容量评估 + 积压告警。
- 查
- 用 Celery 和直接起个后台线程有什么区别?
扩展信息
- 相关:
kombu(broker 抽象)、flower(监控面板)、django-celery-beat(定时任务入库管理) - 替代品对照:
RQ(轻量 Redis 队列)、Dramatiq、arq(asyncio)——复杂度不高时RQ足够;生态/功能完整性Celery最强
- 相关:
🤔 简述 Django CSRF 工作原理?
CSRF(跨站请求伪造)防护原理是“请求携带只有本站知道的随机令牌”:Django给每个会话生成csrf_token,表单里嵌隐藏字段(或AJAX带X-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
- 双重提交 + 掩码:默认令牌存
csrftokencookie(非会话绑定),提交时对 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 Token和JWT有什么区别(都能防CSRF吗)?CSRF token专防“借 cookie 的伪造请求”;JWT 放在Authorization头(浏览器不自动携带)时天然免疫CSRF——所以纯 JWT 的前后端分离可少管CSRF。但 JWT 存 cookie 就又回到CSRF风险。规律:凭据自动携带(cookie)就需要CSRF防护,手动携带(header)就不需要。
SameSitecookie 和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 TestCase→class 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)(跳过认证流程直接种会话) DRF:from rest_framework.test import APIClient(用法类似,多format='json')
数据与隔离
setUp造数据 / 工厂库(factory_boy)生成测试对象;每个测试方法独立(事务回滚隔离);多方法共用数据用setUpTestData(类级只建一次,更省时)- 外部依赖隔离:
unittest.mock.patch打桩(requests/celery任务不真发)——@patch('app.views.run_cmd') pytest-django增强:dbfixture、clientfixture、参数化更灵活(现代项目主流)
测什么(运维平台建议)
- API 状态码与响应结构(回归保护)、权限(未登录 401/越权 403)、模型方法与校验、URL 路由覆盖
- 断言行为而非实现细节(换实现不换行为时测试不该挂)
协助记忆
- 口诀:“继承
TestCase,test_开头写用例;self.client发请求、assert*验结果;外部依赖mock掉,每例事务自动滚”。
- 口诀:“继承
进阶思考
TestCase和SimpleTestCase/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)
- 通信模型:HTTP 请求→响应(客户端发起);
握手过程(WebSocket 从 HTTP 升级而来)
- 客户端发
HTTP GET+Upgrade: websocket+Connection: Upgrade+Sec-WebSocket-Key - 服务端回
101 Switching Protocols→ 之后同一 TCP 连接切换为WebSocket帧协议
- 客户端发
适用场景
- WebSocket:实时推送(告警/行情/监控大盘)、聊天/协同、在线终端(
WebShell/K8sexec)、多人编辑 - HTTP:常规 CRUD/页面/API(请求-响应语义天然匹配)
- 轮询/SSE 对比:HTTP 轮询浪费请求;SSE 单向推送(服务端→客户端)够用且简单;真双向才上
WebSocket
- WebSocket:实时推送(告警/行情/监控大盘)、聊天/协同、在线终端(
运维要点
- 反代支持:
nginx需proxy_set_header透传Upgrade/Connection升级头;超时参数(proxy_read_timeout)要拉长 - 连接保持:心跳(
ping/pong帧)防中间设备断链;扩容用粘性会话或Redispub/sub 跨实例广播
- 反代支持:
协助记忆
- 口诀:“HTTP 一问一答短平快,
WebSocket握手一次双向长聊——实时推送选WS,普通接口选 HTTP;SSE 是单向的折中”。
- 口诀:“HTTP 一问一答短平快,
进阶思考
- 轮询、长轮询、SSE、WebSocket 怎么演进与选择?
- 轮询(定时全量问,浪费)→ 长轮询(有更新才答,挂住连接)→ SSE(服务端单向流,HTTP 原生)→
WebSocket(全双工)。选择:只要服务端推且不需要客户端高频上行 → SSE 足够(简单可靠);双向交互(终端/聊天)才上WebSocket。
- 轮询(定时全量问,浪费)→ 长轮询(有更新才答,挂住连接)→ SSE(服务端单向流,HTTP 原生)→
- 运维平台里
WebSocket的典型坑?nginx忘透传Upgrade头(握手 200 而非 101);长连接被负载均衡空闲超时掐掉(要心跳);多实例部署消息只到一台(广播要走Redispub/sub/MQ);连接数即资源(fd上限、内存),要监控与上限控制。
- 轮询、长轮询、SSE、WebSocket 怎么演进与选择?
扩展信息
- 实现库:
websockets/aiohttp(异步)、channels(DjangoWebSocket,走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、贪婪改?”。
- 口诀:“
进阶思考
match、search、fullmatch的区别场景?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+)
- IP:
协助记忆
- 口诀:“点任意、星加问量、尖括号定首尾、方括号挑字符;
d数字w词s空白、大写全反;数量加问变懒惰、点要转义别忘”。
- 口诀:“点任意、星加问量、尖括号定首尾、方括号挑字符;
进阶思考
- 为什么匹配 IP 的
.必须转义?- 未转义的
.是“任意字符”——192.168.1.1写成未转义正则会把点当通配,192x168y1z1也匹配(误匹配/安全漏洞)。字符类内[.]或转义\.才是字面点。
- 未转义的
.*和.*?在日志解析里怎么选?.*贪婪会“吃满一行再回溯”(嵌套引号/多字段时错位);.*?最短匹配在第一个结束符停。解析带引号字段(UA、referer)必须.*?;注意两者都可能回溯暴增——能精确字符类(排除引号的[^"xxx]思路)就别用.*。
- 为什么匹配 IP 的
扩展信息
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→ 自动诊断脚本(看日志/重启策略)→ 钉钉通知 + 处理记录
- 批量巡检平台:Python + 线程池/
工具类项目(脚本利器)
- 批量执行工具(
subprocess+ssh+并发)、证书到期检查、OSS/备份校验、数据库慢查询采集 - 特点:
argparse参数化、logging、可定时跑
- 批量执行工具(
集成类项目(云原生)
- 云管脚本/平台:
ECS定时开关(降成本)、OSS生命周期治理、RAM(阿里云资源访问控制)权限审计(云 SDK) K8s运维:集群巡检(kubernetesclient)、Pod异常自动处置、发布平台对接K8s API- 机器人集成:告警/发布/审批通知到钉钉/企微群(
webhook)
- 云管脚本/平台:
回答模板(每项目 1 分钟版)
- 痛点:之前怎么做、什么问题(人工/慢/易错)→ 方案:我设计的架构与流程 → 技术栈:语言/框架/组件 → 效果:量化(工时节省/故障率下降/覆盖台数)→ 角色:独立开发/主导设计/参与模块
协助记忆
- 口诀:“平台(
CMDB/发布/工单)+ 自动化(巡检/日志/告警)+ 工具(批量/校验)+ 集成(云/K8s/机器人)——每项目讲痛点、方案、技术栈、量化收益”。
- 口诀:“平台(
进阶思考
- 没有大项目经验怎么回答(包装原则)?
- 从“重复劳动”找素材:手动巡检 → 写成自动化脚本就是项目(讲采集维度/并发量/报告形式);手工备份 → 备份校验工具(覆盖率/失败告警)。真实小项目 + 深度讲透远胜虚构大项目——追问三层就露馅,诚实 + 量化是底线。
- 面试官追问“系统架构怎么设计的”怎么应对?
- 按层拆:入口(Web/CLI)→ 逻辑层(任务调度/权限/状态机)→ 执行层(并发框架/Agent)→ 存储(MySQL/Redis)→ 集成(MQ/通知)。同时主动讲踩过的坑(并发控制/失败重试/幂等)——坑讲得具体,可信度立刻上来。
- 没有大项目经验怎么回答(包装原则)?
扩展信息
- 加分项:开源贡献/内部工具开源化、
IaC(Terraform/Ansible代码化)、监控体系(Prometheus栈)建设经验、CI/CD流水线治理 - 能力映射:平台项目考工程能力(Django/
DRF/数据库),自动化考语言功底(并发/异常/日志),集成考云与K8s视野——三线并举是运维开发的核心画像
- 加分项:开源贡献/内部工具开源化、