英文:
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
不可变,因此我们现在一眼就能看出,即使我们传递一个 list
给 add_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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论