十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python基础知识储备:从环境搭建到工程实践的完整梳理

Python基础知识储备:从环境搭建到工程实践的完整梳理 学Python这事儿说起来门槛低但其实特别讲究基础储备。我带过不少新人也看过很多写了两三年业务脚本的同事写起代码来能跑可一问他为什么列表推导式比普通for循环快为什么函数默认参数不能用可变对象为什么字典遍历的顺序有时候稳定有时候不稳定往往就卡壳了。这些都属于Python基础知识储备里的核心内容——不是让你背文档而是把语法背后的规则、常见的坑、选型的逻辑都捋清楚。这篇内容定位很明确面向刚接触Python的初学者也适合那些断断续续写过一阵子、但基础不牢靠想系统性梳理一遍的人。我会从环境安装讲起一路覆盖数据类型、流程控制、函数、数据结构、模块化和异常处理最后简单说说面向对象。每一部分不是简单罗列知识点而是结合我实际写代码时踩过的坑告诉你为什么要这样写。1. 安装Python之前先把这两件事搞清楚1.1 版本选择和安装步骤里的隐性陷阱很多新手在网上搜python安装教程看到哪个点哪个结果装了一堆乱七八糟的版本最后连python命令都调不起来。我建议只从Python官网python.org下载安装包不要去应用商店随便装一个因为商店版本往往不是最新稳定版而且文件路径和PATH配置经常和官方版不一致后面装第三方库容易出幺蛾子。版本选型方面不需要追求最新也不要停留在老版本。以当前的稳定节奏看选择最新的稳定版比如3.11或3.12是合理的。不要用2.xPython 2早已停止维护所有语法习惯都和3.x有差异现在学2.x纯属给自己添堵。Windows安装过程中最关键的一步是勾选Add python.exe to PATH这个选项。PATH是什么简单说就是系统的查找路径你在命令行里输入python系统是靠PATH环境变量去找到python.exe的。如果不勾选输入python会提示不是内部或外部命令。这几乎是Windows新手遇到最多的问题没有之一。macOS用户要特别注意系统自带的python3是老版本而且和系统底层工具绑定不建议直接动它。建议用Homebrew安装一份独立的Pythonbrew install python3Linux用户一般用自带的包管理器安装比如Ubuntu/Debian上sudo apt update sudo apt install python3 python3-pip -y安装完成后打开终端验证一下python --version python3 --version pip --version只要能看到版本号环境就算通了。如果你在Windows上装了官方版大概率python和python3都能用但在Linux上通常只有python3可用需要的话可以用alias把它映射成python不过我更建议直接习惯用python3避免在别人的服务器上找不到命令。1.2 两种运行模式别混着用Python代码有两种运行方式一种是直接在终端输入python进入交互式解释器REPL一行一行执行立刻看到结果适合验证小片段比如试一下某个函数返回值、看一下某段逻辑另一种是把代码写进.py文件然后通过python 文件名.py整体运行这种适合逻辑完整、有大量缩进结构的程序。新手容易犯的错误是用交互式写一大段逻辑。REPL里每输入一行回车就执行一行如果这段逻辑有依赖关系一旦中间写错就得从头再输入一遍效率极低。反过来有人习惯把所有代码都写成脚本文件但临时想验证某个小函数的行为也老老实实写文件再运行白白浪费时间。我的建议是小问题用REPL大逻辑写文件。而且写文件时最好从一开始就养成看完整报错信息traceback的习惯。Python的报错信息里最底部一行是错误类型和描述上面是调用栈顺着栈往下看就能定位到具体是哪个文件哪一行出的错。很多人看报错只看最后一行的末尾这等于浪费了整个traceback的提示价值。1.3 虚拟环境比想象中更重要装好Python之后很多人下一步直接pip install 某个库装完就开写。短期内没问题但时间一长不同项目的依赖版本就会打架项目A需要Foo库1.x项目B需要2.x你全装在全局环境里很快就会出现这次能跑、换个项目跑不了的尴尬局面。Python官方推荐的解决方案是venv也就是虚拟环境。它相当于给每个项目单独开一个Python客房房间里的库和全局环境互不干扰。创建虚拟环境在项目目录下打开终端python -m venv myenvWindows激活方式myenv\Scripts\activatemacOS/Linux激活方式source myenv/bin/activate激活后终端提示符前面会多一个(myenv)前缀。以后在这个终端里执行pip install装到的库都只属于这个项目。我见过有人嫌激活麻烦长期不用虚拟环境直到某天一个项目的依赖升级影响了另一个项目的线上脚本才回头补课。所以基础储备这件事越早建立越好——虚拟环境就是第一条要养成的工程习惯。2. 数据类型是地基搞懂变量、不可变对象和运算符2.1 变量不是盒子是名字贴很多教程喜欢把变量比喻成装数据的盒子。这个比喻对新手友好但到了一定阶段会产生误导。Python官方的说法是变量是名字对象才是实体。你给变量赋值实际上是让这个名字指向某个对象。看个例子a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]b a并没有复制一个新的列表它只是让b这个名字也指向了a指向的同一个列表对象。所以通过b修改列表a看到的内容也变了。这种设计在Python里叫引用语义。理解这一点很多诡异的bug都能想明白。另一个相关概念是用id()函数可以查看对象在内存中的标识a 1 b 1 print(id(a) id(b)) # True小整数有缓存机制这里涉及Python的内存池优化对于一些小整数-5到256和较短字符串解释器会复用对象所以两个名字指向同一个对象。但这属于实现细节写代码时不要依赖它只要记住比较值用比较身份是不是同一个对象用is就行。比如判断一个变量是不是None习惯上写if x is None:因为None是单例对象这样写既快又语义清晰。2.2 不可变对象为什么有的操作改了等于没改Python里常见的不可变类型有int、float、str、tuple、frozenset。所谓不可变就是对象一旦创建里面的内容就不能被修改。听起来像是限制但实际上这是Python很多行为的基础。拿字符串举例s hello s.upper() print(s) # 还是 helloupper()返回的是一个新的字符串对象原字符串s没有变化。如果你想让s变成大写就得写s s.upper()把新对象重新绑定到s这个名字上。这个特性对元组影响很大。元组本身不可变但如果它里面装了一个列表那列表的内容是可以变的t ([1, 2], 3) t[0].append(99) print(t) # ([1, 2, 99], 3)所以元组不可变准确来说是指元组的长度和元素引用不能变但元素指向的对象如果是可变的它自身还是能变。理解不可变对象还有一个实际用处它们可以被安全地作为字典的键因为键需要可哈希hashable而哈希值在对象生命周期内不能变。列表就不能当字典的键就是因为列表可变、哈希值不稳定。2.3 运算符和类型转换别踩隐式转换的坑Python的运算符设计很直观但有几个地方特别容易踩坑/是浮点除法1 / 2结果是0.5。//是整除1 // 2结果是0注意它和int()截断不一样-3 // 2结果是-2因为//是向下取整而int(-3 / 2)是-1。%是取余结果的正负号和除数有关这一点和很多C系语言不同。bool是int的子类True 3的结果是4虽然业务代码里很少这样写但知道这个机制能帮你理解不少第三方库的返回值处理。逻辑运算符and、or也有一个容易忽略的点它们返回的不一定是True或False而是决定结果的那个操作数。result or 默认值 print(result) # 默认值这是因为Python的逻辑运算遵循短路求值or从左往右遇到第一个真值就返回它and遇到第一个假值就返回它。这种写法在日常代码中非常常见比如给函数参数设置默认值、选择非空字段等。类型转换方面写int(42)没问题但写int(42.0)会抛ValueError。很多新手在解析用户输入时踩这个坑。推荐的写法是先把字符串转成float再转成int或者用try/except显式处理转换失败的情况不要假设输入一定是合法数字。3. 流程控制和函数写出结构清晰的代码3.1 缩进是语法不是什么代码风格Python最特殊的地方是用缩进来表示代码块。别的语言用花括号或者begin/endPython用冒号和缩进。这意味着缩进错误不只是不好看而是直接IndentationError程序根本跑不起来。实际编码中最常见的缩进问题是混用Tab和空格。很多编辑器默认把Tab显示成4个空格的宽度导致肉眼看着对齐但解释器认为缩进层级不同直接报错。解决方法是在编辑器里强制设置用空格代替Tab一般叫tab 4 spaces或expand tabs。如果你用的开发工具比如VS Code打开设置搜索insert spaces勾上之后Tab键自动转成空格。缩进风格上官方PEP 8建议使用4个空格。不要用2个空格更不要为了省行数把多层嵌套压成一行——代码首先是给人读的其次才是给机器运行的。3.2 if/elif/else与条件判断的最佳实践if判断的语法很简单但有几个习惯值得建立起来尽量直接判断对象的真假性不要写if x True:或if len(list) ! 0:。Python里每个对象都有一个真假值0、0.0、空字符串、空列表、空元组、空字典、None、空集合都是假其他基本都是真。所以直接写if x:比写if len(x) 0:更Pythonic也更快。判断是否为None时用is None不要用 None。因为可能被子类重载或者被某些对象实现成奇怪的逻辑is是身份比较语义最精准。Python支持链式比较0 x 10这个写法在别的语言里往往需要写成x 0 x 10。Python的链式比较在语义上等价于0 x and x 10而且只计算一次x简洁又安全。3.3 for循环、while循环和那些实用的内置搭档Python的for循环不是传统的计数器循环它本质上是迭代器遍历。你写for i in range(10):实际上是让range(10)这个对象一次一次产生数值。这种设计的好处是省内存range(1, 1000000)并不会真的创建一个包含一百万个数字的列表它只是按需生成。遍历列表时经常需要同时拿到索引和值最自然的写法是用enumeratefor index, value in enumerate([a, b, c], start1): print(index, value)第二个参数start1表示索引起始值默认是0。这个函数能让你少写很多手动计数的代码。并行遍历两个列表可以用zipnames [张三, 李四] scores [88, 92] for name, score in zip(names, scores): print(name, score)zip会以最短的那个序列长度为准截断如果两个列表长度不一致你可能会意外丢数据。如果需要严格等长可以检查两者的len或者在Python 3.10以上用itertools.zip_longest并显式指定fillvalue。while循环本身不复杂但要注意退出条件不然很容易写出死循环。我见过最典型的情况是循环里忘了更新计数变量i 0 while i 5: print(i) # 忘了 i 1这种代码一跑就卡死只能强制终止进程。写while时先把这个循环靠什么结束想清楚再动手写。另一个值得一提的语法是for循环配合else子句。很多初学者不知道for...else的存在如果循环正常结束没有被break中断else块会执行。适合用来做查找失败的处理for item in items: if item target: print(找到了) break else: print(没找到)不过说实话这个特性在团队开发中容易引起误读因为它和很多人的直觉不同。我个人的建议是在有经验的场景下可以使用但如果你在维护一个新人居多的项目不如用标志变量或函数返回值来得直白。3.4 函数参数设计是门学问函数是组织代码的基本单元Python的函数有几个特殊机制必须掌握。第一个是默认参数。很多人觉得def func(x, items[]):看起来挺好实际上这是个经典坑默认参数在函数定义时就被创建了之后的每次调用如果没传items用的都是同一个列表对象对象会累积上一次调用的修改。def add_item(item, items[]): items.append(item) return items print(add_item(1)) # [1] print(add_item(2)) # [1, 2] 而不是 [2]正确的写法是默认参数用不可变对象内部再重新创建def add_item(item, itemsNone): if items is None: items [] items.append(item) return items第二个是*args和**kwargs。*args用来接收任意数量的位置参数打包成一个元组**kwargs用来接收任意数量的关键字参数打包成一个字典。在定义函数时用它们做参数转发很方便尤其是写装饰器或者封装第三方库接口的时候。第三个是函数返回多个值。return a, b看似返回了两个值实际上是返回了一个元组(a, b)。调用时可以同时用两个变量接收a, b func()这个语法叫元组解包。写函数的时候我还建议从一开始就用类型注解type hintsdef greet(name: str, age: int 18) - str: return f{name}{age}岁类型注解本身不会影响程序运行但它能把你的意图告诉IDE和相关工具比如IDE能根据注解提示错误、自动补全大型项目配合mypy做静态检查很多隐患在写代码阶段就能暴露出来。基础储备阶段就养成写类型注解的习惯后面接手复杂项目会轻松很多。4. 四种内置数据结构用对场景才算会用4.1 列表和元组可变与不可变的取舍列表是我用得最多的数据结构。它支持任意类型混装、支持动态增删改查灵活性极高。但正因为太灵活新手经常用过头——能用元组解决的也非要用列表。元组和列表的区别表面上看是能不能改本质上是语义承诺。元组一旦创建就不能增删改所以它适合用来表达一组固定的数据比如三维坐标(x, y, z)、一个用户的多个属性等。这种不可变性也是一种隐式文档读代码的人看到元组就知道这段数据的结构不会变可以在多线程或跨函数传递时放心使用。列表的常用方法里我特别强调append和extend的区别a [1, 2] a.append([3, 4]) # [1, 2, [3, 4]] a.extend([5, 6]) # [1, 2, [3, 4], 5, 6]append是把参数作为一个元素放进去extend是把参数的可迭代对象展开逐个放进去。用错的话列表的嵌套结构会完全不一样经常导致后续遍历逻辑莫名出错。切片slice是Python列表的一个大杀器nums [0, 1, 2, 3, 4, 5] nums[1:4] # [1, 2, 3] nums[::-1] # [5, 4, 3, 2, 1, 0] nums[::2] # [0, 2, 4][::-1]逆序、[::2]隔一个取一个这种写法极具Python特色。但要注意切片返回的是新列表原列表不变如果原列表元素本身是可变对象切片中的元素还是引用修改它们会影响原列表。列表推导式list comprehension是我日常写代码时使用频率最高的语法之一squares [x * x for x in range(10) if x % 2 0]这行代码等价于squares [] for x in range(10): if x % 2 0: squares.append(x * x)推导式代码更紧凑而且执行速度通常更快底层有优化。但不要贪心写太长的推导式嵌套两层以上可读性就会明显下降。这时候用普通for循环加if反而更清晰代码是给人看的不是用来炫耀语法的。4.2 字典Python里的万金油数据结构字典是Python里最重要、最常用的数据结构之一。从Python 3.7开始字典保持插入顺序这个特性让字典在实际使用中兼具了记录顺序的能力比如做JSON序列化时键的顺序可以稳定输出。字典的基本操作很简单但有几个方法值得专门练习get(key, default)取键对应的值键不存在时返回默认值不会抛KeyError。setdefault(key, default)键不存在时给字典设置默认值并返回该值。defaultdict来自collections模块可以在创建字典时指定默认值工厂比如defaultdict(list)后访问不存在的键会自动创建一个空列表并返回特别适合做分组统计的场景。from collections import defaultdict result defaultdict(list) result[alpha].append(1) # 即使alpha不存在也不会报错字典合并可以用update方法也可以在Python 3.9直接用|运算符d1 {a: 1, b: 2} d2 {b: 3, c: 4} merged d1 | d2 print(merged) # {a: 1, b: 3, c: 4}|运算符生成新字典原字典不受影响。注意后面的字典覆盖前面对应键的值。遍历字典时如果要同时拿键和值直接for key, value in d.items():比for key in d:再访问d[key]更快也更清晰。默认遍历字典拿到的是键如果想遍历值用.values()如果想以元组形式拿键值对用.items()。4.3 集合去重和集合运算的一把好手集合set是用来存储不重复元素的数据结构。最常见的用途是去重items [1, 2, 2, 3, 3, 3] unique_items list(set(items))这个操作非常简单高效但有两个注意点一是集合里的元素必须是可哈希的列表和字典这种可变类型放不进去二是set本身是无序的如果有去重后还要保持原顺序的需求用set就不行得用别的方法比如遍历并用seen set()记录已出现元素items [3, 1, 2, 3, 1] seen set() result [] for item in items: if item not in seen: seen.add(item) result.append(item)集合运算也是它的强项交集、并集|、差集-、对称差集^。处理两个名单里都出现的人只在一边出现的人这类问题非常顺手。创建空集合时有个小坑{}会生成空字典不是空集合。要创建空集合必须写set()。我在刚开始学Python时在这里栽过跟头后来养成了习惯看到{}先想到字典看到set()才想到集合。4.4 选型经验别一上来就词典打天下四种内置数据结构的选型本质上是业务需求决定的如果你需要频繁按下标查找元素选列表如果元素顺序不重要又需要快速判断是否存在选集合如果需要根据键快速取值选字典如果数据是固定的、不应该被修改选元组。不过现实中很容易出现列表套字典套列表的深层嵌套。说实话当嵌套超过三层代码的可读性就开始崩了。这时候更好的做法是定义一个数据类dataclass把结构显式表达出来而不是靠层层字典去猜数据结构。Python的dataclasses模块从3.7版本开始提供对初学者来说可能有点超前基础储备阶段可以先记住嵌套过深不是好事一定有更清晰的结构在等着你。5. 模块化与异常处理从单文件脚本走向工程化5.1 import机制Python组织代码的核心方式当代码量超过几百行之后全塞进一个文件就很难维护了。Python的模块化设计本质上是通过import语句把不同文件里的代码组织起来。import的基本语法有几个变体import math # 导入整个模块用 math.sqrt 调用 from math import sqrt # 导入指定函数直接用 sqrt 调用 import numpy as np # 起别名方便书写 from package.module import func # 从包中导入from xxx import yyy写起来更简洁但也有风险如果两个模块里都有同名函数后导入的会覆盖先导入的。在大型项目里我倾向于用import module或import module as aliases的方式这样每个函数都有明确的命名空间前缀读代码时能一眼看出它是从哪里来的也不容易产生命名冲突。还有一个新手经常困惑的问题是为什么有时候自己写的模块导入失败原因是Python导入模块时会在sys.path里依次搜索。sys.path包括当前脚本所在目录、标准库目录以及环境变量PYTHONPATH指定的目录。如果脚本在工作目录之外运行当前目录不在sys.path里就会ModuleNotFoundError。解决方法是把项目根目录加进PYTHONPATH或者用项目内的相对导入方式配合包的__init__.py。包package本质上是包含__init__.py文件的目录。这个文件可以为空也可以写包的初始化逻辑。Python 3.3之后即使没有__init__.py也能导入目录但为了让包结构更清晰、避免某些情况下命名空间不明的行为我还是建议保留这个文件哪怕它是空的。5.2 ifname main每个脚本都该有的门卫这个语句是Python里最经典又最容易被新手忽略的惯用法。它的作用很简单判断当前文件是被直接运行还是被别的模块导入。当一个.py文件被直接执行时Python会把它内置的__name__变量设为__main__而当它被import时__name__会被设为模块名。所以if __name__ __main__:块里的代码只会在直接运行时执行被导入时不会执行。不加这个判断会怎样你写了一个脚本里面有一些测试代码或者示例调用结果别的文件import你这个模块时那些代码也会跟着执行一遍轻则浪费资源重则产生副作用。所以写模块时把主逻辑放在这个if块里是一个基本素养。它同时也是区分可复用模块和可执行脚本的惯用标记。5.3 异常处理不是吞掉错误是管理错误初学者面对报错第一反应往往是赶紧try一下让程序不崩溃。这种思路其实没抓住异常处理的核心——异常处理的目的是让你能识别错误、做出合适的响应而不是简单地让程序闭嘴。Python的异常处理结构是try/except/else/finallytry: result int(user_input) except ValueError: print(输入的不是合法数字) except (TypeError, KeyError) as e: print(f发生了类型或键错误: {e}) else: print(f转换成功: {result}) finally: print(无论如何都会执行)else块在try没有异常时执行finally块无论是否异常都会执行适合做资源清理比如关闭文件、释放连接。写except时有一个非常重要的规范不要只写裸的except:或except Exception:。裸捕获会连KeyboardInterrupt用户按CtrlC退出和SystemExit都抓下来导致你按了CtrlC程序毫无反应只能强杀进程。except Exception虽然排除了上面两个但依然可能隐藏掉很多意料之外的问题。比较好的做法是尽量捕获具体异常类型比如ValueError、KeyError、FileNotFoundError等至少要在日志里记录完整的traceback信息而不是只打印一句出错啦。自定义异常在写库或业务代码时很有用。你可以创建一个继承Exception的类class TooLargeError(Exception): pass def check(value): if value 100: raise TooLargeError(f值 {value} 超过上限)这样调用方可以根据异常类型分别处理不同业务错误而不是对着一个通用异常猜原因。这一点配合logging模块记录错误详情在项目长大后价值极其明显。6. 面向对象入门理解Python的一切皆对象6.1 类与实例先明白属性查找的顺序Python的面向对象语法不难难点在于理解它的设计哲学。Python里面类class本身也是一个对象实例instance也是对象函数也是对象一切皆对象这句话不是修辞是事实——所以你才能把一个函数当作参数传给另一个函数把一个类当作值赋给变量。定义一个最简单的类class Person: species 人类 # 类属性所有实例共享 def __init__(self, name: str, age: int): self.name name # 实例属性每个实例独有 self.age age def introduce(self) - str: return f我叫{self.name}今年{self.age}岁__init__方法在实例创建时自动调用作用是对新对象做初始化。self是实例本身的引用所有实例方法的第一个参数都必须是它。写成self只是一个约定你可以改成this或者其他名字但没人会这么做——约定大于配置改名的坏处远大于好处。一个常见困惑是实例属性和类属性重名时哪个生效答案是实例属性优先。对实例的某个属性赋值时不会修改类属性而是给实例创建一个新的同名属性于是实例属性覆盖了类属性。理解这个属性查找顺序实例 → 类 → 父类之后Golang也好、Java也好各种语言的继承模型就不会再有迷惑了。6.2 继承、多态和鸭子类型继承是面向对象的传统特征Python里实现继承很简单class Student(Person): def __init__(self, name: str, age: int, grade: int): super().__init__(name, age) self.grade grade def introduce(self) - str: return super().introduce() f我读{self.grade}年级super()用来调用父类的方法。子类同名方法会覆盖父类方法这就是重写override。当你调用stu.introduce()时实际执行的是子类的版本。Python的多态靠的更多是鸭子类型duck typing——如果它走起来像鸭子叫起来像鸭子那它就是鸭子。也就是说一个对象能不能传给某个函数不在于它继承了什么类而在于它有没有实现对应的方法。你写一个函数接收introduce方法那么任何实现了introduce方法的对象都能传进去不要求它们有共同的基类。这种设计让Python代码非常灵活但代价是类型约束变弱。所以在团队项目中我依然建议在函数签名里写清楚类型注解配合mypy做检查弥补鸭子类型带来的过于自由。判断对象类型时优先使用isinstance(obj, ClassName)而不是type(obj) ClassName。isinstance支持继承关系比如一个Student实例同时是Person的实例而type()精确比较就相对死板。又因为Python强调鸭子类型很多场景其实连isinstance都不该用直接试试它有没有某个方法更符合Python的风格。6.3 基础储备之后往哪个方向走学完上面这些内容你已经具备一个比较扎实的Python基础底座。接下来怎么进阶我个人的建议是两条腿走路一条腿是继续深入语言本身。建议通读官方教程Python官方文档的The Python Tutorial重点看迭代器、生成器、装饰器、上下文管理器这几个主题。这些是我在带新人时发现的分水岭掌握它们的人写代码的方式会和只会for循环的人完全不一样特别是在处理流式数据、设计框架代码时。另一条腿是赶紧做小项目。基础知识的储备不能只靠看没有一个项目作为载体这些语法很快会忘。我推荐几个适合练习的入门项目方向批量文件重命名工具用os、pathlib、日志分析脚本用正则、collections.Counter、命令行待办事项管理器用文件读写和函数设计、简单的网页数据抓取用requests加标准库html.parser注意遵守目标网站的爬虫协议和数据使用规范。每做一个项目就去翻一遍标准库文档看看有没有更合适的现成工具——这是我从会用语法到会选方案之间最关键的一步。我的经验是基础知识储备至少要过三遍第一遍看会第二遍边看边敲第三遍不看文档直接写小项目卡住了再回头看。到第三遍时你会发现那些文档上写过但当时没感觉的细节一个个都开始真正属于你了。别急着追新框架、新库地基层面的东西值得反复打磨因为它们在哪个方向都用得上。
返回列表