从Python中的类元信息类型提示__init__函数

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Python中的类元信息类型提示__init__函数相关的知识,希望对你有一定的参考价值。

我想做的是用它的SQLAlchemy类来复制DeclarativeMeta所做的事情。有了这段代码,

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Person(Base):
    __tablename__ = 'person'
    id = Column(Integer, primary_key=True)

    name = Column(String)
    age = Column(Integer)

当你在PyCharmPerson(...创建一个人时,你会得到关于id: int, name: str, age: int的打字提示,

Type-hinting with Python, PyCharm and SQLAlchemy

它在运行时的工作原理是通过SQLAlchemy的_declarative_constructor函数,

def _declarative_constructor(self, **kwargs):
    cls_ = type(self)
    for k in kwargs:
        if not hasattr(cls_, k):
            raise TypeError(
                "%r is an invalid keyword argument for %s" %
                (k, cls_.__name__))
        setattr(self, k, kwargs[k])
_declarative_constructor.__name__ = '__init__'

为了得到非常好的类型提示(如果你的类有一个id字段,Column(Integer)你的构造函数类型 - 暗示它为id: int),PyCharm实际上做了一些幕后魔法,特定于SQLAlchemy,但我不知道我需要它是好的/好的,我只是想能够从类的元信息中以编程方式添加类型提示。

所以,简而言之,如果我有一个类,

class Simple:
    id: int = 0

    name: str = ''
    age: int = 0

我希望能够像上面那样启动类,Simple(id=1, name='asdf'),但也可以获得类型提示。我可以得到一半(功能),但不是类型提示。

如果我像SQLAlchemy那样设置它,

class SimpleMeta(type):
    def __init__(cls, classname, bases, dict_):
        type.__init__(cls, classname, bases, dict_)


metaclass = SimpleMeta(
    'Meta', (object,), dict(__init__=_declarative_constructor))


class Simple(metaclass):
    id: int = 0

    name: str = ''
    age: int = 0


print('cls', typing.get_type_hints(Simple))
print('init before', typing.get_type_hints(Simple.__init__))
Simple.__init__.__annotations__.update(Simple.__annotations__)
print('init after ', typing.get_type_hints(Simple.__init__))
s = Simple(id=1, name='asdf')
print(s.id, s.name)

它有效,但我没有提示,

No __init__ type hint

如果我传递参数,我实际上得到一个Unexpected Argument警告,

Unexpected Argument

在代码中,我手动更新了__annotations__,这使得get_type_hints返回正确的东西,

cls {'id': <class 'int'>, 'name': <class 'str'>, 'age': <class 'int'>}
init before {}
init after  {'id': <class 'int'>, 'name': <class 'str'>, 'age': <class 'int'>}
1 asdf
答案

__annotations__更新__init__是去那里的正确方法。可以在基类上使用元类,classdecorator或适当的__init_subclass__方法来实现。

然而,PyCharm提出这个警告应该被视为Pycharm本身的一个错误:Python已经记录了语言中的机制,以便object.__new__将忽略类实例化(这是一个“类调用”)的额外参数,如果在任何一个中定义了__init__继承链中的子类。在产生此警告时,pycharm的行为实际上与语言规范不同。

围绕它的工作是使用相同的机制来更新__init__以创建具有相同签名的代理__new__方法。但是,此方法必须吞下任何args本身 - 因此,如果您的类层次结构需要某个实际的__new__方法,则获得正确的行为是一个复杂的边缘情况。

使用__init_subclass__的版本或多或少:

class Base:
    def __init_subclass__(cls, *args, **kw):
        super().__init_subclass__(*args, **kw)
        if not "__init__" in cls.__dict__:
            cls.__init__ = lambda self, *args, **kw: super(self.__class__, self).__init__(*args, **kw)
        cls.__init__.__annotations__.update(cls.__annotations__)
        if "__new__" not in cls.__dict__:
            cls.__new__ = lambda cls, *args, **kw: super(cls, cls).__new__(cls)
                cls.__new__.__annotations__.update(cls.__annotations__)

Python在继承时正确地更新了一个类'.__annotations__属性,因此即使这个简单的代码也适用于继承(和多重继承) - __init____new__方法总是使用正确的注释设置,即使对于超类中定义的属性也是如此。

以上是关于从Python中的类元信息类型提示__init__函数的主要内容,如果未能解决你的问题,请参考以下文章

python元类深入解析

python中的__new__与__init__,新式类和经典类(2.x)

在声明“__init__”和“__str__”之类的类时,python中的内置方法是啥?

继承类中的python __init__方法

Python中的类

python 的元类