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 = "李明"
score = 0
city = "北京"

三行代码,三样东西 Python 现在会记住。对每一行,Python 先处理右边的值,然后把它存在左边的名字下面。之后用那个名字,Python 就会把值还给你。

每一行创建一个绑定:左边的名字指向右边的对象。Python 先评估右边,然后创建绑定。

每次赋值都在当前作用域(代码的一个区域,在那里这个名字是可见的,通常是你工作的函数或文件)里绑定一个名字。Python 总是先完整计算右边的整个部分,然后才绑定任何东西,这正是为什么 a, b = b, a 能在一行里交换两个值:右边先被完整计算,然后两个名字都指向结果。

存储一个值

= 符号差不多能绊倒每个从数学课来的人。在 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() 返回一个对象的标识,一个在对象存在期间独有的数字,所以两个相等的 id 意味着"完全相同的对象",不是两个恰好相等的对象。指向和复制的差别现在很容易忽视,但到了可变对象(你可以就地改变的对象,比如列表)就会咬你一口:把两个名字指向一个列表,通过任一名字改变它,改变会在两个都显示。列表和字典在后面的章节会把这个具体化。

一个类型注解记录你期望一个名字持有的类型。它是给那些读你代码但不运行它的工具(类型检查器和你的编辑器)的笔记,Python 本身在程序运行时忽略它:

python
name: str = "李明"
score: int = 0
ratio: float = 0.85
Juno存储一个值= 不是数学课里的"等于",是"把这个值放在这个名字底下"。随时改变它,Python 保留最新的那个。没有声明,没有设置,你取个名字就存在了。我花了太久才停止过度思考这个。
Juno存储一个值= 把一个名字指向一个值,你可以随时重新指向。名字是标签,值是东西。这就是全部诀窍。
Juno存储一个值= 不是把值放进一个盒子,它把一个名字绑到一个对象。重新赋值只是移动名字,旧对象留在原地。在列表出现之前记住这点,要不然两个名字会偷偷共享一个。

给变量命名

你选择名字。Python 有几条硬规则,社区遵循从第一天开始就值得学的惯例。清晰的名字让几周后的代码可读。密码一样的名字造成麻烦。

Python 对一个标识符(命名法的技术术语)的语法规则很少。超过那些,PEP 8 的惯例是整个 Python 代码库和工具的实际标准。

Python 对一个标识符(名字的技术术语)实际强制的规则很少。其余的都是 PEP 8,Python 的官方风格指南。解释器(运行你代码的程序)不强制它,但linter(标记风格问题和可能的 bug 的工具)、类型检查器和每个专业代码库都期待它。违背它主要是给读你代码的人造成摩擦。

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。这只是给其他开发者的一个信号。

遮蔽一个内建意味着做一个名字来匹配 Python 已经提供的(比如 list)。当 Python 查找一个名字时,它在你的本地名字之前检查自己的内建,所以你的版本赢了,真正的 list 从那点开始被隐藏。它仍然能通过 builtins.list 到达,但普通代码再也看不到它。至于常量,UPPER_SNAKE_CASE 只是个视觉信号;Python 会愉快地让你重新赋值。如果你想要一个工具能实际强制的常量,用 typing.Final 注解它,它把名字标记为"不打算重新赋值",这样类型检查器如果你真的做了就会标记它。

Juno给变量命名 字母、数字、下划线,以字母或下划线开头。坚持 snake_case 你就没问题。我一个新手错误:别叫变量 listprint。Python 让你这么干,然后之后一切都变奇怪了,零警告。
Juno给变量命名 名字用 snake_case,常量用 UPPER_SNAKE_CASE,每个工具都期待这样。别给东西起 listprint 这样的名字,你会静默破坏内建。
Juno给变量命名 解释器几乎不强制命名,但每个 linter 都强制,所以对着 snake_case 对抗只是自找麻烦。真正的陷阱是遮蔽一个内建比如 list:它静默破坏,离你做的地方远得很。

你能存什么

Python 有四种类型你在几乎每个程序里都用得上。Python 从你怎么写值来推断是哪种类型。你从不显式声明一个类型。

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

Python 里的每个值都是一个完整的对象,甚至字面量(直接写进代码的值,比如 42"hi")。作为对象意味着值携带自己的方法(附在它上面的函数),所以 "hi".upper()(3).bit_length() 直接在字面量上工作,不需要先包装或打开。你很少需要想这个,那就是重点。下面的四种类型是你在几乎每个程序里都伸手去拿的。

文本(str

任何文本都放在引号里,单引号或双引号。引号告诉 Python 你是指字面字符,不是变量名。一旦创建,字符串不能就地改变。字符串章节覆盖了你能用字符串做的所有事。

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

如果你的文本包含一个撇号,用双引号来避免得转义它:

python
note = "这是美好的一天"
note = '这是美好的一天'   # 同样的结果

字符串用单引号或双引号持有任何文本。它们是不可变的:没有操作能改变一个字符串的地方;每个转变都返回一个新的。这对性能重要:在循环里重复的 + 在每一步创建一个新字符串对象。字符串章节覆盖了高效的替代方案。

python
player_name = "李明"
city = "北京"
note = "这是美好的一天"

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

python
player_name = "李明"
city = "北京"
note = "这是美好的一天"
Juno文本(str) 文本放在引号里,单或双,你选。一旦存在你就不能就地改变它,那没问题。任何看起来编辑字符串的东西实际上都还给你一个全新的。
Juno文本(str) 字符串是不可变的。没什么编辑它们就地,你总是得到一个新字符串。从你开始链接方法那刻起记住这点。
Juno文本(str) 一个不可变的 Unicode 游程,所以 len("咖啡") 是 2 字符串能充当字典键。你会实际用到的:用 == 比较,从不用 is

整数(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任意精度的:它增长来容纳你的 RAM 能装下的任何整数,所以没有溢出(你在有固定大小整数的语言里会碰到的环绕或错误)要设计周围。其次,用 == 比较数字(相等值),从不用 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) 整数增长到你的 RAM 完了为止,溢出就成了非议题。一个值得保留的习惯:用 == 比较数字,不是 is。同一性是个缓存怪癖,不要靠它。

小数(float

任何有小数点的数字是一个float。它们对大多数计算工作如你预期。一件要知道的事:一些小数值不能精确地存储在二进制,所以你能得到个小舍入误差:

python
price = 4.99
temperature = 36.6

0.1 + 0.2   # 0.30000000000000004

日常工作这很少有关系。对财务计算分厂很重要的地方,Python 有个 decimal 模块正确处理它。这在数字章节覆盖。

任何有小数点的数字成为 float。Python float 是双精度:大约 15 到 17 有效数字,存储在二进制里。那个二进制存储是众所周知的坑:0.1 + 0.20.30000000000000004,不是 Python bug 而是二进制怎么表示小数的后果。对于钱,或任何精确小数有关系的地方,拿 Python 的 decimal 模块,在数字章节覆盖。

python
price = 4.99
temperature = 36.6

Float 存储在二进制(二进制),大多数小数分数,任何分母不是二的幂的(比如 1/10),在二进制里不能精确写。那是 0.1 + 0.2 出来 0.30000000000000004 的地方。生产里有关系的规则:从不用 float 对金钱,或任何你会检查精确相等的东西。当你需要精确的十进制算术时拿 decimal.Decimal,或 fractions.Fraction 对精确比率。两个都随 Python 的标准库来(随 Python 本身捆绑的工具),在模块章节覆盖。

python
price = 4.99
temperature = 36.6
Juno小数(float) 一个小数点让它成为 float,它们对日常东西很好。经典坑每个人碰一次:0.1 + 0.20.30000000000000004。不是 bug,那是二进制。对钱,拿 decimal.Decimal
Juno小数(float) Float 是二进制,所以一些小数不能精确,0.1 + 0.2 不太是 0.3。对大多数数学很好,从不对钱。当它必须精确时用 decimal.Decimal
Juno小数(float) Float 漂移,那是为什么 0.1 + 0.2 看起来坏了。规则:没 float 对钱或精确相等检查。精确十进制用 decimal.Decimal,比率用 fractions.Fraction

真或假(bool

一些事情是开或关。Python 用布尔对这个:恰好两个值,TrueFalse。在这个阶段它们看起来很小,但你程序里的每个条件和分支都运行在布尔上。

python
is_logged_in = True
has_errors = False

Python 也把某些值当作它们是 False 当在条件里用时:00.0"",和 None(Python 的"这里没值")都表现得像 False。其他一切表现得像 True。这在控制流章节变得有用。

bool 持有恰好 TrueFalse。它由比较返回和被条件消费。Python 有更广的一套真值假值:零值、空容器和 None 是假值;其他一切是真值。一个有用的细节:boolint 的一个子类,所以 True + True 评估为 2

python
is_logged_in = True
has_errors = False

bool 建立在 int 之上(它子类化它,意味着它是一个 int 的特化种类),TrueFalse 是仅有的两个 bool 对象那会曾经存在,价值正好 1 和 0。在条件里,假值(数下作假的)是:零(00.0)、空容器(""[](){})、NoneFalse 本身。其他一切是真值。你自己的类能通过定义 __bool____len__(特殊的"dunder"方法,用双下划线命名,Python 当它需要是-或-否答案时调用)决定它们的真值。因为 boolintisinstance(True, int)True,这能在检查类型的代码里抓到你。

python
is_logged_in = True
has_errors = False
Juno真或假(bool) 两个值,TrueFalse,它们在你会写的每个 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)Truebool 是一种 int)而 type(True) == intFalse(精确匹配检查)。在真代码,拿 isinstance() 作为你的类型守卫,确认一个值是你期望的类型的检查在你用它之前。

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

Python 不自动混合类型。连接一个字符串和一个数字提出 TypeError

python
score = 42
print("你的分数是 " + score)        # TypeError
print("你的分数是 " + 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 字符串在文本里嵌入值:在开头引号前放 f 并包装任何变量在 {}。Python 把它替换为变量的实际值。你会在下一章适当地学它们。

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

print(f"{player_name} 在第 {level} 关,准度 {accuracy:.0%}。")
print(f"高级账户:{is_premium}")

类型重要因为 level + 1 工作而 player_name + 1 不工作。每个变量持有一种精确的东西;Python 不会为你偷偷混合它们。

一个现实的配置块有所有四种类型,常量分离出来从运行时状态。f"..." 语法是一个f 字符串:任何 {} 里面的表达式在运行时评估和嵌入在输出里。在输出和输入章节完整覆盖。

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 在程序运行时忽略它:

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。在旧版本你写同样的东西为 Optional[str],从内建 typing 模块导入。str | None 形式是当你的最小版本允许时在现代 Python 里喜欢的。