动态定义类的 __init__ 参数(使用先前定义的字典)

Posted

技术标签:

【中文标题】动态定义类的 __init__ 参数(使用先前定义的字典)【英文标题】:Defining a class' __init__ arguments dynamically (using a previously defined dictionary) 【发布时间】:2021-10-15 03:37:03 【问题描述】:

有没有办法使用字典来定义类的参数?

例如,假设我有一个包含一堆键值对的字典。每个键都应该是类的一个参数,每个值应该是该参数的默认值。

就代码而言,我正在寻找这样的东西:

d = 'apples':0,'bananas':1
class A:
    def __init__(self,k=v for k,v in d.items())
        print('apples',apples)
        print('bananas',bananas)
A(4)

应该输出:

-> apples 4
-> bananas 1

【问题讨论】:

__init__() 方法的 body 呢,它怎么知道要打印什么? (或者您是否也希望动态生成?) 【参考方案1】:

你问:

有没有办法使用字典来定义类的参数?

确实如此。动态构建类通常使用所谓的metaclass 来完成,即实例是其他类的类。

下面是一个做你想做的事的例子。除了使用字典定义类的__init__() 方法的参数外,它还生成方法的主体,该主体仅打印出其所有参数及其值。我添加了这个是因为你从来没有回答我在评论中问你是否想要完成这个问题。

简而言之,它的作用是生成定义函数所需的源代码,该函数具有所需的参数,然后是打印它们的代码行。完成后,使用内置的exec() 函数执行代码以获得执行结果(即函数对象的可执行字节码)。最后,将一个带有"__init__" 键和一个可执行字节码值的字典条目添加到类的字典(classdict)中,然后将其传递给内置的type() 函数以构造成一个可用的class 对象。

呸!解释比代码更复杂。 ;¬)

class MyMetaClass(type):
    def __new__(cls, name, bases, classdict, **kwargs):
        initargs = classdict.pop('initargs', None)
        if not initargs:
            INIT_METHOD = f"def __init__(self):\n\tprint('name.__init__() called')\n"
        else:
            args = ", ".join(f"k=v" for (k, v) in initargs.items())
            body = "\n".join(f"\tprint(k!r, k)" for k in initargs.keys())
            INIT_METHOD = (f'def __init__(self, ' + f'args' + '):\n'
                           f'\tprint("name.__init__() called")\n'
                           f'body\n')
        result = '__builtins__' : None, 'print': print  # Must explicitly list any builtins.
        exec(INIT_METHOD, result)
        init_method = result['__init__']
        classdict.update('__init__': init_method)
        return type.__new__(cls, name, bases, classdict, **kwargs)


class A(metaclass=MyMetaClass):
    initargs = 'apples': 0, 'bananas': 1


A(4)

输出:

A.__init__() called
apples 4
bananas 1

【讨论】:

【参考方案2】:

虽然我喜欢这个概念并且很想了解它,但我不确定这样的东西是否在本地存在。

无论如何,这是一个手动解决方案:

d = 'apples':0,'bananas':1
class A:
    def __init__(self, *args, **kwargs):
        for arg, k in zip(args, d):
            setattr(self, k, arg)

        for k, v in kwargs.items():
            if hasattr(self, k):
                raise TypeError("multiple values for argument ''".format(k))
            elif k not in d:
                raise TypeError("unexpected keyword argument ''".format(k))
            setattr(self, k, v)

        for k, default_v in d.items():
            if not hasattr(self, k):
                setattr(self, k, default_v)

        print('apples', self.apples)
        print('bananas', self.bananas)


A(4)

# apples 4
# bananas 1

A(bananas=5)

# apples 0
# bananas 5

A(oranges=18)

# TypeError: unexpected keyword argument 'oranges'

它可能需要 python >= 3.6(不确定版本)才能工作。我认为 dict 之前不能保证被订购。

【讨论】:

【参考方案3】:

一种选择是定义默认值,然后从kwargsdefaults 中选择密钥并将其推送到__dict__ 类中

这样,传递给类的任何不是字典中的键的参数都将被忽略。比如:

class A():
    def __init__(self, **kwargs):
        defaults = 'apples': 0, 'bananas': 1
        for key, val in defaults.items():
            # Add value from kwargs if set, otherwise use default value
            self.__dict__[key] = kwargs.get(key, val)
        print(self.apples, self.bananas)

A()
# 0, 1

A(apples=5, bananas=6)
# 5, 6

A(apples=5, carrots=10)
# 5, 1

唯一的缺点是参数必须作为关键字 args 传递给类 - 普通 args 不起作用。

编辑 可以使用命令命令对 *args 执行相同操作的事实,但这更像是一种 hack:

class A():
    def __init__(self, *args):
        defaults = 'apples': 0, 'bananas': '1'
        for i, key in enumerate(defaults.keys()):
          try:
            # Get the value from args by index
            self.__dict__[key] = args[i]
          except IndexError:
            # Use the default value
            self.__dict__[key] = defaults[key]
        print(self.apples, self.bananas)

A()
# 0, 1

A(5)
# 5, 1

A(5, 6, 7)
# 5, 6

【讨论】:

以上是关于动态定义类的 __init__ 参数(使用先前定义的字典)的主要内容,如果未能解决你的问题,请参考以下文章

如何将场景添加到先前定义的 QGraphicsView

面向对象+unittest

__init__方法,类的实例化,对象

python 类的定义

python元类深入解析

python之__init__使用方法