Skip to content
This page has been auto-translated and may contain errors.View in English

变量和类型

每个程序都需要记住一些东西。问答游戏需要记住玩家的名字。游戏需要记住当前的分数。天气脚本需要记住你正在查询的城市。Python 为此使用变量:你可以将名字附加到值上,以便在整个程序中使用它们。

变量是对值的命名引用。Python 将一个名字绑定到 = 右侧的对象,并允许你随时重新绑定它。类型存在于值上,而不是名字上。

在 Python 中,变量是一种名字绑定:命名空间中的一个引用,在运行时解析为一个对象。对象携带类型;名字不携带。这就是动态类型:同一个名字在不同语句中可以引用完全不同类型的对象。

python
player_name = "小明"
score       = 0
city        = "北京"

三行。Python 现在记住了三件事。之后使用这些名字中的任何一个,Python 都会把对应的值返回给你。

每一行都创建一个绑定:左边的名字引用右边的对象。Python 首先对右侧求值,然后创建绑定。

每次赋值都会在当前作用域的本地命名空间中创建或重新绑定一个名字。Python 在绑定生效之前会完整地对右侧表达式求值。名字本身不携带任何类型信息。

存储一个值

= 符号几乎让每个从数学课来的人都摔跟头。在 Python 中,= 不表示"等于"。它意味着将这个值存储在这个名字下。从左到右阅读:

python
city = "北京"

city 得到了 "北京"。你是在告诉 Python:记住 "北京" 并把它标记为 city

你可以随时替换变量的值。Python 只使用最新的那个:

python
score = 0
score = 10   # score 现在是 10
score = 15   # score 现在是 15

=赋值:它将一个名字绑定到当前作用域中的一个对象。更新变量的一种标准简写是增强赋值

python
score = 0
score += 10   # 等同于:score = score + 10
score *= 2    # 等同于:score = score * 2

你也可以一次绑定多个名字:

python
x, y, z = 1, 2, 3
a = b = 0        # 两者都从零开始

赋值将一个名字绑定到一个对象;它不会将值复制到容器中。两个名字可以引用同一个对象:

python
a = "hello"
b = a
print(id(a) == id(b))   # True(内存中是同一个对象)

b = "world"             # b 重新绑定到一个新对象
print(id(a) == id(b))   # False
print(a)                # 仍然是 "hello":重新绑定 b 不影响 a

id() 返回对象的标识(在 CPython 中是它的内存地址)。这种名字绑定与复制之间的区别在处理可变对象(如列表和字典)时更为重要,将在后续章节中介绍。

类型注解为静态分析工具记录了预期的类型。它们在运行时没有任何效果:

python
name:  str   = "小明"
score: int   = 0
ratio: float = 0.85

给变量命名

名字由你选择。Python 有一些硬性规则,社区也遵循一些值得从第一天就采用的约定。清晰的名字让代码在几周后仍然可读。晦涩的名字会带来痛苦。

Python 强制执行一小组标识符语法规则。除此之外,PEP 8 约定是每个 Python 代码库和工具的事实标准。

Python 的标识符语法规则很少。PEP 8 约定不是由解释器强制执行的,但被代码检查工具、类型检查器和每个专业的 Python 代码库所假定。偏离它们会产生摩擦。

Python 强制执行的规则:

  • 只能使用字母、数字和下划线。不能有空格或连字符。
  • 必须以字母或下划线开头,不能以数字开头
  • 区分大小写:scoreScoreSCORE 是三个不同的变量

每个人都遵循的约定(PEP 8):

事物风格示例
变量和函数snake_caseuser_nametotal_price
常量UPPER_SNAKE_CASEMAX_RETRIESBASE_URL
PascalCaseUserAccountDataLoader
python
# 清晰的名字,一眼就能读懂
user_name    = "小明"
total_price  = 49.99
is_logged_in = True
MAX_RETRIES  = 3

# 一个小时内你就会后悔这些
x   = "小明"
tp  = 49.99
b   = True

值得早点知道的一个陷阱:不要用 Python 内置名字(如 listinputtypeprint)来给变量命名。Python 允许这样做,但你会悄无声息地破坏该作用域中剩余部分的内置功能,由此产生的错误很难追踪。

不要遮蔽 Python 的内置名字。给 listtypeinputprintstr 赋值会在没有任何警告的情况下覆盖该作用域中剩余部分的内置名字。这是一个难以发现的隐性 bug。

UPPER_SNAKE_CASE 是一种约定,并非强制。Python 不会阻止你之后重新赋值 MAX_RETRIES = 99。它只是给其他开发者的一个信号,仅此而已。

遮蔽内置名字会创建一个本地绑定,在正常的名字查找顺序中优先于内置名字。内置名字仍可通过 builtins.print 等访问,但遮蔽名字会在正常使用中隐藏它。UPPER_SNAKE_CASE 没有语言级别的强制执行。对于工具可检查的真正不可变性保证,注解中的 typing.Final 是标准方法。

你可以存储什么

Python 有四种类型,你几乎会在每个程序中使用它们。Python 会根据你书写值的方式来判断你指的是哪种类型。你永远不需要显式声明类型。

Python 从字面量语法推断类型。这四种类型涵盖了基本的值空间;语言中的其他一切都建立在它们之上。

Python 的四种基本类型各自映射到不同的运行时对象,具有不同的内存布局、精度特性和操作语义。类型由对象决定,而不是由名字决定。

文本 (str)

任何文本都放在引号内,单引号或双引号都行。引号告诉 Python 你指的是字面字符,而不是变量名。一旦创建,字符串就不能就地修改。字符串章节会介绍你能对它们做的所有事情。

python
player_name = "小明"
city        = "北京"
message     = '游戏结束'

如果你的文本包含撇号,使用双引号可避免转义:

python
note = "It's a great day"
note = 'It\'s a great day'   # 相同的结果,使用转义

字符串保存任何单引号或双引号中的文本。它们是不可变的:没有任何操作会就地修改字符串;每个转换都会返回一个新的字符串。这对性能很重要:循环中反复使用 + 会在每一步创建一个新的字符串对象。字符串章节会介绍高效的替代方法。

python
player_name = "小明"
city        = "北京"
note        = "It's a great day"

str 是 Unicode 码点的不可变序列,而不是字节。len("café") 是 4,不是 5。不可变性使字符串可哈希:可作为字典键和集合成员。CPython 会驻留看起来像标识符的短字符串;两个被赋予相同短字面量的变量通常在内存中共享一个对象。两种引号风格产生完全相同的对象。

python
player_name = "小明"
city        = "北京"
note        = "It's a great day"

整数 (int)

整数不带引号或小数点。Python 称它们为整数。它们可以任意大;Python 会处理任意大的数字,你无需做任何特殊处理。

python
score      = 0
age        = 28
population = 8_100_000_000   # 下划线只是为了可读性

整数不带引号或小数点。Python 的整数是任意精度的:它们会增长以容纳任何值,不像 C 或 Java 中固定大小的 32 位或 64 位整数。数字字面量中的下划线只是装饰,Python 会忽略它们。

python
score      = 0
age        = 28
population = 8_100_000_000

Python 中的 int 是任意精度的:对象会随着值的增长分配额外的内存,仅受可用内存限制。CPython 将 -5 到 256 之间的小整数缓存为单例;id(1) == id(1) 始终为 True。在此范围之外,每个字面量都会创建一个不同的对象。这就是为什么 is 在整数比较中给出不可靠的结果;始终使用 ==

python
score      = 0
age        = 28
population = 8_100_000_000

小数 (float)

任何带小数点的数字都是浮点数。对于大多数计算它们都按预期工作。需要注意的一件事:某些小数值不能在二进制中精确存储,因此你可能会得到一个微小的舍入误差:

python
price       = 4.99
temperature = 36.6

0.1 + 0.2   # 0.30000000000000004

对于日常工作,这很少有影响。对于需要精确到分以下的金融计算,Python 有一个 decimal 模块可以正确处理。这在数字章节中介绍。

任何带小数点的数字都会变成 float。Python 的浮点数是 IEEE 754 binary64:64 位,约 15-17 位有效十进制数的精度。众所周知的问题:0.1 + 0.20.30000000000000004。不是 Python 的 bug;这是二进制表示的结果。对于需要精确小数的金融计算,Python 的 decimal 模块是正确的工具,在数字章节中介绍。

python
price       = 4.99
temperature = 36.6

float 映射到 C 的 double:IEEE 754 binary64,53 位尾数,相对精度为 2^-52 ≈ 2.2e-16。分母含有 2 以外的素因子的分数(如 1/10 = 1/(2×5))在二进制中是无限循环的,无法精确存储。对于精确的十进制算术,Python 的 decimal.Decimal 使用任意精度的十进制。对于精确的有理算术,fractions.Fraction 存储分子/分母对。两者都在标准库中,在模块章节中介绍。

python
price       = 4.99
temperature = 36.6

真或假 (bool)

有些事情就是开或关。Python 为此使用布尔值:只有两个值,TrueFalse。它们在这个阶段看起来不起眼,但你程序中的每个条件和分支都依赖于布尔值。

python
is_logged_in = True
has_errors   = False

Python 在条件中使用某些值时也会将它们视为 False00.0""None(Python 的"此处无值")都表现得像 False。其他一切都表现得像 True。这在控制流章节中会变得有用。

bool 恰好保存 TrueFalse。它由比较返回,并由条件消费。Python 有更广泛的真值假值集合:零值、空容器和 None 是假值;其他一切都是真值。一个有用的细节:boolint 的子类,所以 True + True 求值为 2

python
is_logged_in = True
has_errors   = False

boolint 的子类。TrueFalse 是分别具有整数值 1 和 0 的单例。假值:零值(00.0)、空序列和映射(""[](){})、NoneFalse。其他一切都是真值。自定义对象通过 __bool____len__ 控制这一点。isinstance(True, int)True,这在泛型类型检查代码中很重要。

python
is_logged_in = True
has_errors   = False

检查和转换类型

当你不确定一个值的类型时,type() 会告诉你。要检查一个值是否为特定类型,isinstance() 是更可靠的工具:

python
print(type("hello"))   # <class 'str'>
print(type(42))        # <class 'int'>
print(type(3.14))      # <class 'float'>
print(type(True))      # <class 'bool'>

isinstance(42, int)    # True
isinstance("hi", str)  # True

type() 返回对象的精确类型。在自己的代码中进行类型检查时,首选 isinstance():它处理继承,而 type() 比较不处理。

python
print(type(42))          # <class 'int'>
isinstance(True, int)    # True   (bool 是 int 的子类)
type(True) == int        # False  (仅精确匹配,不包括子类)

type(x) 返回 x 的类型对象。isinstance(x, T) 遍历 MRO (x.__class__.__mro__),处理 type() 比较错过的子类关系。实际情况:isinstance(True, int)True,因为 boolint 的子类;type(True) == intFalse,因为它是精确的身份检查。在生产代码中使用 isinstance() 作为类型守卫。

python
isinstance(True, int)    # True
type(True) == int        # False

Python 不会自动混合类型。连接字符串和数字会引发 TypeError

python
score = 42
print("Your score is " + score)        # TypeError
print("Your score is " + str(score))   # 可以工作

使用类型名作为函数进行显式转换:

调用结果
str(42)"42"
int(3.9)3(截断,不四舍五入)
float("3.14")3.14
int("3.14")ValueError:不能直接将小数字符串转换为 int
int(float("3.14"))3(先转换为 float,然后转换为 int)
bool(0) / bool("")False

实践应用

所有四种类型在一个小脚本中协同工作。输出行使用 f-string 将值嵌入文本:在开头的引号前加 f,并将任何变量用 {} 包裹起来。Python 会用变量的实际值替换它。你将在下一章中正式学习它们。

python
player_name = "小明"
level       = 3
accuracy    = 0.94
is_premium  = True

print(f"{player_name} is on level {level} with {accuracy:.0%} accuracy.")
print(f"Premium account: {is_premium}")

类型很重要,因为 level + 1 可以工作,而 player_name + 1 不行。每个变量恰好保存一种东西;Python 不会悄悄为你混合它们。

一个包含所有四种类型的真实配置块,常量与运行时状态分离。f"..." 语法是 f-string{} 中的任何表达式都会在运行时求值并嵌入到输出中。在输出和输入章节中完整介绍。

python
BASE_URL    = "https://api.example.com"
MAX_RETRIES = 3
DEBUG       = False

user_name     = "小明"
request_count = 0
last_response = None

request_count += 1
print(f"[{request_count}] {BASE_URL} | debug={DEBUG}")

None 是"尚无值"的标准占位符。它的类型是 NoneType,在条件中表现为假值。把它用作那些在程序后期才有意义的变量的默认值。

带有内联类型注解的相同配置。注解是给类型检查器和 IDE 的文档;它们在运行时没有任何效果:

python
BASE_URL:    str  = "https://api.example.com"
MAX_RETRIES: int  = 3
DEBUG:       bool = False

user_name:     str        = "小明"
request_count: int        = 0
last_response: str | None = None

str | None 是 Python 3.10 中的联合语法:变量保存字符串或 None。在早期版本中,等效的是来自 typing 模块的 Optional[str]。当最低版本允许时,现代 Python 中首选 str | None 形式。