20、Python3 异常vs模块vs分发

作者: Brinnatt 分类: python 术 发布时间: 2023-03-30 10:31

20.1、异常处理

错误 Error:

  • 逻辑错误(算法写错了,加法写成了减法;函数或类使用错误);
  • 笔误(变量名写错了,语法错误);
  • 总之,错误是可以避免的。

异常 Exception,本意就是意外情况:

  • 这有个前提,没有出现上面说的错误,也就是说程序写的没有问题,但是在某些情况下,会出现一些意外,导致程序无法正常的执行下去。

  • 例如 open 函数操作一个文件,文件不存在,或者创建一个文件时已经存在了,或者访问一个网络文件,突然断网了,这就是异常,是个意外的情况。

  • 异常不可能避免。

错误和异常:

  • 在高级编程语言中,一般都有错误和异常的概念,异常是可以捕获,并被处理的,但是错误是不能被捕获的。

举例:对比异常和错误。

# 异常
with open('abc') as f:
    pass

Traceback (most recent call last):
  File "D:\JetBrains\pythonProject\main.py", line 1, in <module>
    with open('abc') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'abc'

# 错误        
def 0A():
    pass

  File "D:\JetBrains\pythonProject\main.py", line 1
    def 0A():
        ^
SyntaxError: invalid decimal literal

一个健壮的程序尽可能的避免错误,尽可能的捕获、处理各种异常。

20.1.1、产生异常

产生:

  • raise 语句显式的抛出异常。

  • Python 解释器自己检测到异常并引发它。

def foo():
    print('before')

    def bar():
        print(1 / 0)  # 除零异常

    bar()
    print('after')

foo()

def bar():
    print('before')
    raise Exception('my exception')
    print('after')

bar()

程序会在异常抛出的地方中断执行,如果不捕获,就会提前结束程序。

raise 语句:

raise 后什么都没有,表示抛出最近一个被激活的异常,如果没有被激活的异常,则抛类型异常。这种方式很少用。

raise 后要求应该是 BaseException 类的子类或实例,如果是类,将被无参实例化。

20.1.2、异常的捕获

try:
    待捕获异常的代码块
except [异常类型]:
    异常的处理代码块
try:
    print('begin')
    c = 1 / 0
    print('end')
except:
    print('catch the exception')

print('outer')

上例执行到 c=1/0 时产生异常并抛出,由于使用了 try...except 语句块则捕捉到了这个异常,异常产生的位置之后的语句将不再执行,转而执行对应的 except 部分的语句,最后执行 try...except 语句块之外的语句。

捕获指定类型的异常:

try:
    print('begin')
    c = 1 / 0
    print('end')
except ArithmeticError:
    print('catch the ArithmeticError')

print('outer')

20.1.3、异常类及继承层次

# Python 异常的继承
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- EncodingWarning
           +-- ResourceWarning

20.1.4、BaseException 及子类

BaseException:所有内建异常类的基类是 BaseException。

SystemExit:sys.exit() 函数引发的异常,异常不捕获处理,就直接交给 Python 解释器,解释器退出。

import sys

print('before')
sys.exit(1)
print('outer')  # 是否执行

# 捕获这个异常
try:
    sys.exit(1)
except SystemExit:
    print('SysExit')
print('outer')  # 是否执行?

Keyboardlnterrupt 对应的捕获用户中断行为 Ctrl+C

import time

try:
    while True:
        time.sleep(0.5)
        pass
except KeyboardInterrupt:
    print('ctrl + c')
print('outer')

20.1.5、Exception 及子类

Exception 是所有内建的、非系统退出的异常的基类,自定义异常应该继承自它。

SyntaxError 语法错误:Python 将这种错误也归到异常类 Exception 下的子类,但是这种错误是不可捕获的。

def a():
    try:
        0a = 5
    except:
        pass
# 错误
  File "D:\JetBrains\pythonProject\main.py", line 3
    0a = 5
    ^
SyntaxError: invalid decimal literal

ArithmeticError 所有算术计算引发的异常,其子类有除零异常等。

LookupError:使用映射的键或序列的索引无效时引发的异常的基类:IndexError,KeyError。

自定义异常:从 Exception 继承的类。

class MyException(Exception):
    pass

try:
    raise MyException()
except MyException:  # 捕获自定义异常
    print('catch the exception')

20.1.5.1、多异常的捕获

except 可以捕获多个异常。

class MyException(Exception):
    pass

try:
    a = 1 / 0
    raise MyException()  # 自定义
    open('a1.txt')
except MyException:
    print('catch MyException')
except ZeroDivisionError:
    print('1/0')
except Exception:  # 调整except的顺序
    print('Exception')

捕获规则:

  • 捕获是从上到下依次比较,如果匹配,则执行匹配的 except 语句块。
  • 如果被一个 except 语句捕获,其他 except 语句就不会再次捕获了。
  • 如果没有任何一个 except 语句捕获到这个异常,则该异常向外抛出。

捕获的原则:从小到大,从具体到宽泛。

20.1.5.2、as 子句

被抛出的异常,应该是异常的实例,如何获得这个对象呢?使用 as 子句。

class MyException(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message

try:
    raise MyException
except MyException as e:
    print('MyException = {} {}'.format(e.code, e.message))
except Exception as e:
    print('Exception = {}'.format(e))

输出:
Exception = MyException.__init__() missing 2 required positional arguments: 'code' and 'message'

raise 后跟类名是无参构造实例,因此需要 2 个参数。

class MyException(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message

try:
    raise MyException(200, 'ok')
except MyException as e:
    print('MyException = {} {}'.format(e.code, e.message))
except Exception as e:
    print('Exception = {}'.format(e))
输出:
MyException = 200 ok

20.1.5.3、finally 子句

finally:最终,即最后一定要执行的,try...finally 语句块中,不管是否发生了异常,都要执行 finally 的部分。

try:
    f = open('test.txt')
except FileNotFoundError as e:
    print('{} {} {}'.format(e.__class__, e.errno, e.strerror))
finally:
    print('清理工作')
    f.close()

注意上例中的的作用域,解决的办法是在外部定义 f。

finally 中一般放置资源的清理、释放工作的语句。

f = None
try:
    f = open('access.log')
except FileNotFoundError as e:
    print('{} {} {}'.format(e.__class__, e.errno, e.strerror))
finally:
    print('清理工作')
    if f:
        print('f will be closed')
        f.close()

也可以在 finally 中再次捕捉异常:

try:
    f = open('test.txt')
except FileNotFoundError as e:
    print('{} {} {}'.format(e.__class__, e.errno, e.strerror))
finally:
    print('清理工作')
    try:
        f.close()
    except NameError as e:
        print(e)

20.1.5.4、finally 执行时机

def foo():
    try:
        return 3
    finally:
        print('finally')
    print('==')

print(foo())

进入 try,执行 return 3,虽然函数要返回,但是 finally 一定还要执行,所以打印了 finally 后,函数返回。

def foo():
    try:
        return 3
    finally:
        return 5
    print('==')

print(foo())

进入 try,执行 return 3,虽然函数要返回,但是 finally 一定还要执行,所以执行 return 5,函数返回。5 被压在栈顶,所以返回 5。简单说,函数的返回值取决于最后一个执行的 return 语句,而 finally 则是 try...finally 中最后执行的语句块。

20.1.6、异常的传递

def foo1():
    return 1 / 0

def foo2():
    print('foo2 start')
    foo1()
    print('foo2 stop')

foo2()

foo2 调用了 foo1,foo1 产生的异常,传递到了 foo2 中。

异常总是向外层抛出,如果外层没有处理这个异常,就会继续向外抛出。

如果内层捕获并处理了异常,外部就不能捕获到了。

如果到了最外层还是没有被处理,就会中断异常所在的线程的执行。

import threading
import time

def foo1():
    return 1 / 0

def foo2():
    time.sleep(3)
    print('foo2 start')
    foo1()
    print('foo2 stop')

t = threading.Thread(target=foo2)
t.start()

while True:
    time.sleep(1)
    print('Everything is OK')
    if t.is_alive():
        print('alive')
    else:
        print('dead')

20.1.6.1、try 嵌套

try:
    try:
        ret = 1 / 0
    except KeyError as e:
        print(e)

    else:
        print('inner OK')
    finally:
        print('inner fin')
except:
    print('outer catch')
finally:
    print('outer fin')

内部捕获不到异常,会向外层传递异常。

但是如果内层有 finally 且其中有 return、break 语句,则异常就不会继续向外抛出。

def foo():
    try:
        ret = 1 / 0
    except KeyError as e:
        print(e)
    finally:
        print('inner fin')
        return  # 异常被丢弃

try:
    foo()
except:
    print('outer catch')
finally:
    print('outer fin')

20.1.6.2、异常的捕获时机

1、立获捕获

需要立即返回一个明确的结果:

def parse_int(s):
    try:
        return int(s)
    except:
        return 0

print(parse_int('s'))

2、边界捕获

封装产生了边界。

例如,写了一个模块,用户调用这个模块的时候捕获异常,模块内部不需要捕获、处理异常,一旦内部处理了,外部调用者就无法感知了。

例如,open 函数,出现的异常交给调用者处理,文件存在了,就不用再创建了,看是否修改还是删除。

例如,自己写了一个类,使用了 open 函数,但是出现了异常不知道如何处理,就继续向外层抛出,一般来说最外层也是边界,必须处理这个异常了,否则线程退出。

20.1.6.3、else 子句

try:
    ret = 1 * 0
except ArithmeticError as e:
    print(e)
else:
    print('OK')
finally:
    print('fin')

else 子句:没有任何异常发生,则执行。

20.1.6.4、总结

try:
    <语句>      # 运行别的代码
except <异常类>:
    <语句>      # 捕获某种类型的异常
except <异常类> as <变量名>:
    <语句>      # 捕获某种类型的异常并获得对象
else:
    <语句>      # 如果没有异常发生则执行
finally:
    <语句>      # 退出try时总会执行

try 的工作原理:

1、如果 try 中语句执行时发生异常,搜索 except 子句,并执行第一个匹配该异常的 except 子句。

2、如果 try 中语句执行时发生异常,却没有匹配的 except 子句,异常将被递交到外层的 try,如果外层不处理这个异常,异常将继续向外层传递。如果都不处理该异常,则会传递到最外层,如果还没有处理,就终止异常所在的线程。

3、如果在 try 执行时没有发生异常,将执行 else 子句中的语句。

4、无论 try 中是否发生异常,finally 子句最终都会执行。

20.2、模块化

一般来说,编程语言中,库、包、模块是同一种概念,是代码组织方式。

Python 中只有一种模块对象类型,但是为了模块化组织的便利,提供了一个概念叫

模块 module,指的是 Python 的源代码文件。

包 package,指的是模块组织在一起的和包名同名的目录及其相关文件。

20.2.1、导入语句

语句 含义
import 模块1 [, 模块2, ...] 完全导入
import ... as ... 模块别名

import 语句:

1、找到指定的模块,加载和初始化它,生成模块对象。找不到,抛出 ImportError 异常。

2、在 import 所在的作用域的局部命名空间中,增加模块名称和上一步创建的对象关联。

单独运行下面例子,体会区别:

import functools  # 导入模块

print(dir())  # [..., 'functools']
print(functools)  # <module 'functools' from 'D:\\Python\\Python310\\lib\\functools.py'>
print(functools.wraps)  # <function wraps at 0x0000020E483A8160>
import os.path  # 导入 os.path, os 加入当前名称空间

print(dir())  # [..., 'os']
print(os)  # <module 'os' from 'D:\\Python\\Python310\\lib\\os.py'>
print(os.path)  # 完全限定名称访问path
import os.path as osp  # 导入os.path并赋给osp

print(dir())  # [..., 'osp']
print(osp)

总结:

导入顶级模块,其名称会加入到本地名词空间中,并绑定到其模块对象。

导入非顶级模块,只将其顶级模块名称加入到本地名词空间中。导入的模块必须使用完全限定名称来访问。

如果使用了 as,as 后的名称直接绑定到导入的模块对象,并将该名称加入到本地名词空间中。

语句 含义
from ... import ... 部分导入
from ... import ... as ... 别名
from pathlib import Path, PosixPath  # 在当前名词空间导入该模块指定的成员

print(dir())  # ['Path', 'PosixPath', ...]
from pathlib import *  # 在当前名称空间导入该模块所有公共成员(非下划线开头成员)或指定成员

print(dir())  # ['Path', 'PosixPath', 'PurePath', 'PurePosixPath', 'PureWindowsPath', 'WindowsPath', ...]
from functools import wraps as wr, partial  # 别名

print(dir())  # [..., 'partial', 'wr']
from os.path import exists  # 加载、初始化os、os.path模块,exists 加入本地名词空间并绑定

if exists('o:/t'):
    print('Found')
else:
    print('Not Found')

print(dir())  # [..., 'exists']
print(exists)

import os

print(os.path.exists)
print(exists)
print(os.path.__dict__['exists'])
print(getattr(os.path, 'exists'))

# 上面4种方式获得同一个对象

总结:

  • 找到 from 子句中指定的模块,加载并初始化它(注意不是导入)。

  • 对于 import 子句后的名称。

    • 先查 from 子句加载的模块是否具有该名称的属性。

    • 如果没有,则尝试导入该名称的子模块。

    • 还没有找到,则抛出 ImportError 异常。

    • 这个名称保存到本地名词空间中,如果有 as 子句,则使用 as 子句后的名称。

from pathlib import Path  # 导入类Path

print(Path, id(Path))

import pathlib as pl  # 导入模块使用别名

print(dir())
print(pl)
print(pl.Path, id(pl.Path))
# 可以看出导入的名词Path和pl.Path是同一个对象

20.2.2、自定义模块

自定义模块:.py 文件就是一个模块。

# t1.py 文件
print("This is t1 module")

class A:
    def showmodule(self):
        print("{}.a = {}".format(self.__module__, self))
        print(self.__class__.__name__)

a = A()
a.showmodule()

# t2.py 文件
import t1

a = t1.A()
a.showmodule()

# t3.py 文件
from t1 import A as cls

a = cls()
a.showmodule()

20.2.2.1、自定义模块命名规范

  • 模块名就是文件名

  • 模块名必须符合标识符的要求,是非数字开头的字母数字和下划线的组合。test-module.py 这样的文件名不能作为模块名。

  • 不要使用系统模块名,避免冲突,除非你明确知道这个模块名的用途。

  • 通常模块名为全小写,下划线来分割。

20.2.2.2、模块搜索顺序

使用 sys.path 查看搜索顺序。

import sys

for p in sys.path:
    print(p)

显示结果为,python 模块的路径搜索顺序。

当加载一个模块的时候,需要从这些搜索路径中从前到后依次查找,并不搜索这些目录的子目录,搜索到模块就加载,搜索不到就抛异常。

路径也可以为字典、zip 文件、egg 文件。

.egg 文件,由 setuptools 库创建的包,第三方库常用的格式。添加了元数据(版本号、依赖项等)信息的 zip 文件。

路径顺序为:

  • 程序主目录,程序运行的主程序脚本所在的目录。

  • PYTHONPATH 目录,环境变量 PYTHONPATH 设置的目录也是搜索模块的路径。

  • 标准库目录,Python 自带的库模块所在目录。

  • sys.path 可以被修改,追加新的目录。

20.2.2.3、模块的重复导入

# t1.py 文件
print("This is t1 module")

class A:
    def showmodule(self):
        print("{}.a = {}".format(self.__module__, self))
        print(self.__class__.__name__)

a = A()
a.showmodule()

# t2.py 文件
import t1

print('local module')
import t1
import t1

从执行结果来看,不会产生重复导入的现象。

所有加载的模块都会记录在 sys.modules 中,sysmodules 是存储已经加载过的所有模块的字典。

20.2.3、模块运行

__name__,每个模块都会定义一个 __name__ 特殊变量来存储当前模块的名称,如果不指定,则默认为源代码文件名,如果是包则有限定名。

解释器初始化的时候,会初始化 sys.modules 字典( 保存已加载的模块),创建 builtins( 全局函数、常量)模块、__main__ 模块、sys 模块,以及模块搜索路径 sys.path

Python 是脚本语言,任何一个脚本都可以直接执行,也可以作为模块被导入。

当从标准输入(命令行方式代码)、脚本($ python test.py)或交互式读取的时候,会将模块的 __name__ 设置为 __main__,模块的顶层代码就在 __main__ 这个作用域中执行。

顶层代码:模块中缩进最外层的代码。

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

# t1.py 文件

import t2

# t2.py 文件
# 判断模块是否以程序的方式运行 $python test.py
if __name__ == '__main__':
    print('in __main__')  # 程序的方式运行的代码
else:
    print('in import module')  # 模块导入的方式运行的代码

运行t1.py输出:
in import module

if __name__ == '__main__': 用途:

  • 本模块的功能测试,对于非主模块,测试本模块内的函数、类。

  • 避免主模块变更的副作用。

    顶层代码,没有封装,主模块使用时没有问题。但是,一旦有了新的主模块,老的主模块成了被导入模块,由于原来代码没有封装,一并执行了。

20.2.4、模块的属性

属性 含义
__file__ 字符串,源文件路径
__cached__ 字符串,编译后的字节码文件路径
__spec__ 显示模块的规范
__name__ 模块名
__package__ 当模块是包,同__name__;否则,可以设置为顶级模块的空字符串

20.2.5、包

包,特殊的模块。那么 Python 模块支持目录吗?

实验:项目中新建一个目录 m,使用下面的代码。

import m

print(m)
print(type(m))
print(dir(m))  # 没有__file__

输出:
<module 'm' (<_frozen_importlib_external._NamespaceLoader object at 0x00000223DE628DC0>)>
<class 'module'>
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

竟然可以导入目录 m,目录也是文件,所以可以导入,不过问题是,目录模块怎么写入代码?

为了解决这个问题,Python 要求在目录下建立一个特殊文件 __init__.py,在其中写入代码。

pycharm 中,创建 Directory 和创建 Python package 不同,前者是创建普通的目录,后者是创建一个带有 __init__.py 文件的目录,就是所谓的包。

Python 中,目录可以作为模块,这就是包,不过代码需要写在该目录下 __init__.py 中。

20.2.6、子模块

包目录下的 py 文件、子目录都是其子模块。

m
├── __init__.py
├── m1.py
└── m2
    ├── __init__.py
    └── m21
        └── __init__.py

如上建立子模块目录和文件,所有的 py 文件中就写一句话 print(__name__)

# 注意查看模块的加载,当前名词空间
# import m
# import m.m1
# from m import m1
# from m.m2 import m21
import m.m2.m21

print(dir())

import sys

print(sorted(filter(lambda x: x.startswith('m'), sys.modules.keys())))

删除 __init__.py 试一试,可以发现删除并不影响导入,但是这不是良好的习惯,请保留 __init__.py 文件。

20.2.7、模块和包的总结

包能够更好的组织模块,尤其是大的模块,代码行数很多,可以把它拆分成很多子模块,便于使用某些功能时就加载相应的子模块。

包目录中 __init__.py` 是在包第一次导入的时候就会执行,内容可以为空,也可以是用于该包初始化工作的代码,最好不要删除它(低版本不可删除)。

导入子模块一定会加载父模块,但是导入父模块一定不会导入子模块。

包目录之间只能使用 . 点号作为间隔符,表示模块及其子模块的层级关系。

模块也是封装,如同类、函数,不过它能够封装变量、类、函数。

模块就是命名空间,其内部的顶层标识符,都是它的属性,可以通过 __dict__ 或 dir(module) 查看。

包也是模块,但模块不一定是包,包是特殊的模块,是一种组织方式,它包含 __path__ 属性。

问题:

from json import encoder 之后,json.dump 函数用不了,为什么?

import json.encoder 之后呢?json.dump 函数能用吗?

原因是 from json import encoder 之后,当前名词空间没有 json,但是 json 模块已经加载过了,没有 json 的引用,无法使用 dump 函数。

import json.encoder 也加载 json 模块,但是当前名词空间有 json,因此可以调用。

20.2.8、绝对导入、相对导入

20.2.8.1、绝对导入

在 import 或者 from 导入模块时,模块名称最前面不是以 . 点开头的。

绝对导入总是去搜索路径中找模块。

20.2.8.2、相对导入

只能在包内使用,且只能用在 from 语句中。使用 . 点号,表示当前目录内,.. 表示上一级目录,不要在顶层模块中使用相对导入。

举例 a.b.c 模块,c 是模块,c 的代码中,使用
from . import d --> a.b.d
from .. import e --> a.e
from .d import x --> a.b.d.x
from ..e import x --> a.e.x

... 三点表示上上一级。

20.2.9、访问控制

20.2.9.1、下划线开头的模块名

_ 或者 __ 开头的模块是否能够被导入呢?

创建文件名为 _xyz.py 或者 __xyz.py 测试。

都可以成功的导入,因为它们都是合法的标识符,就可以用作模块名。

20.2.9.2、模块内的标识符

# xyz.py
print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8

# t1.py 中
import xyz
import sys

print(sorted(sys.modules.keys()))
print(dir())
print(xyz.A, xyz._B, xyz.__C, xyz.__my__)

普通变量、保护变量、私有变量、特殊变量,都没有被隐藏,也就是说模块内没有私有的变量,在模块中定义不做特殊处理。

from 语句:

from xyz import A, _B as B, __my__, __C as C
import sys

print(sorted(sys.modules.keys()))
print(dir())

print(A, B, __my__, C)

依然可以使用 from 语句,访问所有变量。

20.2.10、from ... import *__all__

20.2.10.1、from ... import *

# xyz.py
print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8

# t1.py 中
from xyz import *
import sys

print(1, '-->', sorted(sys.modules.keys()))
print(2, '-->', dir())
print(3, '-->', locals()['A'])

A = 55
print(4, '-->', locals()['A'])

结果是只导入了 A,下划线开头的都没有导入。

20.2.10.2、使用 __all__

__all__ 是一个列表,元素是字符串,每一个元素都是一个模块内的变量名。

# xyz.py 中
__all__ = ['X', 'Y']
print(__name__)
A = 5
_B = 6
__C = 7
__my__ = 8
X = 10
Y = 20

# t1.py 中
from xyz import *
import sys

print(1, '-->', sorted(sys.modules.keys()))
print(2, '-->', dir())
# print(3, '-->', locals()['A'])
print(4, '-->', locals()['X'])
print(5, '-->', locals()['Y'])

修改 __all__ 列表,加入下划线开头变量,看看什么效果:

# xyz.py
__all__ = ['X', 'Y', '_B', '__C']
print(__name__)
A = 5
_B = 6
__C = 7
__my__ = 8
X = 10
Y = 20

# t1.py 中

from xyz import *
import sys

print(1, '-->', sorted(sys.modules.keys()))
print(2, '-->', dir())
# print(3, '-->', locals()['A'])
print(4, '-->', locals()['X'])
print(5, '-->', locals()['Y'])
print(6, '-->', locals()['_B'])
print(7, '-->', locals()['__C'])

可以看到使用 from xyz import * 导入 __all__ 列表中的名称。

20.2.11、包和子模块

m
├── __init__.py
├── m1.py
# __init__.py中
print(__name__)
x = 1

# m1.py中
print(__name__)
y = 5

# t1.py中
# 如何访问到m1.py中的变量y
# 方法1,直接导入m1模块
import m.m1
print(m.m1.y)

# 方法2,直接导入m.m1的属性y
from m.m1 import y
print(y)

# 方法3,from m import *
# 该方法导入后,无法看到子模块m1,无法访问y
# 在__init__.py增加__all__=['x', 'm1'],使用__all__提供导出的名称
from m import *
print(m1.y)

# 方法4,不使用__all__
# 在__init__.py增加from . import m1
from m import *
print(m1.y)

__init__.py 中有什么变量,则使用 from m import * 加载什么变量,这依然符合模块的访问控制。

20.2.12、总结

一、使用 from xyz import * 导入

  1. 如果模块没有 __all__from xyz import * 只导入非下划线开头的模块的变量。如果是包,子模块也不会导入,除非在 __all__ 中设置,或 __init__.py 中使用相对导入。
  2. 如果模块有 __all__from xyz import * 只导入 __all__ 列表中指定的名称,哪怕这个名词是下划线开头的,或者是子模块。
  3. from xyz import * 方式导入,使用简单,但是其副作用是导入大量不需要使用的变量,甚至有可能造成名称的冲突。而 __all__ 可以控制被导入模块在这种导入方式下能够提供的变量名称,就是为了阻止 from xyz import * 导入过多的模块变量,从而避免冲突。因此,编写模块时,应该尽量加入 __all__

二、from module import name1, name2 导入

这种方式的导入是明确的,哪怕是导入子模块,或者导入下划线开头的名称。程序员可以有控制的导入名称和其对应的对象。

三、模块变量的修改

# xyz.py
print(__name__)
X = 10

# t2.py
import xyz
print(xyz.X)

# t1.py
import xyz

print(xyz.X)
xyz.X = 50

import t2

模块对象是同一个,因此模块的变量也是同一个,对模块变量的修改,会影响所有使用者。

除非万不得已,或明确知道自己在做什么,否则不要修改模块的变量。

前面学习过的猴子补丁,也可以通过打补丁的方式,修改模块的变量、类、函数等内容。

20.3、包管理

Python 的模块或者源文件直接可以复制到目标项目目录中,就可以导入使用了。

但是为了更多项目使用,或者共享给别人,就需要打包,或发布到网络,以便复用。

Pypi( Python Package lndex),公共的模块存储中心,https://pypi.org/

20.3.1、distutils

官方库 distutils,使用安装脚本 setup.py 来构建、安装包。从 1998 年就是标准库的一部分,直到 2000 年停止开发。

20.3.2、setuptools

它是替代 distutils 的增强版工具集,包含 easy_install 工具,使用 ez_setup.py 文件。支持 egg 格式的构建和安装。

提供查询、下载、安装、构建、发布、管理等包管理功能。

setuptools 是包管理的核心模块。后来,setuptools 开发缓慢了,出现基于 setuptools 的 distribute 来替代 setuptools。2013 年,这两个项目重新合并,distribute 被废弃,setuptools 依然是 Python 安装打包的标准方式。

20.3.3、pip

pip 是目前包管理的事实标准。构建在 setuptools 之上,替代 easy_install 的。同样提供丰富的包管理功能。从 Python3.4 开始直接包含在安装文件中。

20.3.4、wheel

提供 bdist_wheel 作为 setuptools 的扩展命令,这个命令可以用来生成新打包格式 wheel。

pip 从1.4 版本开始提供了一个 wheel 子命令来安装 wheel 包。当然,需要先安装 wheel 模块。

它可以让 Python 库以二进制形式安装,而不需要在本地编译。

20.3.5、使用 setup.py 打包

setup.py 创建一个源代码分发包的例子,参照例子:https://docs.python.org/3/distutils/setupscript.html

# 包结构
m
├── __init__.py
├── m1.py
└── m2
    ├── __init__.py
    └── m21
        └── __init__.py

在项目根目录下建立一个 setup.py,内容如下:

from distutils.core import setup

# 导入 setup 函数并传参
setup(
    name='m',
    version='0.1.0',
    description='Python test m',
    author='Brinnatt',
    author_email='brinnatt@gmail.com',
    # packages=['m', 'm.m1', 'm.m2', 'm.m2.m21'],
    packages=['m']
)

# name 名字
# version 版本
# packages=[] 打包列表
# packages=['m'], 指定m,就会把m所有的非目录子模块打包
# ['m', 'm.m1.m2.m3'], 逐级建立目录,但是只把m的所有非目录子模块打包,把m.m1.m2.m3打包
# ['m', 'm.m1', 'm.m1.m2', 'm.m1.m2.m3']
# description 描述信息
# author 作者
# author_email 作者邮件
# url 包的主页,可以不写

查询命令的帮助:

(venv) PS D:\JetBrains\pythonProject> python.exe .\setup.py --help
Common commands: (see '--help-commands' for more)                

  setup.py build      will build the package underneath 'build/' 
  setup.py install    will install the package                   

Global options:                                                  
  --verbose (-v)      run verbosely (default)                       
  --quiet (-q)        run quietly (turns verbosity off)             
  --dry-run (-n)      don't actually do anything                    
  --help (-h)         show detailed help message                    
  --no-user-cfg       ignore pydistutils.cfg in your home directory 
  --command-packages  list of packages that provide distutils commands

Information display options (just display information, ignore any commands)
  --help-commands     list all available commands
  --name              print package name
  --version (-V)      print package version
  --fullname          print <package name>-<version>
  --author            print the author's name
  --author-email      print the author's email address
  --maintainer        print the maintainer's name
  --maintainer-email  print the maintainer's email address
  --contact           print the maintainer's name if known, else the author's
  --contact-email     print the maintainer's email address if known, else the
                      author's
  --url               print the URL for this package
  --license           print the license of the package
  --licence           alias for --license
  --description       print the package description
  --long-description  print the long package description
  --platforms         print the list of platforms
  --classifiers       print the list of classifiers
  --keywords          print the list of keywords
  --provides          print the list of packages/modules provided
  --requires          print the list of packages/modules required
  --obsoletes         print the list of packages/modules made obsolete

usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

20.3.6、build 编译

以下是 packages=['m'] 配置编译后的结果,会创建一个 build 目录:

(venv) PS D:\JetBrains\pythonProject> python.exe .\setup.py build
running build
running build_py
creating build
creating build\lib
creating build\lib\m
copying m\m1.py -> build\lib\m
copying m\__init__.py -> build\lib\m

在项目目录下多了 build 目录,有一个 lib 子目录,lib 下就是模块 m 的目录了。m 目录下的 *.py 文件被复制了,但是子目录没有被复制。

以下是 packages=['m.m2.m21'] 配置的结果:

(venv) PS D:\JetBrains\pythonProject> python.exe .\setup.py build
running build
running build_py
creating build
creating build\lib
creating build\lib\m
creating build\lib\m\m2
creating build\lib\m\m2\m21
copying m\m2\m21\__init__.py -> build\lib\m\m2\m21

可以看出,逐级构建了同样的目录结构,并只拷贝了 m21 的 __init__.py 文件。

以下是 packages=['m', 'm.m2.m21'] 配置的结果。

(venv) PS D:\JetBrains\pythonProject> python.exe .\setup.py build
running build
running build_py
creating build
creating build\lib
creating build\lib\m
copying m\m1.py -> build\lib\m
copying m\__init__.py -> build\lib\m
creating build\lib\m\m2
creating build\lib\m\m2\m21
copying m\m2\m21\__init__.py -> build\lib\m\m2\m21

build 得到的文件,直接拷贝到其他项目就可以使用。

20.3.7、install 安装

build 后就可以 install,直接运行,如果没有 build,会先 build 编译,然后安装。

$ python setup.py install

20.3.8、sdist 分发

$ python setup.py sdist

创建源代码的分发包。产生一个dist目录,里面生成一个带版本号的压缩包。

在其他地方解压这个文件,里面有setup.py,就可以使用 $ python setup.py install 安装了,也可以 $ pip install m-0.1.0.zip 直接使用 pip 安装这个压缩包。

$ python setup.py bdist_rpm # 打包成rpm
$ python setup.py bdist_wheel # 打包成二进制
$ python setup.py bdist_msi # 打包成exe

20.4、插件开发

动态导入:运行时,根据用户需求(提供字符串),找到模块的资源动态加载起来。

20.4.1、内建函数__import__()

__import__(name, globals=None, locals=None, fromlist=(), level=0)

name 模块名,import 语句本质上就是调用这个函数。但是不鼓励直接使用它。建议使用 importlib.import_module()

sys = __import__('sys') 等价于 import sys。

# t1.py
class A:
    def showme(self):
        print('I am A')

# 主程序模块main.py
if __name__ == '__main__':
    mod = __import__('t1')
    cls = getattr(mod, 'A')
    cls().showme()

20.4.2、importlib.import_module()

importlib.import_module(name, package=None):支持绝对导入和相对导入,如果是相对导入,package 必须设置。

# t1.py
class A:
    def showme(self):
        print('I am A')

# 主程序模块main.py
import importlib

def plugin_load(plugin_name: str, sep=":"):
    m, _, c = plugin_name.partition(sep)
    mod = importlib.import_module(m)
    cls = getattr(mod, c)
    return cls()

if __name__ == '__main__':
    # 装载插件
    a = plugin_load('t1:A')
    a.showme()

上面的例子就是插件化编程的核心代码。

20.4.3、插件化编程技术

20.4.3.1、依赖的技术

反射:运行时获取类型的信息,可以动态维护类型数据。

动态 import:推荐使用 importlib 模块,实现动态 import 模块的能力。

多线程:可以开启一个线程,等待用户输入,从而加载指定名称的模块。

20.4.3.2、加载的时机

什么时候加载合适?

程序启动的时候,还是程序运行中?

  1. 程序启动时

    像 pycharm 这样的工具,需要很多组件,这些组件也可能是插件,启动的时候扫描固定的目录,加载插件。

  2. 程序运行中

    程序运行过程中,接受用户指令或请求,启动相应的插件。

两种方式各有利弊,如果插件过多,会导致程序启动很慢,如果用户需要时再加载,如果插件太大或者依赖多,插件也会启动慢。

所以先加载必须的、常用的插件,其他插件使用时,发现需要,动态载入。

20.4.3.3、应用

软件的设计不可能尽善尽美,或者在某些功能上,不可能做的专业,需要专业的客户自己增强。比如 Photoshop 的滤镜插件。

Notepad++,它只需要做好一个文本编辑器就可以了,其它增强功能都通过插件的方式提供拼写检查、HTML 预览、正则插件等。

要定义规范,定义插件从哪里加载、如何加载、必须实现的功能等。

接口和插件的区别?

接口往往是暴露出来的功能,例如模块提供的函数或方法,加载模块后调用这些函数完成功能。

接口也是一种规范,它约定了必须实现的功能(必须提供某名称的函数),但是不关心怎么实现这个功能。

插件是把模块加载到系统中,运行它,增强当前系统功能,或者提供系统不具备的功能,往往插件技术应用在框架设计中。系统本身设计简单化、轻量级,实现基本功能后,其他功能通过插件加入进来,方便扩展。

20.5、补充

20.5.1、__slots__

问题的引出

都是字典惹的祸。字典为了提升查询效率,必须用空间换时间。一般来说,一个对象,属性多一点,都存储在字典中便于查询,问题不大。

但是如果数百万个对象,那么字典占得就有点大了。这个时候,能不能把属性字典 __dict__ 省了?

Python 提供了 __slots__

class A:
    X = 1

    def __init__(self):
        self.y = 5
        self.z = 6

    def show(self):
        print(self.X, self.y, self.z)

a = A()
print(A.__dict__)
print(a.__dict__)

思考:上面 2 个字典,谁的字典是个问题?

实例多达百万个的时候,这么多存放实例属性的字典是个问题。

class A:
    X = 1

    # __slots__ = ('y', 'z')
    # __slots__ = ['y', 'z']
    # __slots__ = 'y', 'z'
    __slots__ = 'y',

    def __init__(self):
        self.y = 5
        # self.z = 6

    def show(self):
        print(self.X, self.y)

a = A()
a.show()

print('A', A.__dict__)
# print('obj', a.__dict__)
print(a.__slots__)

__slots__ 告诉解释器,实例的属性都叫什么,一般来说,既然要节约内存,最好还是使用元组比较好。

一旦类提供了 __slots__,就阻止实例产生 __dict__ 来保存实例的属性。

尝试为实例 a 动态增加属性:

a.newx = 5 --> AttributeError: 'A' object has no attribute 'newx'。

说明实例不可以动态增加属性了。

A.NEWX=20,这是可以的,因为这是类属性。

20.5.1.1、继承

class A:
    X = 1

    __slots__ = ('y', 'z')

    # __slots__ = ['y', 'z']
    # __slots__ = 'y', 'z'
    # __slots__ = 'y',

    def __init__(self):
        self.y = 5
        # self.z = 6

    def show(self):
        print(self.X, self.y)

a = A()
a.show()

print('A', A.__dict__)
# print('obj', a.__dict__)
print(a.__slots__)

class B(A):
    pass

print('B', B().__dict__)

__slots__ 不影响子类实例,不会继承下去,除非子类里面自己也定义了 __slots__

20.5.1.2、应用场景

使用在需要构建数百万以上对象,且内存容量较为紧张,实例的属性简单、固定且不用动态增加的场景。

20.5.2、未实现和未实现异常

print(type(NotImplemented))
print(type(NotImplementedError))

输出:
<class 'NotImplementedType'>
<class 'type'>

NotImplemented 是个值,单值,是 NotImplementedType 类的实例。

NotImplementedError 是类型,是异常,返回 type。

20.5.3、运算符重载中的反向方法

前面学习过运算符重载的方法,例如 add 和 iadd。

class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print(1, '-->', self, 'add')
        return self.x + other.x

    def __iadd__(self, other):
        print(2, '-->', self, 'iadd')
        return A(self.x + other.x)

    def __radd__(self, other):
        print(3, '-->', self, 'radd')
        return self.x + other.x

a = A(4)
b = A(5)
print(4, '-->', a, b)
print(5, '-->', a + b)
print(6, '-->', b + a)
b += a
a += b

输出:
4 --> <__main__.A object at 0x00000162D2C83FD0> <__main__.A object at 0x00000162D2C83E80>
1 --> <__main__.A object at 0x00000162D2C83FD0> add
5 --> 9
1 --> <__main__.A object at 0x00000162D2C83E80> add
6 --> 9
2 --> <__main__.A object at 0x00000162D2C83E80> iadd
2 --> <__main__.A object at 0x00000162D2C83FD0> iadd

__radd__ 方法根本没有执行过,为什么?因为都是 A 的实例,都是调用的 __add__,无非就是实例 a 还是 b 调用而己。

测试一下 a + 1:

class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print(1, '-->', self, 'add')
        return self.x + other.x

    def __iadd__(self, other):
        print(2, '-->', self, 'iadd')
        return A(self.x + other.x)

    def __radd__(self, other):
        print(3, '-->', self, 'radd')
        return self.x + other.x

a = A(4)
a + 1

输出:
1 --> <__main__.A object at 0x000001973B1D3FD0> add
Traceback (most recent call last):
  File "D:\JetBrains\Projects\main.py", line 19, in <module>
    a + 1
  File "D:\JetBrains\Projects\main.py", line 7, in __add__
    return self.x + other.x
AttributeError: 'int' object has no attribute 'x'

出现了 AttributeError,因为 1 是 int 类型,没有 x 这个属性,还是 __add__ 被执行了。

测试 1 + a,运行结果如下:

3 --> <__main__.A object at 0x0000017AE64F3FD0> radd
Traceback (most recent call last):
  File "D:\JetBrains\Projects\main.py", line 19, in <module>
    1 + a
  File "D:\JetBrains\Projects\main.py", line 15, in __radd__
    return self.x + other.x
AttributeError: 'int' object has no attribute 'x'

这次执行的是实例 a 的 __radd__ 方法。

1 + a 等价于 1.__add__(a),而 int 类型实现了 __add__ 方法的,为什么却不抛出异常,而是执行了实例 a 的 __radd__ 方法?

再看一个例子:

class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print(1, '-->', self, 'add')
        return self.x + other.x

    def __iadd__(self, other):
        print(2, '-->', self, 'iadd')
        return A(self.x + other.x)

    def __radd__(self, other):
        print(3, '-->', self, 'radd')
        return self.x + other.x

class B:
    def __init__(self, x):
        self.x = x

a = A(4)
b = B(10)
print(a + b)
print(b + a)

输出:
1 --> <__main__.A object at 0x0000014485F63E80> add
14
3 --> <__main__.A object at 0x0000014485F63E80> radd
14

b + a 等价于 b.__add__(a),但是类B没有实现__add__方法,就去找a的__radd__方法。

1 + a 等价于 1.__add__(a),而int类型实现了__add__方法的,不过这个方法对于这种加法的返回值是 NotImplemented,解释器发现是这个值,就会发起对第二操作对象的 __radd__ 方法的调用。

1 + a 能解决吗?

class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print(1, '-->', self, 'add')
        try:
            x = other.x
            return self.x + other.x
        except AttributeError:
            try:
                x = int(other)
            except:
                x = 0

            return self.x + x

    def __iadd__(self, other):
        print(2, '-->', self, 'iadd')
        return A(self.x + other.x)

    def __radd__(self, other):
        print(3, '-->', self, 'radd')
        try:
            x = other.x
            return self.x + other.x
        except AttributeError:
            try:
                x = int(other)
            except:
                x = 0

            return self.x + x

class B:
    def __init__(self, x):
        self.x = x

a = A(4)
b = B(10)
print(4, '-->', a + b)
print(5, '-->', b + a)
print(6, '-->', a + 2)
print(7, '-->', 2 + a)
# print(a + 'abc')
# print('abc' + a)

'abc'+a,字符串也实现了 __add__ 方法,不过默认是处理不了和其他类型的加法,就返回 Notlmplemented。

标签云