Generic vs Specific MyPy types of functions

huangapple go评论70阅读模式
英文:

Generic vs Specific MyPy types of functions

问题

我记得在某处阅读或听说过,对于一个函数,输入类型应尽可能通用(Iterable 而不是 list),但返回类型应尽可能具体。这是否在某个官方文件中有明确规定,以便在团队讨论中引用?还是我疯了,这实际上不是一个准则?

英文:

I remember reading, or hearing somewhere that for a function, input types should be as generic as possible (Iterable over list), but return types should be as specific as possible.

Is this written down somewhere official that I can reference when this comes up in team discussions? Or am I crazy and this isn't actually a guideline?

答案1

得分: 4

以下是您要翻译的内容:

"Making parameter types as general as possible"(使参数类型尽可能通用)

  • 主要好处在于使调用代码更简单。假设您有一个类似这样的函数:
def add_ints(nums: list[int]) -> int:
    return sum(nums)

这个函数可以正常工作,但如果您的调用方有一个 tuple[int, int, int] 呢?

nums = (1, 2, 3)
print(add_ints(nums))        # 失败
print(list(add_ints(nums)))  # 可行

这显然很愚蠢;他们没有必要将其元组转换为列表,除非您决定要求函数的参数类型为列表。这样会增加额外的代码编写(和阅读),而且在运行时也会稍微变慢。您应该改为将 add_ints 定义为接受 Iterable[int]

from typing import Iterable

def add_ints(nums: Iterable[int]) -> int:
    return sum(nums)

第二个好处是,从类型注释中更容易推断出函数的作用。如果函数接受一个 list,那么可能会发生它会进行修改的情况,因为 list 接口允许修改;而 Iterable 不可变,因此我们现在一眼就能看出,即使我们传递一个 listadd_ints,它也不会尝试修改它 - mypy 也将在 add_ints 的实现中强制执行这一点!

"Making return types as specific as possible"(使返回类型尽可能具体)

  • 这只是上面的对应情况。假设您有:
def nums_up_to(top: int) -> Iterable[int]:
    return list(range(top))

这在技术上是有效的,但如果我们的调用方需要一个列表呢?再次,我们强制他们进行不必要的检查/转换:

nums = nums_up_to(5)
nums.append(add_ints(nums))  # 失败,不能附加到可迭代对象
nums = nums_up_to(5)
assert isinstance(nums, list)
nums.append(add_ints(nums))  # 工作,因为我们通过 assert 缩小了类型
nums = list(nums_up_to(5))
nums.append(add_ints(nums))  # 工作,因为我们明确构造了一个列表

同样,这可以通过改进类型注释来更容易地解决:

def nums_up_to(top: int) -> list[int]:
    return list(range(top))

nums = nums_up_to(5)
nums.append(add_ints(nums))  # 没问题!

"Remember: YAGNI"(记住:YAGNI)

  • 值得记住的是,应用这些准则不一定需要严格遵守,扩展参数类型和缩小返回类型都是与调用方兼容的变更。
  • 在实际操作中,通常在编写调用代码时,当发现自己正在进行一些不必要的类型转换时,可以考虑应用这些准则,而不是在自己的代码中绕过它,相信 mypy 会在类型注释不匹配实际实现时提醒您。
英文:

A quick google hasn't found anything "official", but the benefits seem self-evident to me, so I'll take a crack at explaining them and you can decide whether the explanation sounds official enough.

Making parameter types as general as possible

The main benefit of this is in making the calling code simple. Suppose you have a silly function like this:

def add_ints(nums: list[int]) -> int:
    return sum(nums)

This works fine, but what if your caller has a tuple[int, int, int]?

nums = (1, 2, 3)
print(add_ints(nums))        # fails
print(list(add_ints(nums)))  # works

This is silly; there's no good reason for them to have to convert their tuple to a list, other than the fact that you decided to annotate your function to require one. It's extra code to write (and read) and it'll also make it a little slower at runtime. You should instead define add_ints to take an Iterable[int]:

from typing import Iterable

def add_ints(nums: Iterable[int]) -> int:
    return sum(nums)

A second benefit is that it is easier to infer from the type annotation what the function does. If the function takes a list, there is a possibility that it might mutate it, since the list interface allows mutation; an Iterable isn't mutable, so we can now tell at a glance that even if we pass add_ints a list, it isn't going to try to modify it -- and mypy will enforce that within the implementation of add_ints as well!

Making return types as specific as possible

This is just the corollary to the above. Suppose you have:

def nums_up_to(top: int) -> Iterable[int]:
    return list(range(top))

This is technically valid -- but what if our caller needs a list? Again, we're forcing them to do needless checking/conversion:

nums = nums_up_to(5)
nums.append(add_ints(nums))  # fails, can't append to an iterable
nums = nums_up_to(5)
assert isinstance(nums, list)
nums.append(add_ints(nums))  # works because we narrowed the type with that assert
nums = list(nums_up_to(5))
nums.append(add_ints(nums))  # works because we explicitly constructed a list

Again, this is much more easily fixed by just improving the type annotation:

def nums_up_to(top: int) -> list[int]:
    return list(range(top))

nums = nums_up_to(5)
nums.append(add_ints(nums))  # fine!

Remember: YAGNI

It's worth remembering that applying these guidelines is something that doesn't necessarily need to be done rigorously up front -- widening a parameter type and narrowing a return type are both backwards-compatible changes as far as the caller is concerned.

In practice I usually find myself applying these guidelines when I'm in the process of writing calling code, I find that I'm doing some unnecessary type conversion, and I resolve the issue by loosening/tightening the annotation in the dependency rather than working around it in my own code, trusting that mypy will let me know if my annotation doesn't match the actual implementation.

huangapple
  • 本文由 发表于 2023年2月7日 02:13:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/75365105.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定