使用协议参数键入包装功能

发布于 2025-02-03 03:43:26 字数 3380 浏览 2 评论 0原文

我正在处理一些代码,其中具有包装器函数,这些功能将功能从类型t提升为可选[t],但是当我使用的类型是协议时,出错。

例如,我可以有这样的东西:

from typing import (
    TypeVar,
    Callable as Fn,
    Optional as Opt,
    Protocol,
    Any,
)
from functools import (
    wraps
)

_T = TypeVar('_T')

# Lifting a general function of type T x T -> T
def lift_op(f: Fn[[_T, _T], _T]) -> Fn[[Opt[_T], Opt[_T]], Opt[_T]]:
    """Lift op."""
    @wraps(f)
    def w(x: Opt[_T], y: Opt[_T]) -> Opt[_T]:
        return x if y is None else y if x is None else f(x, y)
    return w

提起操作员op:t x t - &gt; t <代码> opt [t] x opt [t] - &gt;选择[t] 。 (我已经缩短了可选callable opt 和fn获得较短的行,仅此而已)。

大多数情况下,这似乎可以正常工作,但是,如果我具有适用于协议的通用类型的函数,则会破裂。

假设我的功能需要我的类型来支持&lt;。然后,我可以使用协议

# Protocol for types supporting <
class Ordered(Protocol):
    """Types that support < comparison."""

    def __lt__(self: Ord, other: Any) -> bool:
        """Determine if self is < other."""
        ...


Ord = TypeVar('Ord', bound=Ordered)

并定义min函数

# Min for two optionals
def min_direct(x: Opt[Ord], y: Opt[Ord]) -> Opt[Ord]:
    return x if y is None else y if x is None else \
           y if y < x else x # on ties choose x

,如果我用两个整数调用它,

# mypy accepts that ints are Opt[Ord]
min_direct(1, 2)  # No problem here

Mypy将接受intopt [ord] opt [ord] 。

但是,如果我使用升力功能,它会破裂:

@lift_op
def lift_min(x: Ord, y: Ord) -> Ord:
    """Return min of x and y."""
    return y if y < x else x

# Now int is no longer an Opt[Ord]!
lift_min(1, 2)    # Type error for both args.

我会发现错误

error: Argument 1 to "lift_min" has incompatible type "int"; expected "Optional[Ord]"
error: Argument 2 to "lift_min" has incompatible type "int"; expected "Optional[Ord]"

,因此显然int不是opt [ord]在此上下文中。

如果我为int特别编写min函数,那很好

# Lifting a function with a concrete type (int) instead
# of a protocol
@lift_op
def imin(x: int, y: int) -> int:
    """Hardwire type to int."""
    return x if x <= y else y

# Again int is Opt[Ord]
imin(1, 2)  # All is fine here...

,或者如果我明确指定包装函数的类型:

# Try setting the type of the lifted function explicitly    
def lift_min_(x: Ord, y: Ord) -> Ord:
    """Return min of x and y."""
    return y if y < x else x

f: Fn[[Opt[Ord],Opt[Ord]], Opt[Ord]] = lift_op(lift_min_)
f(1, 2) # No problem here

我怀疑lift_op的返回类型包装器与fn [[opt [ord],opt [ord]],opt [ord]]作为f的类型注释,但我不确定什么方式。这不是wraps()调用,而是没有区别。但是,也许ord类型被以某种方式被绑定,然后对以不同的方式解释?

我不知道,我不知道该如何解决。我需要做什么才能使包装器功能起作用,以便它可以接受int,例如满足协议opt [ord]

如果您想要上下文中的代码,

I'm working on some code where I have wrapper functions that lift functions from type T to Optional[T], but when I use a type that is a protocol, something goes wrong.

For example, I could have something like this:

from typing import (
    TypeVar,
    Callable as Fn,
    Optional as Opt,
    Protocol,
    Any,
)
from functools import (
    wraps
)

_T = TypeVar('_T')

# Lifting a general function of type T x T -> T
def lift_op(f: Fn[[_T, _T], _T]) -> Fn[[Opt[_T], Opt[_T]], Opt[_T]]:
    """Lift op."""
    @wraps(f)
    def w(x: Opt[_T], y: Opt[_T]) -> Opt[_T]:
        return x if y is None else y if x is None else f(x, y)
    return w

to lift an operator op: T x T -> T to Opt[T] x Opt[T] -> Opt[T]. (I've shortened Optional and Callable to Opt and Fn to get shorter lines, nothing more).

This seems to work okay, mostly, but if I have a function that works on a generic type restricted to a protocol, something breaks.

Say I have a function that needs my type to support <. I can then use the protocol

# Protocol for types supporting <
class Ordered(Protocol):
    """Types that support < comparison."""

    def __lt__(self: Ord, other: Any) -> bool:
        """Determine if self is < other."""
        ...


Ord = TypeVar('Ord', bound=Ordered)

and define a min function as

# Min for two optionals
def min_direct(x: Opt[Ord], y: Opt[Ord]) -> Opt[Ord]:
    return x if y is None else y if x is None else \
           y if y < x else x # on ties choose x

and if I call it with two integers

# mypy accepts that ints are Opt[Ord]
min_direct(1, 2)  # No problem here

mypy will accept that int is an Opt[Ord].

But if I use the lift function, it breaks:

@lift_op
def lift_min(x: Ord, y: Ord) -> Ord:
    """Return min of x and y."""
    return y if y < x else x

# Now int is no longer an Opt[Ord]!
lift_min(1, 2)    # Type error for both args.

I get the errors

error: Argument 1 to "lift_min" has incompatible type "int"; expected "Optional[Ord]"
error: Argument 2 to "lift_min" has incompatible type "int"; expected "Optional[Ord]"

So apparently int isn't an Opt[Ord] in this context.

It is fine if I write a min function specifically for int

# Lifting a function with a concrete type (int) instead
# of a protocol
@lift_op
def imin(x: int, y: int) -> int:
    """Hardwire type to int."""
    return x if x <= y else y

# Again int is Opt[Ord]
imin(1, 2)  # All is fine here...

or if I specify the type of the wrapped function explicitly:

# Try setting the type of the lifted function explicitly    
def lift_min_(x: Ord, y: Ord) -> Ord:
    """Return min of x and y."""
    return y if y < x else x

f: Fn[[Opt[Ord],Opt[Ord]], Opt[Ord]] = lift_op(lift_min_)
f(1, 2) # No problem here

I suspect that the return type of the lift_op wrapper isn't the same Fn[[Opt[Ord],Opt[Ord]],Opt[Ord]] as the type annotation for f above, but I'm not sure in what way. It isn't the wraps() call, that doesn't make a difference. But perhaps the Ord type gets bound somehow and is then interpreted differently?

I don't know, and I don't know how to figure it out. What do I need to do to make the wrapper function work so it will accept an int, say, as satisfying the protocol Opt[Ord]?

If you want the code in context, here is a playground

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

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

一个人的夜不怕黑 2025-02-10 03:43:26

不幸的是,这看起来像mypy错误。您的ORD类型var在装饰级别上得到解决,因为t出现在装饰器def的左右两侧。这是部分正确的(您确实想确认t在原始功能和转换功能中是相同的),但是使此变量取消结合。这就是为什么您尝试作业的尝试:当您

f: Fn[[Opt[Ord], Opt[Ord]], Opt[Ord]] = lift_op(lift_min_)

进行ord时,再次绑定。我认为您应该重新检查mypy问题跟踪器,并在此错误之前提交此错误(我找不到)。

要重现此错误,您甚至可以使用简单的类型变量而无需限制:

from typing import TypeVar, Callable as Fn, Optional as Opt

_T = TypeVar('_T')

def allow_none(f: Fn[[_T], _T]) -> Fn[[Opt[_T]], Opt[_T]]:
    return f  # type: ignore

@allow_none
def simple(x: _T) -> _T:
    return x

reveal_type(simple)  # N: Revealed type is "def (Union[_T`-1, None]) -> Union[_T`-1, None]"
simple(1)  # E: Argument 1 to "simple" has incompatible type "int"; expected "Optional[_T]"

现在几乎任何类型(除了任何和特定的类型vars除外),无法将其传递给因为它不是_T。界变量表示为def [_t](union [_t` -2,none]) - &gt;联合[_t`-2,无]

这是相关的mypy问题单独报告会非常好(这可能会导致维护者提高优先级和更快的修复速度)。此错误也用pyre重现,因此可能我正在误解某些内容 - 但看起来确实很奇怪,类型变量应该在colleable中不再绑定。

我尝试了一些涉及generic协议的解决方案,但是它们似乎没有用:type var binting在函数定义中binding binding binding在通用的行为非常奇怪,在in in indo> indo> indoce上下文中解决&lt; nothene&gt;(不要因为小写类别命名而杀死我,它与<<<<<代码> classMethod属性并故意左小写):

class _F(Protocol[_T]):
    def __call__(self, __x: _T) -> _T:
        ...

# Fails
class allow_none(Generic[_T]):
    def __call__(self, f: _F[_T], /) -> _F[_T | None]:
        return f  # type: ignore

reveal_type(allow_none[Ord]().__call__)  # N: Revealed type is "def (__main__._F[Ord?]) -> __main__._F[None]"

# But this works for some reason
class allow_none(Generic[_T]):
    def __call__(self, f: _F[_T], /) -> _F[_T]:
        return f  # type: ignore

reveal_type(allow_none[Ord]().__call__)  # N: Revealed type is "def (__main__._F[Ord?]) -> __main__._F[Ord?]"

这可能也是一个错误,我会尽快报告。

对我来说,您最初使用2步定义的尝试看起来像是最好的解决方法。

我还建议您查看 returns /a> by Nikita Sobolev-此链接指向也许涉及您用例的容器。尽管我真的不喜欢它,但有些人认为它可以更好地替代的链条是检查。它随着此类情况(几乎)的键入免费。

This looks like mypy bug, unfortunately. Your Ord type var gets resolved on decorator level, because T appears both on left and right sides of decorator def. It is partially correct (you do really want to confirm that T is the same in original and transformed functions), but makes this variable unbound. That's why your attempt with assignment works: when you do

f: Fn[[Opt[Ord], Opt[Ord]], Opt[Ord]] = lift_op(lift_min_)

you make Ord bound again. I think you should recheck mypy issue tracker and submit this bug if it was not done before (I was unable to find).

To reproduce this bug, you can use even simple type variables without restrictions:

from typing import TypeVar, Callable as Fn, Optional as Opt

_T = TypeVar('_T')

def allow_none(f: Fn[[_T], _T]) -> Fn[[Opt[_T]], Opt[_T]]:
    return f  # type: ignore

@allow_none
def simple(x: _T) -> _T:
    return x

reveal_type(simple)  # N: Revealed type is "def (Union[_T`-1, None]) -> Union[_T`-1, None]"
simple(1)  # E: Argument 1 to "simple" has incompatible type "int"; expected "Optional[_T]"

Now almost any type (except for Any and specific type vars) can not be passed as simple argument, because it is not _T. Bound variables are represented like def [_T] (Union[_T`-2, None]) -> Union[_T`-2, None].

Here is related mypy issue, but it doesn't cover exactly your case, so it would be great to report this separately (it may lead to raising priority and faster fix from maintainers). This bug reproduces with pyre too, so probably I'm misunderstanding something - but it really looks odd, type variable that should be bound within Callable is not bound any more.

I have tried a few solutions involving Generic and Protocol, but none of them seem to work: type var binding in function definition is strict enough and callable with type variable from Generic behaves really odd, resolving to <nothing> in Union context (don't kill me for lowercase class naming, it is too similar to classmethod or property and left lowercase intentionally):

class _F(Protocol[_T]):
    def __call__(self, __x: _T) -> _T:
        ...

# Fails
class allow_none(Generic[_T]):
    def __call__(self, f: _F[_T], /) -> _F[_T | None]:
        return f  # type: ignore

reveal_type(allow_none[Ord]().__call__)  # N: Revealed type is "def (__main__._F[Ord?]) -> __main__._F[None]"

# But this works for some reason
class allow_none(Generic[_T]):
    def __call__(self, f: _F[_T], /) -> _F[_T]:
        return f  # type: ignore

reveal_type(allow_none[Ord]().__call__)  # N: Revealed type is "def (__main__._F[Ord?]) -> __main__._F[Ord?]"

It is probably a bug too, I'll report it asap.

For me your initial attempt with 2-step definition looks like the best workaround, unfortunately.

I also suggest to have a look at returns library by Nikita Sobolev - this link points to Maybe container that deals with your use case. Although I don't really like it, some people consider it better alternatives to chain of is None checks. It comes with typing for such cases (almost) for free.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文