一个管理全局实例的框架,利用元类(metaclass)和混合类(mixin) 。
1 代码实现
这段代码通过元类和混合类的机制提供了一个可以全局访问且线程安全的单例模式框架。这种设计模式在多线程应用程序中非常有用,尤其是在需要全局管理资源或实例的情况下。
# Copyright (c) OpenMMLab. All rights reserved.
import inspect
import threading
import warnings
from collections import OrderedDict
from typing import Type, TypeVar
_lock = threading.RLock()
T = TypeVar('T')
def _accquire_lock() -> None:
"""Acquire the module-level lock for serializing access to shared data.
This should be released with _release_lock().
"""
if _lock:
_lock.acquire()
def _release_lock() -> None:
"""Release the module-level lock acquired by calling _accquire_lock()."""
if _lock:
_lock.release()
class ManagerMeta(type):
"""The metaclass for global accessible class.
The subclasses inheriting from ``ManagerMeta`` will manage their
own ``_instance_dict`` and root instances. The constructors of subclasses
must contain the ``name`` argument.
Examples:
>>> class SubClass1(metaclass=ManagerMeta):
>>> def __init__(self, *args, **kwargs):
>>> pass
AssertionError: <class '__main__.SubClass1'>.__init__ must have the
name argument.
>>> class SubClass2(metaclass=ManagerMeta):
>>> def __init__(self, name):
>>> pass
>>> # valid format.
"""
def __init__(cls, *args):
cls._instance_dict = OrderedDict()
params = inspect.getfullargspec(cls)
params_names = params[0] if params[0] else []
assert 'name' in params_names, f'{cls} must have the `name` argument'
super().__init__(*args)
class ManagerMixin(metaclass=ManagerMeta):
"""``ManagerMixin`` is the base class for classes that have global access
requirements.
The subclasses inheriting from ``ManagerMixin`` can get their
global instances.
Examples:
>>> class GlobalAccessible(ManagerMixin):
>>> def __init__(self, name=''):
>>> super().__init__(name)
>>>
>>> GlobalAccessible.get_instance('name')
>>> instance_1 = GlobalAccessible.get_instance('name')
>>> instance_2 = GlobalAccessible.get_instance('name')
>>> assert id(instance_1) == id(instance_2)
Args:
name (str): Name of the instance. Defaults to ''.
"""
def __init__(self, name: str = '', **kwargs):
assert isinstance(name, str) and name, \
'name argument must be an non-empty string.'
self._instance_name = name
@classmethod
def get_instance(cls: Type[T], name: str, **kwargs) -> T:
"""Get subclass instance by name if the name exists.
If corresponding name instance has not been created, ``get_instance``
will create an instance, otherwise ``get_instance`` will return the
corresponding instance.
Examples
>>> instance1 = GlobalAccessible.get_instance('name1')
>>> # Create name1 instance.
>>> instance.instance_name
name1
>>> instance2 = GlobalAccessible.get_instance('name1')
>>> # Get name1 instance.
>>> assert id(instance1) == id(instance2)
Args:
name (str): Name of instance. Defaults to ''.
Returns:
object: Corresponding name instance, the latest instance, or root
instance.
"""
_accquire_lock()
assert isinstance(name, str), \
f'type of name should be str, but got {type(cls)}'
instance_dict = cls._instance_dict # type: ignore
# Get the instance by name.
if name not in instance_dict:
instance = cls(name=name, **kwargs) # type: ignore
instance_dict[name] = instance # type: ignore
elif kwargs:
warnings.warn(
f'{cls} instance named of {name} has been created, '
'the method `get_instance` should not accept any other '
'arguments')
# Get latest instantiated instance or root instance.
_release_lock()
return instance_dict[name]
@classmethod
def get_current_instance(cls):
"""Get latest created instance.
Before calling ``get_current_instance``, The subclass must have called
``get_instance(xxx)`` at least once.
Examples
>>> instance = GlobalAccessible.get_current_instance()
AssertionError: At least one of name and current needs to be set
>>> instance = GlobalAccessible.get_instance('name1')
>>> instance.instance_name
name1
>>> instance = GlobalAccessible.get_current_instance()
>>> instance.instance_name
name1
Returns:
object: Latest created instance.
"""
_accquire_lock()
if not cls._instance_dict:
raise RuntimeError(
f'Before calling {cls.__name__}.get_current_instance(), you '
'should call get_instance(name=xxx) at least once.')
name = next(iter(reversed(cls._instance_dict)))
_release_lock()
return cls._instance_dict[name]
@classmethod
def check_instance_created(cls, name: str) -> bool:
"""Check whether the name corresponding instance exists.
Args:
name (str): Name of instance.
Returns:
bool: Whether the name corresponding instance exists.
"""
return name in cls._instance_dict
@property
def instance_name(self) -> str:
"""Get the name of instance.
Returns:
str: Name of instance.
"""
return self._instance_name
2 代码详细解析
2.1 导入依赖和初始化锁
import inspect
import threading
from collections import OrderedDict
from typing import Type, TypeVar
_lock = threading.RLock()
T = TypeVar('T')
inspect
用于获取类和函数的内部信息。threading
和RLock
用于同步多线程环境下对数据的访问,以保证线程安全。OrderedDict
保持插入顺序的字典,用于存储实例。TypeVar
和Type
用于类型注解,提高代码的可读性和健壮性。
2.2 锁的获取与释放
def _accquire_lock() -> None:
"""Acquire the module-level lock for serializing access to shared data."""
if _lock:
_lock.acquire()
def _release_lock() -> None:
"""Release the module-level lock acquired by calling _accquire_lock()."""
if _lock:
_lock.release()
这两个函数用于在多线程环境中获取和释放锁,确保对共享数据的访问是线程安全的。
2.3 元类 ManagerMeta
class ManagerMeta(type):
def __init__(cls, *args):
cls._instance_dict = OrderedDict()
params = inspect.getfullargspec(cls)
params_names = params[0] if params[0] else []
assert 'name' in params_names, f'{cls} must have the `name` argument'
super().__init__(*args)
- 这是一个元类,用于创建可以全局访问的类的子类。
cls._instance_dict
是一个有序字典,用于存储该类的所有实例。- 检查类构造函数中必须包含
name
参数,这是管理不同实例的关键字段。
2.4 混合类 ManagerMixin
class ManagerMixin(metaclass=ManagerMeta):
def __init__(self, name: str = '', **kwargs):
assert isinstance(name, str) and name, 'name argument must be an non-empty string.'
self._instance_name = name
@classmethod
def get_instance(cls: Type[T], name: str, **kwargs) -> T:
...
@classmethod
def get_current_instance(cls):
...
@classmethod
def check_instance_created(cls, name: str) -> bool:
...
@property
def instance_name(self) -> str:
...
ManagerMixin
是一个使用ManagerMeta
作为元类的基类,用于实现全局访问和实例管理。__init__
方法初始化实例,必须提供非空的name
参数。get_instance
方法根据name
创建或获取实例。根据name
检查实例是否存在。如果不存在,则创建一个新实例并添加到_instance_dict
。如果存在,忽略kwargs
并返回现有实例。get_current_instance
获取最近创建的实例。获取_instance_dict
中最后创建的实例。check_instance_created
检查是否已创建指定名称的实例。检查_instance_dict
中是否存在名为name
的实例。instance_name
属性返回实例名称。
这段代码定义了一个可以全局访问的单例模式的框架,使用元类和混合类(mixin)。为了更好地理解如何使用这个框架,我将给出一个具体的应用示例。
3 示例场景1
假设我们需要创建一个全局配置管理器,这个管理器可以在程序的多个地方被访问和修改,同时确保全局只有一个实例。
3.1 定义配置管理器类
首先,我们利用 ManagerMixin
和 ManagerMeta
来定义一个 ConfigurationManager
类。
class ConfigurationManager(ManagerMixin):
def __init__(self, name, settings={}):
super().__init__(name)
self.settings = settings
def set(self, key, value):
"""Set a configuration value."""
self.settings[key] = value
def get(self, key):
"""Get a configuration value."""
return self.settings.get(key, None)
3.2 使用全局配置管理器
接下来,我们将展示如何在不同部分的代码中创建和访问这个全局的配置管理器实例。在 init(self, name, settings={}) 方法中,name 参数起着关键的作用,它用于标识和区分不同的 ConfigurationManager 实例。这一设计是基于 ManagerMixin 和 ManagerMeta 提供的全局访问和单例管理机制。ManagerMixin 和 ManagerMeta 设计的目的是实现单例模式,即确保每个具有特定名称的类实例是唯一的。这通过内部的 _instance_dict 来管理每个实例的创建和存储实现,其中每个实例都与一个特定的 name 关联。
当你通过 get_instance 方法请求一个实例时,系统会检查是否已经存在一个与提供的 name 相关联的实例。如果存在,它将返回该实例;如果不存在,它将创建一个新实例。
在程序的初始化阶段或配置读取阶段,我们可以创建和初始化 ConfigurationManager
的实例。
# 创建全局配置管理器实例
config_manager = ConfigurationManager.get_instance('global_config', settings={'debug_mode': False})
# 修改设置
config_manager.set('api_key', '12345')
在程序的其他部分,我们可以通过名称访问这个已经创建的配置管理器实例,而不需要再次创建它。
# 在另一个文件或模块中访问全局配置管理器
config_manager = ConfigurationManager.get_instance('global_config')
api_key = config_manager.get('api_key')
print(f"API Key: {api_key}")
假设一个应用程序需要连接到两个不同的数据库
这里,production_db 和 development_db 作为 name 的值,标识了两个不同的数据库配置管理器实例。这使得它们可以存储和管理独立的设置,而不会相互干扰。这种方式在实际开发中非常实用,尤其是在大型或复杂的系统中。
db_config_prod = ConfigurationManager.get_instance('production_db', settings={'host': 'prod_host', 'port': 3306})
db_config_dev = ConfigurationManager.get_instance('development_db', settings={'host': 'dev_host', 'port': 3306})
3.3 说明
通过 ManagerMixin
和 ManagerMeta
的设计,ConfigurationManager
类保证了即使在复杂的多模块程序中,global_config
的实例也只被创建一次。此外,ManagerMixin
提供的 get_instance
方法使得访问这些全局实例变得简单和一致。
这种方式特别适用于需要跨多个模块共享单个实例的场景,如配置管理、数据库连接管理等。它提供了一种结构化和线程安全的方式来管理全局实例。
4 示例场景2
4.1 代码实现
class MMLogger(Logger, ManagerMixin):
"""Formatted logger used to record messages.
``MMLogger`` can create formatted logger to log message with different
log levels and get instance in the same way as ``ManagerMixin``.
``MMLogger`` has the following features:
- Distributed log storage, ``MMLogger`` can choose whether to save log of
different ranks according to `log_file`.
- Message with different log levels will have different colors and format
when displayed on terminal.
Examples:
>>> logger = MMLogger.get_instance(name='MMLogger',
>>> logger_name='Logger')
>>> # Although logger has name attribute just like `logging.Logger`
>>> # We cannot get logger instance by `logging.getLogger`.
>>> assert logger.name == 'Logger'
>>> assert logger.instance_name = 'MMLogger'
>>> assert id(logger) != id(logging.getLogger('Logger'))
>>> # Get logger that do not store logs.
>>> logger1 = MMLogger.get_instance('logger1')
>>> # Get logger only save rank0 logs.
>>> logger2 = MMLogger.get_instance('logger2', log_file='out.log')
>>> # Get logger only save multiple ranks logs.
>>> logger3 = MMLogger.get_instance('logger3', log_file='out.log',
>>> distributed=True)
Args:
name (str): Global instance name.
logger_name (str): ``name`` attribute of ``Logging.Logger`` instance.
If `logger_name` is not defined, defaults to 'mmengine'.
log_file (str, optional): The log filename. If specified, a
``FileHandler`` will be added to the logger. Defaults to None.
log_level (str): The log level of the handler. Defaults to
'INFO'. If log level is 'DEBUG', distributed logs will be saved
during distributed training.
file_mode (str): The file mode used to open log file. Defaults to 'w'.
distributed (bool): Whether to save distributed logs, Defaults to
false.
file_handler_cfg (dict, optional): Configuration of file handler.
Defaults to None. If ``file_handler_cfg`` is not specified,
``logging.FileHandler`` will be used by default. If it is
specified, the ``type`` key should be set. It can be
``RotatingFileHandler``, ``TimedRotatingFileHandler``,
``WatchedFileHandler`` or other file handlers, and the remaining
fields will be used to build the handler.
Examples:
>>> file_handler_cfg = dict(
>>> type='TimedRotatingFileHandler',
>>> when='MIDNIGHT',
>>> interval=1,
>>> backupCount=365)
`New in version 0.9.0.`
"""
def __init__(self,
name: str,
logger_name='mmengine',
log_file: Optional[str] = None,
log_level: Union[int, str] = 'INFO',
file_mode: str = 'w',
distributed=False,
file_handler_cfg: Optional[dict] = None):
Logger.__init__(self, logger_name)
ManagerMixin.__init__(self, name)
# Get rank in DDP mode.
if isinstance(log_level, str):
log_level = logging._nameToLevel[log_level]
global_rank = _get_rank()
device_id = _get_device_id()
# Config stream_handler. If `rank != 0`. stream_handler can only
# export ERROR logs.
stream_handler = logging.StreamHandler(stream=sys.stdout)
# `StreamHandler` record month, day, hour, minute, and second
# timestamp.
stream_handler.setFormatter(
MMFormatter(color=True, datefmt='%m/%d %H:%M:%S'))
# Only rank0 `StreamHandler` will log messages below error level.
if global_rank == 0:
stream_handler.setLevel(log_level)
else:
stream_handler.setLevel(logging.ERROR)
stream_handler.addFilter(FilterDuplicateWarning(logger_name))
self.handlers.append(stream_handler)
@classmethod
def get_current_instance(cls) -> 'MMLogger':
"""Get latest created ``MMLogger`` instance.
:obj:`MMLogger` can call :meth:`get_current_instance` before any
instance has been created, and return a logger with the instance name
"mmengine".
Returns:
MMLogger: Configured logger instance.
"""
if not cls._instance_dict:
cls.get_instance('mmengine')
return super().get_current_instance()
业务代码中使用
if not MMLogger.check_instance_created('OpenCompass'):
logger = MMLogger.get_instance('OpenCompass',
logger_name='OpenCompass',
log_level=log_level)
else:
logger = MMLogger.get_instance('OpenCompass')
4.2 代码解释
这段代码定义了一个 MMLogger
类,继承自 Logger
类和 ManagerMixin
混合类。MMLogger
用于创建格式化的日志记录器,具有全局实例管理功能,并支持不同日志级别的消息记录。这个类增加了多个特性,包括分布式日志存储和终端彩色显示等。以下是详细的解释:
MMLogger 类定义与功能
MMLogger
结合了Logger
的日志功能和ManagerMixin
的全局实例管理能力。- 它支持不同的日志级别,并且可以根据配置将日志存储到不同的文件中。
- 日志消息可以在终端以不同的颜色和格式显示,提高日志的可读性。
构造函数 (init)
-
参数解释:
name
: 全局实例名称,用于通过ManagerMixin
管理。logger_name
:Logger
实例的名称,默认为 “mmengine”。log_file
: 如果指定,日志将被写入到这个文件中。log_level
: 日志级别,默认为 “INFO”。file_mode
: 文件打开模式,默认为 ‘w’,表示写模式。distributed
: 是否保存分布式日志,默认为False
。file_handler_cfg
: 文件处理配置,用于创建不同类型的FileHandler
。
-
实现细节:
- 首先,根据
logger_name
初始化Logger
基类。 - 调用
ManagerMixin
的初始化函数,设置实例名。 - 根据
log_level
的字符串表示转换为对应的日志级别数值。 - 获取当前设备的全局排名 (
global_rank
) 和设备 ID (device_id
),用于分布式日志记录。 - 根据
global_rank
配置stream_handler
,只有在全局排名为 0 的设备上,才会记录低于错误级别的日志。 - 如果指定了
log_file
,将根据是否是分布式环境来配置文件名,包括主机名和排名等信息。 - 根据
file_handler_cfg
配置或创建默认的FileHandler
,并设置格式化器和日志级别。
- 首先,根据
类方法和属性
get_current_instance
: 获取最近创建的MMLogger
实例。如果没有实例被创建,它会首先通过默认名称 “mmengine” 创建一个实例。
用法示例
MMLogger.get_instance
方法用于获取或创建MMLogger
的实例,确保每个实例名称对应唯一的日志记录器对象。- 使用
MMLogger
记录的日志不受第三方日志配置的影响,这通过实例管理和专用的获取方法来保证。 - 日志文件可以根据是否是分布式训练来分别保存,支持复杂的文件处理配置。
cls._instance_dict的说明
instance_dict = cls._instance_dict这段代码的意思是从类 cls
中获取 _instance_dict
属性,并将其赋值给本地变量 instance_dict
。这里的 _instance_dict
是一个类属性,通常用于存储与该类相关的一些实例数据。
ManagerMixin
类中,_instance_dict
被用作一个有序字典(OrderedDict
),用来存储该类的所有实例。这允许类通过名字快速访问其任意实例,实现了一种全局访问和管理的模式。cls._instance_dict
通常在类方法中被访问或修改,如创建新实例、获取实例、检查实例是否已创建等。
在 ManagerMixin
的类方法 get_instance
中,cls._instance_dict
被用来检查一个具有特定名称的实例是否已存在。如果存在,则返回该实例;如果不存在,则创建一个新实例,将其添加到 _instance_dict
中,然后返回新创建的实例。这种方式确保了每个实例的唯一性,并允许全局管理。
get_instance
方法用于获取名为 name
的实例。如果这个名字的实例不存在,方法会创建一个新的实例并将其添加到 _instance_dict
中。
为何需要 MMLogger(Logger, ManagerMixin)
在Python中,logging.Logger
类本身并不是单例模式。虽然 logging.getLogger(name)
能保证同一个 name
返回相同的 Logger
实例,但这是通过内部维护一个字典实现的,而不是真正意义上的单例模式。Logger
实例可以多次创建并拥有不同的配置,这意味着不同的地方可能会创建具有不同行为的相同名称的 Logger
实例。
MMLogger
类继承自 Logger
并混合使用了 ManagerMixin
,这样的设计是为了解决以下几个方面的需求:
-
增强的单例管理:
ManagerMixin
提供了一个基于名称的实例管理机制,确保全局只有一个唯一的实例,即使是在不同的模块和组件中也是如此。- 这种管理方式使得
MMLogger
可以在整个应用程序中共享和访问,无论是在哪个模块中创建。
-
定制化和独立性:
- 使用
MMLogger
允许开发者对日志系统进行定制化配置,而不受默认logging
模块的全局配置的影响。例如,可以实现高度定制的格式化器、处理器和过滤器。 - 它可以避免第三方库或其他模块中的日志配置对自己应用的影响。
- 使用
-
分布式日志功能:
MMLogger
设计了分布式日志功能,能够根据运行环境(如不同的服务器或处理器)自动调整日志的存储和显示。这在大规模分布式应用程序中非常有用,特别是在机器学习或数据处理等领域。
-
日志的动态配置:
MMLogger
通过ManagerMixin
提供的get_instance
方法允许动态创建或获取日志实例,并且可以为不同的日志实例设置不同的文件路径、日志级别等参数。- 这使得日志系统的扩展性和灵活性大大提高,能够满足不同场景下的需求。
总结
虽然 logging.getLogger(name) 提供了一种方便的方式来复用 Logger 实例,但它并没有实现真正意义上的单例模式,因为 Logger 的配置是可变的,且可以在不同的上下文中被修改,导致潜在的配置冲突和行为不一致。这种设计在灵活性方面有优势,允许对 Logger 进行细粒度的配置,但也需要开发者在大型应用中仔细管理日志配置,以避免意外的行为。假设在一个大型应用程序中,有两个模块都使用名为 “MyApp” 的 Logger。在模块A中,可能设置了将日志输出到控制台的处理器,而在模块B中,则可能添加了一个将日志写入文件的处理器。如果模块A先于模块B执行,那么在模块B执行时,“MyApp” 的 Logger 实例已经包含了一个输出到控制台的处理器,这可能不是模块B期望的行为。
但 MMLogger
的设计旨在提供更加强大和灵活的日志管理能力,通过 ManagerMixin
实现更严格的单例控制,以及通过继承和扩展 Logger
提供的功能来满足特定的业务需求。这种设计模式在需要高度定制化和在多个地方需要访问同一日志实例的应用中尤为重要。