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

时间:2018-03-04 13:01:17

标签: python sqlalchemy pycharm type-hinting

我想要做的是复制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),{{ 1}}实际上是在做一些特别针对SQLAlchemy的幕后魔术,但我并不需要它那么好/好,我只是想能够以编程方式添加类型提示,从班级的元信息。

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

PyCharm

我希望能够像上面的class Simple: id: int = 0 name: str = '' age: int = 0 一样初始化类,但也可以获得类型提示。我可以中途(功能),但不是类型提示。

如果我像SQLAlchemy那样设置它,那么

Simple(id=1, name='asdf')

有效,但我没有提示类型,

No __init__ type hint

如果我传递参数,我实际上会收到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) 警告,

Unexpected Argument

在代码中,我手动手动更新Unexpected Argument,这使__annotations__返回正确的内容,

get_type_hints

1 个答案:

答案 0 :(得分:1)

__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__方法总是设置为即使对于超类中定义的属性,也要更正注释。