__call__ 来自元类影子 __init__ 的签名

问题描述 投票:0回答:5

我希望在下面的代码中,当我输入

instance_of_A = A(
时,假定参数的名称是
init_argumentA
而不是
*meta_args, **meta_kwargs
。但不幸的是,显示了元类的
__call__
方法的参数。

class Meta(type):    
    def __call__(cls,*meta_args,**meta_kwargs):
        # Something here
        return super().__call__(*meta_args, **meta_kwargs)

class A(metaclass = Meta):
    def __init__(self,init_argumentA):
        # something here 

class B(metaclass = Meta):
    def __init__(self,init_argumentB):
        # something here

我搜索了解决方案并发现了问题如何动态更改子类中方法的签名? 签名更改装饰器:正确记录附加参数。但没有一个,似乎完全是我想要的。第一个链接使用检查来更改赋予函数的变量数量,但我似乎无法让它适用于我的情况,我认为必须有一个更明显的解决方案。 第二个并不完全是我想要的,但这种方式可能是一个不错的选择。

编辑:我在 Spyder 工作。我想要这个是因为我有数千个元类型的类,每个类都有不同的参数,这是不可能记住的,所以我们的想法是用户在看到正确的参数出现时可以记住它。

python spyder metaclass
5个回答
2
投票

使用您提供的代码,您可以更改

Meta

class Meta(type):
    def __call__(cls, *meta_args, **meta_kwargs):
        # Something here
        return super().__call__(*meta_args, **meta_kwargs)


class A(metaclass=Meta):
    def __init__(self, x):
        pass

import inspect

class Meta(type):
    def __call__(cls, *meta_args, **meta_kwargs):
        # Something here

        # Restore the signature of __init__
        sig = inspect.signature(cls.__init__)
        parameters = tuple(sig.parameters.values())
        cls.__signature__ = sig.replace(parameters=parameters[1:])

        return super().__call__(*meta_args, **meta_kwargs)

现在 IPython 或某些 IDE 将向您显示正确的签名。


1
投票

我发现 @johnbaltis 的答案是 99%,但并不完全是确保签名到位所需要的。

如果我们使用

__init__
而不是
__call__
,如下所示,我们会得到所需的行为

import inspect

class Meta(type):
    def __init__(cls, clsname, bases, attrs):

        # Restore the signature
        sig = inspect.signature(cls.__init__)
        parameters = tuple(sig.parameters.values())
        cls.__signature__ = sig.replace(parameters=parameters[1:])

        return super().__init__(clsname, bases, attrs)

    def __call__(cls, *args, **kwargs):
        super().__call__(*args, **kwargs)
        print(f'Instanciated: {cls.__name__}')

class A(metaclass=Meta):
    def __init__(self, x: int, y: str):
        pass

这将正确给出:

In [12]: A?
Init signature: A(x: int, y: str)
Docstring:      <no docstring>
Type:           Meta
Subclasses:     

In [13]: A(0, 'y')
Instanciated: A

0
投票

好吧 - 尽管你想要的原因似乎是模棱两可的,因为任何“诚实的”Python检查工具都应该显示

__init__
签名,你所要求的需要的是为每个类生成一个动态元类,其中
__call__
方法与类自己的
__init__
方法具有相同的签名。

要伪造

__init__
上的
__call__
签名,我们可以简单地使用
functools.wraps
。 (但您可能想检查答案 https://stackoverflow.com/a/33112180/108205

对于动态创建额外的元类,可以在

__metaclass__.__new__
本身上完成,只需注意避免在
__new__
方法上无限递归 -threads.Lock 可以以比简单的更一致的方式帮助实现这一点全局标志。

from functools import wraps
creation_locks = {} 

class M(type):
    def __new__(metacls, name, bases, namespace):
        lock = creation_locks.setdefault(name, Lock())
        if lock.locked():
            return super().__new__(metacls, name, bases, namespace)
        with lock:
            def __call__(cls, *args, **kwargs):
                return super().__call__(*args, **kwargs)
            new_metacls = type(metacls.__name__ + "_sigfix", (metacls,), {"__call__": __call__}) 
            cls = new_metacls(name, bases, namespace)
            wraps(cls.__init__)(__call__)
        del creation_locks[name]
        return cls

我最初想到使用元类

__new__
参数的命名参数来控制递归,但随后它将传递给创建的类的
__init_subclass__
方法(这将导致错误) - 因此使用 Lock。


0
投票

不确定这是否对作者有帮助,但就我而言,我需要将

inspect.signature(Klass)
更改为
inspect.signature(Klass.__init__)
才能获得类
__init__
的签名,而不是元类
__call__


0
投票

虽然这个问题是 6 年前提出的,但我想指出这个更简单的实现也有效:

from functools import wraps

class Meta(type):

@wraps(super().__call__)
def __call__(cls, *meta_args, **meta_kwargs):
    # Something here
    return super().__call__(*meta_args, **meta_kwargs)


class A(metaclass=Meta):
    def __init__(self, x):
    """Documentation for 'A.__init__'"""
        pass
© www.soinside.com 2019 - 2024. All rights reserved.