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

变量与类型 ​

每个程序都需要记住一些东西。一个问答游戏需要记住玩家的名字。一个游戏需要记住当前得分。一个天气脚本需要记住你要查询的城市。Python 用变量来做这件事:给值起个名字,这样你就能在整个程序中反复使用它。

变量是指向值的具名引用。Python 会把 = 右边的对象绑定到一个名字上,而且你可以随时重新绑定它。类型是附着在值上的,而不是名字上的。

在 Python 中,变量是一种**名称绑定**:一个指向对象的名字(对象是 Python 在内存中保存的任何值,比如字符串或数字),而不是一个装着值本身的带标签的盒子。类型属于对象,而不属于名字,所以同一个名字可以在这一行指向一个 str,下一行又指向一个 int,不会报错。这种自由被称为动态类型:名字本身不限制它可以指向什么。

python
player_name = "Alice"
score = 0
city = "Tokyo"

三行代码,Python 现在记住了三件事。对每一行,Python 会先算出右边的值,再把它存到左边这个名字下面。之后用到这个名字时,Python 就会把这个值还给你。

每一行都会创建一个绑定:左边的名字指向右边的对象。Python 先计算右边的值,再创建这个绑定。

每次赋值都会在当前作用域(也就是这个名字可见的那部分代码区域,通常是你正在写的函数或文件)中绑定一个名字。Python 总是先算出整个右边的值,然后才进行绑定,这正是 a, b = b, a 能在一行内交换两个值的原因:右边先被完整计算出来,然后两个名字才分别指向计算结果。

存储一个值 ​

= 符号几乎会让每个从数学课过来的人栽跟头。在 Python 里,= 不代表"等于"。它的意思是**把这个值存到这个名字下面**:

python
city = "Tokyo"

city 得到了 "Tokyo"。你是在告诉 Python:记住 "Tokyo",并给它贴上标签 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        # 两者都从 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() 返回一个对象的标识,是这个对象在存在期间独一无二的数字,所以两个相等的 id 意味着"字面上就是同一个对象",而不是两个恰好相等的不同对象。现在你可能觉得"指向"和"复制"的区别无所谓,但一旦碰到可变对象(可以就地修改的对象,比如列表),这个区别就会咬你一口:让两个名字指向同一个列表,通过任意一个名字修改它,两边都会看到变化。列表和字典章节会让这一点变得更具体。

类型标注记录了你期望一个名字保存什么类型的值。它只是给不实际运行代码就分析代码的工具看的注释(类型检查器和你的编辑器),Python 自身在运行程序时会完全忽略它:

python
name: str = "Alice"
score: int = 0
ratio: float = 0.85
Juno存储一个值= 不是数学课上的"等于",它的意思是"把这个值存到这个名字下面"。你想改就改,Python 只记得最新的那个。不用声明,不用准备工作,你起个名字它就存在了。我在这上面纠结了太久。
Juno存储一个值= 让一个名字指向一个值,你随时可以让它重新指向别处。名字是标签,值是那个东西本身。整套把戏就这么简单。
Juno存储一个值= 不是把值塞进一个盒子,而是把一个名字系在一个对象上。重新赋值只是挪动名字,旧对象原地不动。在列表登场之前先记住这点,不然你会在不知不觉中让两个名字共享一个对象。

给变量起名字 ​

名字由你来定。Python 有几条硬性规则,社区还有一些值得从第一天就遵守的约定。清晰的名字能让几周后的代码依然可读。晦涩的名字只会带来痛苦。

Python 强制执行一小部分标识符语法规则。除此之外,PEP 8 的约定实际上是所有 Python 代码库和工具通用的事实标准。

Python 对**标识符(这是"名字"的技术术语)真正强制执行的规则很少。剩下的都是 PEP 8,Python 的官方风格指南。解释器(运行你代码的那个程序)并不会强制执行它,但代码检查工具**(用来标出风格问题和潜在 bug 的工具)、类型检查器,以及每一个专业代码库都指望你遵守它。不遵守它,基本上只会给下一个读你代码的人添麻烦。

Python 强制执行的规则:

  • 只能用字母、数字和下划线。不能有空格或连字符。
  • 必须以字母或下划线开头,绝不能以数字开头
  • 大小写敏感:score、Score 和 SCORE 是三个不同的变量

大家都遵守的约定(PEP 8):

内容风格示例
变量和函数snake_case(蛇形命名)user_name、total_price
常量UPPER_SNAKE_CASE(全大写蛇形命名)MAX_RETRIES、BASE_URL
类PascalCase(帕斯卡命名)UserAccount、DataLoader
python
# 清晰的名字,一眼就能看懂
user_name = "Alice"
total_price = 49.99
is_logged_in = True
MAX_RETRIES = 3

# 用不到一小时你就会后悔这样起名
x = "Alice"
tp = 49.99
b = True

有一个坑值得早点知道:不要把变量名起成 Python 内置的名字,比如 list、input、type 或 print。Python 允许你这么做,但这会悄无声息地让该作用域后面的代码都用不了这个内置功能了,出的错还很难追查。

不要遮蔽 Python 的内置名字。把值赋给 list、type、input、print 或 str 会在该作用域后面的代码里覆盖掉这个内置功能,而且没有任何警告。这是一个很难发现的静默 bug。

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

遮蔽内置名字,指的是你自己起的名字和 Python 已经提供的某个名字(比如 list)撞车了。Python 查找名字时,会先检查你的本地名字,再检查它自己的内置名字,所以你的版本会胜出,真正的 list 从这一点开始就被隐藏了。它仍然可以通过 builtins.list 访问到,但普通代码已经看不到它了。至于常量,UPPER_SNAKE_CASE 只是一个视觉上的信号;Python 完全允许你重新给它赋值。如果你想要一个工具能真正强制约束的常量,用 typing.Final 给它加标注,这会把这个名字标记为"不应被重新赋值",一旦你这么做,类型检查器就会标出来。

Juno给变量起名字 字母、数字、下划线,并且以字母或下划线开头。坚持用 snake_case 就万无一失。我踩过的一个新手坑:别把变量叫 list 或 print。Python 允许你这么做,然后后面所有东西都会莫名其妙地出问题,还没有任何警告。
Juno给变量起名字 名字用 snake_case,常量用 UPPER_SNAKE_CASE,每个工具都指望你这么做。还有别把东西命名成 list 或 print,你会悄无声息地把内置功能覆盖掉。
Juno给变量起名字 解释器几乎不管命名,但每个代码检查工具都管,所以和 snake_case 对着干只会给自己找罪受。真正的坑是遮蔽了像 list 这样的内置名字:它出问题很安静,而且离你出错的地方很远。

你能存储什么 ​

Python 有四种你几乎在每个程序中都会用到的类型。Python 会根据你写值的方式判断你指的是哪种类型。你从来不需要显式声明类型。

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

Python 中的每一个值都是一个完整的对象,即使是字面量(直接写在代码里的值,比如 42 或 "hi")也不例外。是对象就意味着这个值自带方法(附加在它身上的函数),所以 "hi".upper() 和 (3).bit_length() 可以直接在字面量上调用,不需要先做任何包装或拆包。你几乎不需要去想这件事,这正是它的意义所在。下面这四种类型,是你在几乎每个程序里都会用到的。

文本(str) ​

任何文本都放在引号里,单引号或双引号都行。引号告诉 Python 你说的是字面上的字符,而不是变量名。字符串一旦创建,就不能就地修改。字符串一章会讲到关于字符串的所有操作。

python
player_name = "Alice"
city = "Tokyo"
message = 'Game over'

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

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

字符串可以用单引号或双引号包住任何文本。它们是不可变的:没有任何操作会就地修改字符串;每一次转换都会返回一个新字符串。这一点关系到性能:在循环里反复使用 + 会在每一步都创建一个新的字符串对象。字符串一章会讲到更高效的替代方案。

python
player_name = "Alice"
city = "Tokyo"
note = "It's a great day"

str 是一个不可变的 Unicode 码点序列(不可变意味着一旦创建就永远不能被改变;码点指的是字符本身,而不是它们被存储成的原始字节),这就是为什么 len("café") 是 4 而不是 5。因为字符串不能改变,它是可哈希的,可以用作字典的键或集合的成员(一个内容永不改变的值,才能安全地按内容归档)。由此可以引出一条你实际会用到的规则:用 == 比较字符串(比较字符是否相同),永远不要用 is(比较是不是内存中同一个对象),因为两个相等的字符串是否共享同一个对象,是一个你不能依赖的实现细节。

python
player_name = "Alice"
city = "Tokyo"
note = "It's a great day"
Juno文本(str) 文本放在引号里,单引号或双引号,随你挑。一旦创建就不能就地修改,这没问题。任何看起来像是在修改字符串的操作,实际上都是给你一个全新的字符串。
Juno文本(str) 字符串是不可变的。没有任何操作能就地修改它们,你总是拿到一个新字符串。链式调用方法的时候要记住这一点。
Juno文本(str) 一段不可变的 Unicode 序列,所以 len("café") 是 4,字符串也能当字典键用。你实际会用到的一点:用 == 比较,永远不用 is。

整数(int) ​

整数不带引号也不带小数点。Python 把它们叫做整数(integer)。它们可以大到你需要的任何程度;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 是任意精度的:它会随着数值增长,直到你的内存放不下为止,所以不存在溢出(在固定大小整数的语言里你会碰到的回绕或报错)需要考虑。第二,比较数字要用 ==(比较值是否相等),不要用 is(比较是不是同一个对象)。两个相等的整数是否是同一个对象,是一个实现细节:CPython(标准的 Python 实现)会复用小整数,所以 id(1) == id(1) 是 True,但对更大的数字这个结论就悄悄不成立了,所以永远别依赖它。

python
score = 0
age = 28
population = 8_100_000_000
Juno整数(int) 整数,不带引号,不带小数点。最棒的是,Python 的整数能长到你想要的任意大小,没有那种诡异的溢出问题。8_100_000_000 里的下划线只是为了让你读得清楚。
Juno整数(int) 整数是任意精度的,所以不用担心溢出。数字里的下划线只是装饰,位数多的时候用上它。
Juno整数(int) 整数能一直长到你的内存用完为止,所以溢出根本不是问题。一个值得保持的习惯:用 == 比较数字,不要用 is。身份比较只是一个缓存上的怪现象,别依赖它。

小数(float) ​

任何带小数点的数字都是浮点数(float)。对大多数计算来说,它们的表现和你预期的一样。有一点要知道:有些小数值无法在二进制中精确存储,所以你可能会碰到微小的舍入误差:

python
price = 4.99
temperature = 36.6

0.1 + 0.2   # 0.30000000000000004

对于日常工作来说,这几乎不重要。对于财务计算这种连一分钱的零头都要计较的场景,Python 有一个 decimal 模块能正确处理这个问题。这在数字一章里有讲。

任何带小数点的数字都会变成 float。Python 的浮点数是双精度的:大约有 15 到 17 位有效数字,以二进制存储。这种二进制存储正是那个著名的坑:0.1 + 0.2 得到的是 0.30000000000000004,这不是 Python 的 bug,而是二进制表示小数的方式导致的结果。对于金钱,或者任何精确小数很重要的场合,用 Python 的 decimal 模块,这在数字一章里有讲。

python
price = 4.99
temperature = 36.6

浮点数以二进制(以 2 为底)存储,而大多数十进制小数——分母不是 2 的幂的数(比如 1/10)——都无法在二进制中精确表示。这正是 0.1 + 0.2 会得到 0.30000000000000004 的原因。在生产环境中最重要的一条规则:永远不要用浮点数表示金钱,也不要用它做精确相等的判断。需要精确的十进制运算时用 decimal.Decimal,需要精确比例时用 fractions.Fraction。这两者都随 Python 标准库(Python 自带的工具集)提供,在模块一章里有讲。

python
price = 4.99
temperature = 36.6
Juno小数(float) 带小数点就是浮点数,日常用完全没问题。经典的坑大家都会踩一次:0.1 + 0.2 得到 0.30000000000000004。不是 bug,这就是二进制的特性。要算钱就用 decimal.Decimal。
Juno小数(float) 浮点数是二进制的,所以有些小数没法精确表示,0.1 + 0.2 并不完全等于 0.3。大多数运算没问题,但绝不能用来算钱。需要精确的时候用 decimal.Decimal。
Juno小数(float) 浮点数会有偏差,这就是 0.1 + 0.2 看起来"坏了"的原因。规则是:金钱和精确相等判断都别用浮点数。精确十进制用 decimal.Decimal,精确比例用 fractions.Fraction。

真或假(bool) ​

有些东西是开或关的。Python 用布尔值来表示这种情况:只有两个值,True 和 False。现阶段它们看起来不起眼,但你程序里的每一个条件和分支都靠布尔值运行。

python
is_logged_in = True
has_errors = False

Python 在条件判断中还会把某些值当成 False 来对待:0、0.0、"" 和 None(Python 里表示"这里没有值")都表现得像 False。其他一切都表现得像 True。这在控制流一章里会用得上。

bool 只能是 True 或 False。它是比较运算的返回值,也是条件判断消费的值。Python 还有一套更广义的**真值(truthy)和假值(falsy)**概念:零值、空容器和 None 都是假值;其他一切都是真值。有一个有用的细节:bool 是 int 的子类,所以 True + True 的结果是 2。

python
is_logged_in = True
has_errors = False

bool 建立在 int 之上(它继承自 int,也就是说它是一种特殊的 int),True 和 False 是仅存在的两个 bool 对象,分别对应 1 和 0。在条件判断中,假值(判定为假的那些值)包括:零(0、0.0)、空容器(""、[]、()、{})、None,以及 False 本身。其他一切都是真值。你自己写的类可以通过定义 __bool__ 或 __len__(那些名字前后带双下划线的特殊"魔术方法",Python 需要一个"是或否"的答案时会调用它们)来决定自己的真假性。而且因为 bool 是 int 的一种,isinstance(True, int) 会是 True,这在做类型检查的代码里可能会让你踩坑。

python
is_logged_in = True
has_errors = False
Juno真或假(bool) 只有两个值,True 和 False,它们藏在你写的每一个 if 背后。有趣的一点:0、""、[] 和 None 都算 False,其他一切都算 True。
Juno真或假(bool)bool 是比较运算得到的 True/False。Python 还有真值和假值的概念:空的东西、零和 None 都是假值。这就是为什么 if my_list: 这样写很顺手。
Juno真或假(bool)bool 骨子里其实是个 int,所以 isinstance(True, int) 是 True,这一点有一天会在类型检查中让你意外。假值就是零、空值和 None。自定义对象可以用 __bool__ 或 __len__ 决定自己的真假性。

检查和转换类型 ​

当你不确定一个值是什么类型时,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(方法解析顺序,Python 沿着继承链——也就是一个类是从哪些父类型一路构建出来的——查找类的有序列表,可以通过 x.__class__.__mro__ 查看),所以对于父类型它也会返回 True。这就是为什么 isinstance(True, int) 是 True(bool 是一种 int),而 type(True) == int 是 False(精确匹配检查)。在实际代码中,把 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
Juno检查和转换类型type() 告诉你某个东西是什么,isinstance() 检查它是不是给定的类型。而且 Python 不会把字符串和数字硬粘在一起,所以要先用 str() 或 int() 转换。这个坑每个人都踩过一次。
Juno检查和转换类型 优先用 isinstance() 而不是 type() ==,它会考虑继承关系。类型转换永远是显式的,所以拼接字符串和数字之前要先转换。
Juno检查和转换类型isinstance() 是你的类型守卫,它会沿着类的继承树查找,而 type(x) == T 只做精确匹配。类型转换刻意保持显式,int("3.14") 宁愿报错也不会去猜,所以要先经过 float 中转一下。

实战演练 ​

一个把四种类型放在一起工作的小脚本。输出这几行用了f-string来把值嵌入文本:在开头的引号前加个 f,把变量包在 {} 里,Python 就会把它替换成变量的实际值。你会在下一章里正式学到它们。

python
player_name = "Alice"
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 = "Alice"
request_count = 0
last_response = None

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

None 是"暂时没有值"这种情况的标准占位符。它的类型是 NoneType,在条件判断中表现为假值。把它用作那些程序运行到后面才有意义的变量的默认值。

同样的配置,现在加上了行内类型标注。标注记录了你期望一个名字保存什么类型的值。它是给类型检查器和你的编辑器(IDE)看的,Python 在运行程序时会忽略它:

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

user_name: str = "Alice"
request_count: int = 0
last_response: str | None = None

str | None 是一种联合类型,在 Python 3.10 中加入:它表示这个值可以是字符串,也可以是 None。在更老的版本里,同样的意思写作 Optional[str],需要从内置的 typing 模块导入。只要你的最低支持版本允许,在现代 Python 中都应该优先使用 str | None 这种写法。