元类中的__call__方法遮盖了__init__方法的签名。

6

我希望在以下代码中,当我输入 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

我已经搜索了解决方案,并发现了问题如何动态更改子类中方法的签名?更改签名的装饰器:适当记录附加参数。但是,似乎没有一个完全符合我的要求。第一个链接使用inspect来更改传递给函数的变量数量,但我似乎无法让它在我的情况下工作,并且我认为必须有更明显的解决方案。第二个链接不完全符合我的要求,但在这方面的某些内容可能是一个很好的替代品。

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


1
你需要更具体地说明 何时 应该显示它们。你是在Python shell、IDLE、IPython、Jupyter、Visual Studio code还是其他什么工具上工作?你是否想要它们显示在IDE中,或者这与 help() 或文档构建有关? - MSeifert
感谢您的反馈,我更新了问题并希望现在更清楚了。 - NotableQuestioner
1
这似乎是IDE的问题,而不是Python本身的问题。 - Azat Ibrakov
1
好的,我无法重现那个问题。如果我使用你的代码(在__init__中加入pass)在Spyder中运行,当我打印A(B(时会显示A.__init__B.__init__的签名。你能否提供更多信息,比如Spyder版本、Python版本、一个可用的代码示例(你的代码会产生缩进错误),以及可能是不正确建议的截图? - MSeifert
4个回答

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__方法相同的签名。

为了在__call__上伪造__init__签名,我们可以简单地使用functools.wraps。(但你可能想查看https://dev59.com/2XM_5IYBdhLWcg3wXx1N#33112180中的答案)

而对于动态创建额外的元类,可以在__metaclass__.__new__本身上完成,只需注意避免在__new__方法上产生无限递归 - 线程锁可以以比简单的全局标志更一致的方式帮助解决这个问题。

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__方法中(这将导致错误)- 因此使用锁定。


0

不确定这是否对作者有所帮助,但在我的情况下,我需要将 inspect.signature(Klass) 更改为 inspect.signature(Klass.__init__),以获取类 __init__ 的签名,而不是元类 __call__


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接