python模块化
一般来说,编程语言中、库、包模块是同一概念,是代码组织方式。
Python中只有一种模块对象类型,但是为了模块化组织模块的便利,提供了”包”的概念。
模块module: 指的是python的源代码文件;
包package: 指的是模块组织在一起的和包名同名的目录及期相关文件。
导入语句
语句 | 含义 |
---|---|
import 模块1[,模块2] |
完全导入 |
import ... as ... |
模块别名 |
import
语句- 找到指定的模块,加载和初始化它,生成模块对象,找不到,抛出异常;
- 在
import
所在的作用域的局部命名空间中,增加名称和上一步创建的对象关联;
import os # 'os' os标识符,指向os模块对象
- 示例
'''我是一个模块'''
import os # 'os' os标识符,指向os模块对象
print(__name__)
print(dir()) #相当于sorted(locals().keys())
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'os']
print(*globals().items(),sep='\n')
# ('__name__', '__main__')
# ('__doc__', '我是一个模块')
# ('__package__', None)
# ('__loader__', <_frozen_importlib_external.SourceFileLoader object at 0x100b1cca0>)
# ('__spec__', None)
# ('__annotations__', {})
# ('__builtins__', <module 'builtins' (built-in)>)
# ('__file__', '/Users/rjnn/Desktop/Program/python_project/rjfiles/17-import-04.py')
# ('__cached__', None)
# ('os', <module 'os' from '/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/os.py'>)
print(locals()) # 如果locals 处于全局作用域,那么locals() 和 globals() 是一样的
import os.path # 这里指向的是os
import os.path as osp # osp 指向 ==> path 模块
import pathlib # import 必须是模块类型, .py 或者目录
- 总结
- 导入顶级模块,其名称会加入到本地名词空间,并绑定到其模块对象;
- 导入非顶级模块,只将其顶级模块名称加入到本地名词空间中。导入的模块必须使用完全限定名称来访问;
- 如果使用了
as
,as
后的名称直接绑定到导入的模块对象,并将该名称加入到本地名词空间中; import
之后只能是模块类型;
语句 | 含义 |
---|---|
from ... import ... |
部分导入 |
from ... import ... as ... |
别名 |
import os
print(os.path.exists)
print(sorted(os.path.__dict__.keys()),sep=',')
print(dir(os.path))
print(os.path.__dict__['exists']) # 模块的全局变量,都放在该模块的__dict__属性中
print(getattr(os.path,'exists')) # 通过getattr函数获取模块的全局变量
# <function exists at 0x104ecf5e0>
# ['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_get_sep', '_joinrealpath', '_varprog', '_varprogb', 'abspath', 'altsep', 'basename', 'commonpath', 'commonprefix', 'curdir', 'defpath', 'devnull', 'dirname', 'exists', 'expanduser', 'expandvars', 'extsep', 'genericpath', 'getatime', 'getctime', 'getmtime', 'getsize', 'isabs', 'isdir', 'isfile', 'islink', 'ismount', 'join', 'lexists', 'normcase', 'normpath', 'os', 'pardir', 'pathsep', 'realpath', 'relpath', 'samefile', 'sameopenfile', 'samestat', 'sep', 'split', 'splitdrive', 'splitext', 'stat', 'supports_unicode_filenames', 'sys']
# ['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_get_sep', '_joinrealpath', '_varprog', '_varprogb', 'abspath', 'altsep', 'basename', 'commonpath', 'commonprefix', 'curdir', 'defpath', 'devnull', 'dirname', 'exists', 'expanduser', 'expandvars', 'extsep', 'genericpath', 'getatime', 'getctime', 'getmtime', 'getsize', 'isabs', 'isdir', 'isfile', 'islink', 'ismount', 'join', 'lexists', 'normcase', 'normpath', 'os', 'pardir', 'pathsep', 'realpath', 'relpath', 'samefile', 'sameopenfile', 'samestat', 'sep', 'split', 'splitdrive', 'splitext', 'stat', 'supports_unicode_filenames', 'sys']
# <function exists at 0x104ecf5e0>
# <function exists at 0x104ecf5e0>
- 总结:
- 找到
from
子句中指的模块、加载并初始化它(注意不是导入) - 对于
import
子句后的名称- 先查
from
子句导入的模块是否具有该名称的属性; - 如果不是,则尝试导入名称的子模块;
- 还没有找到,则抛出
ImportError
异常; - 这个名称保存到本地名词空间中,如果有
as
子句,则使用as
子句后的名称;
- 先查
- 找到
定义一个模块
自定义模块命名规范
- 模块名就是文件名;
- 模块名必须符合标识符的要求,是非数字开头的字母,数字和下划线的组合,
test-module.py
这样的文件名不能作为模块名。也不要使用中文; - 不要使用系统模块名来避免冲突,除非你明确知道这个模块名的用途;
- 通常模块名为全小写,下划线来分割;
# cat t1.py
print('=' * 30 )
class Point:
def __init__(self,x,y):
self.x = x
self.y = y
t = Point(1,2)
print('~' * 30 )
# cat 17-import-04.py
from t1 import t
print(t.x)
import t1
print('-' * 30)
import sys
print(*sys.modules.items(),sep='\n') # 其实在加载t1模块之前,sys.modules中已经有了很多模块,比如os、sys、builtins等等
模块搜索顺序
- 使用
sys.path
查看搜索顺序
import sys
print(*sys.path, sep='\n')
显示结果为,python模块的路径搜索顺序;
当加载一个模块的时候,需要从这些搜索路径中前到后依次查找,并不搜索这些目录的子目录;
搜索到模块就加载,搜索不到就抛异常;
模块的重复导入
从执行结果来看,不会产生重复导入的现象;
所有加载的模块都会记录在sys.modules
中,sys.modules
是存储已经加载的过的所有模块的字典;
打钱sys.modules
可以看到builtins
os
os.path
sys
等模块已经加载了。
模块的运行
Python 模块和解释器初始化
__name__
变量:- 每个模块都会定义一个
__name__
特殊变量来存储当前模块的名称。 - 如果不指定,默认为源代码文件名,如果是包则有限定名。
- 每个模块都会定义一个
解释器初始化:
- 初始化时,会初始化
sys.modules
字典,用于保存已加载的模块。 - 加载
builtins
模块(全局函数、常量)、__main__
模块、sys
模块。 - 初始化模块搜索路径
sys.path
。
- 初始化时,会初始化
Python 是脚本语言:
- 任何脚本都可以直接执行,也可以作为模块被导入。
模块加载顺序:
- 从标准输入(命令行方式敲代码)。
- 从脚本执行(
$ python test.py
)。 - 交互式读取。
_main_
模块:- 当从标准输入、脚本或交互式读取时,会将模块的
__name__
设置为_main_
。 - 模块的顶层代码在
_main_
作用域中执行。
- 当从标准输入、脚本或交互式读取时,会将模块的
模块导入:
- 如果使用
import
导入模块,其__name__
默认就是模块名。
- 如果使用
if __name__ == '__main__':
用途
在 Python 中,if __name__ == '__main__':
常用于两个主要目的:
本模块的功能测试:
- 用于执行本模块的功能测试。
- 对于非主模块,可以在此处测试本模块内的函数和类。
避免主模块变更的副作用:
- 通过
if __name__ == '__main__':
,确保顶层代码只在当前模块作为主模块执行时执行。 - 在顶层代码中的不受封装的逻辑,如果模块被导入时也执行,可能会导致不必要的副作用。
- 当有新的主模块时,老的主模块成为被导入模块,由于原来代码没有封装,一并执行了。
- 通过
通过这种方式,可以使模块的顶层代码在作为主模块执行时有效,而在被导入时不执行,从而避免潜在的问题和副作用。
hash
方法 | 意义 |
---|---|
__hash__ |
内建函数hash() 调用的返回值,返回一个整数。如果定义这个方法该类的实例就可hash |
class Person:
def __init__(self,name):
self.name = name
def __hash__(self):
return 1
def __repr__(self):
return self.name
def __eq__(self,other):
return self.name == other.name
t1 = Person('Tom')
t2 = Person('Tom')
print(hash(t1), hash(t2))
print('-' * 30 )
print({t1, t2})
print({t1, t1})
# 1 1
# ------------------------------
# {Tom}
# {Tom}
__hash__
和 __eq__
方法在 Python 中的作用
在 Python 中,__hash__
方法用于返回一个对象的哈希值,通常用作集合(例如 set)的键。然而,仅仅实现 __hash__
方法是不够的,因为相同的哈希值可能对应不同的对象,这就是所谓的哈希冲突。
为了在集合中去重,还需要实现 __eq__
方法,该方法用于判断两个对象是否相等。__eq__
方法的实现对去重的有效性起着关键作用。
总结如下:
__hash__
方法:- 返回对象的哈希值,用作集合键。
- 哈希值相等的两个对象不一定相等,可能存在哈希冲突。
__eq__
方法:- 判断两个对象是否相等。
- 去重依赖于
__eq__
方法,因为相等的对象应该在集合中只出现一次。
不可哈希对象:
- 使用
isinstance(obj, collections.Hashable)
来检查对象是否可哈希。 - 如果对象不可哈希,那么
__hash__
方法应该未被实现,而__eq__
方法可以被实现来进行对象比较。
- 使用
通过同时实现 __hash__
和 __eq__
方法,可以确保在集合中正确地进行去重操作,避免哈希冲突带来的问题。
class Point:
def __init__(self, x , y):
self.x = x
self.y = y
def __eq__(self,other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x,self.y))
def __repr__(self):
return "Point({}, {})".format(self.x,self.y)
__str__ = __repr__
p1 = Point(1,2)
p2 = Point(3,4)
print(p1 == p2)
print(p1 is p2)
print({ p1, p2 })
包
import m
print(m)
print(dir(m))
print(m.__dict__)
print(type(m))
# 可以看到以下输出:
# <module 'm' (namespace)>
# ['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']
# {'__name__': 'm', '__doc__': None, '__package__': 'm', '__loader__': <_frozen_importlib_external._NamespaceLoader object at 0x10483b970>, '__spec__': ModuleSpec(name='m', loader=<_frozen_importlib_external._NamespaceLoader object at 0x10483b970>, submodule_search_locations=_NamespacePath(['/Users/rjnn/Desktop/Program/python_project/rjfiles/m'])), '__file__': None, '__path__': _NamespacePath(['/Users/rjnn/Desktop/Program/python_project/rjfiles/m'])}
# <class 'module'>
如何让目录具备有写代码的能力
可以,对照以上m目录下创建一个文件__init__.py
,可以理解成初始化这个模块的文件;
因此我们创建包的时候、一定要创建一个__init__.py
子模块
- 包目录下的py文件、子目录都是其子模块,可以通过import导入。
- 模块要用必须加载,应该在
sys.modules
中有记录。 - 模块的加载是最小加载原则,即只加载需要的模块。
m
|-- __init__.py
|-- m1.py
|-- m2
| -- __init__.py
| -- m21
| -- __init__.py
| -- m22.py
- 如上建立子模块目录和文件,所有的py文件中就写一句话
print(__name__)
。
# 注意观察已经加载的模块、当前名词空间的名词
# 可以理解到import 采用最小加载原则
import m
import m.m1
import m.m2.m21,m.m2.m22
print('~' * 30)
print(*filter(lambda x:x.startswith('m'), dir())) # 这条语句主要是查看当前名词空间的名词
import sys
print(*filter(lambda x:x.startswith('m'), sys.modules.keys())) # 这条语句主要是查看已经加载的模块
print('=' * 30)
print(m.m1.Y) # 说明:当m.m1 没有导入的时候 m 是最小加载原则,所以获取不到Y ,当导入m.m1的时候,m.m1.Y = 1,最小加载,即不相干的模块不会被加载
# m
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# m
# marshal mpl_toolkits m m.m1 m.m2 m.m2.m22
# ==============================
# 100
- 在 PyCharm 中创建 Directory 和 Python Package 的区别
在 PyCharm 中,创建 Directory
和创建 Python Package
有一些区别:
Directory(目录):
- 创建普通的目录,该目录不包含
__init__.py
文件。 - 通常用于存储普通的文件,而非作为 Python 包来使用。
- 创建普通的目录,该目录不包含
Python Package(Python 包):
- 创建一个带有
__init__.py
文件的目录,即包。 - Python 包是一种组织和模块化 Python 代码的方式,其中包含了
__init__.py
文件,该文件可以为空或包含 Python 代码。 - 该目录下的 Python 文件可以作为包的模块使用。
- 创建一个带有
在 Python 中,目录可以作为模块,这就是包。但是,为了被 Python 解释器认识为一个包,需要在该目录下包含一个 __init__.py
文件。这个文件可以为空,也可以包含初始化包的代码。
关于包的 __file__
属性:
- 当 Python 解释器加载一个包时,
__file__
属性指向该包的__init__.py
文件。 - 这个属性可以用来获取包所在的路径。
总结:
- 使用
Directory
创建普通目录,用于存储非 Python 包的文件。 - 使用
Python Package
创建带有__init__.py
文件的目录,用于组织和模块化 Python 代码,形成一个包。 - 包的
__file__
属性指向该包的__init__.py
文件。
模块和包总结
包能更好地组织模块,尤其是当模块代码较多时,可以将其拆分为多个子模块,以便根据需要加载特定功能的子模块。
在包的目录中,
__init__.py
文件在包第一次导入时执行。该文件的内容可以为空,也可以包含用于初始化包的代码。建议不删除该文件,尤其是在低版本的 Python 中,删除_init_.py
文件可能导致问题。导入子模块时,一定会加载父模块,但导入父模块不会导入子模块。包目录之间只能使用
.
作为间隔符,表示模块及其子模块的层级关系。模块是封装的基本单元,类似于类和函数。不过,与类和函数不同的是,模块能够封装变量、类、函数等。
模块是命名空间,其内部的顶层标识符都是模块的属性。可以通过
__dict__
或dir(module)
查看模块的属性。包也是模块的一种,但不是所有模块都是包。包是一种特殊的模块,它包含
__path__
属性。
总的来说,模块和包是 Python 中组织和封装代码的关键概念,它们提供了良好的代码结构和可维护性。通过适当的组织代码,可以更好地利用 Python 的模块化特性。
绝对导入、相对导入
绝对导入
- 在
import
语句或from
语句中导入模块时,模块名称最前面不以.
点开头。 - 绝对导入总是去模块搜索路径中查找,同时会检查该模块是否已经加载。
相对导入
- 只能用在
from
语句中。 - 使用
.
点号表示当前目录内。 - 使用
:
两点表示上一级目录。 - 使用
...
三点表示上上一级目录。 - 一般只在包内使用,不建议在顶层模块中使用相对导入。
- 一旦在一个模块中使用相对导入,该模块就不可以作为主模块运行。
相对导入的主要用途是在包内进行模块间的相对引用,便于组织和管理包内的模块结构。
# 举例a.b.c模块,a、b是目录,c是py文件
# c的代码如下
from . import d # imports a.b.d
from .. import e # imports a.e
from .d import x # imports a.b.d.x
from ..e import x # imports a.e.x
__init__.py
使用相对的点号,按目录层次来;
访问控制
from t1 import * # 使用*,默认是下划线开头不能导入;如果t1有__all__,则它说了算
import t1
from t1 import Point, _b as b 不受__all__的影响
# t1.py
print('=' * 30)
__all__ = ["Point","_B"] # 它说能导出哪个变量,哪个变量就能被导出
class Point:
def __init__(self,x,y):
self.x = x
self.y = y
def __repr__(self):
return 'Point({},{})'.format(self.x,self.y)
_B = 2
_C = 3
__my__ = 4
if __name__ == '__main__':
t = Point(100,200)
print(t)
print('~' * 30)
# t3.py
from t1 import *
print(*filter(None,dir())) # 当前全局变量都有谁
import sys
print(*filter(lambda x:x.startswith('_'), reversed(sys.modules.keys()))) # 当前加载的模块都有谁
# ==============================
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Point _B __annotations__ __builtins__ __cached__ __doc__ __file__ __loader__ __name__ __package__ __spec__
# _sre _functools _collections _operator _heapq _distutils_hack _virtualenv _bootlocale _locale _sitebuiltins _collections_abc _stat __main__ _abc _signal _codecs _frozen_importlib_external _io _weakref _warnings _thread _imp _frozen_importlib
- 总结
使用 from xyz import *
导入
如果模块没有
__all__
,from xyz import *
只导入非下划线开头的该模块的变量。如果是包,子模块也不会导入,除非在__all__
中设置,或__init__.py
中导入它们。如果模块有
__all__
,from xyz import *
只导入__all__
列表中指定的名称,哪怕这个名称是下划线开头的,或者是子模块。使用
from xyz import *
方式导入,简单但有副作用,可能导入大量不需要的变量,甚至可能造成名称冲突。而__all__
可以控制被导入模块在这种导入方式下提供的变量名称,以阻止导入过多模块变量,避免冲突。
- 明确导入
- 使用
from module import name1, name2, ...
这种方式导入是明确的。 - 即使导入子模块或下划线开头的名称,程序员有控制权,可以选择导入需要的名称及其对应的对象。
包管理
为什么使用包管理
在Python中,模块或源文件可以直接复制到目标项目目录中,然后就可以导入使用。然而,为了更好地调用和共享项目,需要进行包管理,以便打包或发布到网络供他人使用。主要目的是为了代码的复用和更好的组织。
Pypi(Python Package Index)
Pypi 是 Python 的公共模块存储中心。它是一个集中存储和管理Python软件包的地方,使得开发者可以方便地分享、查找和安装Python软件包。
主要工具
distutils:
- 是官方库,使用安装脚本
setup.py
来构建和安装包。 - 从1998年开始是Python标准库的一部分,直到2000年停止开发。
setuptools
- 是一个用于构建、分发和安装Python软件包的增强工具集。它是distutils的替代版本,提供了更多的功能和灵活性,同时支持egg格式的构建和安装。以下是一些关键特点:
- 提供了
easy_install
工具,使用ez_setup.py
文件进行安装。 - 支持查询、下载、安装、构建、发布、管理等多种包管理功能。
- 是包管理的核心模块。
在发展历程中,setuptools 曾经开发较慢,导致出现了基于 setuptools 的 distribute
工具。然而,后来在2013年,这两个项目重新合并,distribute
被废弃,而 setuptools
依然是Python安装打包的标准方式。
通过 setuptools,开发者可以更方便地构建和分发 Python 软件包,提高代码的可维护性和可重用性。
通过使用这些工具,可以更轻松地管理Python项目、打包和分享代码,提高代码的可维护性和可重用性。
pip
目前是事实上的 Python 包管理标准,构建在 setuptools 之上,替代了 easy_install
,并提供了丰富的包管理功能。以下是一些关键特点:
- 提供了更现代、灵活、易用的包管理工具。
- 支持安装、卸载、升级、查看、搜索等多种操作。
- 支持从 PyPI(Python Package Index)等源安装软件包。
在 Python 3.4 之前,需要单独安装 pip,但从 Python 3.4 开始,pip 直接包含在安装文件中。它为 Python 开发者提供了更便捷的方式来管理和使用第三方包,促进了 Python 生态系统的发展和共享。
使用 pip,开发者可以轻松地安装和管理 Python 软件包,使得项目的依赖管理更加方便和高效。
Wheel
是一种包的打包格式,其定义在 PEP427 中。Wheel 文件以 .whl
为扩展名,采用 zip 格式打包,不包含 .pyc
文件。
以下是关于 Wheel 的一些重要信息:
- 提供了
bdist_wheel
setuptools 的扩展命令,用于生成新的打包格式 wheel。 - pip 从版本1.4开始提供了一个 wheel 子命令,用于安装 wheel 包。需要先安装 wheel 模块。
- Wheel 可以让 Python 库以二进制形式安装,避免了在本地编译的过程。
- Wheel 格式逐渐取代了 egg 包的使用。越来越多的安装包新版本采用了 Wheel 包,使得用户可以直接找到适用于其操作系统平台和 Python 版本的包进行下载和安装,而无需本地编译。
使用 Wheel 包能够提高包的安装效率,使得项目的依赖管理更加灵活和便捷。
# 在项目根目录下创建setup.py文件
#!/usr/bin/env python
from distutils.core import setup
setup(name='dataview',
version='1.0',
description='Python dataview',
author='test',
author_email='test@xxx.com',
url='http://test.xxx.com',
#packages=['dataview'] # 这里要指定打包的包(目录),包自身和非子包,必须是包或者py文件
packages=['dataview','dataview.m2','dataview.m2.m21'] # 这样包括了子包
py_modules=['t3'] # 这里指要打包的单个py文件
data_files = [('install', ['requirements',''],('htmls',['index.html','about.html']))] # 这里指定要打包的文件
python_requires='>=3.6' # 这里指定python版本
)
)
# build 生成一个build目录,编译好的文件和目录
# sdist 先build,然后source Destribution打包,tar.gz
# 打包命令: python setup.py sdist
# 打包成xz格式: python setup.py sdist --formats=xztar
# 打包成zip格式: python setup.py sdist --formats=zip
# 打包成二进制格式: python setup.py bdist
# 打包成rpm格式: python setup.py bdist_rpm , python setup.py bdist --formats=rpm
# pip install wheel , # 打包成wheel格式: python setup.py bdist_wheel ,打包不成功时, 注意 from setuptools import setup
# pip install whell , # 打包在egg格式: python setup.py bdist_egg
# 在目标环境安装打包好的whl文件
# pip install /tmp/dataview-1.0-py3-none-any.whl
# 在目标环境卸载打包好的源码包
# tar -xvf /tmp/dataview-1.0.tar.gz
# cd dataview-1.0
# python setup.py install
# 或者
# ipython
import sys.path
sys.path.insert(0,'/tmp/dataview-1.0')
sys.path # 查看是否插入成功