Python中的内省与反射机制及其应用场景

news2024/9/24 9:22:00

1. 概述

在计算机学中,反射式编程(英语:reflective programming)或反射(英语:reflection),是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

要注意术语“反射”和“内省”(type introspection)的关系。内省(或称“自省”)机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。

python 提供了一套灵活的机制来实现内省和反射功能,让程序可以在运行时动态地修改其状态和行为,用来构建灵活的可扩展的模块和框架,本文主要讨论其基本用法及具体应用场景。

  • Python中的内省与反射机制及其应用场景

    • 1. 概述
      • Meta
    • 2. 基本用法
      • 2.1. 内置函数
      • 2.2. 特殊属性
      • 2.3. inspect模块
        • 2.3.1. 获取成员
        • 2.3.2. 获取源代码
        • 2.3.3. 类型注解
        • 2.3.4. 类与函数和调用堆栈
    • 3. 应用场景
      • 3.1. 鸭子类型概念及应用
      • 3.2. 可扩展数据提取器设计
      • 3.3. 简单工厂模式扩展性优化
      • 3.4. 运行时参数类型校验
      • 3.5. 基于参数签名进行子函数的自动调用
      • 3.6. 文档自动生成
    • 4. 总结
  • Meta

{
    "node": "D0B58787-93D2-DBD0-E731-3817F18AED2A",
    "name": "Python中的内省与反射机制及其应用场景",
    "author": "Ais",
    "date": "2023-09-13",
    "tag": ["python", "语法研究", "高级特性", "反射机制", "内省机制", "自省", "动态构建"]
}


2. 基本用法

2.1. 内置函数

内省反射式编程 的基础,在 python 中接触到的最常见的相关函数一般是 dirtype 这两个内置函数。

dir 函数在交互式命令行中使用比较频繁,通常用来查看指定模块或对象的属性和方法。

>>> import path
>>> dir(path)
['CaseInsensitivePattern', 'ClassProperty', 'DirectoryNotEmpty', 'FastPath', 'LINESEPS', 'Multi', 'NEWLINE', 'NL_END', 'Path', 'SpecialResolver', 
'TempDir', 'TreeWalkWarning', 'U_LINESEPS', 'U_NEWLINE', 'U_NL_END', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_multi_permission_mask', '_permission_mask', 'compose', 'contextlib', 'errno', 'fnmatch', 'functools', 'glob', 'hashlib', 'importlib', 'io', 'itertools', 'matchers', 'metadata', 'multimethod', 'only_newer', 'operator', 'os', 're', 'shutil', 'simple_cache', 'sys', 'tempdir', 'tempfile', 'warnings', 'win32security']
>>> dir({})
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

type 函数则用于动态构建类,但其最常用的用法是 type(object),指定一个对象,返回一个 type 对象,可以用该函数来快速查看对象的类型信息。

>>> type({})
<class 'dict'>

除了这两个函数外,python 还提供了以下与 内省反射 机制相关的内置函数。

函数定义功能
hasatterhasattr(object, name)检查 object 中是否具有 {name} 属性
getattergetattr(object, name, default)获取 object 的 {name} 属性,当属性不存在时抛出 AttributeError 异常或者返回 default 默认值
setattrsetattr(object, name, value)更新 object 属性
delattrdelattr(object, name)删除 object 属性
isinstanceisinstance(object, classinfo)判断 object 是否是 {classinfo} 的(直接,间接,虚拟) 子类实例,相比于 type 会考虑继承关系。
issubclassissubclass(class, classinfo)判断 class 是否是 {classinfo} 的子类(直接,间接,虚拟)
globalsglobals()返回实现当前模块命名空间的字典。对于函数内的代码,这是在定义函数时设置的,无论函数在哪里被调用都保持不变。
localslocals()更新并返回表示当前本地符号表的字典。 在函数代码块但不是类代码块中调用 locals() 时将返回自由变量。
varsvars(object)返回模块、类、实例或任何其它具有 dict 属性的对象的 dict 属性。

上述内置函数的完整用法参考官方文档 内置函数。

2.2. 特殊属性

除了内置函数外,python 还支持通过一些 特殊属性 来进行 内省,这些特殊属性通常以 __xxx__ 的形式存在。

一个最常见的属性是 __name__,用于存储 类、函数、方法、描述器或生成器实例的名称。

class A(object):

    def __init__(self):
        self.data = ""
        self.__source = ""
        
    def test(self):
        return self.__class__.__name__

class B(object):
    pass

class C(A, B):
    pass

class D(C):
    pass

print(D().test())  
# D
print(D().test.__name__)
# test
print(D().test.__qualname__)
# A.test

类似的 __qualname__ 属性用于存储 限定名称,详细定义参考 PEP-3155。

另一个常见的特殊属性是 __dict__,这是一个字典或其他类型的映射对象,用于存储对象的(可写)属性。

print(A().__dict__)
# {'data': '', '_A__source': ''}

print(A.__dict__)
# {'__module__': '__main__', '__init__': <function A.__init__ at 0x000002DCFE547B80>, 'test': <function A.test at 0x000002DCFE547C10>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

print(dir(A))
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'test']

要获取对象所属的类信息可以通过 __class__ 属性,该属性是一个 <class ‘type’> 对象。

obj = D()
print(obj.__class__)
# <class '__main__.D'>

type(obj.__class__)
# <class 'type'>

new_obj = obj.__class__()
print(new_obj.__class__)
# <class '__main__.D'>

__bases__ 是一个元组,其存储了类对象的基类。

print(D.__bases__)
# (<class '__main__.C'>,)
print(C.__bases__)
# (<class '__main__.A'>, <class '__main__.B'>)

可以通过对该属性进行递归遍历来获取指定类的 继承链

def DFS(cls):
    [(print(c), DFS(c)) for c in cls.__bases__]

DFS(D)
# <class '__main__.C'>
# <class '__main__.A'>
# <class 'object'>    
# <class '__main__.B'>
# <class 'object'> 

另一个更好的方式是直接使用 __mro__ ,该属性是类组成的元组,用于描述方法解析顺序。MRO(Method Resolution Order/方法解析顺序) 的定义参考 MRO。

print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

除了获取类的基类信息,还可以通过 __subclasses__ 方法来获取直接子类的弱引用列表。

print(A.__subclasses__())
# [<class '__main__.C'>]

特殊属性 的官方文档参考 Python特殊属性

2.3. inspect模块

内置函数特殊属性 提供了对类和对象的一些基本内省功能,Python标准库中的 inspect 模块则提供了更为完善和强大的方法来实现内省机制。

inspect 模块提供了一些有用的函数帮助获取对象的信息,例如模块、类、方法、函数、回溯、帧对象以及代码对象。例如它可以帮助你检查类的内容,获取某个方法的源代码,取得并格式化某个函数的参数列表,或者获取你需要显示的回溯的详细信息。

该模块提供了4种主要的功能:类型检查、获取源代码、检查类与函数、检查解释器的调用堆栈。

上述是 inspect 模块的官方文档描述。接下来了解一下具体的使用方法。

2.3.1. 获取成员

inspect.getmembers 函数用于返回一个对象上的所有成员,其返回值是一个键值对为元素的列表。

import inspect

class Test():

    def __init__(self):
        self.data = {}
        self.path = ""

    def test(self):
        pass

print(inspect.getmembers(Test()))
# [('__class__', <class '__main__.Test'>), ('__delattr__', <method-wrapper '__delattr__' of Test object at 0x0000020A150AFBB0>), ('__dict__', {'data': {}, 'path': ''}), ('__dir__', <built-in method __dir__ of Test object at 0x0000020A150AFBB0>), ('__doc__', None), ('__eq__', <method-wrapper '__eq__' of Test object at 0x0000020A150AFBB0>), ('__format__', <built-in method __format__ of Test object at 0x0000020A150AFBB0>), ('__ge__', <method-wrapper '__ge__' of Test object at 0x0000020A150AFBB0>), ('__getattribute__', <method-wrapper '__getattribute__' of Test object at 0x0000020A150AFBB0>), ('__gt__', <method-wrapper '__gt__' of Test object at 0x0000020A150AFBB0>), ('__hash__', <method-wrapper '__hash__' of Test object at 0x0000020A150AFBB0>), ('__init__', <bound method Test.__init__ of <__main__.Test object at 0x0000020A150AFBB0>>), ('__init_subclass__', <built-in method __init_subclass__ of type object at 0x0000020A151EB210>), ('__le__', <method-wrapper '__le__' of Test object at 0x0000020A150AFBB0>), ('__lt__', <method-wrapper '__lt__' of Test object at 0x0000020A150AFBB0>), ('__module__', '__main__'), ('__ne__', <method-wrapper '__ne__' of Test object at 0x0000020A150AFBB0>), ('__new__', <built-in method __new__ of type object at 0x00007FF80544CB50>), ('__reduce__', <built-in method __reduce__ of Test object at 0x0000020A150AFBB0>), ('__reduce_ex__', <built-in method __reduce_ex__ of Test object at 0x0000020A150AFBB0>), ('__repr__', <method-wrapper '__repr__' of Test object at 0x0000020A150AFBB0>), ('__setattr__', <method-wrapper '__setattr__' of Test object at 0x0000020A150AFBB0>), ('__sizeof__', <built-in method __sizeof__ of Test object at 0x0000020A150AFBB0>), ('__str__', <method-wrapper '__str__' of Test object at 0x0000020A150AFBB0>), ('__subclasshook__', <built-in method __subclasshook__ of type object at 0x0000020A151EB210>), ('__weakref__', None), ('data', {}), ('path', ''), ('test', <bound method Test.test of <__main__.Test object at 0x0000020A150AFBB0>>)]

通过可选参数 predicate 可以筛选指定的成员,比如获取 inspect 模块中以 is 开头的 函数 成员:

import inspect
print(inspect.getmembers(
    inspect, 
    predicate = lambda obj: inspect.isfunction(obj) and obj.__name__.startswith("is") 
))
# [('isabstract', <function isabstract at 0x0000015547068550>), ('isasyncgen', <function isasyncgen at 0x0000015547068040>), ('isasyncgenfunction', <function isasyncgenfunction at 0x0000015547065F70>), ('isawaitable', <function isawaitable at 0x00000155470681F0>), ('isbuiltin', <function isbuiltin at 0x0000015547068430>), ('isclass', <function isclass at 0x0000015547027940>), ('iscode', <function iscode at 0x00000155470683A0>), ('iscoroutine', <function iscoroutine at 0x0000015547068160>), ('iscoroutinefunction', <function iscoroutinefunction at 0x0000015547065EE0>), ('isdatadescriptor', <function isdatadescriptor at 0x0000015547065B80>), ('isframe', <function isframe at 0x0000015547068310>), ('isfunction', <function isfunction at 0x0000015547065D30>), ('isgenerator', <function isgenerator at 0x00000155470680D0>), ('isgeneratorfunction', <function isgeneratorfunction at 0x0000015547065E50>), ('isgetsetdescriptor', <function isgetsetdescriptor at 0x0000015547065CA0>), ('ismemberdescriptor', <function ismemberdescriptor at 0x0000015547065C10>), ('ismethod', <function ismethod at 0x0000015547065A60>), ('ismethoddescriptor', <function ismethoddescriptor at 0x0000015547065AF0>), ('ismodule', <function ismodule at 0x0000015546FE39D0>), ('isroutine', <function isroutine at 0x00000155470684C0>), ('istraceback', <function istraceback at 0x0000015547068280>)]

inspect 模块提供了一系列以 is 开头的函数,用于对对象的类型进行校验,需要注意的是,这里的 类型 指的是更抽象的层面,而非对象的 class 类型。

函数定义功能
inspect.ismoduleinspect.ismodule(object)当该对象是一个模块时返回 True。
inspect.isclassinspect.isclass(object)当该对象是一个类时返回 True,无论是内置类或者 Python 代码中定义的类。
inspect.ismethodinspect.ismethod(object)当该对象是一个 Python 写成的绑定方法时返回 True。
inspect.isfunctioninspect.isfunction(object)当该对象是一个 Python 函数时(包括使用 lambda 表达式创造的函数),返回 True。

来分析一下 inspect.getmembers 函数的源码实现:

def getmembers(object, predicate=None):
    """Return all members of an object as (name, value) pairs sorted by name.
    Optionally, only return members that satisfy a given predicate."""
    # 判断 object 是否是一个类并返回其 __mro__ 属性,该属性包含了 object 的继承链上的所有类对象。
    if isclass(object):
        # getmro -> cls.__mro__
        mro = (object,) + getmro(object)
    else:
        mro = ()
    # 存储结果
    results = []
    # 处理结果去重集
    processed = set()
    # 获取 object 的成员名
    names = dir(object)
    # :dd any DynamicClassAttributes to the list of names if object is a class;
    # this may result in duplicate entries if, for example, a virtual
    # attribute with the same name as a DynamicClassAttribute exists
    try:
        # 遍历 object 基类中的成员
        for base in object.__bases__:
            for k, v in base.__dict__.items():
                # 查找 types.DynamicClassAttribute 装饰的属性,与 property 装饰的属性在访问行为上有差异,具体详见 https://docs.python.org/zh-cn/3/library/types.html
                if isinstance(v, types.DynamicClassAttribute):
                    names.append(k)
    except AttributeError:
        pass
    for key in names:
        # First try to get the value via getattr.  Some descriptors don't
        # like calling their __get__ (see bug #1785), so fall back to
        # looking in the __dict__.
        try:
            # 优先通过 getattr 函数获取成员
            value = getattr(object, key)
            # handle the duplicate key
            if key in processed:
                raise AttributeError
        except AttributeError:
            # 根据 MRO(方法解析顺序) 查找键名为 key 的成员
            for base in mro:
                if key in base.__dict__:
                    value = base.__dict__[key]
                    break
            else:
                # could be a (currently) missing slot member, or a buggy
                # __dir__; discard and move on
                continue
        # 根据 predicate 参数过滤结果,predicate 是一个可调用对象
        if not predicate or predicate(value):
            results.append((key, value))
        processed.add(key)
    # 将结果安装首字母排序
    results.sort(key=lambda pair: pair[0])
    return results

通过上述源码可以看到,inspect.getmembers 函数并没有实现新的内省机制,而是基于前述的 内置函数特殊属性 进行组合实现的。

2.3.2. 获取源代码

inspect 模块还提供了一系列方法来获取源代码相关的信息。

inspect.getdoc 函数用于获取对象的 文档字符串文档字符串 是python中的一个特殊机制,其官方描述如下:

docstring – 文档字符串

作为类、函数或模块之内的第一个表达式出现的字符串字面值。它在代码执行时会被忽略,但会被解释器识别并放入所在类、函数或模块的 __doc__ 属性中。由于它可用于代码内省,因此是对象存放文档的规范位置。

class Test(object):
    """
    测试类
    """

    def test(self, data: dict) -> bool:
        """测试方法
        
        对指定数据进行测试并返回测试结果的真值。

        Args:
            data(dict): 测试数据

        Returns:
            (bool) 测试结果
        """ 
        pass

import inspect
print(inspect.getdoc(Test))
# 测试类
print(inspect.getdoc(Test.test))
# 测试方法
#
# 对指定数据进行测试并返回测试结果的真值。
#
# Args:
#     data(dict): 测试数据
#
# Returns:
#     (bool) 测试结果

inspect.getmodule 尝试猜测一个对象是在哪个模块中定义的。 如果无法确定模块则返回 None。

print(inspect.getmodule(Test))
# <module '__main__' from '.\\test.py'>

inspect.getsourcelines 函数用于返回对象的源代码文本。

print(inspect.getsource(Test.test))
"""
    def test(self, data: dict) -> bool:
        """测试方法

        对指定数据进行测试并返回测试结果的真值。

        Args:
            data(dict): 测试数据

        Returns:
            (bool) 测试结果
        """
        pass
"""

其他相关函数参考官方文档

2.3.3. 类型注解

python采用动态类型的设计,使其具有很强的灵活性,但在某些特定场景下,缺失类型信息也为开发和调试带来了麻烦,随着 Python 语言的持续发展,经过一系列的 PEP 提案,为 python 增加了 类型注解 的功能。其官方文档描述如下:

annotation – 标注

关联到某个变量、类属性、函数形参或返回值的标签,被约定作为 类型注解 来使用。

局部变量的标注在运行时不可访问,但全局变量、类属性和函数的标注会分别存放模块、类和函数的 annotations 特殊属性中。

参见 variable annotation, function annotation, PEP 484 和 PEP 526,对此功能均有介绍。 另请参见 对象注解属性的最佳实践 了解使用标注的最佳实践。

类型注解通过类似元数据的方式来存储变量参数的类型信息,常见形式如下:

def test(data: dict, save: bool = True) -> bool:
    pass

可以通过运行时获取对象的 __annotations__ 特殊属性来查看:

print(test.__annotations__)
# {'data': <class 'dict'>, 'save': <class 'bool'>, 'return': <class 'bool'>}

inspect 模块提供了 signature 函数来对可调用对象的调用签名和返回值标注进行内省。

s = inspect.signature(test)
print(f'[parameters]({s.parameters.__class__}): {s.parameters}')
# [parameters](<class 'mappingproxy'>): OrderedDict([('data', <Parameter "data: dict">), ('save', <Parameter "save: bool = True">), ('kwargs', <Parameter "**kwargs">)])
print(f'[return_annotation]({s.return_annotation.__class__}): {s.return_annotation}')
# [return_annotation](<class 'type'>): <class 'bool'>

inspect.signature 函数接受 可调用对象,并返回一个 inspect.Signature 类的实例。Signature 对象具有两个主要属性:

  • parameters:一个有序字典,存储可调用对象的形式参数信息。
  • return_annotation:返回值类型注解

parameters 属性的值由 inspect.Parameter 类的实例构成,用于描述参数的完整信息,其主要由以下属性:

  • name:参数名字符串。
  • default:参数的默认值。
  • annotation:参数的类型注解。
  • kind:描述如何将值绑定到参数,位置参数还是关键字参数等。
[print(f'[{key}]: {getattr(s.parameters["save"], key)}') for key in ["name", "default", "annotation", "kind"]]
# [name]: save
# [default]: True
# [annotation]: <class 'bool'>
# [kind]: 1

2.3.4. 类与函数和调用堆栈

除了上述用法外,inspect 模块还支持检查类与函数和解释器的调用堆栈,但由于个人在这方面接触到的应用较少,就不在此详解了,inspect 模块的完整使用文档,参考官方文档 inspect — 检查对象


3. 应用场景

在了解了 Python 中 内省机制 的基本用法后,结合具体的应用场景来看看如何实现反射式编程。

3.1. 鸭子类型概念及应用

duck-typing – 鸭子类型

指一种编程风格,它并不依靠查找对象类型来确定其是否具有正确的接口,而是直接调用或使用其方法或属性(“看起来像鸭子,叫起来也像鸭子,那么肯定就是鸭子。”)由于强调接口而非特定类型,设计良好的代码可通过允许多态替代来提升灵活性。鸭子类型避免使用 type() 或 isinstance() 检测。(但要注意鸭子类型可以使用 抽象基类 作为补充。) 而往往会采用 hasattr() 检测或是 EAFP 编程。

鸭子类型强调的是对象的行为,其识别对象的类不是通过类型信息,而是通过对象支持的行为来的。当涉及对象之间的调用关系时,这种方式有很强的灵活性。

python中最常见的应用莫过于各类特殊的 协议方法,比如 上下文管理器协议。当一个对象实现了 __enter____exit__ 方法,则该对象可以被当作一个 上下文管理器with 调用:

class Test(object):

    def __enter__(self):
        print(f'[{self.__class__.__name__}]: enter')
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        print(f'[{self.__class__.__name__}]: exit')

with Test() as t:
    pass

# [Test]: enter
# [Test]: exit

又或者可以通过实现 __call__ 将一个自定义对象变成可调用对象:

class Test(object):

    def __call__(self, data):
        print(f'[{self.__class__.__name__}]: {data}')

t = Test()
t("data")
# [Test]: data

print(callable(t))
# True

同时可以基于前述的内省机制实现自定义的特殊协议方法,比如实现一个自定义的 可序列化协议

import json

class Test(object):

    def __init__(self, data, path="./data"):
        self.data = data
        self.path = path

    # 可序列化协议
    def __serialize__(self) -> str:
        return f'{self.__class__.__name__}({json.dumps({"data": self.data, "path": self.path})})'

# 序列化
def serialize(obj):
    if hasattr(obj, "__serialize__"):
        return obj.__serialize__()
    else:
        raise Exception(f'obj({Test}) is not Serializable')
    
# 判断是否可序列化
def serializable(obj):
    return hasattr(obj, "__serialize__")

t = Test("data")
print(serialize(t))
# Test({"data": "data", "path": "./data"})
print(serializable(t))
# True
print(serialize(123))
# Exception: obj(<class '__main__.Test'>) is not Serializable
print(serializable(123))
# False

需要注意的是,应该避免使用 __xxx__ 方法来实现自定义协议,因为在语言发展过程中,可能会在新特性中使用,从而导致冲突。

自定义对象在实现某些特性时,不需要去显示的继承特定类,而是实现特定方法,对应组件通过检查(内省)目标对象是否具有特定方法来进行调用,正是这种基于鸭子类型的设计,使自定义对象可以通过实现特殊协议方法与内置类型保持一定的一致性。同时也让开发变的更加灵活。

3.2. 可扩展数据提取器设计

给定一段文本数据(str),需要通过一个 数据提取器 从该文本数据中提取出结构化的数据对象,常见的设计方法如下:

class Extracter(object):

    def extract(self, data):
        return {
            "title": self.title(data),
            "context": self.context(data),
            "pubdate": self.pubdate(data)
        }
    
    def title(self, data):
        return f"{data}-title"
    
    def context(self, data):
        return f"{data}-context"
    
    def pubdate(self, data):
        return f"{data}-pubdate"
    

data = Extracter().extract("text")
print(data)
# {'title': 'text-title', 'context': 'text-context', 'pubdate': 'text-pubdate'}

当新增提取字段时,需要创建对应的提取方法并将其调用代码添加到 extract 方法中,这种设计的可扩展性较低,需要频繁的修改 extract 方法,因此可以考虑使用内省和反射来提高其扩展性:

import inspect

class Extracter(object):

    def extract(self, data):
        return {
            extract_method_name.split("_", 1)[-1]: extract_method(data)
            for extract_method_name, extract_method in inspect.getmembers(self, inspect.ismethod)
            if extract_method_name.startswith("extract_")
        }

    def extract_title(self, data):
        return f"{data}-title"
    
    def extract_context(self, data):
        return f"{data}-context"
    
    def extract_pubdate(self, data):
        return f"{data}-pubdate"
    

data = Extracter().extract("text")
print(data)
# {'title': 'text-title', 'context': 'text-context', 'pubdate': 'text-pubdate'}

新的设计通过 inspect.getmembers 函数遍历对象中以 extract_ 为前缀的方法来实现调用,当新增提取字段时,只需新增一个符合规则的方法即可,而不用修改 extract 方法。

3.3. 简单工厂模式扩展性优化

简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

简单工厂模式的基本架构如下:

from abc import ABCMeta, abstractmethod

# 产品基类
class Product(metaclass=ABCMeta):
    
    @abstractmethod
    def use(self):
        pass

# 产品A
class ConcreteProductA(Product):
    """A"""

    def use(self):
        print(f'[{self.__class__.__name__}]: use')

# 产品B
class ConcreteProductB(Product):
    """B"""

    def use(self):
        print(f'[{self.__class__.__name__}]: use')

# 工厂
class Factory(object):

    @staticmethod
    def create(product):
        if product == "A":
            return ConcreteProductA()
        elif product == "B":
            return ConcreteProductB()
        else:
            raise ValueError(f'unknown product({product})')
        

Factory.create("A").use()
# [ConcreteProductA]: use
Factory.create("B").use()
# [ConcreteProductB]: use

可以看到,当新增产品时,由于 Factory.create 中的映射采用硬编码的方式,因此需要对其进行修改,从而导致这种设计的扩展性较差,为了解决这个问题,考虑通过动态的方式来构建映射表:

class Factory(object):

    @staticmethod
    def create(product):
        # 构建映射表
        products = {
            _cls.__doc__: _cls 
            for _cls in Product.__subclasses__()
            if _cls.__doc__ and _cls.__name__ != "Product"
        }
        return products[product]()


Factory.create("A").use()
# [ConcreteProductA]: use
Factory.create("B").use()
# [ConcreteProductB]: use

通过 Product.__subclasses__() 方法来获取 Product 的直接子类,并将产品类的文档字符串作为其键名来动态的构建映射表。通过这种方式,在新增产品类时,不用再修改 Factory.create 方法。需要注意的是,__subclasses__ 方法返回的是直接子类的弱引用列表,如果是多次继承,需要采用递归的方式来获取所有子类,同时由于该方案未经过完整验证与测试,请谨慎用于生产环境。

3.4. 运行时参数类型校验

在某些场景下,可能需要对函数的实际参数类型进行校验,一般的方式是在函数中手动添加类型检查逻辑,但是得益于python的 类型注解内省机制,可以通过一种 “自动化” 的方式进行:

import inspect

# 类型校验器
def type_validator(func):
    # 提取函数签名
    s = inspect.signature(func)
    def type_verified_func(*args, **kwargs):
        # 遍历函数实际参数
        for param, val in s.bind(*args, **kwargs).arguments.items():
            # 通过函数签名中的类型注解对实际参数类型进行校验
            if not isinstance(val, s.parameters[param].annotation):
                raise TypeError(f'param({param}|{type(val)}) is not {s.parameters[param].annotation}')
        # 执行目标函数
        return func(*args, **kwargs)
    return type_verified_func

@type_validator
def test(data:dict, path:str, save:bool=True):
    print(f'data({data}), path({path}), save({save})')


test({"a": 111}, "aaa", save=False)
# data({'a': 111}), path(aaa), save(False)
test(111, "aaa")
# TypeError: param(data|<class 'int'>) is not <class 'dict'>

上述 类型校验器 的核心实现思路是通过 inspect.signature 函数提取目标函数的 类型注解,并在函数调用时与实际参数的类型进行对比实现的,需要注意的是,该方法需要依赖于函数的 类型注解,样例未考虑注解缺失的情况(可以考虑处理成 Any 类型)。

3.5. 基于参数签名进行子函数的自动调用

类型校验器 相近的一个应用,由于 python 未实现 函数重载,因此需要在函数中判断参数类型并进行不同的处理,这种场景同样可以通过 类型注解 来实现子函数的动态调用。

import inspect

class SubFuncAutoCaller(object):

    def __init__(self):
        # 子函数映射表
        self.subfunc = {}

    def overload(self, subfunc):
        # 提取函数类型注解
        s = inspect.signature(subfunc)
        # 基于类型注解来生成参数签名
        params_signature = "|".join([t.annotation.__name__ for t in s.parameters.values()])
        self.subfunc[params_signature] = subfunc

    def __call__(self, *args):
        # 基于实参的参数签名进行子函数调用
        params_signature = "|".join([type(p).__name__ for p in args])
        return self.subfunc[params_signature](*args)
    

# 构建调用器
func = SubFuncAutoCaller()

@func.overload
def func_list(data: list):
    print(f'[func_list]: data({type(data)})')
    return data

@func.overload
def func_int(data: int):
    print(f'[func_int]: data({type(data)})')
    return [data]

@func.overload
def func_str(data: str):
    print(f'[func_str]: data({type(data)})')
    return [int(i) for i in  data.replace(" ", "").split(",")]

@func.overload
def func_dict(data: dict):
    print(f'[func_dict]: data({type(data)})')
    return list(data.values())

# 调用测试
print(func("1, 2, 3"))
# [func_str]: data(<class 'str'>)
# [1, 2, 3]
print(func([1, 2, 3]))
# [func_list]: data(<class 'list'>)
# [1, 2, 3]
print(func(1))
# [func_int]: data(<class 'int'>)
# [1]
print(func({"a": 1, "b": 2, "c": 3}))
# [func_dict]: data(<class 'dict'>)
# [1, 2, 3]

SubFuncAutoCaller 类的 overload 是一个装饰器,在对子函数进行装饰时,会提取函数的类型注解并构建一个参数签名做为内部映射表 subfunc 的键名。通过实现 __call__SubFuncAutoCaller 的实例变成一个可调用对象,当该对象被调用时,通过生成实际参数的参数签名来从 subfunc 中获取目标函数,从而实现子函数的自动调用。

需要注意的是,上述原型样例只考虑了位置参数的情况,在实际应用时需要处理包含 kwargs 的场景。

3.6. 文档自动生成

通过 inspect 模块从源码中自动生成文档。

import inspect

def doc_extracter(func):
    # 提取函数签名
    func_signature = inspect.signature(func)
    # 解析文档字符串
    doc = {
        "name": func.__name__,
        "desc": inspect.getdoc(func).strip(),
        "args": [
            (p.name, p.annotation.__name__, p.default)
            for p in func_signature.parameters.values()
        ], 
        "return": func_signature.return_annotation.__name__
    }
    doc_str = f'{doc["name"]}:\n'
    doc_str += f'Desc: {doc["desc"]}\n'
    doc_str += f'Args:\n'
    doc_str += "\n".join([
        f'  * {p[0]}({p[1]})' 
        if p[2] is inspect._empty 
        else f'  * {p[0]}({p[1]}): default({p[2]})'
        for p in doc["args"]
    ]) + "\n"
    doc_str += "Returns:\n"
    doc_str += f'    type({doc["return"]})'
    return doc_str

def test(data: dict, export: bool=False) -> bool:
    """
    对指定数据(data)进行测试,并返回测试结果的真值。
    """
    pass

print(doc_extracter(test))
# test:
# Desc: 对指定数据(data)进行测试,并返回测试结果的真值。
# Args:
#   * data(dict)
#   * export(bool): default(False)
# Returns:
#     type(bool)

可以基于该原型的思路构建完善的文档自动生成工具。


4. 总结

通过上述的具体应用场景可以看到,基于 内省机制反射式编程,可以让组件和模块在运行时采用一种动态的方式进行构建,从而使其具有更灵活的扩展性。但是需要注意的是,这种方式相对于传统方法可能导致程序运行的性能问题,需要开发者根据具体的应用场景在扩展性和性能需求之间进行平衡。


如果你对Python感兴趣,想要学习python,这里给大家分享一份Python全套学习资料,都是我自己学习时整理的,希望可以帮到你,一起加油!

😝有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓
Python全套学习资料

在这里插入图片描述

1️⃣零基础入门

① 学习路线

对于从来没有接触过Python的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
在这里插入图片描述

② 路线对应学习视频

还有很多适合0基础入门的学习视频,有了这些视频,轻轻松松上手Python~
在这里插入图片描述

③练习题

每节视频课后,都有对应的练习题哦,可以检验学习成果哈哈!
在这里插入图片描述

2️⃣国内外Python书籍、文档

① 文档和书籍资料

在这里插入图片描述

3️⃣Python工具包+项目源码合集

①Python工具包

学习Python常用的开发软件都在这里了!每个都有详细的安装教程,保证你可以安装成功哦!
在这里插入图片描述

②Python实战案例

光学理论是没用的,要学会跟着一起敲代码,动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。100+实战案例源码等你来拿!
在这里插入图片描述

③Python小游戏源码

如果觉得上面的实战案例有点枯燥,可以试试自己用Python编写小游戏,让你的学习过程中增添一点趣味!
在这里插入图片描述

4️⃣Python面试题

我们学会了Python之后,有了技能就可以出去找工作啦!下面这些面试题是都来自阿里、腾讯、字节等一线互联网大厂,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
在这里插入图片描述
在这里插入图片描述

5️⃣Python兼职渠道

而且学会Python以后,还可以在各大兼职平台接单赚钱,各种兼职渠道+兼职注意事项+如何和客户沟通,我都整理成文档了。
在这里插入图片描述

上述所有资料 ⚡️ ,朋友们如果有需要的,可以扫描下方👇👇👇二维码免费领取🆓
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1285309.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何在Linux环境搭建本地SVN服务器并结合cpolar实现公网访问

目录 前言 1. Ubuntu安装SVN服务 2. 修改配置文件 2.1 修改svnserve.conf文件 2.2 修改passwd文件 2.3 修改authz文件 3. 启动svn服务 4. 内网穿透 4.1 安装cpolar内网穿透 4.2 创建隧道映射本地端口 5. 测试公网访问 6. 配置固定公网TCP端口地址 6.1 保留一个固定…

暂时pass的题目的学习笔记(按类型分类 ):动态规划、递归

动态规划类 学习笔记来自公众号labuladong 动态规划的一般形式就是求最值——其核心问题是穷举但动态规划的穷举有些特别&#xff0c;因为这类问题存在重叠子问题 如果暴力穷举的话效率会极其低下&#xff0c;所以需要**「备忘录」或者「DP table」**来优化穷举过程&#xff…

09.复刻ChatGPT,自我进化,AI多智能体

文章目录 复刻ChatGPT原因准备开整ALpacaVicuna GPT-4 EvaluationDolly 2.0其他合集Self-improve 自我进化表现形式法1&#xff1a;自我催眠法2&#xff1a;Agent交互法3&#xff1a;ReasonAct AI多智能体AI规划角色的一天加入亿点点细节&#xff08;外界刺激&#xff09;Refle…

k8s引用环境变量

一 定义环境变量 ① 如何在k8s中定义环境变量 env、configmap、secret补充&#xff1a; k8s 创建Service自带的环境变量 ② 从pod属性中获取 kubectl explain deploy.spec.template.spec.containers.env.valueFrom关注&#xff1a; configMapKeyRef、fieldRef 和 resour…

15、pytest的fixture调用fixture

官方实例 # content of test_append.py import pytest# Arrange pytest.fixture def first_entry():return "a"# Arrange pytest.fixture def order(first_entry):return [first_entry]def test_string(order):# Actorder.append("b")# Assertassert orde…

Angular 进阶之四:SSR 应用场景与局限

应用场景 内容丰富&#xff0c;复杂交互的动态网页&#xff0c;对首屏加载有要求的项目&#xff0c;对 seo 有要求的项目&#xff08;因为服务端第一次渲染的时候&#xff0c;已经把关键字和标题渲染到响应的 html 中了&#xff0c;爬虫能够抓取到此静态内容&#xff0c;因此更…

合理布局CRM系统,提升工作效率

一般来说中小企业试用的CRM系统的销售管理模块主要服务于销售人员&#xff0c;CRM系统通过为销售人员提供一系列销售自动化工具&#xff0c;来简化他们的工作&#xff0c;加速销售周期。那么&#xff0c;中小企业CRM系统如何提高销售效率&#xff1f; 一、通用功能 1、销售管…

Visual Studio2022创建Windows服务程序

文章目录 Visual Studio2022创建Windows服务程序打开工具创建新项目创建成功重命名服务添加安装程序编写逻辑生成程序安装服务打开服务启动服务停止服务卸载服务修改项目配置重新生成安装服务启动服务 Visual Studio2022创建Windows服务程序 打开工具 创建新项目 创建成功 重命…

基于javaweb实现的物业管理系统

一、系统架构 前端&#xff1a;jsp | jquery | bootstrap 后端&#xff1a;servlet | ojdbc 环境&#xff1a;jdk1.6 | mysql 二、 代码及数据库 三、功能介绍 01. 登录页 02. 首页 03. 楼栋管理 04. 房屋管理 05. 业主管理 06. 物资管理 07. 收费管理-收费项目管理 0…

为什么安秉信息的源代码防泄密软件这么稳定?

现在很多研发性企业都会意识到企业的源代码文件需要防泄密保护&#xff0c;现在很多企业对于源代码只是用了git或svn版本管理服务器进行了简单的代码统一管控。虽然现在对于源代码防泄密&#xff0c;有专业的源代码加密软件&#xff0c;但是很多企业&#xff0c;对源代码加密进…

写给初学者的 HarmonyOS 教程 -- 状态管理(@State/@Prop/@Link 装饰器)

State 装饰的变量&#xff0c;或称为状态变量&#xff0c;一旦变量拥有了状态属性&#xff0c;就和自定义组件的渲染绑定起来。当状态改变时&#xff0c;UI 会发生对应的渲染改变&#xff08;类似 Compose 的 mutablestateof &#xff09;。 Prop 装饰的变量可以和父组件建立单…

配置texstudio编译器

目录 1 .编辑器介绍2. 软件下载3. 测试编辑器 1 .编辑器介绍 latex可用的编辑器有配套的texstudio软件&#xff0c;也可以通过配置VScode来作为编辑器&#xff0c;但是个人感觉vscode配置较为复杂&#xff08;失败了&#xff09;&#xff0c;所以本篇介绍texstudio的配置&…

Java Socket编程之基于TCP协议通信

1.说明 Socket&#xff08;套接字&#xff09;是计算机网络编程中用于实现网络通信的一种编程接口或抽象概念。 它提供了一种标准的接口&#xff0c;使应用程序能够通过网络与其他计算机进行通信。 Socket可以看作是应用程序与网络之间的一个通信端点&#xff0c;类似于电话中…

qt 5.15.2 主窗体事件及绘制功能

qt 5.15.2 主窗体事件及绘制功能 显示主窗体效果图如下所示&#xff1a; main.cpp #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.setFixedWidth(600);w.setFixedHeight(6…

什么是 AWS IAM?如何使用 IAM 数据库身份验证连接到 Amazon RDS(下)

在《什么是 AWS IAM&#xff1f;如何使用 IAM 数据库身份验证连接到 Amazon RDS&#xff08;上&#xff09;》中我们已经阅读了有关LAM的部分内容&#xff0c;这篇在文章中我们将继续为您展开↓ dbForge Studio for MySQL是一款专业的数据库管理、开发软件&#xff0c;它能够使…

探索C++14新特性:更强大、更高效的编程

探索C14新特性&#xff1a;更强大、更高效的编程 C14并没有太大的改动&#xff0c;就连官方说明中也指出&#xff0c;C14相对于C11来说是一个比较小的改动&#xff0c;但是在很大程度上完善了C11&#xff0c;所以可以说C14就是在C11标准上的查漏补缺。 C14在2014年8月18日正式…

Qt篇——QChartView实现鼠标滚轮缩放、鼠标拖拽平移、鼠标双击重置缩放平移、曲线点击显示坐标

话不多说。 第一步&#xff1a;自定义QChartView&#xff0c;直接搬 FirtCurveChartView.h #ifndef FITCURVECHARTVIEW_H #define FITCURVECHARTVIEW_H #include <QtCharts>class FitCurveChartView : public QChartView {Q_OBJECTpublic:FitCurveChartView(QWidget *…

apk反编译修改教程系列---简单去除apk开屏广告【五】

往期教程&#xff1a; apk反编译修改教程系列-----修改apk应用名称 任意修改名称 签名【一】 apk反编译修改教程系列-----任意修改apk版本号 版本名 防止自动更新【二】 apk反编译修改教程系列-----修改apk中的图片 任意更换apk桌面图片【三】 apk反编译修改教程系列---简单…

算法学习—排序

排序算法 一、选择排序 1.算法简介 选择排序是一个简单直观的排序方法&#xff0c;它的工作原理很简单&#xff0c;首先从未排序序列中找到最大的元素&#xff0c;放到已排序序列的末尾&#xff0c;重复上述步骤&#xff0c;直到所有元素排序完毕。 2.算法描述 1&#xff…

Linux中项目部署步骤

安装jdk&#xff0c;tomcat 安装步骤 1&#xff0c;将压缩包&#xff0c;拷贝到虚拟机中。 通过工具&#xff0c;将文件直接拖到虚拟机的/home下 2&#xff0c;回到虚拟机中&#xff0c;查看/home下&#xff0c;有两个压缩文件 3&#xff0c;给压缩文件做解压缩操作 tar -z…