使用 Python 的类型注释
Python 被认为是一种狂野西部语言,什么都可以做。除了缩进之外,代码风格和文档大多由编写应用程序的开发人员自行决定,这可能会导致一些代码混乱、难以阅读。
这种模糊的样式结构部分源于 Python 是一种动态类型语言,这意味着类型与变量在某个时间点的值相关,而不是变量本身。这种语言特性意味着变量可以在任何时间点取任意值,并且仅在访问属性或方法时进行类型检查。
请考虑以下代码。在 Python 中,这是可以接受的。
age = 21
print(age) # 21
age = 'Twenty One'
print(age) # Twenty One
在上面的代码中, 的值age
最初是一个int
整数,但后来我们将其更改为str
字符串。每个变量在程序中的任何位置都可以表示任何值。这就是动态类型的威力!
让我们用静态类型语言(例如 Java)做同样的事情。
int age = 21;
System.out.print(age);
age = "Twenty One";
System.out.print(age);
我们最终遇到以下错误,因为我们试图将"Twenty One"
(a String
) 分配给age
声明为 的变量int
。
Error: incompatible types: String cannot be converted to int
要使用静态类型语言,我们必须使用两个独立的变量并使用一些辅助类型转换方法,例如标准toString()
方法。
int ageNum = 21;
System.out.print(ageNum);
String ageStr = ageNum.toString();
System.out.print(ageStr);
这种转换确实有效,但我真的很喜欢 Python 的灵活性,我不想仅仅因为在大多数情况下难以推断类型就牺牲它作为一种动态、可读且对初学者友好的语言的优点。话虽如此,我也喜欢静态类型语言的可读性,因为其他程序员可以很容易地知道特定变量应该是什么类型!因此,为了兼顾两者,Python 3.5 引入了类型注解。
什么是类型注释?
类型注释是PEP 484中新增的功能,允许为变量添加类型提示。它们用于告知代码阅读者变量的预期类型。这种提示为动态类型的 Python 带来了一种静态类型控制的感觉。这是通过在初始化/声明变量或方法后添加给定的类型声明来实现的。
为什么以及如何使用类型注释
静态类型语言的一个实用特性是,变量的值在特定域内始终是已知的。例如,我们知道字符串变量只能是字符串,整数变量只能是整数,等等。而对于动态类型语言,变量的值是什么或应该是什么,基本上谁都无法确定。
注释变量
注释变量时,可以采用以下形式定义
my_var: <type> = <value>
创建具有给定值的给my_var
定类型的变量。<type>
下面是一个例子,当: int
我们声明变量时添加了,以表明年龄应该是int类型。
age: int = 5
print(age)
# 5
需要注意的是,类型注解不会以任何方式影响程序的运行时。这些提示会被解释器忽略,仅用于提高其他程序员和您自己的代码可读性。但需要再次强调的是,这些类型提示并非运行时强制执行,因此调用者仍然需要确保方法/函数/代码块使用了正确的类型。
注释函数和方法
在编写和调用函数时,我们可以利用预期变量的类型来确保传递和使用参数的正确性。如果str
函数预期为 ,而我们传入了int
,那么它很可能无法按照我们预期的方式工作。
请考虑下面的代码:
def mystery_combine(a, b, times):
return (a + b) * times
我们可以看到该函数的作用,但我们知道 、 或 应该是什么a
吗b
?times
请看下面的代码,尤其是调用 的两行,它们mystery_combine
使用了不同类型的参数。观察每个版本的输出,它们显示在每个代码块下方的注释中。
# Our original function
def mystery_combine(a, b, times):
return (a + b) * times
print(mystery_combine(2, 3, 4))
# 20
print(mystery_combine('Hello ', 'World! ', 4))
# Hello World! Hello World! Hello World! Hello World!
嗯,根据我们传递给函数的内容,我们得到了两个完全不同的结果。使用整数时,我们得到了一些不错的PEMDAS数学运算,但是当我们将字符串传递给函数时,我们可以看到前两个参数被连接起来,并且结果字符串被乘以了times
多次。
事实证明,编写该函数的开发人员实际上预料到了第二个版本是的用例mystery_combine
!使用类型注释,我们可以消除这种混淆。
def mystery_combine(a: str, b: str, times: int) -> str:
return (a + b) * times
我们在函数的参数中添加了: str
、: str
和 ,: int
以显示它们的类型。这有望使代码更清晰易读,并更清楚地展现其用途。
我们还添加了-> str
来表明此函数将返回str
。使用-> <type>
,我们可以更轻松地显示任何函数或方法的返回值类型,以避免未来的开发人员感到困惑!
再次强调,我们仍然可以用最初错误的方式调用代码,但希望通过良好的审查,程序员能够发现他们使用函数的方式并非预期。类型注释和提示对于团队和多开发人员的 Python 应用程序非常有用。它消除了阅读代码时的大部分猜测!
我们可以进一步扩展这一步来处理默认参数值。我们mystery_combine
在下面进行了调整,将其用作形参2
的默认参数值times
。此默认值位于类型提示之后。
def mystery_combine(a: str, b: str, times: int = 2) -> str:
return (a + b) * times
方法的类型提示
类型提示与方法的工作方式非常相似,尽管以我的经验来看,省略类型提示是很常见的self
,因为这暗示着它是包含类本身的实例。
class WordBuilder:
suffix = 'World'
def mystery_combine(self, a: str, times: int) -> str:
return (a, self.suffix) * times
您可以看到,上面的代码与之前基于函数的示例非常相似,只是我们删除了类上属性b
的参数。请注意,我们不需要显式地添加到定义中,因为大多数代码编辑器会查看预期类型的默认值。suffix
WordBuilder
: str
suffix
可用类型
上一节处理了类型注释的许多基本用例,但没有什么是仅仅是基本的,所以让我们分解一些更复杂的情况并展示常见的类型。
基本类型
注释对象最基本的方法是使用类类型本身。type
在 Python 中,你可以提供任何满足 的东西。
# Built-in class examples
an_int: int = 3
a_float: float = 1.23
a_str: str = 'Hello'
a_bool: bool = False
a_list: list = [1, 2, 3]
a_set: set = set([1, 2, 3]) # or {1, 2, 3}
a_dict: dict = {'a': 1, 'b': 2}
# Works with defined classes as well
class SomeClass:
pass
instance: SomeClass = SomeClass()
复杂类型
该typing
模块不仅可用于 Python 中的原始类型,还可用于其他用途。它描述了各种类型,以便更详细地提示任何类型的变量。它预装了各种类型注释,例如Dict
、Tuple
、List
、Set
等等!在上面的示例中,我们有一个带类型提示的变量,但没有定义该列表中list
应该包含哪些类型。模块提供的键入容器使我们能够更正确地指定所需的类型。typing
然后,您可以将类型提示扩展为如下例所示的用例。
from typing import Sequence
def print_names(names: Sequence[str]) -> None:
for student in names:
print(student)
这将告诉读者names
应该是sSequence
的一个str
,例如list
,,set
或tuple
字符串的一个。
字典的工作方式类似。
from typing import Dict
def print_name_and_grade(grades: Dict[str, float]) -> None:
for student, grade in grades.items():
print(student, grade)
类型Dict[str, float]
提示告诉我们这grades
应该是一个字典,其中键是str
ings,值是float
s。
类型别名
如果要使用自定义类型名称,可以使用类型别名。例如,假设您正在使用一组点\[x, y\]
作为Tuple
s,那么我们可以使用别名将Tuple
类型映射到另一个Point
类型。
from typing import List, Tuple
# Declare a point type annotation using a tuple of ints of [x, y]
Point = Tuple[int, int]
# Create a function designed to take in a list of Points
def print_points(points: List[Point]):
for point in points:
print("X:", point[0], " Y:", point[1])
多个返回值
如果你的函数以元组形式返回多个值,则将预期输出包装为typing.Tuple[<type 1>, <type 2>, ...]
from typing import Tuple
def get_api_response() -> Tuple[int, int]:
successes, errors = ... # Some API call
return successes, errors
上面的代码返回一个元组,其中包含 API 调用的成功次数和错误次数,这两个值都是整数。通过使用Tuple[int, int]
,我们向阅读此代码的开发人员表明该函数确实返回了多个int
值。
多种可能的返回类型
如果您的函数具有可以采用不同数量形式的值,则可以使用typing.Optional
或typing.Union
类型。
Optional
当值将是给定类型或 时使用None
。
from typing import Optional
def try_to_print(some_num: Optional[int]):
if some_num:
print(some_num)
else:
print('Value was None!')
上述代码表明some_num
可以是类型int
或None
。
Union
当值可以采用更具体的类型时使用。
from typing import Union
def print_grade(grade: Union[int, str]):
if isinstance(grade, str):
print(grade + ' percent')
else:
print(str(grade) + '%')
上面的代码表明grade
可以是 类型int
或str
。这在我们打印成绩的例子中很有用,这样我们就可以打印98%或98%,而不会产生任何意外的后果。
使用数据类
数据类是一种便捷类,它为相应的类提供自动生成的__init__
和方法。它减少了创建构造函数接受多个关键字参数的新类所需的样板代码__repr__
量。这些数据类使用类型提示和类级属性定义来确定哪些关键字参数和关联值可以传递给并由 打印。__init__
__repr__
以下代码直接来自dataclasses
文档。它定义了一个InventoryItem
,其上定义了三个属性,均使用类型提示: a name
、unit_price
和quantity_on_hand
。
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
使用类型提示和@dataclass
装饰器,InventoryItem
可以用以下代码创建新的,数据类将负责将关键字参数映射到属性。
common_item = InventoryItem(name='My Item', unit_price=2.99, quantity_on_hand=60)
other_item = InventoryItem(name='My Item', unit_price=2.99) # uses default value of 10 quantity
关于 es 的一个重要注意事项@dataclass
是,任何定义了默认值的类属性都必须在任何没有默认值的属性之后声明。这意味着quantity_on_hand
必须在name
和 之后声明unit_price
。当处理从父数据类扩展的数据类时,这可能会变得很有趣,所以要小心,但 Python 解释器应该会帮你发现这些问题。
更多示例
如需更多示例,请查看该模块的官方 Python 文档typing
。文档中有大量不同版本的示例可供您参考。我在这里只是略微介绍了冰山一角,但希望能够激发您对使用 Python 类型注解编写更简洁、更易读的代码的兴趣。
与往常一样,如果您有任何意见或问题,请联系、点赞、评论或分享!
文章来源:https://dev.to/dan_starner/using-pythons-type-annotations-4cfe