来自 Python 类元信息的 __init__ 函数的类型提示 [英] Type-hinting for the __init__ function from class meta information in Python
问题描述
我想要做的是复制 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)
当你在 PyCharm
、Person(...
中创建一个人时,你会得到关于 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屋!