【问题标题】:Type-hinting for the __init__ function from class meta information in Python来自 Python 中的类元信息的 __init__ 函数的类型提示
【发布时间】:2018-08-12 05:39:21
【问题描述】:

我想做的是复制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 的输入提示,

它在运行时的工作方式是通过 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)

工作,但我没有得到任何类型提示,

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

【问题讨论】:

  • 考虑使用在 Py3.7 标准库中引入的 dataclasses,可作为包或 attrs 库用于 Py3.6。命名元组也很有用。
  • 不幸的是,Pycharm 目前对 sqlalchemy 和 pydantic 等其他库没有很好的支持(至少在自动完成和类型提示方面)。尽管它确实对内置库(如前面提到的数据类)提供了很好的支持。

标签: python sqlalchemy pycharm type-hinting


【解决方案1】:

从上面的python 3.7,您可以通过使用@dataclass并在实例字段中添加适当的类型提示来达到相同的效果。

https://docs.python.org/3/library/dataclasses.html

【讨论】:

    【解决方案2】:

    __init__ 更新__annotations__ 是正确的方法。可以在基类上使用元类、类装饰器或适当的__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__ 方法也总是设置有正确的注释在超类中。

    【讨论】:

    • 刚刚通过重新定义class Simple(Base): 进行了尝试,但在__init_subclass__ 中,不更改Simple"__init__" 中没有"__init__" 键,因此您的其余代码不会运行。
    • 只需更改if 条件以创建一个空的__init__ 即可。这应该是微不足道的。
    猜你喜欢
    • 1970-01-01
    • 2022-01-24
    • 2021-12-14
    • 2018-05-12
    • 2020-11-21
    • 2019-11-05
    • 2017-06-24
    • 1970-01-01
    • 2015-12-08
    相关资源
    最近更新 更多