ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

大模型开发必备:Python变量、数据类型与print调试全解析

大模型开发必备:Python变量、数据类型与print调试全解析 这篇真的是给想往大模型方向走、但Python基础还不牢的朋友写的。我自己从传统后台开发转到大模型相关的工作流最大的体会就是在接触Transformer、LoRA、Agent之前最先拦住我的不是算法反而是变量到底怎么存、类型怎么转、输出怎么打这些“小学内容”。可这些东西一旦理解透后面看任何模型代码都会顺很多。这篇笔记是“8天极速入门”系列的第二篇专门讲变量、数据类型和print输出全程结合大模型工程师的实际场景来讲不是干巴巴的语法罗列。1. 先把地基打牢为什么大模型工程师要死磕这三件事很多人一听说“大模型工程师”第一反应是赶紧去看论文、复现Transformer、跑微调脚本。结果代码一下来就傻眼了满屏都是input_ids、attention_mask、token_type_ids每个变量都是“好像认识但不确定”改一行就报错最后卡在数据预处理上动不了。其实问题不在模型多难而是最底层的Python基本功没有内化成直觉。这里我把变量、数据类型、print输出单独拎出来讲不是为了凑篇幅。这三个东西在大模型开发日常里几乎是最高频的动作变量承载了模型权重、中间激活值、token序列、超参数、训练状态等一切“命名后的数据”。你在PyTorch里看到的model、optimizer、scheduler本质上都是变量。数据类型直接决定了GPU显存占用、训练精度、推理速度。FP16、BF16、INT8这些大模型离不开的概念说到底就是数据类型的问题。选错类型轻则显存OOM重则loss变成NaN。print输出你调试数据管道、检查张量形状、监控loss下降第一反应就是打印一行看看。print用不顺排查问题的时间会翻倍。这篇笔记适合两类人一是零基础但铁了心要学大模型开发的二是写过一点脚本但从来没有系统整理过Python基础的朋友。看完之后你至少能无压力地读懂大部分大模型开源项目的入门级代码并且自己动手写数据检查脚本时不会老报错。2. 变量Python里的“标签”不是“盒子”2.1 先忘掉C语言的“盒子理论”如果你学过C或者Java会有一种根深蒂固的认知变量是一个“盒子”赋值就是把数据放进盒子里。Python完全不是这个逻辑。在Python里变量更像是给数据对象贴的一张“标签”。数据对象本身在内存里变量只是指向它的名字。a [1, 2, 3] b a # b不是新列表它和a指向同一个列表对象 b.append(4) print(a) # 输出 [1, 2, 3, 4]a也被改了这个例子让无数初学者栽过跟头。b a并没有复制出一个新列表只是给同一个列表又贴了一张名字叫“b”的标签。你通过b修改内容a看到的当然也是修改后的结果。在写大模型训练脚本时这个性质尤其需要警惕。比如你用train_data.load_from_disk(path)加载了一份数据然后又写了一句val_data train_data接着在val_data里做过滤操作训练集也会莫名其妙少一批样本。这种bug非常隐晦报错查不出来最后只能一步步打印内存地址来定位。2.2 变量赋值的底层逻辑引用语义Python里每个对象都有一个唯一的内存地址可以用内置函数id()查看。你可以把变量理解为“名字 → 内存地址”的映射关系x 42 print(id(x)) # 输出一串数字比如 140715574345616 y 42 print(id(y)) # 大概率和上面的id一致因为小整数会被缓存复用再看一个稍微绕一点的例子a [1, 2, 3] b a print(id(a), id(b)) # 两个id完全相同 b [4, 5, 6] # 重新赋值b换了张新标签 print(a) # 输出 [1, 2, 3]a没变第二段代码里b [4, 5, 6]是给b重新贴了一张标签指向一个新的列表对象。它不会影响a原来的指向。理解了“标签”和“对象”的关系很多诡异的变量问题都能迎刃而解。2.3 变量命名规范写给自己三个月后看的“接口文档”大模型工程师为了赶实验经常写出d、x1、tmp这种变量名。实验跑了三天第四天回来看代码完全不知道tmp到底是什么。我这里定几条我自己的硬规矩使用snake_case小写加下划线learning_rate、batch_size、train_dataloader让人一眼看出含义。不要用关键字和内置函数名list、dict、str、type这些名字一旦被覆盖后续的list()、str()调用全都会崩。变量名要有单位或语义后缀seq_len比n清楚max_length比m清楚learning_rate_init比lr0清楚。临时变量允许短名但要有范围意识在列表推导式里的i、x没问题函数内部一次性使用的tmp也勉强接受但不能出现在整个脚本的核心逻辑里。还有一个常见坑不要用中文做变量名虽然Python3允许但不同团队的编码风格、不同操作系统下的文件处理方式都可能因此出问题。老老实实用英文这是通用性最好的做法。2.4 大模型场景里的高频变量操作实际在写训练脚本、推理脚本时有几种变量操作几乎天天用解包赋值这是处理数据集时最顺手的一个操作。texts, labels zip(*samples) # 把样本列表拆成文本列表和标签列表 input_ids, attention_mask batch[input_ids], batch[attention_mask]链式赋值有时候要初始化几个状态值。best_loss current_loss float(inf)交换两个变量的值这是Python的一个经典优雅操作。a, b b, a在排序算法或者某些手写逻辑里这一行能省掉临时变量而且性能很好。变量的作用域写训练脚本时最容易犯的错是把函数内部的局部变量和全局变量搞混。global_batch_size 32 def update_batch_size(x): global_batch_size x # 这行只是创建了一个局部变量没有修改全局的 update_batch_size(64) print(global_batch_size) # 仍然是32如果确实要在函数内部修改全局变量需要声明global global_batch_size。但更好的习惯是不要随便改全局而是通过返回值传递。3. 数据类型内存里的“格式决定一切”3.1 Python内置基本类型一览Python内置的数据类型不算多但每一个都值得吃透。对大模型工程师来说尤其要理解它们的底层差异。类型名称是否可变常见用途int整数不可变索引、计数、步长、epoch数float浮点数不可变loss、学习率、精确率bool布尔不可变标志位、条件判断str字符串不可变文本数据、路径、模型名NoneType空值不可变占位、表示缺失list列表可变批量数据、动态数组tuple元组不可变固定结构、函数多返回值dict字典可变配置项、批量数据、特征映射set集合可变去重、成员判断这里“可变/不可变”的概念极其重要。可变对象可以在原有内存位置修改内容不可变对象一旦创建就不能改变。字符串是不可变的所以str.replace()会返回一个新字符串而不是原地修改。列表是可变的所以list.append()直接改原列表。3.2 字符串大模型工程师的“原材料”大模型处理纯文本字符串是绕不开的。有几个操作必须形成肌肉记忆text Hello, LLM Engineer! print(text.strip()) # Hello, LLM Engineer! print(text.lower()) # hello, llm engineer! print(text.split(,)) # [ Hello, LLM Engineer! ] print(,.join([a, b, c])) # a,b,c print(text.replace(LLM, NLP)) # Hello, NLP Engineer! 字符串格式化方面我个人推荐直接使用f-string别再用%s和format()除非代码库原有风格。f-string的可读性最好出了bug也容易排查。epoch 3 loss 0.47211 print(fepoch: {epoch}, loss: {loss:.4f}) # 输出epoch: 3, loss: 0.4721还有几个细节字符串是不可变的做大量拼接时不要用那会反复创建新字符串性能极差。推荐把片段丢进列表最后用.join(list)拼起来。判断子串用in比如if attention in layer_name特别适合遍历模型层名时用。处理多行提示词prompt时用三引号字符串非常方便可以保留换行和缩进。3.3 容器类型list和dict是大模型开发的左右手**列表list**用得最多的是批量任务的分批、样本的堆叠、候选结果的暂存。列表推导式是真的香能一行完成原来三四行的活儿# 把文本列表全部转成小写 cleaned_texts [t.lower().strip() for t in raw_texts] # 过滤掉空句子 non_empty [t for t in raw_texts if len(t.strip()) 0]不过要注意列表推导式在数据量很大的时候也会占用大量内存。如果数据有几百万条直接用推导式生成新列表可能导致内存暴涨这时候可以用生成器表达式把[]换成()来迭代。**字典dict**在大模型工程里的地位堪比“瑞士军刀”。模型的配置参数、数据集的batch、函数的命名参数通通离不开字典。config { model_name: bert-base-uncased, batch_size: 32, learning_rate: 5e-5, num_epochs: 5 }访问字典时强烈建议用.get()方法而不是中括号这样能避免键不存在时的KeyErrorbatch_size config.get(batch_size, 32) # 如果键不存在返回默认值32还有一个可能踩坑的点字典的键必须用不可变类型。列表不能当键但元组可以。比如你想用一个二维坐标作为键(x, y)是没问题的[x, y]则直接报错。3.4 类型转换大模型数据预处理中的“翻译官”大模型训练的第一步通常是把原始文本转成模型可用的数字形式。这个过程中类型转换无处不在。最基本的几个内置转换函数int(42) # 字符串 → 整数得到42 float(3.14) # 字符串 → 浮点数得到3.14 str(4096) # 整数 → 字符串得到4096 list(abc) # 字符串 → 列表得到[a, b, c]但类型转换有几个容易翻车的点分组转换时容易出错# 比如从JSON读进来的数值都是字符串直接相加会变成拼接 data [0.23, 0.45] print(data[0] data[1]) # 输出 0.230.45这是错的 print(float(data[0]) float(data[1])) # 输出 0.68这才是对的字符串转数字时格式必须严格匹配int(42.0) # ValueError不能把42.0直接转成int先转float再转int或者先清洗字符串才能规避这个坑。old-fashioned的type()判断不要用type(x) int这种写法用isinstance(x, int)后者支持继承关系也更规范。3.5 大模型工程师的专属类型NumPy与PyTorch中的dtype这里必须展开讲一下因为大模型工程师日常接触最多的“类型”其实不是Python内置的int和float而是数组的张量dtype。在PyTorch里torch.float32、torch.float16、torch.bfloat16都是数据类型。它们的区别直接关系到显存占用和数值范围数据类型位数指数位小数位大模型典型用途float32fp3232位8位23位默认精度训练基线float16fp1616位5位10位混合精度训练显存减半bfloat16bf1616位8位7位大模型训练的默认神器范围大int88位--推理量化极大降低显存我见过很多同学把训练数据用float64装进DataLoader结果模型一加载就GPU显存不够一脸懵。其实预处理阶段用.astype(np.float32)管住类型就是这个原因。在使用PyTorch时数据类型的转换经常这样写import torch tensor torch.randn(16, 768) print(tensor.dtype) # torch.float32 tensor_half tensor.half() # 转float16 tensor_bf16 tensor.bfloat16() # 转bfloat16为什么大模型训练要用bfloat16而不是float16因为bf16的指数范围和fp32一样大数值稳定不会动不动就上溢或下溢代价是精度低一些但训练过程中的梯度更新通常够用。这也是为什么现在主流的训练框架默认开启混合精度或者直接用bf16。3.6 可变与不可变大模型代码中“改不动”的坑再强调一次可变与不可变的区别因为真出问题的时候真的能找一整天。str、tuple、int、float、bool、NoneType是不可变的。list、dict、set是可变的。这个特性直接影响函数的默认参数。这是Python最经典的坑之一def append_sample(sample, samples[]): samples.append(sample) return samples print(append_sample(hello)) # [hello] print(append_sample(world)) # [hello, world]而不是[world]默认参数[]只会在定义函数时创建一次后续所有调用共享同一个列表函数就“有记忆了”。正确的写法是def append_sample(sample, samplesNone): if samples is None: samples [] samples.append(sample) return samples4. print输出大模型调试的“第一只手电筒”4.1 别小看print它是大模型工程师最常用的调试工具虽然IDE里的断点、日志框架、可视化工具都很强大但print一直是最快、最直接的观察手段。你写不出一个从没Print过的深度学习项目。问题是怎么Print得高效、有信息量、不刷屏是门学问。最简单的print用法print(Training start!) print(batch_size:, 32) print(loss:, 0.4782334)但逗号分隔输出多个值中间会自动加空格格式不是总好看。所以进阶的第一件事就是用f-stringprint(fepoch: {epoch}, step: {step}, loss: {loss:.4f}){loss:.4f}的意思是只保留4位小数。对大模型训练里那些动辄0.38742636的loss值来说这种格式化输出能让日志一眼看过去清爽好多。4.2 通过print摸清张量形状和dtype拿到一个新模型或新数据第一件事就是打印张量的形状shape和数据类型dtype。这个习惯能让你少踩无数坑。# 打印每个关键张量的信息 for name, param in model.named_parameters(): if param.requires_grad: print(f{name}: {param.shape}, {param.dtype})处理batch数据时也是一样print(finput_ids{input_ids.shape}) print(fattention_mask{attention_mask.shape}) print(flabels{labels.shape}) print(f数字类型{input_ids.dtype})很多错误本质都是维度对不上或者dtype不匹配。比如模型要求torch.long的input_ids你预处理时却忘转了某处运算直接变成浮点向量可能不会立刻报错但某些算子就会炸。打印出来就能一眼定位。4.3 进阶输出进度条和loss曲线训练过程中直接主打一个大print会把日志刷得跟瀑布一样根本没法看。这时候有几个实用的做法。用tqdm做进度条这是大模型工程里最常用的进度可视化库。from tqdm import tqdm for epoch in range(config[num_epochs]): for step, batch in enumerate(tqdm(train_dataloader, descfEpoch {epoch})): # 训练代码 passdesc参数就是进度条左边的那段描述文字你可以把它写成Epoch 2, loss: 0.43之类的这样每一轮的整体状态一目了然。关键指标周期性打印不要每个step都print每N个step打印一次if step % 100 0: print(f[Epoch {epoch}] step: {step}, loss: {loss.item():.4f}, lr: {scheduler.get_last_lr()[0]:.2e})这样日志既不刷屏又不漏掉重要信息。用logging替代printprint在训练环境里痕迹太轻不好分级不好落盘。标准做法是引入logging模块import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) logger.info(Loading model...) logger.warning(Learning rate too high, consider decaying.) logger.error(CUDA out of memory.)日志的好处是可以配合时间戳、级别过滤、输出到文件以后排查问题方便得多。我的习惯是临时调试用print正式训练脚本全部用logging。结合断点调试pdbprint解决不了的时候可以在可疑位置插入breakpoint()程序运行到那里会进入交互式调试界面能输入命令查看变量值。不过慎用忘了删的话训练进程会卡在断点处不走。4.4 print在推理场景的妙用除了训练推理服务里的print同样重要。部署LLM对外提供API时你会想知道每次请求是否命中了缓存、生成了多少token、首token延迟多少。这些都可以用print或logging记录下来print(fprompt tokens{input_len}, generated tokens{num_generated}, 首token耗时{first_token_time*1000:.1f}ms)有了这些输出你能及时判断推理瓶颈在哪是prompt处理太慢还是逐token生成太慢。5. 实操写一个“迷你训练前检查”脚本串起本篇所有知识点这里我们写一个非常小的实战脚本模拟大模型开发中训练开始前的准备工作。它会把本篇涉及的核心知识点都用起来代码可以直接复制改着玩。5.1 任务目标我们模拟的场景是从一批原始文本数据出发做基本的类型转换、数据统计、格式化输出最终打印一份“数据集信息卡”让你在跑模型前先确认数据没问题。5.2 代码实现# preflight_check.py import random from datetime import datetime def clean_text(s): 清洗字符串去空格、转小写 if not isinstance(s, str): s str(s) return s.strip().lower() def build_dataset_info(raw_data, val_ratio0.2): 统计数据集信息返回一个字典 total len(raw_data) val_count int(total * val_ratio) train_count total - val_count info { total: total, train_count: train_count, val_count: val_count, val_ratio: val_ratio, avg_length: round(sum(len(x) for x in raw_data) / total, 2), created_at: datetime.now().strftime(%Y-%m-%d %H:%M:%S), } return info def main(): # 模拟一批原始文本数据 raw_texts [ Hello World , PyTorch is great, Python basics for LLM Engineer, 注意有些样本可能很长, short, ] # 1. 列表推导式清洗 cleaned [clean_text(t) for t in raw_texts] print(f清洗后的样本数: {len(cleaned)}) # 2. 类型转换与验证 sample cleaned[0] print(f第一个样本类型: {type(sample)}, 内容: {sample}) print(f是否是字符串: {isinstance(sample, str)}) # 3. 类型转换练习 total_str 128 batch_size 16 steps_per_epoch int(total_str) // batch_size print(f每轮step数(整数除法): {steps_per_epoch}) # 4. 字典配置项 config { model_name: demo-small, batch_size: batch_size, learning_rate: 1e-4, num_epochs: 3, } print(f配置字典内容: {config}) # 5. 构建数据集信息 info build_dataset_info(cleaned, val_ratio0.2) print(\n 数据集信息卡 ) for k, v in info.items(): print(f{k}: {v}) if __name__ __main__: main()5.3 运行结果运行这段脚本输出大概长这样清洗后的样本数: 5 第一个样本类型: class str, 内容: hello world 是否是字符串: True 每轮step数(整数除法): 8 配置字典内容: {model_name: demo-small, batch_size: 16, learning_rate: 0.0001, num_epochs: 3} 数据集信息卡 total: 5 train_count: 4 val_count: 1 val_ratio: 0.2 avg_length: 23.4 created_at: 2025-06-20 14:30:165.4 复盘这段代码里你练到了什么变量raw_texts、cleaned、sample、config、info都是变量有的是列表、字典有的是整数、浮点、字符串。数据类型用了str、int、float、dict、list、datetime对象并且用isinstance验证类型。类型转换str(s)、int(total_str)能正常工作注意到int(total_str) // batch_size是整数除法。print格式化用了f-string、type()、.format()的替代品同时用循环print把字典内容一行行打出来。这个脚本虽然小但它模拟的是你拿到一份真实数据准备开始探索、清洗、统计、预览的完整流程。把这次实操跑通远比单纯看语法来得有用。6. 常见问题与排查技巧实录6.1 问题速查表报错信息或现象常见原因解决方案NameError: name x is not defined变量名拼写错误或变量还没定义就使用检查变量名拼写确认赋值语句在调用之前TypeError: can only concatenate str (not int) to str把字符串和数字用直接拼接用str()转换或用f-string:f{name}: {age}ValueError: invalid literal for int() with base 10字符串转整数时内容不是数字先清洗字符串或捕获异常KeyError: batch_size字典里没有这个键但你用[]访问使用dict.get(batch_size, default)IndexError: list index out of range列表索引越界先确认len(list)再访问AttributeError: NoneType object has no attribute xxx某函数返回了None你却把它当对象用检查函数是否有返回值确认数据处理流程没有中途丢失训练loss变成NaN学习率太高、数据里有NaN、类型溢出降低学习率检查数据用bf16/fp16时注意溢出GPU显存OOMbatch_size太大或数据类型是fp64/fp32降低batch_size转成fp16/bf16清空无用变量6.2 我最常遇到的一个“隐形错误”在数据处理时写了一个函数处理完文本后没有return返回了None。接着下一行调用tokenizer.clean_text(x)结果拿到的是None再一访问方法就报AttributeError。排查半天才发现函数忘记写return。这类错误在Python里太常见了尤其是习惯在函数里写print而不写return的初学者。我的经验是每定义一个函数第一件事就想清楚“这个函数要返回什么”没有返回值也要显式写return哪怕返回None也要心里有数。6.3 关于数据类型的最后一个提醒在写大模型训练脚本时一定要对“Python类型”和“张量类型”有清晰的区分。Python里的float和PyTorch里的torch.float32不是同一个概念。前者的精度和范围都由Python解释器决定后者由GPU/CPU的数值格式决定。这两套类型体系要在大脑里分开装否则调试混合精度训练时很容易绕晕。我之前遇到过一次数据加载出来明明是float32喂进模型后却变成了float64导致GPU显存直接翻倍。后来一查是某个第三方库的默认dtype设置成了float64。所以每次打印dtype都很有必要不要偷懒。写在最后的实际感受我经常和团队里刚转大模型的同学说一句话“你先把变量搞清楚把类型搞清楚把print用明白再谈Transformer和Attention。”不是夸张也不是故意压低难度而是因为我在实际工作中真的见过太多人论文读得头头是道一写代码就卡在KeyError和TypeError上几个小时起步非常消耗耐心。这篇笔记里的内容你在很多Python教程里都能看到但视角我特意换成了“大模型工程师专用的角度”。变量不只是语法它关系到你是不是会把训练集悄悄改掉数据类型不只是内存它关系到你的模型能不能在GPU上跑起来print不只是输出它关系到你的实验日志能不能帮你快速定位bug。带着这个视角去写代码你会比单纯背语法的人进步快很多。下一篇我准备写控制流和函数那是把代码从“脚本”变成“工具”的一道坎。到时候见。
返回列表