【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

跟老齐学 Python 之编写模块

  • 2016-04-17
  • 本文字数:5730 字

    阅读完需:约 19 分钟

编者按:InfoQ 开设栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自齐伟著《跟老齐学Python 从入门到精通》中的章节“模块之编写模块”,介绍Python 如何编写自己的模块。

随着对Python 学习的深入,其优点日渐突出,让读者也感觉到Python 的强大了,强大感觉之一就是“模块自信”,因为Python 不仅有自带的模块(称之为标准库),还有海量的第三方模块,并且很多开发者还在不断贡献自己开发的新模块,正是有了这么强大的“模块自信”,Python 才被很多人钟爱。并且这种方式也正在不断被其他更多语言所借鉴,几乎成为普世行为了(不知道Python 是不是首倡者)。

“模块自信”的本质是:开放。

Python 不是一个封闭的体系,而是一个开放系统。开放系统的最大好处就是避免了“熵增”。

熵的概念是由德国物理学家克劳修斯于 1865 年所提出,是一种测量在动力学方面不能做功的能量总数,也就是当总体的熵增加,其做功能力也下降,熵的量度正是能量退化的指标。

熵亦被用于计算一个系统中的失序现象,也就是计算该系统混乱的程度。

根据熵的统计学定义,热力学第二定律说明一个孤立系统倾向于增加混乱程度。换句话说就是对于封闭系统而言,会越来越趋向于无序化。反过来,开放系统则能避免无序化。

编写模块

想必读者已经熟悉了 import 语句,曾经有这样一个例子:

复制代码
>>> import math
>>> math.pow(3,2)
9.0

这里的 math(是 Python 标准库之一,在本章,我们要逐渐理解模块、库之类的术语。)就是一个模块,用 import 引入这个模块,然后可以使用模块里面的函数,比如 pow() 函数。显然,这里是不需要自己动手写具体函数的,我们的任务就是拿过来使用。这就是模块的好处:拿过来就用,不用自己重写。

1. 模块是程序

“模块是程序”一语道破了模块的本质,它就是一个扩展名为.py 的 Python 程序。

我们能够在应该使用它的时候将它引用过来,节省精力,不需要重写雷同的代码。

但是,如果我自己写一个.py 文件,是不是就能作为模块 import 过来呢?还不那么简单。必须得让 Python 解释器能够找到你写的模块。比如,在某个目录中,我写了这样一个文件:

复制代码
#!/usr/bin/env python
# coding=utf-8
lang = "python"

并把它命名为 pm.py,那么这个文件就可以作为一个模块被引入。不过由于这个模块是我自己写的,Python 解释器并不知道,得先告诉它我写了这样一个文件。

复制代码
>>> import sys
>>> sys.path.append("~/Documents/VBS/StartLearningPython/2code/pm.py")

用这种方式告诉 Python 解释器,我写的那个文件在哪里。在这个方法中,也用了模块 import sys,不过由于 sys 是 Python 标准库之一,所以不用特别告诉 Python 解释器其位置。

上面那个一长串的地址是 Ubuntu 系统的地址格式,如果读者使用的是 Windows 系统,请写你所保存的文件路径。

复制代码
>>> import pm
>>> pm.lang
'python'

在 pm.py 文件中有一个赋值语句,即 lang = “python”,现在将 pm.py 作为模块引入(注意作为模块引入的时候不带扩展名),就可以通过“模块名字”+“.”+“属性或方法名称”来访问 pm.py 中的东西。当然,如果要访问不存在的属性,肯定是要报错的。

复制代码
>>> pm.xx
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'xx'

请读者回到 pm.py 文件的存储目录,查看一下是不是多了一个扩展名是.pyc 的文件?

解释器,英文是:interpreter,在 Python 中,它的作用就是将.py 的文件转化为.pyc 文件,而.pyc 文件是由字节码(bytecode)构成的,然后计算机执行.pyc 文件。

很多人喜欢将这个世界简化再简化,比如编程语言就分为解释型和编译型,不但如此,还将两种类型的语言分别贴上运行效率高低的标签,解释型的运行速度就慢,编译型的运行速度就快。一般人都把 Python 看成是解释型的,于是就得出它运行速度慢的结论。不少人都因此上当受骗了,认为 Python 不值得学,或者做不了什么“大事”。这就是将本来复杂的、多样化的世界非得划分为“黑白”的结果,喜欢用“非此即彼”的思维方式考虑问题。

世界是复杂的,“敌人的敌人就是朋友”是幼稚的,“一分为二”是机械的。

如同刚才看到的那个.pyc 文件一样,当 Python 解释器读取了.py 文件,先将它变成由字节码组成的.pyc 文件,然后这个.pyc 文件交给一个叫作 Python 虚拟机的东西去运行(那些号称编译型的语言也是这个流程,不同的是它们先有一个明显的编译过程,编译好了之后再运行)。如果.py 文件修改了,Python 解释器会重新编译,只是这个编译过程不全显示给你看。

有了.pyc 文件后,每次运行就不需要重新让解释器来编译.py 文件了,除非.py 文件修改了。这样,Python 运行的就是那个编译好了的.pyc 文件。

是否还记得前面写有关程序然后执行时常常要用到 if __name__ == “__main__”,那时我们直接用“python filename.py”的格式来运行该文件,此时我们也同样有了.py 文件,不过是作为模块引入的。这就得深入探究一下,同样是.py 文件,它怎么知道是被当作程序执行还是被当作模块引入?

为了便于比较,将 pm.py 文件进行改造。

复制代码
#!/usr/bin/env python
# coding=utf-8
def lang():
return "python"
if __name__ == "__main__":
print lang()

沿用先前的做法:

复制代码
$ python pm.py
python

如果将这个程序作为模块,导入,会是这样的:

复制代码
>>> import sys
>>> sys.path.append("~/Documents/VBS/StarterLearningPython/2code/pm.py")
>>> import pm
>>> pm.lang()
'python'

查看模块属性和方法,可以使用 dir()。

复制代码
>>> dir(pm)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'lang']

同样一个.py 文件,可以把它当作程序来执行,也可以将它作为模块引入。

复制代码
>>> __name__
'__main__'
>>> pm.__name__
'pm'

如果要作为程序执行,则 __name__ == “__main__”;如果作为模块引入,则 pm.__name__ == “pm”,即变量 __name__ 的值是模块名称。

用这种方式就可以区分是执行程序还是作为模块引入了。

在一般情况下,如果仅仅是用作模块引入,不必写 if __name__ == “__main__”。

2. 模块的位置

为了让我们自己写的模块能够被 Python 解释器知道,需要用 sys.path.append("~/Documents/ VBS/StarterLearningPython/2code/pm.py")。其实,在 Python 中,所有模块都被加入到了 sys.path 里面。用下面的方法可以看到模块所在位置:

复制代码
>>> import sys
>>> import pprint
>>> pprint.pprint(sys.path)
['',
'/usr/local/lib/python2.7/dist-packages/autopep8-1.1-py2.7.egg',
'/usr/local/lib/python2.7/dist-packages/pep8-1.5.7-py2.7.egg',
'/usr/lib/python2.7',
'/usr/lib/python2.7/plat-i386-linux-gnu',
'/usr/lib/python2.7/lib-tk',
'/usr/lib/python2.7/lib-old',
'/usr/lib/python2.7/lib-dynload',
'/usr/local/lib/python2.7/dist-packages',
'/usr/lib/python2.7/dist-packages',
'/usr/lib/python2.7/dist-packages/PILcompat',
'/usr/lib/python2.7/dist-packages/gtk-2.0',
'/usr/lib/python2.7/dist-packages/ubuntu-sso-client',
'~/Documents/VBS/StarterLearningPython/2code/pm.py']

从中也发现了我自己写的那个文件。

凡在上面列表所包括位置内的.py 文件都可以作为模块引入。不妨举个例子,把前面自己编写的 pm.py 文件修改为 pmlib.py,然后复制到’/usr/lib/python2.7/dist-packages 中。(这是以 Ubuntu 为例说明,如果是其他操作系统,读者用类似方法也能找到。)

复制代码
$ sudo cp pm.py /usr/lib/python2.7/dist-packages/pmlib.py
[sudo] password for qw:
$ ls /usr/lib/python2.7/dist-packages/pm*
/usr/lib/python2.7/dist-packages/pmlib.py

文件放到了指定位置。看下面的:

复制代码
>>> import pmlib
>>> pmlib.lang
<function lang at 0xb744372c>
>>> pmlib.lang()
'python'

将模块文件放到指定位置是一种不错的方法,但感觉此法受到了拘束,程序员都喜欢自由,能不能放到别处呢?

当然能,用 sys.path.append() 就是不管把文件放在哪里,都可以把其位置告诉 Python 解释器。虽然这种方法在前面用了,但其实是很不常用的,因为它也有麻烦的地方,比如在交互模式下,如果关闭了,再开启,还得重新告知。

比较常用的方法是设置 PYTHONPATH 环境变量。

环境变量,不同的操作系统设置方法略有差异。读者可以根据自己的操作系统,到网上搜索设置方法。

以 Ubuntu 为例,建立一个 Python 的目录,然后将我自己写的.py 文件放到这里,并设置环境变量。

复制代码
:~$ mkdir python
:~$ cd python
:~/python$ cp ~/Documents/VBS/StarterLearningPython/2code/pm.py mypm.py
:~/python$ ls
mypm.py

然后将这个目录~/python,即 /home/qw/python 设置环境变量。

vim /etc/profile要用 root 权限,在打开的文件最后增加 export PATH = /home/qw/python:$PATH,然后保存退出即可。

注意,我是在~/python 目录下输入 Python,然后进入到交互模式:

复制代码
:~$ cd python
:~/python$ python
>>> import mypm
>>> mypm.lang()
'python'

如此,就完成了告知过程。

3. __all__ 在模块中的作用

上面的模块虽然比较简单,但是已经显示了编写模块,以及在程序中导入模块的基本方式。在实践中,所编写的模块也许更复杂一点,比如,有这么一个模块,其文件命名为 pp.py

复制代码
# /usr/bin/env python
# coding:utf-8
public_variable = "Hello, I am a public variable."
_private_variable = "Hi, I am a private variable."
def public_teacher():
print "I am a public teacher, I am from JP."
def _private_teacher():
print "I am a private teacher, I am from CN."

接下来就是熟悉的操作了,进入到交互模式中。pp.py 这个文件就是一个模块,该模块中包含了变量和函数。

复制代码
>>> import sys
>>> sys.path.append("~/Documents/StarterLearningPython/2code/pp.py")
>>> import pp
>>> from pp import *
>>> public_variable
'Hello, I am a public variable.'
>>> _private_variable
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name '_private_variable' is not defined

变量 public_variable 能够被使用,但是另外一个变量 _private_variable 不能被调用,先观察一下两者的区别,后者是以单下画线开头的,这样的是私有变量。而 from pp import * 的含义是“希望能访问模块(pp)中有权限访问的全部名称”,那些被视为私有的变量或者函数或者类,当然就没有权限被访问了。

再如:

复制代码
>>> public_teacher()
I am a public teacher, I am from JP.
>>> _private_teacher()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name '_private_teacher' is not defined

这不是绝对的,但如果要访问具有私有性质的东西,可以这样做。

复制代码
>>> import pp
>>> pp._private_teacher()
I am a private teacher, I am from CN.
>>> pp._private_variable
'Hi, I am a private variable.'

下面再对 pp.py 文件进行改写,增加一些东西。

复制代码
# /usr/bin/env python
# coding:utf-8
__all__ = ['_private_variable', 'public_teacher']
public_variable = "Hello, I am a public variable."
_private_variable = "Hi, I am a private variable."
def public_teacher():
print "I am a public teacher, I am from JP."
def _private_teacher():
print "I am a private teacher, I am from CN."

在修改之后的 pp.py 中,增加了 __all__ 变量以及相应的值,在列表中包含了一个私有变量的名字和一个函数的名字。这是在告诉引用本模块的解释器,这两个东西是有权限被访问的,而且只有这两个东西。

复制代码
>>> import sys
>>> sys.path.append("~/Documents/StarterLearningPython/2code/pp.py")
>>> from pp import *
>>> _private_variable
'Hi, I am a private variable.'

果然,曾经不能被访问的私有变量,现在能够访问了。

复制代码
>>> public_variable
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'public_variable' is not defined

因为这个变量没有在 __all__ 的值中,虽然以前曾经被访问到过,但是现在就不行了。

复制代码
>>> public_teacher()
I am a public teacher, I am from JP.
>>> _private_teacher()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name '_private_teacher' is not defined

这只不过是再次说明前面的结论罢了。当然,如果以 import pp 引入模块,再用 pp._private_teacher 的方式是一样有效的。

4. 包和库

顾名思义,包和库都是比“模块”大的。一般来讲,一个“包”里面会有多个模块,当然,“库”是一个更大的概念了,比如 Python 标准库中的每个库都有好多个包,每个包都有若干个模块。

一个包由多个模块组成,即有多个.py 的文件,那么这个所谓的“包”就是我们熟悉的一个目录罢了。现在需要解决如何引用某个目录中的模块问题。解决方法就是在该目录中放一个 __init__.py 文件。__init__.py 是一个空文件,将它放在某个目录中,就可以将该目录中的其他.py 文件作为模块被引用。

例如,建立一个目录,名曰:package_qi,里面依次放了 pm.py 和 pp.py 两个文件,然后建立一个空文件 __init__.py

接下来,需要导入这个包(package_qi)中的模块。

下面这种方法很清晰明了。

复制代码
>>> import package_qi.pm
>>> package_qi.pm.lang()
'python'

下面这种方法,貌似简短,但如果多了,恐怕难以分辨。

复制代码
>>> from package_qi import pm
>>> pm.lang()
'python'

在后续制作网站的实战中,还会经常用到这种方式,届时会了解更多。请保持兴趣继续阅读,不要半途而废,不然疑惑得不到解决,好东西就看不到了。

书籍介绍:

本书是面向编程零基础读者的 Python 入门教程,内容涵盖了 Python 的基础知识和初步应用。以比较轻快的风格,向零基础的学习者介绍一门时下比较流行、并且用途比较广泛的编程语言,所以,本书读起来不晦涩,并且在其中穿插了很多貌似与 Python 编程无关,但与学习者未来程序员职业生涯有关的内容。 本书特别强调了学习和使用 Python 的基本方法,学习一种高级语言,掌握其各种规则是必要的,但学会“自省”方法更重要,这也是本书所试图达到的“授人以鱼不如授人以渔”的目的。

2016-04-17 21:0518095

评论

发布
暂无评论
发现更多内容

手把手教你写!2021年Android工作或更难找,最全的BAT大厂面试题整理

欢喜学安卓

android 程序员 面试 移动开发

支持 gRPC 长链接,深度解读 Nacos 2.0 架构设计及新模型

阿里巴巴云原生

云计算 阿里云 开源 微服务 云原生

Spring Cloud 2020.0.0 正式发布,对开发者来说意味着什么?

阿里巴巴云原生

阿里云 容器 开发者 云原生 架构师

7. JDK拍了拍你:字符串拼接一定记得用MessageFormat#format

YourBatman

Spring Framework 类型转换 MessageFormat DateFormat

为移动应用产业开辟出海新航路,华为应用市场是如何“破冰”的?

脑极体

专家:区块链底层技术创新是关键

CECBC

区块链

重磅盘点!2020年区块链行业十件大事

CECBC

区块链

工具词典:Inner Peace

lidaobing

随机漫步的傻瓜 28天写作

CAP 原理 <笔记>

raox

极客大学架构师训练营

JAVA并发编程原理与实战

Geek_53983e

原理 java 并发 实战

冰河又一MySQL力作出版(文末送书)!!

冰河

MySQL 高可用 高并发 高性能 MySQL架构

自研ARM芯片,亲手拆掉Wintel联盟,微软这次是认真的吗?

脑极体

重学JS | 数组去重的7种算法

梁龙先森

大前端 编程语言

在wildfly 21中搭建cluster集群

程序那些事

程序那些事 wildfly wildfly21 集群部署 集群架构

面试官:Android事件分发机制及设计思路,跳槽薪资翻倍

欢喜学安卓

android 程序员 面试 移动开发

甲方日常 76

句子

工作 随笔杂谈 日常

如何给团队制定合理的季度绩效?

Alan

团队管理 绩效 七日更 28天写作

LeetCode题解:剑指 Offer 40. 最小的k个数,快速排序,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

扫地阿姨看完都学会了!万字长文总结Android多进程,满满干货指导

欢喜学安卓

android 程序员 面试 移动开发

架构大作业二

Geek_michael

极客大学架构师训练营

SpringBoot,来实现MySQL读写分离技术

Java架构师迁哥

架构师训练营 - 大作业1

阿甘

重学JS | 找出数组中出现次数最多元素的4种算法

梁龙先森

大前端 编程语言

架构大作业一

Geek_michael

极客大学架构师训练营

与前端训练营的日子 --Week09

SamGo

学习

架构师训练营第五周”技术选型一“作业

随秋

极客大学架构师训练营

K8S 资源可视化利器:Kubectl-Graph

郭旭东

Kubernetes Kubernetes Plugin

测开之函数进阶· 第4篇《匿名函数》

清菡软件测试

测试开发

架构师训练营 - 大作业 2

阿甘

突破2.8万美元关口,比特币为何“疯涨”? ​

CECBC

比特币 比特币数字货币

面试官:我问的是Java内存模型,你回答堆栈方法区干嘛?

Java鱼仔

Java 程序员 JMM 多线程 并发

跟老齐学Python之编写模块_Python_齐伟_InfoQ精选文章