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

变量和类型

docs.scrimba.com

每个程序都需要记住一些东西。一个测验需要记住玩家的名字。一个游戏需要记住当前的分数。一个天气脚本需要记住你要查询的城市。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 的官方风格指南。解释器(运行你代码的程序)不强制执行它,但linters(标记风格问题和可能的 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 = "It's a great day"
note = 'It\'s a great day'   # 相同的结果,使用转义

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

python
player_name = "张三"
city = "北京"
note = "It's a great day"

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

python
player_name = "张三"
city = "北京"
note = "It's a great day"
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)

任何有小数点的数字都是浮点数。它们对大多数计算工作如预期。有一件事要知道:某些十进制值无法在二进制中准确存储,所以你可能会得到一个很小的舍入错误:

python
price = 4.99
temperature = 36.6

0.1 + 0.2   # 0.30000000000000004

对于日常工作,这很少重要。对于金融计算,其中美分的分数很重要,Python 有一个 decimal 模块,可以正确处理它。这在数字章节中讨论。

任何有小数点的数字都变成 float。Python 浮点数是双精度的:大约 15 到 17 位有效数字,以二进制存储。那个二进制存储是众所周知的陷阱:0.1 + 0.20.30000000000000004,不是 Python 的 bug,而是二进制表示十进制的结果。对于钱,或任何精确十进制重要的地方,使用 Python 的 decimal 模块,在数字章节中讨论。

python
price = 4.99
temperature = 36.6

浮点数以二进制(二进制)存储,大多数十进制分数,任何分母不是二的幂的(比如 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 为此使用布尔值:恰好两个值,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

bool 构建在 int 之上(它子类化它,意味着它是一个 int 的专门类型),TrueFalse 是仅有的两个 bool 对象,分别值为 1 和 0。在一个条件中,假值(算作假的值)是:零值(00.0)、空容器(""[](){})、NoneFalse 本身。其他一切是真值。你自己的类可以通过定义 __bool____len__(特殊的"dunder"方法,用双下划线命名,Python 在需要是或否答案时调用)来决定它们的真值。因为一个 bool 是一个 intisinstance(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)True(一个 bool 是一种 int)而 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(float("3.14"))3(先转换为浮点数,再转为整数)
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} is on level {level} with {accuracy:.0%} accuracy.")
print(f"Premium account: {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。在旧版本上,你用从内置 typing 模块导入的 Optional[str] 写相同的东西。str | None 形式是在你的最小版本允许时在现代 Python 中更好的形式。