来自 Python 类元信息的 __init__ 函数的类型提示 [英] Type-hinting for the __init__ function from class meta information in Python

查看:74
本文介绍了来自 Python 类元信息的 __init__ 函数的类型提示的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想要做的是复制 SQLAlchemy 的功能,使用它的 DeclarativeMeta 类.使用此代码,

What I'd like to do is replicate what SQLAlchemy does, with its DeclarativeMeta class. With this code,

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,

When you go to create a person in PyCharm, Person(..., you get typing hints about id: int, name: str, age: int,

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

How it works at runtime is via the SQLAlchemy's _declarative_constructor functions,

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,但我不需要它那么好/好,我只是喜欢能够从类的元信息中以编程方式添加类型提示.

And to get the really nice type-hinting (where if your class has a id field, Column(Integer) your constructor type-hints it as id: int), PyCharm is actually doing some under-the-hood magic, specific to SQLAlchemy, but I don't need it to be that good / nice, I'd just like to be able to programatically add type-hinting, from the meta information of the class.

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

So, in a nutshell, if I have a class like,

class Simple:
    id: int = 0

    name: str = ''
    age: int = 0

我希望能够像上面一样初始化类,Simple(id=1, name='asdf'),但同时也获得类型提示.我可以得到一半(功能),但不能得到类型提示.

I want to be able to init the class like above, Simple(id=1, name='asdf'), but also get the type-hinting along with it. I can get halfway (the functionality), but not the type-hinting.

如果我像 SQLAlchemy 那样进行设置,

If I set things up like SQLAlchemy does it,

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)

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

It works, but I get no type hinting,

如果我确实传递了参数,我实际上会收到一个Unexpected Argument 警告,

And if I do pass parameters, I actually get an Unexpected Argument warning,

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

In the code, I've manually updated the __annotations__, which makes the get_type_hints return the correct thing,

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

推荐答案

__init__ 更新 __annotations__ 是正确的方法.可以在您的基类上使用元类、类装饰器或适当的 __init_subclass__ 方法来做到这一点.

Updating the __annotations__ from __init__ is the correct way to go there. It is possible to do so using a metaclass, classdecorator, or an appropriate __init_subclass__ method on your base classes.

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

However, PyCharm raising this warning should be treated as a bug in Pycharm itself: Python has documented mechanisms in the language so that object.__new__ will ignore extra arguments on a class instantiation (which is a "class call") if an __init__ is defined in any subclass in the inheritance chain. On yielding this warning, pycharm is actually behaving differently than the language spec.

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

The work around it is to have the same mechanism that updates __init__ to create a proxy __new__ method with the same signature. However, this method will have to swallow any args itself - so getting the correct behavior if your class hierarchy needs an actual __new__ method somewhere is a complicating edge case.

带有 __init_subclass__ 的版本或多或少:

The version with __init_subclass__ would be more or less:

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 correctly updates a class' .__annotations__ attribute upon inheritance, therefore even this simple code works with inheritance (and multiple inheritance) - the __init__ and __new__ methods are always set with the correct annotations even for attributes defined in superclasses.

这篇关于来自 Python 类元信息的 __init__ 函数的类型提示的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆