通过名称实例化子类

时间:2019-01-25 12:59:51

标签: python-3.x

考虑以下示例:

from abc import ABC, abstractmethod


class Base(ABC):
    @abstractmethod
    def say(self):
        pass

    @classmethod
    def from_config(cls, config):
        kind = config['kind']
        # TODO: Look at all subclasses, find one with matching kind
        # TODO: and call its constructor with **config['parameters']


class ChildA(Base):
    kind = 'A'

    def say(self):
        return 'Hello, I am A'


class ChildB(Base):
    kind = 'B'

    def __init__(self, name):
        super().__init__()
        self._name = name

    def say(self):
        return 'Hello, I am {}'.format(self._name)


def main():
    configA = {
        'kind': 'A',
        'parameters': {}
    }
    configB = {
        'kind': 'B',
        'parameters': {
            'name': 'Foo'
        }
    }
    print(ChildA(**configA['parameters']).say())
    print(ChildB(**configB['parameters']).say())


if __name__ == '__main__':
    main()

我想在Base中实现一个类方法,该方法查看其所有子类(可能还有它们的子类,无穷大),并根据匹配的{{ 1}},即**config['parameters']

我的问题是:

  • 我可以使用kind来实现这一目标吗?
  • 如果类定义位于不同的模块中会发生什么?

1 个答案:

答案 0 :(得分:1)

在示例中,我将使用以下配置字典:

configA = {
    'kind': 'A',
    'parameters': {}
}

configB = {
    'kind': 'B',
    'parameters': {
        'name': 'Foo'
    }
}

configC = {
    'kind': 'C',
    'parameters': {
        'age': '17'
    }
}

configD = {  # ChildD inherits from both ChildC, ChildA
    'kind': 'D',
    'parameters': {
        'name': 'Bar',
        'age': '17'
    }
}

第一个问题的答案是“是”。您可以通过__subclasses__进行递归迭代(如果存在)。我将extract_all_subclasses定义为一个函数,因为它可以用于(大约任何一个)类。

def extract_all_subclasses(cls):
    output = set() 
    # I use the set to avoid repeating classes in case of multiple inheritance -
    # but lists is possible alternative, just filter it later
    subclasses = set(cls.__subclasses__())
    output = output.union(subclasses)
    for subcls in subclasses:
        if subcls.__subclasses__():  # if there's some subclasses
            for subcls_inner in subcls.__subclasses__():
                output = output.union(extract_all_subclasses(subcls))
        else:  # just append 
            output = output.union(subclasses )
    return output

Base上:

from abc import ABC, abstractmethod


class Base(ABC):
    @abstractmethod
    def say(self):
        pass

    @classmethod
    def from_config(cls, config):
        subclasses = extract_all_subclasses(cls)  # extracting all the subclasses
        subcls = {c.kind: c for c in subclasses}[config['kind']]  # get one from dictionary
        return subcls(**config['parameters'])

目前没有子类:

extract_all_subclasses(Base)
Out:
set()

但是然后:

class ChildA(Base):
    kind = 'A'

    def say(self):
        return 'Hello, I am A'


class ChildB(Base):
    kind = 'B'

    def __init__(self, name, *args, **kwargs):
        self._name = name

    def say(self):
        return 'Hello, I am {}'.format(self._name)

class ChildC(ChildB):
    kind = 'C'

    def __init__(self, age, *args, **kwargs):
        self._age = age

    def say(self):
        return 'Hello, I am Base\'s grandson, my age is {}'.format(self._age)

class ChildD(ChildC, ChildA):
    kind = 'D'

    def __init__(self, age, name):
        self._age = age
        self._name = name

    def say(self):
        return 'Hello, I am Base\'s grandson, my age is {} and my name is {}'.format(self._age, self._name)

extract_all_subclasses(Base)
Out:
{__main__.ChildA, __main__.ChildB, __main__.ChildC, __main__.ChildD}

因此,即使在多重继承的情况下,它也可以正常工作。检查一下:

print(Base.from_config(configA).say())
print(Base.from_config(configB).say())
print(Base.from_config(configC).say())
print(Base.from_config(configD).say())
Out:
Hello, I am A
Hello, I am Foo
Hello, I am Base's grandson, my age is 17
Hello, I am Base's grandson, my age is 17 and my name is Bar

注意!我刚刚删除了对超类的构造函数的调用,因为它会导致错误,如果它期望您不提供的参数,或者提供了它的非预期参数,则可能会导致错误。但这仅在嵌套子类的情况下才是危险的,并且很容易避免。而且我不知道为什么需要它们-如果您重新定义__init__上的行为(可能只是为了简化)。

可能的错误示例(Base相同):

class ChildA(Base):
    kind = 'A'

    def say(self):
        return 'Hello, I am A'


class ChildB(Base):
    kind = 'B'

    def __init__(self, name):
        super().__init__()
        self._name = name

    def say(self):
        return 'Hello, I am {}'.format(self._name)


class ChildC(ChildB):
    kind = 'C'

    def __init__(self, age):
        super().__init__()
        self._age = age

    def say(self):
        return 'Hello, I am Base\'s grandson, my age is {}'.format(self._age)

print(Base.from_config(configA).say())
print(Base.from_config(configB).say())
print(Base.from_config(configC).say())
Out:
Hello, I am A
Hello, I am Foo

---------------------------------------------------------------------------
...
<ipython-input-4-6932e3173d3f> in __init__(self, age)
     21 
     22     def __init__(self, age):
---> 23         super().__init__()
     24         self._age = age
     25 

TypeError: __init__() missing 1 required positional argument: 'name'

修改
解决后一个问题,是的,但有限制。您可以照常构造项目,但如果希望它能正常工作,则应将子类导入名称空间。否则它将无法正常工作。

我使用以下结构:

.
├── __init__.py
├── a.py
├── b.py
├── base.py
├── c.py
├── d.py
├── extractor.py
└── main.py

其中base.py是Base,a.py,b.py,c.py,d.py的定义-ChildAChildB,{{ 1}},ChildC,提取器是子类提取器函数,并且main具有以下列表:

ChildD

-它按计划工作。但是,如果我省略了from subclasses.base import Base from subclasses.a import ChildA from subclasses.b import ChildB from subclasses.c import ChildC from subclasses.d import ChildD config = [ { 'kind': 'A', 'parameters': {} }, { 'kind': 'B', 'parameters': { 'name': 'Foo' } }, { 'kind': 'C', 'parameters': { 'age': '17' } }, { # ChildD inherits from both ChildC, ChildA 'kind': 'D', 'parameters': { 'name': 'Bar', 'age': '17' } } ] if __name__ == '__main__': print( [ Base.from_config(cfg).say() for cfg in config ] ) 的导入-它会引发KeyError-因为在这种情况下,Childs没有导入,而ChildX无法提取它们-它们不存在。