【MMCV 源码解读】一、Config(配置文件相关)

前言

这个文件是关于mmcv中的配置文件加载部分的代码,代码比较多,也比较杂,直接看这个博客估计很难能看懂,建议先看下 b站大佬: 手撸OpenMMlab系列教程(mmcv,mmsegmentation). 的教学视频,把config配置加载相关的代码从mmcv和mmdet中扒出来,自己再初略的debug一遍,再边看这篇博客和下面的Reference边自己再debug,我估计只要这样才能差不多看懂Config里面的主要功能。

因为我自己也是这样学的,边看别人的解读边debug,甚至边百度函数作用,真的是需要debug很多遍才能看懂。当然这也看个人需求,如果你只是要知道各个函数作用,完全可以没必要看这篇,这篇博客就是将源码的,不是看源码的看官可以直接绕道了。

另外想说的是,不要太相信我写的注释,因为我自己也不敢说100%理解对的,里面有一些辅助性的函数我也是没有看的,写这个专题的主要目的是为了掌握主要功能的源码原理,一些次要的辅助性功能我会有选择的舍弃不讲。

最后,强调下,一定要debug,debug,debug!!!

一、通过dict生成config

示例a.py:

from mmcv import Config

cfg = Config(dict(a=1, b=dict(b1=[0, 1])))

print(cfg)  # Config (path: None): {'a': 1, 'b': {'b1': [0, 1]}}

源码实现:

class Config:
    def __init__(self, cfg_dict=None, cfg_text=None, filename=None):
        """
        Config类的初始化函数 主要是给配置文件类Config实例属性赋值
        包括:配置文件内容'_cfg_dict'  配置文件名'_filename'  配置文件绝对地址+配置文件内容'_text'
        :param cfg_dict: 配置文件内容 dict格式
        :param cfg_text: 配置文件绝对地址+配置文件内容 string格式
        :param filename: 配置文件名(不是路径)
        """
        if cfg_dict is None:  # check
            cfg_dict = dict()
        elif not isinstance(cfg_dict, dict):
            raise TypeError('cfg_dict must be a dict, but '
                            f'got {
      
      type(cfg_dict)}')
        for key in cfg_dict:
            if key in RESERVED_KEYS:
                raise KeyError(f'{
      
      key} is reserved for config file')

        # __setattr__魔法函数 主要用于类实例属性赋值
        # 赋值_cfg_dict=ConfigDict(cfg_dict) 可以像类对象一样 优雅的访问字典 cfg.b.b1
        super(Config, self).__setattr__('_cfg_dict', ConfigDict(cfg_dict))
        super(Config, self).__setattr__('_filename', filename)  # 赋值_filename=filename
        if cfg_text:
            text = cfg_text
        elif filename:
            with open(filename, 'r') as f:
                text = f.read()
        else:
            text = ''
        super(Config, self).__setattr__('_text', text)  # 赋值_text=text

    def __setattr__(self, name, value):
        # 给对象的属性赋值
        if isinstance(value, dict):
            value = ConfigDict(value)
        self._cfg_dict.__setattr__(name, value)
        
    def __getattr__(self, name):
        # 使用.获取属性的时候,如果该属性存在就输出其值,如果不存在则会去找__getatrr__
        return getattr(self._cfg_dict, name)   

这里涉及几个额外的小功能:

1.1、字典对象实现属性访问

这个功能能够让配置信息可以像字典一样优雅的被访问,还是上面的例子a.py:

from mycv import Config

cfg = Config(dict(a=1, b=dict(b1=[0, 1])))

print(cfg)  # Config (path: None): {'a': 1, 'b': {'b1': [0, 1]}}
print(cfg.b.b1)  # [0, 1]  可以通过这种 对象.key 的形式访问配置信息

源码(也就是上面的_cfg_dict属性赋值操作调用ConfigDict类):

class ConfigDict(Dict):
    """
    可以像类对象一样 优雅的访问字典
    """
    def __missing__(self, name):
        raise KeyError(name)

    def __getattr__(self, name):
        try:
            value = super(ConfigDict, self).__getattr__(name)  # value = 对象.key
        except KeyError:
            ex = AttributeError(f"'{
      
      self.__class__.__name__}' object has no "
                                f"attribute '{
      
      name}'")
        except Exception as e:
            ex = e
        else:
            return value
        raise ex

打开Dict内部其实可以发现这里这个 self.getitem(name) ,其实就说调用self[name]从dict字典中直接读取key相应的value值实现的。

1.2、字典对象 pretty 输出

当字典对象层级非常深,直接 print 打印时候会非常难看的问题,针对这个问题mmcv的Config类中设计了pretty_text函数,示例b.py:

# pretty
a = 1
b = dict(b1=[0, 1, 2], b2=None)

# I:\Miniconda\open-mylab\mydetection\tests\test_config\b.py --work-dir work_dir --seed 0

debug:
在这里插入图片描述
源码实现(就是一个打印工具 就不仔细分析了):

@property
    def pretty_text(self):
        """
        这个函数是对于任意的字典对象都生成dict_text 为了解决当字典对象层级非常深,直接print打印时候会非常难看的问题
        :return:
        """
        ......
        cfg_dict = self._cfg_dict.to_dict()
        text = _format_dict(cfg_dict, outest_level=True)
        # copied from setup.cfg  定义格式化输出为PEP8
        yapf_style = dict(
            based_on_style='pep8',
            blank_line_before_nested_class_or_def=True,
            split_before_expression_after_opening_paren=True)
        # 输入text:没有经过格式化,但是能够被 FormatCode 解析的 dict_text
        # 输出text:符合pep8规范的文本 可以直接打印
        text, _ = FormatCode(text, style_config=yapf_style, verify=True)

        return text

1.3、@property

从上面的debug图可以看到cfg有三个共有属性(filename、pretty_text、text)和三个保护属性(_cfg_dict、_filename、_text)。但是上面的__init__函数明明只赋值了三个属性,哪来的6个属性,而且后面的三个属性为什么又是保护类型的呢?

原因是@property和__init__结合使用可以将__init__中定义的属性变为只读属性(保护),防止外部变动,而用这个装饰器@property所定义的类又会自动的将用这个方法生成相同名字的公有属性,供外部改动,这样子可以做到有效的保护属性。

主要是因为这三个函数(@property装饰器):

   @property
    def filename(self):
        # @property和__init__结合使用可以将__init__中定义的属性变为只读属性(保护),防止外部变动,
        # 而用这个装饰器@property所定义的类又会自动的将用这个方法生成相同名字的公有属性,供外部改动,这样子可以做到有效的保护属性。
        # 效果:生成self._filename只读属性(保护)、self.filename公用属性
        return self._filename

    @property
    def text(self):
        # 创建只读属性self._text
        return self._text

    @property
    def pretty_text(self):
        """
        这个函数是对于任意的字典对象都生成dict_text 为了解决当字典对象层级非常深,直接print打印时候会非常难看的问题
        :return:
        """
        ......
        cfg_dict = self._cfg_dict.to_dict()
        text = _format_dict(cfg_dict, outest_level=True)
        # copied from setup.cfg  定义格式化输出为PEP8
        yapf_style = dict(
            based_on_style='pep8',
            blank_line_before_nested_class_or_def=True,
            split_before_expression_after_opening_paren=True)
        # 输入text:没有经过格式化,但是能够被 FormatCode 解析的 dict_text
        # 输出text:符合pep8规范的文本 可以直接打印
        text, _ = FormatCode(text, style_config=yapf_style, verify=True)

        return text

二、通过配置文件生成 config

该功能最为常用,配置文件可以是 py、yaml、yml 和 json 等格式。主要用到Config.fromfile函数。

实例b.py:

a = 1
b = dict(b1=[0, 1, 2], b2=None)

# I:\Miniconda\open-mylab\mydetection\tests\test_config\b.py --work-dir work_dir --seed 0

fromfile函数源码解读:

   @staticmethod
    def fromfile(filename,
                 use_predefined_variables=True,
                 import_custom_modules=True):
        """
        这个函数是通过输入的配置文件绝对地址,再生成配置信息dict,最后返回配置文件类Config对象
        :param filename: 配置文件的绝对路径
        :param use_predefined_variables: 是否使用预先设置的变量
        :param import_custom_modules:
        :return: 返回一个Config类对象
                 包含三个self.属性(filename、pretty_text、text) + 3个protected属性(_cfg_dict、_filename、_text)
        """
        # 第一步、根据配置文件绝对路径返回配置文件信息cfg_dict和cfg_text
        # cfg_dict: filename配置文件的内容(dict格式)
        # cfg_text: filename配置文件的完整路径名 + 配置文件的内容(string格式)
        cfg_dict, cfg_text = Config._file2dict(filename,
                                               use_predefined_variables)
        if import_custom_modules and cfg_dict.get('custom_imports', None):
            import_modules_from_strings(**cfg_dict['custom_imports'])
        # 第二步、根据配置文件信息cfg_dict返回配置文件类Config对象
        return Config(cfg_dict, cfg_text=cfg_text, filename=filename)

这里的@staticmethod表示这个函数是静态函数,不需要用到self。

fromfile的主要有两个步骤,第二个步骤和我们在第一节中讲的Config类完全一致,就说根据配置信息cfg_dict,返回配置文件类Cofig对象。所以这里我们重点说说第一步:根据配置文件绝对路径返回配置文件信息cfg_dict和cfg_text。

Config._file2dict函数源码:

   @staticmethod
    def _file2dict(filename, use_predefined_variables=True):
        """
        通过配置文件绝对地址得到这个配置文件的内容
        方法呢是通过sys.path.insert和import_module这种系统路径的方式拿到配置文件内容的
        :param filename: filename配置文件绝对地址
        :param use_predefined_variables: 是否使用预定义的变量 默认True
        :return cfg_dict: filename配置文件的内容(dict格式)
        :return cfg_text: filename配置文件的完整路径名+配置文件的内容(string格式)
        """
        filename = osp.abspath(osp.expanduser(filename))  # 获取配置文件绝对路径
        check_file_exist(filename)  # 检测配置文件是否存在
        fileExtname = osp.splitext(filename)[1]  # 获取配置文件的后缀
        if fileExtname not in ['.py', '.json', '.yaml', '.yml']:  # 只支持这五种类型的配置文件解读
            raise IOError('Only py/yml/yaml/json type are supported now!')

        with tempfile.TemporaryDirectory() as temp_config_dir:  # 打开临时文件夹
            temp_config_file = tempfile.NamedTemporaryFile(  # 创建临时文件对象
                dir=temp_config_dir, suffix=fileExtname)
            if platform.system() == 'Windows':
                temp_config_file.close()  # wins环境下关闭临时文件
            temp_config_name = osp.basename(temp_config_file.name)  # 获取临时文件名 'tmpzdgcua1u.py'
            # 是否使用预定义的变量 Substitute predefined variables
            if use_predefined_variables:
                # 使用正则表达式将配置文件中预定义的变量替换为本次打开的配置文件相关信息 -> 临时文件
                Config._substitute_predefined_vars(filename,
                                                   temp_config_file.name)
            else:
                # 否则直接将本次打开的配置文件中的信息直接复制到临时文件
                shutil.copyfile(filename, temp_config_file.name)
            # Substitute base variables from placeholders to strings
            # 从基类引用变量
            base_var_dict = Config._pre_substitute_base_vars(
                temp_config_file.name, temp_config_file.name)

            # 处理.py结尾的配置文件 将临时配置文件由系统路径中加载进来 再import到变量中 就完成了读取配置文件信息的操作
            if filename.endswith('.py'):
                temp_module_name = osp.splitext(temp_config_name)[0]  # 获取临时文件名 'tmpzdgcua1u'
                sys.path.insert(0, temp_config_dir)  # 将这个临时文件名存入系统路径中 类似可以直接import
                Config._validate_py_syntax(filename)  # 检测这个配置文件的语法是否合格
                # import系统中的temp_module_name模块(配置文件)类似于import torch mod=module模块
                mod = import_module(temp_module_name)
                sys.path.pop(0)  # 再将这个模块从系统路径中删除
                # 从mod=配置文件中读出配置信息  cfg_dict={'a': 1, 'b': {'b1': [0, 1, 2], 'b2': None}, 'c': (1, 2), 'd': 'string'}
                cfg_dict = {
    
    
                    name: value
                    for name, value in mod.__dict__.items()
                    if not name.startswith('__')
                }
                # 删除import过的配置文件module
                del sys.modules[temp_module_name]
            # 如果配置文件不是以.py结尾 就调用mmcv写好的io类load 导入其他格式配置文件
            elif filename.endswith(('.yml', '.yaml', '.json')):
                import mmcv
                cfg_dict = mmcv.load(temp_config_file.name)
            # close temp file
            temp_config_file.close()  # 关闭临时文件 之后会被删除

        cfg_text = filename + '\n'  # 'I:\\Miniconda\\open-mylab\\mydetection\\tests\\test_config\\a.py'
        with open(filename, 'r', encoding='utf-8') as f:
            # cfg_text = 配置文件的绝对路径 + 配置文件的内容
            cfg_text += f.read()

        # BASE_KEY(_base_)在cfg_dict中 也就是说这个配置文件还继承了其他的配置文件
        # 这种情况很常见 需要将继承的配置文件信息一起读进来
        # 这段代码主要做两件事:merge base cfg_dict + merge base cfg_text
        if BASE_KEY in cfg_dict:
            cfg_dir = osp.dirname(filename)  # 拿到当前打开配置文件的目录
            base_filename = cfg_dict.pop(BASE_KEY)  # 拿到当前配置文件的'_base_'键对应的值
            base_filename = base_filename if isinstance(  # 得到所有需要继承的配置文件 list
                base_filename, list) else [base_filename]

            cfg_dict_list = list()  # 把base dict一个个的append进来
            cfg_text_list = list()  # 把base text一个个的append进来
            # 将所有需要继承的配置文件信息append进cfg_dict_list和cfg_text_list中
            for f in base_filename:
                # 通过调用自身_file2dict得到父类(_base_)的配置文件信息
                _cfg_dict, _cfg_text = Config._file2dict(osp.join(cfg_dir, f))
                cfg_dict_list.append(_cfg_dict)
                cfg_text_list.append(_cfg_text)

            # merge config dict
            # base_cfg_dict: 存放所有base需要继承的配置文件的所有配置信息
            # 一般有:model datasets Optimizer checkpoint_config等配置信息
            base_cfg_dict = dict()
            for c in cfg_dict_list:
                if len(base_cfg_dict.keys() & c.keys()) > 0:
                    raise KeyError('Duplicate key is not allowed among bases')
                base_cfg_dict.update(c)  # 把所有base中的key和value一个个加进来 且key不能重复 也就是后出现的不能覆盖前面出现的

            # Subtitute base variables from strings to their actual values
            cfg_dict = Config._substitute_base_vars(cfg_dict, base_var_dict,
                                                    base_cfg_dict)

            # 把配置文件的字典内容(cfg_dict)合并到所有base配置文件的字典内容(base_cfg_dict)中
            base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict)
            cfg_dict = base_cfg_dict

            # merge cfg_text
            cfg_text_list.append(cfg_text)
            cfg_text = '\n'.join(cfg_text_list)

        return cfg_dict, cfg_text

这个函数的附加功能非常的多,下面我们逐一开始介绍

2.1、替换预定义变量

这个功能其实就是根据_file2dict的参数use_predefined_variables来实现自动替换预定义变量功能,主要由函数Config._substitute_predefined_vars实现。在默认情况下只支持替换四个预定义的变量:fileDirname、fileBasename、fileBasenameNoExtension、fileExtname。当然了如果想要定义更多预定义变量可以在下面的源码中添加就是了:

    @staticmethod
    def _substitute_predefined_vars(filename, temp_config_name):
        """
        支持一些预定义变量  使用正则表达式将配置文件中预定义的变量替换为本次打开的配置文件相关信息 -> 临时文件
        :param filename: 配置文件绝对路径
        :param temp_config_name: 临时文件绝对路径
        """
        file_dirname = osp.dirname(filename)  # 配置文件的文件夹地址 'I:\\Miniconda\\open-mylab\\mydetection\\tests\\test_config'
        file_basename = osp.basename(filename)  # 配置文件的文件名(不是文件地址 单单就是.py文件名) 'a.py'
        file_basename_no_extension = osp.splitext(file_basename)[0]  # 出去扩展名的配置文件的文件名 'a'
        file_extname = osp.splitext(filename)[1]  # 配置文件的扩展名 '.py'
        support_templates = dict(  # 暂时只支持以下四个预定义变量 如果还需要可以自行在下面定义
            fileDirname=file_dirname,  # 当前打开配置文件的目录名
            fileBasename=file_basename,  # 当前打开配置文件的文件名
            fileBasenameNoExtension=file_basename_no_extension,  # 当前打开配置文件不包含扩展名的文件名
            fileExtname=file_extname)  # 当前打开配置文件不包含扩展名的文件名
        with open(filename, 'r', encoding='utf-8') as f:  # 打开配置文件 读取配置信息
            # Setting encoding explicitly to resolve coding issue on windows
            config_file = f.read()  # str 配置文件信息 'a = 1\nb = dict(b1=[0, 1, 2], b2=None)\nc = (1, 2)\nd = \'string\''
        for key, value in support_templates.items():  # 遍历这四个预定于变量 一个个替换config_file中的预定义变量(如果有)
            regexp = r'\{\{\s*' + str(key) + r'\s*\}\}'
            value = value.replace('\\', '/')
            config_file = re.sub(regexp, value, config_file)
        with open(temp_config_name, 'w') as tmp_config_file:  # 再更新临时文件中的配置信息 写入替换了预定义变量后的配置信息(如果有)
            tmp_config_file.write(config_file)

2.2、导入自定义模块

这个模块是由Config.fromfile函数控制的,我们可以看到这个函数除了传入filename和use_predefined_variables变量,还传入了import_custom_modules变量,这个变量就是控制是否导入自定义模块。默认为True,即当cfg中存在 custom_imports 键时候会对里面的内容进行自动导入,其输入格式要么是 str 要么是 list[str],表示待导入的模块路径。

一个比较典型的用法:假设你在 mmdet 中新增了自定义模型 MobileNet,你需要在 mmdet/models/backbones/init.py 里面加入如下代码,否则在调用时候会提示该模块没有被注册进去:

from .mobilenet import MobileNet

但是上述做法在某些场景下会比较麻烦。例如该模块处于非常深的层级,那么就需要逐层修改 init.py,有了本参数,便可以采用如下做法优雅避免:

# .py 文件里面存储如下内容
custom_imports = dict(
    imports=['mmdet.models.backbones.mobilenet'],
    allow_failed_imports=False)

# 自动导入 mmdet.models.backbones.mobilenet
Config.fromfile(cfg_file, import_custom_modules=True)

好了,看了这个功能的用法和使用场景,下面一起来看看它的源码实现:

mmcv/utils/misc.import_modules_from_strings():

def import_modules_from_strings(imports, allow_failed_imports=False):
    """
    优雅的从指定路径中导入自定义模块  就不需要在__init__中一层层的写
    :param imports: 导入的模块路径名(list | str | None) 如:['mmdet.models.backbones.mobilenet']
    :param allow_failed_imports: True 导入失败返回None  False 导入失败返回False
    :return: imported list[modules] 返回导入的自定义模块
    :example:
    custom_imports = dict(imports=['mmdet.models.backbones.mobilenet'], allow_failed_imports=False)
    Config.fromfile(cfg_file, import_custom_modules=True)
    """
    
    if not imports:
        return
    single_import = False  # 是否只导入一个自定义模块
    if isinstance(imports, str):
        single_import = True
        imports = [imports]
    if not isinstance(imports, list):
        raise TypeError(
            f'custom_imports must be a list but got type {
      
      type(imports)}')
    imported = []  # 存放所有导入的自定义模块
    for imp in imports:
        if not isinstance(imp, str):
            raise TypeError(
                f'{
      
      imp} is of type {
      
      type(imp)} and cannot be imported.')
        try:
            imported_tmp = import_module(imp)  # 先存放在temp中
        except ImportError:   # 判断有没有问题
            if allow_failed_imports:
                warnings.warn(f'{
      
      imp} failed to import and is ignored.',
                              UserWarning)
                imported_tmp = None
            else:
                raise ImportError
        imported.append(imported_tmp)  # 没问题了再从temp append进imported中
    if single_import:  # 如果single_import=True 只导入一个自定义模块
        imported = imported[0]
    return imported  # return 需要导入的自定义模块

2.3、从基类配置文件继承配置信息

实现了三种继承,一种是不含重复键值对从基类配置文件继承,第二种是含重复键值对从基类配置文件继承,第三种是从具有忽略字段的配置文件继承。这个功能实现的主要的代码是Config._file2dict函数中部分代码实现的:

        # BASE_KEY(_base_)在cfg_dict中 也就是说这个配置文件还继承了其他的配置文件
        # 这种情况很常见 需要将继承的配置文件信息一起读进来
        if BASE_KEY in cfg_dict:
            cfg_dir = osp.dirname(filename)  # 拿到当前打开配置文件的目录
            base_filename = cfg_dict.pop(BASE_KEY)  # 拿到当前配置文件的'_base_'键对应的值
            base_filename = base_filename if isinstance(  # 得到所有需要继承的配置文件 list
                base_filename, list) else [base_filename]

            cfg_dict_list = list()
            cfg_text_list = list()
            # 将所有需要继承的配置文件信息append进cfg_dict_list和cfg_text_list中
            for f in base_filename:
                # 通过调用自身_file2dict得到父类(_base_)的配置文件信息
                _cfg_dict, _cfg_text = Config._file2dict(osp.join(cfg_dir, f))
                cfg_dict_list.append(_cfg_dict)
                cfg_text_list.append(_cfg_text)

            # base_cfg_dict: 存放所有base需要继承的配置文件的所有配置信息 一般有:model datasets Optimizer checkpoint_config等配置信息
            base_cfg_dict = dict()
            for c in cfg_dict_list:
                if len(base_cfg_dict.keys() & c.keys()) > 0:
                    raise KeyError('Duplicate key is not allowed among bases')
                base_cfg_dict.update(c)

            # Subtitute base variables from strings to their actual values
            cfg_dict = Config._substitute_base_vars(cfg_dict, base_var_dict,
                                                    base_cfg_dict)

            # 把配置文件a的字典内容合并到配置文件b的字典内容中
            base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict)
            cfg_dict = base_cfg_dict

            # merge cfg_text
            cfg_text_list.append(cfg_text)
            cfg_text = '\n'.join(cfg_text_list)

先说说第一种实例:不含重复键值对从基类配置文件继承

规则:出现新的key,直接添加进去就是了

a.py:

a = 1
b = dict(b1=[0, 1, 2], b2=None)

b.py:

_base_ = './a.py'
c = (1, 2)
d = 'string'

传入配置文件b.py,输出cfg配置:
在这里插入图片描述

再说说第二种实例:含重复键值对从基类配置文件继承

规则:出现相同键值对,merge_a_into_b(a,b) 直接将a中的key对应的value覆盖在b中对应的key上
c.py:

_base_ = './a.py'
b = dict(b1=2, b2=1)
c = (1, 2)

传入配置文件c.py输出cfg配置:
在这里插入图片描述

再说说第三种实例:从具有忽略字段的配置文件继承

规则:直接忽略a.py中相应的字段

d.py:

_base_ = './config_a.py'
b = dict(_delete_=True, b2=None, b3=0.1)
c = (1, 2)

传入配置文件d.py输出cfg配置:
在这里插入图片描述
这个做法很常用,比如:在 RetinaNet 算法中,其采用的 bbox 回归 loss 配置如下:

loss_bbox=dict(type='L1Loss', loss_weight=1.0,其他参数)

FASF 算法中采用的是 IOULoss,现在要做的事情是在 FASF 配置中自动覆盖掉 base 配置中的 L1Loss,可以采用如下做法:

loss_bbox=dict(
    _delete_=True,
    type='IoULoss',
    eps=1e-6,
    loss_weight=1.0,
    reduction='none')

如果没有 delete=True 参数,则两个配置会自动合并,L1Loss 中的其他参数始终会保留,无法删除,这肯定是不正确的( IoULoss 中不需要 L1Loss 的初始化参数),现在通过引入 delete 保留字则可以实现忽略 base 相关配置,直接采用新配置文件字段功能。

看完上面三种实例,再看下面的源码马上就豁然开朗了,就说通过递归自身的方式实现的:

    @staticmethod
    def _merge_a_into_b(a, b, allow_list_keys=False):
        """
        将dict a merge 到 dict b
        :param a: The source dict to be merged into ``b``
        :param b: The origin dict to be fetch keys from ``a``
        :param allow_list_keys: True 允许a有int string类型的key而且还会替代b(list)中相应index的value 如:'0' 默认False
        :return: The modified dict of ``b`` using ``a``
        :Examples:
            # Normally merge a into b.
            >>> Config._merge_a_into_b(
            ...     dict(obj=dict(a=2)), dict(obj=dict(a=1)))
            {'obj': {'a': 2}}

            # Delete b first and merge a into b.
            >>> Config._merge_a_into_b(
            ...     dict(obj=dict(_delete_=True, a=2)), dict(obj=dict(a=1)))
            {'obj': {'a': 2}}

            # b is a list
            >>> Config._merge_a_into_b(
            ...     {'0': dict(a=2)}, [dict(a=1), dict(b=2)], True)
            [{'a': 2}, {'b': 2}]
        """
        # 情况1、不含重复键值对从基类配置文件继承:直接一直递归直接else即可
        # 情况2、含重复键值对从基类配置文件继承:重复的键用a的key的value覆盖b相应key的value
        # 情况3、从具有忽略字段的配置文件继承:直接忽略对应的字段
        b = b.copy()
        for k, v in a.items():
            # 如果allow_list_keys=True and k是int string类型(如'1')的key and b是list类型 就替换b中对应index的value
            if allow_list_keys and k.isdigit() and isinstance(b, list):
                k = int(k)
                if len(b) <= k:
                    raise KeyError(f'Index {
      
      k} exceeds the length of list {
      
      b}')
                b[k] = Config._merge_a_into_b(v, b[k], allow_list_keys)
            # 如果value是dict类型 and k在b中已经有了 and value中DELETE_KEY
            # not v.pop(DELETE_KEY, False)=True 进入 而且pop忽略掉第一个value
            elif isinstance(v, dict) and k in b and not v.pop(DELETE_KEY, False):
                # check type
                allowed_types = (dict, list) if allow_list_keys else dict
                if not isinstance(b[k], allowed_types):
                    raise TypeError(
                        f'{
      
      k}={
      
      v} in child config cannot inherit from base '
                        f'because {
      
      k} is a dict in the child config but is of '
                        f'type {
      
      type(b[k])} in base config. You may set '
                        f'`{
      
      DELETE_KEY}=True` to ignore the base config')
                # 直接调用自身递归方式merge
                b[k] = Config._merge_a_into_b(v, b[k], allow_list_keys)
            # 否则 value=dict/string/int or b中没有相同的k 直接添加即可
            else:
                b[k] = v
        return b

2.4、从基类引用变量

实例base.py:

item1 = 'a'
item2 = dict(item3 = 'b')

e.py:

_base_ = ['./base.py']
item = dict(a={
    
    {
    
    _base_.item1}}, b={
    
    {
    
    _base_.item2.item3}})

传入配置文件e.py输出cfg配置:
在这里插入图片描述
源码实现(不写了,辅助性功能):

 @staticmethod
    def _pre_substitute_base_vars(filename, temp_config_name):
        """Substitute base variable placehoders to string, so that parsing
        would work."""
        ...

三、总结

重要的函数代码有fromfile、_file2dict、_merge_a_into_b、__init__四个函数。其中最重要的函数是_file2dict。

_file2dict函数大致的逻辑是:

# 1. 先解析最上层文件
mod = import_module(temp_module_name)
cfg_dict = {
    
    
    name: value
    for name, value in mod.__dict__.items()
    if not name.startswith('__')
}

if BASE_KEY in cfg_dict:
  # 2. 解析 base 文件(list)
  for f in base_filename:
    _cfg_dict, _cfg_text = Config._file2dict(osp.join(cfg_dir, f))
    cfg_dict_list.append(_cfg_dict)

  # 3. 不允许多个 base 文件有重复 key
  for c in cfg_dict_list:
    if len(base_cfg_dict.keys() & c.keys()) > 0:
        raise KeyError('Duplicate key is not allowed among bases')
  # 4. 合并 base 内容到同一个字典中
  base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict)
  
  return cfg_dict, cfg_text

Reference

知乎: MMCV 核心组件分析(四): Config.

BiliBili: 手撸OpenMMlab系列教程(mmcv,mmsegmentation).

Github: OpenMMLab.

Github: mmdetection最小学习版.

猜你喜欢

转载自blog.csdn.net/qq_38253797/article/details/121471389
今日推荐