Python 动态加载

Python 动态加载

Python 一般存在以下三种方式:

  • 内置 __import__() 函数
  • importlib 模块
  • exec 函数

最后介绍一下在动态加载时经常用到的 inspect 模块。

__import__ 函数

__import__ (name[, globals[, locals[, fromlist[, level]]]])

参数说明:

  • name (required): 被加载 module 的名称
  • globals (optional): 包含全局变量的字典,该选项很少使用,采用默认值 global()
  • locals (optional): 包含局部变量的字典,内部标准实现未用到该变量,采用默认值 local()
  • fromlist (Optional): 被导入的 submodule 名称
  • level (Optional): 导入路径选项,Python 2 中默认为 -1,表示同时支持 absolute import 和 relative import。Python 3 中默认为 0,表示仅支持 absolute import。如果大于 0,则表示相对导入的父目录的级数,即 1 类似于 '.',2 类似于 '..'。
# test.py
def test():
	print("test")

class Test(object):
	def __init__(self):
		print("Test Create")
		
class SubTest(Test):
	def __init__(self):
		print("Test2 Create")

动态加载 test.py 

c = __import__('test')

print(c)

c.test()
obj = c.Test()
obj_sub = c.SubTest()
'''

test
Test Create
Test2 Create
'''

如果输入的参数如果带有 “.”,采用 __import__ 直接导入 module 容易造成意想不到的结果。 OpenStack 的 oslo.utils 封装了 __import__,支持动态导入 class, object 等。

# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""
Import related utilities and helper functions.
"""

import sys
import traceback


def import_class(import_str):
    """Returns a class from a string including module and class.
    .. versionadded:: 0.3
    """
    mod_str, _sep, class_str = import_str.rpartition('.')
    __import__(mod_str)
    try:
        return getattr(sys.modules[mod_str], class_str)
    except AttributeError:
        raise ImportError('Class %s cannot be found (%s)' %
                          (class_str,
                           traceback.format_exception(*sys.exc_info())))


def import_object(import_str, *args, **kwargs):
    """Import a class and return an instance of it.
    .. versionadded:: 0.3
    """
    return import_class(import_str)(*args, **kwargs)


def import_object_ns(name_space, import_str, *args, **kwargs):
    """Tries to import object from default namespace.
    Imports a class and return an instance of it, first by trying
    to find the class in a default namespace, then failing back to
    a full path if not found in the default namespace.
    .. versionadded:: 0.3
    .. versionchanged:: 2.6
       Don't capture :exc:`ImportError` when instanciating the object, only
       when importing the object class.
    """
    import_value = "%s.%s" % (name_space, import_str)
    try:
        cls = import_class(import_value)
    except ImportError:
        cls = import_class(import_str)
    return cls(*args, **kwargs)


def import_module(import_str):
    """Import a module.
    .. versionadded:: 0.3
    """
    __import__(import_str)
    return sys.modules[import_str]


def import_versioned_module(module, version, submodule=None):
    """Import a versioned module in format {module}.v{version][.{submodule}].
    :param module: the module name.
    :param version: the version number.
    :param submodule: the submodule name.
    :raises ValueError: For any invalid input.
    .. versionadded:: 0.3
    .. versionchanged:: 3.17
       Added *module* parameter.
    """

    # NOTE(gcb) Disallow parameter version include character '.'
    if '.' in '%s' % version:
        raise ValueError("Parameter version shouldn't include character '.'.")
    module_str = '%s.v%s' % (module, version)
    if submodule:
        module_str = '.'.join((module_str, submodule))
    return import_module(module_str)


def try_import(import_str, default=None):
    """Try to import a module and if it fails return default."""
    try:
        return import_module(import_str)
    except ImportError:
        return default


def import_any(module, *modules):
    """Try to import a module from a list of modules.
    :param modules: A list of modules to try and import
    :returns: The first module found that can be imported
    :raises ImportError: If no modules can be imported from list
    .. versionadded:: 3.8
    """
    for module_name in (module,) + modules:
        imported_module = try_import(module_name)
        if imported_module:
            return imported_module

    raise ImportError('Unable to import any modules from the list %s' %
                      str(modules))

importlib 模块

动态加载某个模块

import importlib

itertools = importlib.import_module('itertools')

importlib.import_module(name, package=None) 参数说明:

  • name:以绝对或相对导入方式导入什么模块 (比如要么像这样 pkg.mod 或者这样 ..mod)
  • package:如果参数 name 使用相对导入的方式来指定,那么那个参数 packages 必须设置为那个包名,这个包名作为解析这个包名的锚点 (比如 import_module('..mod', 'pkg.subpkg') 将会导入 pkg.md)。

import_module() 函数是一个对 importlib.__import__() 进行简化的包装器。 这意味着该函数的所有主义都来自于 importlib.__import__()。 这两个函数之间最重要的不同点在于 import_module() 返回指定的包或模块 (例如 pkg.mod),而 __import__() 返回最高层级的包或模块 (例如 pkg)。

如果动态导入一个自从解释器开始执行以来被创建的模块(即创建了一个 Python 源代码文件),为了让导入系统知道这个新模块,可能需要调用 invalidate_caches()。

判断某个模块是否可以被加载

import importlib.util
import sys

# For illustrative purposes.
name = 'itertools'

spec = importlib.util.find_spec(name)
if spec is None:
    print("can't find the itertools module")
else:
    # If you chose to perform the actual import ...
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    # Adding the module to sys.modules is optional.
    sys.modules[name] = module

其中 importlib.util.find_spec(name, package=None) 参数与 importlib.import_module(name, package=None)  意义相同,其返回一个命名空间,其中包含用于加载模块的相关导入信息。importlib.util.module_from_spec(spec) 从 spec 中创建一个新的模块,之后就可以是使用 module 当 itertools 使用。spec.loader.exec_module(module) 执行某个模块。

直接从文件中加载

import importlib.util
import sys

# For illustrative purposes.
import tokenize
file_path = tokenize.__file__
module_name = tokenize.__name__

spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Optional; only necessary if you want to be able to import the module
# by name later.
sys.modules[module_name] = module

importlib.util.spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None) 其根据指向某个文件的路径创建一个 ModuleSpec 实例。例子中 module 可以直接当成 tokenize 使用。

通过 importer 来管理 finder 和 loader

import importlib.machinery
import sys

# For illustrative purposes only.
SpamMetaPathFinder = importlib.machinery.PathFinder
SpamPathEntryFinder = importlib.machinery.FileFinder
loader_details = (importlib.machinery.SourceFileLoader,
                  importlib.machinery.SOURCE_SUFFIXES)

# Setting up a meta path finder.
# Make sure to put the finder in the proper location in the list in terms of
# priority.
sys.meta_path.append(SpamMetaPathFinder)

# Setting up a path entry finder.
# Make sure to put the path hook in the proper location in the list in terms
# of priority.
sys.path_hooks.append(SpamPathEntryFinder.path_hook(loader_details))

实现 importlib.import_module 函数

import importlib.util
import sys

def import_module(name, package=None):
    """An approximate implementation of import."""
    absolute_name = importlib.util.resolve_name(name, package)
    try:
        return sys.modules[absolute_name]
    except KeyError:
        pass

    path = None
    if '.' in absolute_name:
        parent_name, _, child_name = absolute_name.rpartition('.')
        parent_module = import_module(parent_name)
        path = parent_module.__spec__.submodule_search_locations
    for finder in sys.meta_path:
        spec = finder.find_spec(absolute_name, path)
        if spec is not None:
            break
    else:
        msg = f'No module named {absolute_name!r}'
        raise ModuleNotFoundError(msg, name=absolute_name)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    sys.modules[absolute_name] = module
    if path is not None:
        setattr(parent_module, child_name, module)
    return module

exec 函数

eval(expression, globals=None, locals=None)

exec(object[, globals[, locals]])

参数说明:

  • globals: 可选参数,表示全局命名空间(存放全局变量),如果被提供,则必须是一个字典对象。通常传入 globals(),其返回一个表示当前全局标识符表的字典。这永远是当前模块的字典(在一个函数或方法内部,这是指定义该函数或方法的模块,而不是调用该函数或方法的模块)
  • locals:可选参数,表示当前局部命名空间(存放局部变量),如果被提供,可以是任何映射对象。如果该参数被忽略,那么它将会取与globals相同的值。通常传入 locals(),其更新并返回一个表示当前局部标识符表的字典。自由变量在函数内部被调用时,会被locals()函数返回;自由变量在类累不被调用时,不会被locals()函数返回。

exec 函数与 eval 函数有很多类似之处,主要的不同有以下两点:

  • eval()函数只能计算单个表达式的值,而exec()函数可以动态运行代码段。
  • eval()函数可以有返回值,而exec()函数返回值永远为None。
x = 10

def func():
    y = 20
    a = eval('x + y')
    print('a: ', a)
    b = eval('x + y', {'x': 1, 'y': 2})
    print('b: ', b)
    c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
    print('c: ', c)
    d = eval('print(x, y)')
    print('d: ', d)

func()

'''
a:  30
b:  3
c:  4
10 20
d:  None
'''
x = 10
expr = """
z = 30
sum = x + y + z
print(sum)
"""
def func():
    y = 20
    exec(expr)
    exec(expr, {'x': 1, 'y': 2})
    exec(expr, {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
    
func()
'''
60
33
34
'''

inspect 模块

  • 对类,模块的操作,成员,类,模块类型的判断
  • 获取源码
  • 获取类或函数的参数信息
  • 解析堆栈
inspect.getmembers(object[, predicate])

其实现了提取某个对象 object 中的所有成员,以(name,value)对组成的列表返回。其中第二个参数通常可以根据需要调用如下16个方法:

  1. inspect.ismodule(object): 是否为模块

  2. inspect.isclass(object):是否为类

  3. inspect.ismethod(object):是否为方法(bound method written in python)

  4. inspect.isfunction(object):是否为函数(python function, including lambda expression)

  5. inspect.isgeneratorfunction(object):是否为python生成器函数

  6. inspect.isgenerator(object):是否为生成器

  7. inspect.istraceback(object): 是否为traceback

  8. inspect.isframe(object):是否为frame

  9. inspect.iscode(object):是否为code

  10. inspect.isbuiltin(object):是否为built-in函数或built-in方法

  11. inspect.isroutine(object):是否为用户自定义或者built-in函数或方法

  12. inspect.isabstract(object):是否为抽象基类

  13. inspect.ismethoddescriptor(object):是否为方法标识符

  14. inspect.isdatadescriptor(object):是否为数字标识符,数字标识符有__get__ 和__set__属性; 通常也有__name__和__doc__属性

  15. inspect.isgetsetdescriptor(object):是否为getset descriptor

  16. inspect.ismemberdescriptor(object):是否为member descriptor

你可能感兴趣的:(Python,动态加载)