如何在 Python 中输入提示返回泛型类型并符合协议的函数

发布于 2025-01-14 19:14:54 字数 2612 浏览 2 评论 0原文

如果我有一个函数接受 T 类型的实例,并输出相同的实例,但经过修改,使其另外符合 Protocol,我应该如何输入提示?我的主要目标是让我的 IDE (VSCode) 知道返回的对象具有原始对象的所有属性 + “添加”协议中定义的所有属性。

具体来说,我尝试通过以下方式将“元数据感知”添加到来自第三方库的一堆对象(代码稍微简化以仅关注类型提示):

@runtime_checkable
class HasMetadata(Protocol):
    """Protocol for objects which have a `metadata` property."""

    @property
    def metadata(self) -> Dict[str, Any]: ...


class MetadataMixin(HasMetadata):
    """A mixin addig a `metadata` property to an existing class."""

    def __init__(self, *args, metadata: Optional[Dict[str, Any]] = None, **kwargs):
        """
        Adds a metadata dictionary to this instance and calls the superclass ctor.

        Args:
            metadata (optional): The metadata to associate to this object. Defaults to
                None, meaning an empty dictionary.
        """
        super().__init__(*args, **kwargs)
        self._metadata = metadata or {}

    @property
    def metadata(self) -> Dict[str, Any]:
        return self._metadata


# Here's what I fail to type-hint correctly
T = TypeVar('T')
def inject_metadata(obj: T, metadata: Optional[Dict[str, Any]] = None) -> ??? # In swift I'd write something like `T & HasMetadata`
    """Dynamically injects metadata in the given object."""
    if isinstance(t, HasMetadata):
        raise TypeError('Cannot inject metadata in an object that already has it.')

    # Create a new type which acts like the old but also has `MetadataMixin` in its
    # mro, then change the type of the given object to this new type. This will give
    # the object the `metadata` property.
    old_type = type(t)
    new_type_name = f'_{old_type.__name__}WithMetadata'

    # Retrieve the new type if it was already created, or create it otherwise.
    # Simplified for demo purposes.. just always create the new type.
    # Also don't mind about the Mixin added last as a base class; I know technically it
    # should come first to be a true Mixin but that doesn't matter here.
    new_type = type(new_type_name, (old_type, MetadataMixin), dict(old_type.__dict__))
    obj.__class__ = new_type

    # Now set the actual metadata.
    metadata = metadata or {}
    setattr(obj, '_metadata', metadata)

    return obj

我知道我可以做到这一点通过创建辅助类型来为特定的 - 预先已知的 - 类工作,例如:

class Bar:
    pass

class Baz(Bar, HasMetadata):
    @property
    def metadata(self) -> Dict[str, Any]: ...

def inject_metadata(obj: Bar, ...) -> Baz:
    ...

但这只有在我具体知道我想要注入元数据的类型时才有效。理想情况下,我会使用任何 T< /代码>。

If I have a function taking in an instance of type T, and outputting that same instance but modified so it additionally conforms to a Protocol, how should I type hint that? My main goal is to allow my IDE (VSCode) to know that the returned object has all attributes from the original object + all attributes defined in the 'added' protocol.

Specifically, I'm trying to add 'metadata-awareness' to a bunch of objects coming from a 3rd-party library in the following way (code simplified a bit to focus on the type-hinting only):

@runtime_checkable
class HasMetadata(Protocol):
    """Protocol for objects which have a `metadata` property."""

    @property
    def metadata(self) -> Dict[str, Any]: ...


class MetadataMixin(HasMetadata):
    """A mixin addig a `metadata` property to an existing class."""

    def __init__(self, *args, metadata: Optional[Dict[str, Any]] = None, **kwargs):
        """
        Adds a metadata dictionary to this instance and calls the superclass ctor.

        Args:
            metadata (optional): The metadata to associate to this object. Defaults to
                None, meaning an empty dictionary.
        """
        super().__init__(*args, **kwargs)
        self._metadata = metadata or {}

    @property
    def metadata(self) -> Dict[str, Any]:
        return self._metadata


# Here's what I fail to type-hint correctly
T = TypeVar('T')
def inject_metadata(obj: T, metadata: Optional[Dict[str, Any]] = None) -> ??? # In swift I'd write something like `T & HasMetadata`
    """Dynamically injects metadata in the given object."""
    if isinstance(t, HasMetadata):
        raise TypeError('Cannot inject metadata in an object that already has it.')

    # Create a new type which acts like the old but also has `MetadataMixin` in its
    # mro, then change the type of the given object to this new type. This will give
    # the object the `metadata` property.
    old_type = type(t)
    new_type_name = f'_{old_type.__name__}WithMetadata'

    # Retrieve the new type if it was already created, or create it otherwise.
    # Simplified for demo purposes.. just always create the new type.
    # Also don't mind about the Mixin added last as a base class; I know technically it
    # should come first to be a true Mixin but that doesn't matter here.
    new_type = type(new_type_name, (old_type, MetadataMixin), dict(old_type.__dict__))
    obj.__class__ = new_type

    # Now set the actual metadata.
    metadata = metadata or {}
    setattr(obj, '_metadata', metadata)

    return obj

I know I can make this work for specific - known up front - classes by creating helper types, e.g.:

class Bar:
    pass

class Baz(Bar, HasMetadata):
    @property
    def metadata(self) -> Dict[str, Any]: ...

def inject_metadata(obj: Bar, ...) -> Baz:
    ...

but that only works if I know specifically which type(s) I want to inject metadata in. Ideally I'd make this work with any T.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文