抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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 或者目录
  • 总结
  1. 导入顶级模块,其名称会加入到本地名词空间,并绑定到其模块对象;
  2. 导入非顶级模块,只将其顶级模块名称加入到本地名词空间中。导入的模块必须使用完全限定名称来访问;
  3. 如果使用了asas后的名称直接绑定到导入的模块对象,并将该名称加入到本地名词空间中;
  4. 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子句后的名称;

定义一个模块

自定义模块命名规范

  1. 模块名就是文件名;
  2. 模块名必须符合标识符的要求,是非数字开头的字母,数字和下划线的组合,test-module.py 这样的文件名不能作为模块名。也不要使用中文;
  3. 不要使用系统模块名来避免冲突,除非你明确知道这个模块名的用途;
  4. 通常模块名为全小写,下划线来分割;
# 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 是脚本语言:

    • 任何脚本都可以直接执行,也可以作为模块被导入。
  • 模块加载顺序:

    1. 从标准输入(命令行方式敲代码)。
    2. 从脚本执行($ python test.py)。
    3. 交互式读取。
  • _main_ 模块:

    • 当从标准输入、脚本或交互式读取时,会将模块的 __name__ 设置为 _main_
    • 模块的顶层代码在 _main_ 作用域中执行。
  • 模块导入:

    • 如果使用 import 导入模块,其 __name__ 默认就是模块名。

if __name__ == '__main__': 用途

在 Python 中,if __name__ == '__main__': 常用于两个主要目的:

  1. 本模块的功能测试:

    • 用于执行本模块的功能测试。
    • 对于非主模块,可以在此处测试本模块内的函数和类。
  2. 避免主模块变更的副作用:

    • 通过 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 有一些区别:

  1. Directory(目录):

    • 创建普通的目录,该目录不包含 __init__.py 文件。
    • 通常用于存储普通的文件,而非作为 Python 包来使用。
  2. 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 * 导入

  1. 如果模块没有 __all__from xyz import * 只导入非下划线开头的该模块的变量。如果是包,子模块也不会导入,除非在 __all__ 中设置,或 __init__.py 中导入它们。

  2. 如果模块有 __all__from xyz import * 只导入 __all__ 列表中指定的名称,哪怕这个名称是下划线开头的,或者是子模块。

  3. 使用 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 # 查看是否插入成功

评论