
中间的那位名字可能会和别人的一样, 出现重名的情况当只是使用了只有一个函数, 并且这个名字是独有的时候。numpy as np名字很长, 而且社区里面存在着约定俗成的缩写情况。from math *只有在交互式命令行里面的临时试验场景这个条件限定之下, 才会去进行使用。三、自己写一个模块任何一个用 .py 来结尾的文件, 在本质上面它就相当于是一个模块, 大家根本就不需要去特意做什么样的声明动作。接下来我们不妨把两个函数以及一个常量全部都存放进一个 .py 文件里面的这个过程之中去好好地感受一下, 大家就能够清晰地认识到所谓写模块这件听起来似乎挺高大上的事情, 归根结底其实也就是像在写一个最普通不过的文本文件罢了。# 文件 .py我的工具模块def add(a, b):a bdef (a, b):a * b圆周率的值为3.14159。完成书写工作之后, 在位于相同路径中的另一个文件里边可以实施直接使用这种操作, 这是由于系统默认状态会把当前被执行的那个脚本所存在于的那个目录, 算作是要去查找的相关路径的内容, 关于这个具体的原理和机制, 会在后面的第九节内容里头给讲解清楚。print(.add(3, 4)) # 7print(.PI) # 3.14159from add,print(add(2, 5)) # 7你需要注意, 在“我的工具模块”这句话里, 它并不是什么普通的注释, 而是属于文档字符串的一部分, 它会被系统自动保存进该模块的信息里面, 当你使用帮助功能的时候, 打印出来的内容就是这一部分。为什么要去写这么一段东西呢?原因是当其他人要去查看你的代码并且想要了解情况的时候, 他们第一个想知道的问题通常是你这个文件到底是用来做什么的, 有了这一行文字之后, 运行帮助命令就会有具体的内容可以展示, 如果没有写的话, 那就只能让人自己去翻开源代码进行查看。这种放在文件级别的说明文本的作用, 就相当于给这个模块做了一层面封皮一样。它与#符号用来表示的注释在性质上是不一样的, 这个不同的关键点在于, 前者是给人类开发者看的说明文字, 程序执行结束之后就会消失掉, 而后者实际上是属于一个在当前时刻真正被创建并且存在于内存里面的属性。四、 的作用这行代码, 它通常是初学者最容易直接复制粘贴的那一句, 不过大多数人并没有搞懂里面具体的意思。我们接下来先看一看它的完整结构长得什么样子, 然后再去详细说明一下它具体拦截或者防范了什么类型的风险和问题。# .py 改进版def add(a, b):a bif :打印测试信息以及数字一加数字二相加之后的结果。核心问题来了, 就是到底是什么东西, 并且它为什么会一会儿等于那个空字符串一会儿又等于另一个那个空字符串呢, 谜底其实就藏在解释器启动的那个逻辑里面, 因为每一个模块在刚刚被创建出来的时候, 解释器都会把一个叫做这个的内置变量直接塞进到它的命名空间里面去。当这个文件是被直接运行的那个主程序.py的时候, 解释器给它起的名字, 就是那个固定字符串。当它呈现为被引入进来的模块这样的状态之时, 其对应到的名称就是这一个模块本身所固有的名字, 也就是空字符串。所以这行判断翻译成现在人能直接看懂的普通话书面语是, 如果我这个文件是被当作主程序跑的, 就执行下面这段, 如果我是被别人调用的, 就跳过。它究竟防范的是什么情况呢? 所防范的, 是在别人进行某项操作的时候, 模块内部那些用于测试或者演示目的的代码程序会被连带着一起执行出去。请你去观察一个没有采用这种编写方式的反面例子来加以对比。这是一个反面案例, 指的是缺乏独立思考与审慎评估的情况下, 把原本用于功能验证的测试指令代码直接堆砌在代码结构的最顶端区域。def add(a, b):a bprint(测试, add(1, 2)) # 这行会在每次 时都跑一遍实验结果展示出来是, 别的人仅仅只写了一个特定的语句之后, 你的测试程序的输出环节就会在没有预先设置任何显示动作的情况下, 直接在他的整个程序执行结果里面打印出一行内容为“测试: 3”的文本来。大家单凭肉眼仔细看这一行单独存在的内容时, 也许会感觉它看起来完全没有一点危害之处, 可是要是当前的顶层指令正在执行像是“打开数据库连接”, 或者“发送一封电子邮件”, 又或者“删除临时存储文件”这类操作的话, 那么那个特定的语句就会立刻触发一系列不可预知的连带反应过程, 这就是大家口中常说的典型的静默副作用现象, 而且这种错误类型是最难以被查找并定位的。因此, 大家所判断出来的、真正需要去进行保护的那种情况是这样的, 也就是把导入一个模块以及运行一个模块这两件事分开来看, 不让前一件事直接触发了后一件事所要执行的这个动作。有一个关于机制的补充说明, 这就是用于判断当前程序是不是主程序的唯一可靠的办法, 命令行里的 -c ; print(.) 会打印一些内容, 而直接 .py 的时候打印的是另一些内容, 你可以自己跑一遍进行验证, 这样就能确认具体情况了。五、常用标准库模块它自身是自带有一个“电池仓”的, 也就是那个被称为标准库的东西了。在下面这一些里头, 日常时候出现频率是最高的那几种。大家先混个脸熟就可以, 到了需要的时候再去查阅文档就好了, 完全没有必要非得去背诵下来不可的。math # 数学sqrt/ceil/floor/pi# 随机//# 日期date//os # 系统path//sys # 这个解释器会处理那个叫做argv的列表, 也会关注exit函数, 还有那个path变量。关于json这个事物来说, 它的dump和load这两个操作。re # 正则match//其中 和 用得最勤各看一个最小可复现的例子print(.(1, 10)) # 1-10 之间的随机整数print(.(a, b, c先从这一连串的东西里头, 随随便便地挑出一个来。print(.date.today()) # 今天日期print(..now()), 也就是显示现在的时间。为啥隔几行之后就不会重新加载呢? 原因在于同一个模块在整个进程内部只会实际运行一次, 等到第二回需要使用的时候, 直接就从小册子缓存里面抓取结果, 这套逻辑就是第九节里头要彻底搞清楚的规则机制。请你先把结论记在脑子里, 等会儿后面会有相应的证据支撑。六、模块的文件夹, 这个通常就被叫做包。当模块的数量增加到几十个的时候, 如果还是光靠把它们平铺在目录里进行管理的话, 那就一定会乱套的, 所以包这个问题的解决方案其实是非常简单的, 那就是用一个文件夹把这些模块全都装进去, 下面这个就是关于一个最小包的目录结构的相关内容。所谓包, 实际上就是指一个内部包含了许多以py作为文件后缀名的文件的文件夹。# 目录结构# /# .py # 包标识可为空#。py##第一个模块。这是文件编号为点py点的代码模块, 具体的内容是第二章。通过利用这个已经创建好的列表, 在实施引用操作的环节里面所采用的方式, 将会被加上一个类似于文件夹名称的前缀字符, 而这种前缀本身的实际属性, 就是一种属于命名空间结构层级的存在。# from# from . add# . as st.py这个后缀的文件, 主要是用来干什么的呢? 它有两个作用。一个是向系统声明文件夹是一个包。在三.三版本之前, 这是硬性要求。另一个是当包自身被导入的时候, 这个文件会被执行。所以它可以专门用来做包的初始化工作。比如在包里预先加载好那些常用的子模块。这样做是为了让用户在调用时, 少写几层路径。即便把它变成一个空文件, 也是合法的。这种情况等于说我只是承认自己是个包, 但除此之外没有别的事要去做。包里面的那些模块, 彼此之间要是想互相引用对方, 那么用的是什么, 用的就是相对导入方式。假设你是在某个后缀叫做 .py 的源代码文件里面编程, 然后你还想要去用同一个包里面其他的那个后缀也叫 .py 的代码文件的话:# 在 /.py 内部# from . # . 表示当前包从当前这个包里面的, 这个clean模块当中, 去提取, 也就是获取到, 那个clean对象。这里的句点符号是用来代表当前这个包的, 而两个连续的句点则表示上一层的包。大家可能会问, 为什么会不采用像 from 这样的写法呢? 这是因为如果那样写的话, 就会把具体的包名字给固定住了。一旦有人对这个包进行更名操作, 或者把它移动到其他的位置去, 那么代码就会立刻出问题而且无法正常运行。相对导入这种方式, 它只认“我这个包所在位置的相对关系”, 所以在整个包进行搬迁的时候, 完全不需要去修改任何一行代码。这就是它在设计时所做出的一个选择, 也就是付出一点代价, 牺牲掉一点点那种能够一眼就看出它到底属于哪个包儿的直观感觉, 但是这样做的结果是换来了在搬迁工作完成时可以实现零改动的效果。七、进行到进阶的部分了, 就是安装那些属于第三方的库, 这一步通常会用到叫做pip的这个工具。在标准库以外, 真正构成生态系统的地方是在第三方 PyPI上面, 你要进行安装的话, 所用的命令就是 pip。# 安装pip, 这个是用来进行网络请求的东西。pip, 这个是用来进行数据分析的。# 安装指定版本生产环境务必锁版本pip 2.31.0# 查看已安装 / 卸载pip listpip把东西都安装好之后, 使用的情况会和标准里的情况完全相同。这两个地方的情况都是非常一样的。这个现象的出现, 其实是因为模块机制的存在使得整体变得统一了。从第三方提供的库和系统自带的库的角度来看, 在计算机的眼里, 它们所处的地位是完全一致的。## resp .get()# print(resp.) # 200国内加速pip -i我们要讨论一下关于依赖管理的事情。做法是用 pip 工具把这个操作指向到 .txt 文件上面, 意思是导出当前环境里面所有的版本情况。然后别人那边就可以用 pip 加上 -r 参数再去读取那个 .txt 文件, 这样就可以实现一键还原的效果。那么问题就来了, 为什么要必须进行版本锁定呢? 原因就是现在可以正常运行, 并不等于到了下周也仍然能够正常运行的。因为上游的那个库一旦发布了一个新的版本, 就有可能产生兼容性的破坏。这就是线上项目里面最常见的出故障的场景之一。八、很多人都会犯的一个错误, 这个点是必须要点出来看的, 第一个错误的情况就是自己的模块名和标准库发生了冲突。有一种事故非常隐蔽, 这种事故的发生原因是你自定义的文件名称和标准库提供的文件名称重复了, 从而导致你的文件名把标准库给覆盖掉了。# 反例自己建个 .py → 之后 变成你自己的文件# 尽量别用常见名字做文件名不要去使用那些叫做 .py 文件, 也不要去使用 math。py 这个名称, 更不要去用 json。py 以及 utils。py 这些名字, 原因就在于这些称谓是太过于通用的了, 所以在命名时应该避免这样处理。为什么会这样呢? 因为的搜索顺序里, 当前目录排在标准库前面第九节详解。于是找的第一个就是你自己那个。后果级别很高。你自己的往往没有。一调用就。更坏的是它可能碰巧有。于是你拿到的随机数根本不是随机的。比如永远返回0。程序不报错。结果全错。错误2循环导入为什么两个文件互相会失败? 先看这个最典型的互相引用结构, 根因在第十节专门拆解。# a.py: from b x# b.py: from a y# : name x from# 避免循环导入把公共代码放第三个文件请注意报错信息里面出现的那些关键词, 具体来说就是那个提示只初始化了一半的模块的内容, 这个现象直接指明了导致问题发生的根本原因在哪一个点上, 因为变量a还没有执行到完成的那个阶段就已经去调用变量b了而在调用过程中, 变量b又转回头向变量a里面去寻找某个东西, 可是那个东西在当前的情况下并没有被定义成一个可以使用的状态, 关于这个问题的完整的根因解释, 以及提供给你两种可以使用的解决方案, 全部都已经在第十节里面作为专门的话题来进行详细讲解和处理。在错误三的这种情况之中, 人们会注意到从某个地方进行导入的操作所对应的那个名称实际上是不存在的, 这就导致了出错的后果。为什么会出现找不到这个名称的情况, 一个可能性就是你拼写错误了, 另一个可能性是对方那个模块里面根本就没有它。先看看这个错误到底是什么样子。# from 后面的名字必须真实存在于模块里# from # : name 为什么会出现这个错误, 原因是从当前模块去导入那个叫作x的东西, 系统硬性要求必须先完成对于模块m以及整个模块m内部的具体执行过程, 并且在这个已执行的模块空间里必须确定存在一个被定义为x的标识或者名称, 才会把该值抓过来, 如果你的拼写出了差错, 或者是目标模块在初始化之后依然没有产生对应的名字定义这些情况都会在第一步导入校验阶段就被拦截住并报错, 你可以在报错信息里面找到指向的那个模块的名称, 然后直接对着去检查对应的代码文件里面是否真的存在这个名字, 这种照着报错信息一步步核对实际环境的做法, 比靠你瞎猜和碰运气要有意义得多。错误4以为必须有 .py大多数人都记得那个包必须得带着 .py 这样的后缀才行的说法, 可是这条规则到了 3.3 版本以后就已经变得不再那么严格了。为什么会变成这样松弛的状态呢? 我们不妨先看一看当前呈现出来的实际面貌。版本号在 3.3 及以上版本的那个包是可以不带有那个 .py 的后缀文件的, 这种情况对应的术语叫做命名空间包。为了做到能够兼容而且还要把问题说清楚, 所以还是建议把这个东西给保留下来。# 空文件也行.py这里要特别注意边界问题, 当目录的名字是以“ 3.3 ”开头的时候, 即便这个文件夹里面没有带 .py 后缀的文件, 也是能够被当作包来直接导入的, 这种现象在专业领域里有一个专门的称呼, 那就是“命名空间包”, 但是官方强烈不建议采用这种方式, 首先原因是因为非常老旧版本的软件环境根本就不支持这种用法, 其次是因为如果没有包含 .py 文件的话, 你就无法在包的内部执行任何初始化操作, 总而言之, 这种办法仅仅只适用于极少数特定的临时场合, 就是说你只是暂时想要借用一下别人早就已经整理好目录结构, 并且你完全没有打算去对那个目录进行任何修改的特殊情况。---九、关于它到底是什么事情, 或者到底具体做了哪些内容, 这就是它的原理。在前面的内容里反复提到过会执行文件、第二次走缓存这些情况, 现在在这里给出能够作为硬性依据的证据。的完整三步当这个情况发生的时候, 要接连着做三件大事, 任何一件都不能少。第一件事情就是找, 根据 sys.path 提供给我们的那些目录列表, 一个一个地去查看, 看看里面有没有 .py 文件或者是有对应的包, 只要找到第一个符合条件的, 那就立刻停下来不再找后面的了。第二件事情是执行, 把刚才那个被找到的文件从头到尾完整地运行一遍, 这里需要特别注意, 整个文件内容都要被执行, 不仅仅是里面的函数定义部分, 文件中写在最顶层代码位置的 print 输出语句、赋值的操作代码以及 open 打开文件的动作, 都需要真正地跑起来实实在在地被执行到。第三步叫做缓存, 它的意思是把已经执行结束以后所产生的模块对象拿来, 让模块名充当键值对里的键, 然后把这个东西存到sys这个全局字典里去。这个被称为执行整个文件的情况, 是弄懂后面所有内容的一个重点: 在模块里面存在的每一条处于顶层位置的语句, 当它们被加载进来的一瞬间, 就已经产生出了副作用效果。所以把那些非常费力的工作任务, 包括连网去获取数据或者是建立数据库的链接动作, 安插到模块的最上层位置的做法是完全错误的, 因为这样做的后果是, 当其他人员只是想要使用一个辅助函数的简单需求时, 这些重活的代码会毫不犹豫地立马就开始运行。证据一, 是关于系统的缓存这个情况。在随后进行的第二次操作环节中, 系统明确显示出提示信息称, 针对该特定文件的处理过程并未予以重新开启和再次执行。sys这是第一次, 把整个文件完整地执行一遍, 如果文件里面有 print 这种语句, 你就会看到相应的输出结果。print( in sys.) # True已经被缓存在第二次进行调用操作的时候, 选择直接去读取缓存里头的东西, 这个时候对应的文件内容就不会再被执行一遍了。之所以第二次没有执行, 原因是在进行文件查找这个动作之前, 系统还有一步检查工作要去做, 也就是先去看看 sys 这个东西里面是不是已经有了这个名字, 如果发现确实已经有了, 那就直接就把结果返回去了, 根本就不会再去费劲找那个文件了。这其实就是大家都知道的所谓“同一个模块在进程里面只会被加载一次”的那个机制是怎么来的, 这也是为什么第五节那里可以反复去写的原因所在。证据二, 也就是关于搜索路径sys.path的顺序问题。sys.path, 它属于一个列表类型, 在处理导入过程的时候, 会从最前面的位置开始, 接着向后面的位置进行顺序的扫描操作:sysfor p in sys.path:第一个部分通常是空字符串, 这指的是当前运行脚本文件所属的那个文件夹路径。顺序大概是这样的, 先从当前脚本所在的目录开始找, 接着去环境变量里面定义的目录中寻找, 然后再到标准库所在的目录里查找, 最后才轮到第三方库所在的site-目录。这个顺序直接解释了两次真实的事故情况, 第一种情况属于错误编号为一的情况, 也就是所谓的自己的以点py结尾的程序文件把标准的库给顶替掉了, 这是因为当前的目录是排在标准的库的前面的位置, 所以本地的文件就赢了这一点, 第二种情况是反过来说的, 如果你的想要去用某个第三方的库去覆盖标准库里面那些相同名字的模块内容的话, 仅仅只是依靠放置一个本地的文件是没有用的, 原因在于标准的库可能还是排在更前一些的位置上面, 因此只能去修改那个环境来解决这个问题。这就是为什么大家要别用常见名字命名文件的这个原因, 它不是一条需要死记硬背的规范条令, 而是由搜索顺序这个必然结果推导出来的。扩展一下, sys.path 是在运行的过程里面可以被更改的, 打个比方就是可以用 sys.path.0, 某个目录这样的方法, 所以说, 临时添加一个搜索目录是可以做到的。可是, 把目录塞进了 sys.path 里面, 这就属于一种隐式的依赖情况了, 因为它把在某一台机器上面的绝对路径给写死到了代码里, 一旦换了另一台机器的话, 必然就会失败掉。在正式的工程项目里面, 大家更推荐去使用虚拟环境配合 pip 这种方式。---十、出现循环导入现象的根本原因在于两个文件之间存在互相依赖的情况, 之所以会发生错误, 是因为代码的导入操作实际上是伴随着执行过程来注册模块信息的, 这种机制并不是要求先把所有的准备工作都提前做好然后再去统一执行。把第九节的三步拆开来看, 其中的第二步叫做执行, 这一步是一行一行去做的, 至于模块对象, 它是在开始执行之前就被放进去了, 并且放的位置是sys.这个目录里面, 而且放进去的只是半成品状态的东西, 所以导致了循环导入出现这种像死亡一样的路线。1. 第一步是先让那个编号叫a的脚本文件去跑起来, 然后呢就开始执行名为apy的那个程序。2. 运行a.py的时候, 在第一行位置, 系统会看到有一个从b里面导入模块的代码存在, 然后它就会转过头去专门执行b.py里面的内容。3. 在 b.py 这个文件的第一行那里, 有一个操作是从 a 模块里面把 y 这个东西给导进来, 然后系统就回头要去找到 a 模块里的 y 变量到底是个什么值, 可是此时 a.py 才刚刚执行到第一行这一步, y 这个变量还没有被定义过。4. 把从a里面拿到的那个东西, 它的名字叫y这个值, 给抛出来。为什么不一开始就报出一个更让人容易接受的错误信息呢?实际上, 它已经给出了非常准确的提示——在初始化的过程进行到一半的时候, 这个情况就是答案所在: 出现状况的原因并不是代码的语法有问题, 而是因为变量 a 还没有完成定义操作, 但是变量 b 却在这个时候要求获取它的值, 这种时间顺序上的冲突才是核心。解法一把公共依赖抽到第三个文件采取最为洁净的做法, 就是要使得公共部分仅仅去依赖那些底层模块, 因为这些底层模块的一个明显特点就是谁都不依赖它们。那个后缀是点py的文件, 没有人会在乎它究竟是谁, 这个文件只是被使用罢了。def ():42# a.pyfrom# b.pyfrom现在是a和b这两个, 它们全部都只去依赖那个东西, 所以就变成了一条只有一个方向流动的链条子, 不像是以前那样构成一个闭环的了。把挪进函数内部, 也就是延迟到调用的时候才执行。如果在实际的情况之下, 这两个模块确实是需要互相使用彼此内部的函数的, 这种情况下, 就应该选择把其中一个位置上的代码定义, 具体是从这个文件的顶部那个区域, 给移动挪到函数体的内部这个范围里面去。# a.pydef use_b():从字母b和字符x以及井号开始, 直到真正去调用函数use_b的那一刻, 才会去执行这行的代码。x()# b.pya的顶部这部分内容是完全没有问题的, 之所以说它没有问题, 是因为a这个顶层的位置已经不再存在任何的反向依赖关系了。为什么这样做能够避开之前的问题, 原因其实很简单, 因为把代码放在函数里面以后, 它就推迟到了函数实际被调用的那个瞬间才去执行, 而在那个时间节点到来时, 那两个模块早就已经完成了初始化, 变量 x 也已经实打实地存在了, 所以自然就不会再撞上那些还没做好的半成品东西, 这样做是有代价的, 每一次调用都要去查一下 sys 这个对象, 虽然系统里面有缓存机制, 所以花费的时间非常少, 但这确实是一个额外的步骤, 而且依赖关系被巧妙地藏进了函数体内部, 不像前面的那种解法那么一目了然。所以优先使用解法一, 只有在实在抽不出公共模块的时候才使用解法二, 如果两个模块几乎每一个函数都要互相调用, 那就说明它们本来就不应该被拆成两个。---十一、常见的误区有哪些, 每一个条目都必须把搞错的地方讲清楚, 并且要把造成的后果说明白。1. 有些人把测试程序里的代码直接放到模块的最上面一层去运行, 而且还不加上那种用来判断当前状态的条件语句, 这就导致只要是有人去导入这个模块或者直接运行它, 你的打印操作、联网请求、还有删除文件的行为就会全部被触发, 最终会引发那些悄无声息地改变了系统状态的副作用行为, 这种行为是最难去查找根源的, 正确的操作方法应该是不让测试代码随意执行, 而是把它们都包在一个条件判断里面, 去检查当前的程序变量值是否满足等于 _这个字符串的标准, 只有满足了才放行。2. 请给您的文件名起一些非常普遍的名称, 举例来说可以是 .py 这样的文件后缀名, 或者是 json.py 以及 utils.py , 这样做会覆盖掉标准库中的内容, 或是被将来版本中的自己所覆盖, 其导致的不良后果包括程序无法直接运行, 又或者更严重的状况是发生偶发性同名的静默行为从而给出错误的处理结果。3. 原本以为这仅仅是引入一个定义而已, 并没有任何副作用, 于是就把建立连接、读取配置的操作放在了模块的最顶层位置了, 结果导致了运行速度变慢, 甚至出现了一种情况, 就是在仅仅想要获取一个常量值的时候, 却意外地触发了整个初始化流程。4. 图省事就是利用from m * 的方法, 一次性把几十个名字搬进来, 在产生撞车的情况之后, 后面的静默就会覆盖掉先前的结果, 造成的后果是名字被覆盖了, 在进行排查的时候根本看不出是谁改的。5. 在遇到循环导入的问题时, 反复地去调换两个代码文件的上下顺序, 这实际上只是一种侥幸心理是在赌在这次执行的过程中到底是谁先被运行出来, 这种做法并没有从根本上解决代码执行的时序问题, 其导致的后果往往是代码在本机上能够正常运行, 但是一旦更换到其他的机器上或者修改了入口文件的地方, 整个程序就会崩溃, 正确的解决方法应当是把公用的部分抽取出来, 形成一个单独的公共模块。6. 许多人会有一种错误的想法, 觉得只要修改完了模块的源代码, 那么在当前正在运行中的那个进程就会自动地让这个修改产生效力, 然而事实情况是, 系统中的 sys 这个部分会把已经加载好的模块给缓存住, 仅仅是去改动文件的话, 并不会让系统自动地进行重新加载, 这就导致了一个后果, 那就是你辛辛苦苦改了好半天, 心里头可能还总以为是自己没有生效, 但实际上是因为这个进程还在死抱着它手里的那个旧版本不放, 所以在这种情况下, 你必须得重新启动一下这个进程, 或者显式地去使用某些方法来强行让它加载新的内容。十二、边界什么时候这些写法不适用这句话只对入口脚本这一点有意义: 对于那些真正被执行到的模块来说, 它们根本就不会走到那个分支代码那里去, 所以你不要指望用这个方法来实现所谓的按照不同的入口来切换行为的功能如果你确实需要对不同的入口进行区分处理, 你应该把这些不同的入口分别拆分成各自独立的脚本来进行编写。相对导入这种做法, 只有在包内部才是合法的。一个单独运行的脚本, 比如a.py里面去写from . b的话, 会直接报错显示with no known。限制条件是这样的, 相对导入这只适用于这个文件是作为包的一部分被导入进来的这种场景。针对自己的项目, 是不推荐使用命名空间包的, 也就是那些没有包含 .py 文件的包, 因为它们仅仅只适合拿来临时借用别人的目录对于自己新建的包, 应该是老老实实地放一个 .py 文件。关于sys.path, 它实际上并不是在进行依赖管理: 它的行为是将这台机器上面的某个绝对路径给写死到了代码里面, 当更换了一台机器的时候必然就会发生失败的情况。请注意在正式的工程项目开发当中请使用虚拟环境以及相关的文本文件来管理依赖, 而不要去修改系统路径。循环导入里面那个写在函数里面的解法, 它绝对不是不要钱的午餐, 这里面的前提是这两个模块相互调用次数不能太多, 如果差不多每一个函数都要互相调用的话, 那就应该把它们合并在一起, 不是一直往里面加那个函数内的代码。十三、小结可以把模块理解为以.py为后缀文件的对象, 同时, 需要把一个包含了.py文件以及.py的文件夹看作是一个包的存在, 由此可知, 文件本身就可以当作是模块来看待, 而文件夹目录则直接可以被认定为是包。共有四种写法, 分别为整体模块、具体名字、别名以及全部导入且后一种情况应当尽量少用。它们之间的主要区别在于名字的摆放位置不同, 以及是否存在名称冲突的问题。其步骤分为三步, 首先查找文件。接着执行整个文件的内容。最后把内容存入sys缓存中。第二次访问的时候, 直接调用缓存。不再重新执行文件内容。搜索路径系统里面的sys.path是有一个明确顺序的, 当前目录会被排在标准库的前面, 所以本地的同名文件就会把标准库给顶替掉, 也就是让标准库失效, 这就是为什么不能随便用那些普通常见的名字来给自己的文件命名的根本原因。如果它等于空字符串的话, 那么这样做就是为了防止在导入的时候执行了那些原本只应当在直接被运行的情况下去执行的动作。循环导入的根因是边执行边注册的时序解法一是抽公共模块优先解法二是函数内 次选。.py文件用来声明包, 还要承担包层面的初始化工作使用点和双点代表的相对导入方法, 可以让整个包进行搬移操作, 而不用修改代码。十四、作业请先将这段用来作为起点的代码执行一下, 然后基于此结果进行修改, 并且在去改动之前, 必须要想好针对每个步骤的缘由。我们使用标准库的方法来得到一个随机的密码, 这个密码包含8位的由数字和字母混合组成的内容。chars . .这句话的意思是把某个地方的八个字符拿出来, 然后连在一起变成一个字符串。print()就是举个例子, 意思是要用这个打印函数。1. 把生成密码这种逻辑弄成一个叫作 .py 的文件, 把它给封装好, 然后对外头, 提供出来一个函数, 就是 (n) , 这样的。2. 需要在模块内部添加一段用于自我测试的代码内容, 不过在处理的时候必须确保当被某种时机触发的时候这段代码不要去执行, 这个具体的功能应该要想明白究竟是利用哪一个技术手段来实现。3. 进行验证缓存的操作: 构造一个独立的代码模块, 并在这个模块的顶层位置直接添加一行用于输出的程序语句, 具体内容为打印出包含“我被加载了”这些字的字符串信息, 随后在另一个完全独立的脚本文件内部反复对这个刚创建的模块实施导入或者引用的动作, 该操作需要确切地执行两次, 在此期间仔细监视终端输出窗口中究竟显示出了多少次该提示文字, 并且结合相关的运行机理去深入分析与解释产生上述现象的根本原因。4. 关于制造循环依赖这个问题, 我们需要创建两个相互引用的代码模块。首先, 执行代码运行操作来查看原始的报错信息内容。接下来, 我们要分别使用抽取公共模块的方式以及函数内部调整的方式来对代码进行修复处理。最后, 需要对这两种不同的修改方式进行比较分析, 从而判断出哪种解决思路会更加清晰明确。思考题1. 在这样一个模块数量特别多、标识符名称也特别多的项目环境里, 使用math模块再配合从math模块中导入sqrt这一具体操作, 究竟哪一个方式会变得更加安全一些呢? 这背后的原因在于当面临大量的命名空间冲突风险时, 通过明确指定来源的方式来引入功能可以降低出错的可能性。2. 你把文件命名为 .py 后 会拿到谁如果那个文件里碰巧也定义了 会报错吗后果是什么3. 咱们不妨从“依赖关系能不能一眼看懂”这个具体的视角出发, 来好好琢磨一下, 为啥说把代码抽成公共模块这种方式, 通常要比把它留在函数内部显得更值得优先考虑一点。