Leodanis Pozo Ramos
在 Python 中,变量是与存储在计算机内存中的具体对象或值相关联的名称。通过将变量与某个值关联,你可以使用一个具有描述性的名称来引用该值,并在代码中按需多次复用它。
变量的行为就如同它们所引用的值本身。要在代码中使用变量,首先需要学习如何创建变量,这在 Python 中非常简单直接。
通过赋值创建变量
在 Python 中创建变量的主要方式是使用赋值运算符(=)并遵循以下语法:
variable_name = value
在此语法中,左侧是变量名,接着是赋值运算符(=),右侧是你希望赋给该变量的值。这里的值可以是任意 Python 对象,包括字符串、数字、列表、字典,甚至是自定义对象。
以下是几个变量示例:
>>> word = "Python"
>>> number = 42
>>> coefficient = 2.87
>>> fruits = ["apple", "mango", "grape"]
>>> ordinals = {1: "first", 2: "second", 3: "third"}
>>> class SomeCustomClass: pass
>>> instance = SomeCustomClass()
在上述代码中,你通过为名称赋值定义了多个变量。前五个示例包含引用不同内置类型的变量。最后一个示例表明变量也可以引用自定义对象,例如 SomeCustomClass 类的一个实例。
设置和更改变量的数据类型
除了变量的值之外,考虑其值的数据类型也非常重要。当你思考变量的类型时,你是在判断该变量是指向字符串、整数、浮点数、列表、元组、字典、自定义对象还是其他数据类型。
Python 是一种动态类型语言,这意味着变量的类型是在运行时确定和检查的,而不是在编译期间。因此,在创建变量时,你无需显式指定变量的类型。Python 会根据所赋的对象自动推断变量的类型。
注意:在 Python 中,变量本身没有数据类型。真正拥有类型的是变量所引用的对象。
例如,考虑以下变量:
>>> name = "Jane Doe"
>>> age = 19
>>> subjects = ["Math", "English", "Physics", "Chemistry"]
>>> type(name)
<class 'str'>
>>> type(age)
<class 'int'>
>>> type(subjects)
<class 'list'>
在此示例中,name 引用了 "Jane Doe" 值,因此 name 的类型是 str。同理,age 引用了整数 19,所以其类型是 int;而 subjects 引用了列表,因此其类型是 list。请注意,你不需要显式告诉 Python 每个变量的类型,Python 会通过检查所赋值的类型自动确定并设置类型。
由于 Python 是动态类型的,你可以通过重新赋值使变量在不同时刻引用不同类型的数据对象:
>>> age = "19"
>>> type(age)
<class 'str'>
>>> subjects = {"Math", "English", "Physics", "Chemistry"}
>>> type(subjects)
<class 'set'>
现在,age 引用的是一个字符串,而 subjects 引用的是一个集合(set)对象。通过为现有变量赋予不同类型的值,你就改变了该变量的类型。
在 Python 中使用变量
变量是 Python 编程中的核心概念,它们是你程序的构建模块。到目前为止,你已经学习了创建变量的基础知识。在本节中,你将探索在 Python 中使用变量的不同方式。
你将从在表达式中使用变量开始,然后深入探讨计数器(counters)和累加器(accumulators)——它们对于在迭代过程中跟踪值至关重要。你还将学习变量的其他常见用例,例如临时变量、布尔标志、循环变量和数据存储变量。
表达式
在 Python 中,表达式是一个简单的语句,Python 可以对其进行求值并返回一个值。例如,考虑以下计算两个不同圆周长的表达式:
>>> 2 * 3.1416 * 10
62.912
>>> 2 * 3.1416 * 20
125.824
每个表达式都代表一个特定的计算。为了构建这些表达式,你使用了数值和乘法运算符(*)。Python 对每个表达式进行求值并返回结果值。
上述表达式有些僵化。对于每个表达式,你都必须重复输入值,这是一项容易出错且重复性高的任务。
现在考虑以下示例:
>>> pi = 3.1416
>>> radius = 10
>>> 2 * pi * radius
62.912
>>> radius = 20
>>> 2 * pi * radius
125.824
在此示例中,你首先定义变量来保存输入值,然后在表达式中使用这些变量。请注意,当你使用变量构建表达式时,Python 会用变量的实际值替换变量名。如示例所示,你可以方便地在不同表达式中重用这些值。
另一个需要注意的重要点是,现在你有了描述性的名称,可以清楚地标识表达式中使用的值。
总之,变量非常适合在表达式中重用值,并使用随时间变化的数据运行计算。通常,变量允许你为对象命名或打标签,以便稍后在程序中引用和操作它们。在接下来的章节中,你将看到变量在实际中的用例。
对象计数器(Object Counters)
计数器是一种整型变量,用于统计对象的数量。计数器通常初始值为零,并在每次出现给定对象时递增。例如,假设你需要统计给定对象列表中字符串对象的数量。在这种情况下,你可以执行如下操作:
>>> str_counter = 0
>>> for item in ("Alice", 30, "Programmer", None, True, "Department C"):
... if isinstance(item, str):
... str_counter += 1
...
>>> str_counter
3
在此示例中,你通过将 str_counter 初始化为 0 来创建该变量。然后,你对一个包含不同类型对象的列表运行 for 循环。在循环内部,你使用内置函数 isinstance() 检查当前对象是否为字符串。如果当前对象是字符串,则将计数器加一。
循环结束时,str_counter 的值为 3,反映了输入列表中字符串对象的数量。
注意:上面示例中的高亮行使用了表达式
str_counter += 1,这是str_counter = str_counter + 1的简写形式。+=运算符被称为增强加法运算符。
你在循环的每次迭代中重复使用 str_counter += 1 表达式来递增 str_counter 的值,使其随时间变化。这种动态更新是变量的关键特性之一。正如“变量”(variables)这个名字所暗示的,它们被设计用来保存随时间变化的值。
累加器(Accumulators)
累加器是编程中另一种常见的变量类型。累加器是一种用于将连续的值相加以形成总和的变量,该总和可作为不同计算中的中间步骤使用。
累加器的一个经典示例是计算数值的总和:
>>> numbers = [1, 2, 3, 4]
>>> total = 0
>>> for number in numbers:
... total += number
...
>>> total
10
在此示例中,循环遍历一个数字列表,并将每个值累加到 total 中。你还可以将累加器用作更大计算的一部分,例如计算数字列表的平均值:
>>> total / len(numbers)
2.5
然后,你利用 total 并结合内置函数 len() 来计算平均值。你本可以使用对象计数器代替 len()。同样,Python 提供了多个累加器函数,通常可以替代显式的累加器。例如,你可以使用 sum() 而不是像上面那样手动计算 total。
临时变量(Temporary Variables)
临时变量用于保存更复杂计算所需的中间结果。临时变量的一个经典用例是在变量之间交换值:
>>> a = 5
>>> b = 10
>>> temp = a
>>> a = b
>>> b = temp
>>> a
10
>>> b
5
在此示例中,你使用名为 temp 的临时变量保存 a 的值,以便在 a 和 b 之间交换值。一旦完成交换,temp 就不再需要了。
注意:在 Python 中,有一种简洁优雅的方式可以在不使用临时变量的情况下交换变量的值。你将在“可迭代解包”一节中学习这一技巧。
对于使用临时变量的更复杂示例,考虑以下计算数值样本方差的函数:
>>> def variance(data, degrees_of_freedom=0):
... return sum((x - sum(data) / len(data)) ** 2 for x in data) / (
... len(data) - degrees_of_freedom
... )
...
>>> variance([3, 4, 7, 5, 6, 2, 9, 4, 1, 3])
5.24
你用作函数返回值的表达式相当复杂且难以理解。调试也很困难,因为你在一个表达式中执行了多个操作。
为了使代码更易于理解和调试,你可以采用增量开发方法,使用临时变量进行中间计算:
>>> def variance(data, degrees_of_freedom=0):
... number_of_items = len(data)
... mean = sum(data) / number_of_items
... total_square_dev = sum((x - mean) ** 2 for x in data)
... return total_square_dev / (number_of_items - degrees_of_freedom)
...
>>> variance([3, 4, 7, 5, 6, 2, 9, 4, 1, 3])
5.24
在此 variance() 函数的替代实现中,你分几步计算方差。每一步都由一个具有有意义名称的临时变量表示,使你的代码更具可读性。
布尔标志(Boolean Flags)
布尔标志帮助你管理程序中的控制流和决策。顾名思义,这些变量的值只能是 True 或 False。你可以在条件语句、while 循环和布尔表达式中使用它们。
假设你需要在循环中交替执行两个不同的操作。在这种情况下,你可以使用一个标志变量在每次迭代时切换操作:
>>> toggle = True
>>> for _ in range(4):
... if toggle:
... print(f"✅ toggle is {toggle}")
... print("Do something...")
... else:
... print(f"❌ toggle is {toggle}")
... print("Do something else...")
... toggle = not toggle
...
✅ toggle is True
Do something...
❌ toggle is False
Do something else...
✅ toggle is True
Do something...
❌ toggle is False
Do something else...
每次此循环运行时,条件语句都会检查 toggle 的值以决定采取哪种操作。在循环末尾,你使用 not 运算符更改 toggle 的值。下一次迭代将运行替代操作。
标志也常用作函数参数。考虑以下玩具示例:
>>> def greet(name, verbose=False):
... if verbose:
... print(f"Hello, {name}! It's great to see you!")
... else:
... print(f"Hello, {name}!")
...
>>> greet("Pythonista")
Hello, Pythonista!
>>> greet("Pythonista", verbose=True)
Hello, Pythonista! It's great to see you!
在此示例中,verbose 参数是一个布尔变量,让你决定显示哪条问候消息。
你会发现一些 Python 内置函数也使用标志参数。sorted() 函数就是一个很好的例子:
>>> sorted([4, 2, 7, 5, 1, 6, 3])
[1, 2, 3, 4, 5, 6, 7]
>>> sorted([4, 2, 7, 5, 1, 6, 3], reverse=True)
[7, 6, 5, 4, 3, 2, 1]
sorted() 函数接受一个可迭代对象作为参数,并返回一个排序后的对象列表。该函数有一个 reverse 参数,这是一个默认为 False 的标志。如果你将此参数设为 True,则会得到按逆序排序的对象。
在实践中,你会发现布尔变量通常使用 is_ 或 has_ 的命名模式:
>>> age = 20
>>> is_adult = age > 18
>>> is_adult
True
在此示例中,is_adult 变量是一个标志,其值取决于 age 的值。请注意,is_ 命名模式可以清晰地传达变量的目的。然而,这种命名约定只是一种常见做法,并非 Python 强制要求。
循环变量(Loop Variables)
循环变量帮助你在 for 循环(有时也在 while 循环)中处理数据。在 for 循环中,变量在每次循环时都会取输入可迭代对象中当前元素的值:
>>> colors = [
... "red",
... "orange",
... "yellow",
... "green",
... "blue",
... "indigo",
... "violet"
... ]
>>> for color in colors:
... print(color)
...
red
orange
yellow
green
blue
indigo
violet
在此示例中,你使用 for 循环遍历颜色列表。循环变量 color 在每次迭代中保存当前颜色。这样,你就可以在遍历数据时对当前项执行某些操作。
Python 的 for 循环可以有多个循环变量。例如,假设你想将每种颜色与其索引映射起来。在这种情况下,你可以执行如下操作:
>>> for index, color in enumerate(colors):
... print(index, color)
...
0 red
1 orange
2 yellow
3 green
4 blue
5 indigo
6 violet
在此示例中,你使用内置函数 enumerate() 在遍历输入数据时生成索引。请注意,此循环有两个变量。为了提供多个循环变量,你使用逗号分隔的变量序列。
你还可以使用变量来控制 while 循环。以下是一个玩具示例:
>>> count = 5
>>> while count:
... print(count)
... count -= 1
...
5
4
3
2
1
在此示例中,循环变量充当定义迭代次数的计数器。在循环内部,你使用增强减法运算符(-=)更新变量的值。
数据存储变量(Data Storage Variables)
数据存储变量允许你使用容器(如列表、元组、字典或集合)来处理值。例如,假设你正在编写一个通讯录应用程序,该程序使用元组列表来存储联系人信息:
>>> contacts = [
... ("Linda", "111-2222-3333", "linda@example.com"),
... ("Joe", "111-2222-3333", "joe@example.com"),
... ("Lara", "111-2222-3333", "lara@example.com"),
... ("David", "111-2222-3333", "david@example.com"),
... ("Jane", "111-2222-3333", "jane@example.com"),
... ]
这个 contacts 变量允许你使用单个描述性名称来操作你的数据。你可以在循环中使用这些变量,例如:
>>> for contact in contacts:
... print(contact)
...
('Linda', '111-2222-3333', 'linda@example.com')
('Joe', '111-2222-3333', 'joe@example.com')
('Lara', '111-2222-3333', 'lara@example.com')
('David', '111-2222-3333', 'david@example.com')
('Jane', '111-2222-3333', 'jane@example.com')
>>> for name, phone, email in contacts:
... print(phone, name)
...
111-2222-3333 Linda
111-2222-3333 Joe
111-2222-3333 Lara
111-2222-3333 David
111-2222-3333 Jane
第一个循环遍历联系人列表中的项目并以元组形式打印它们。第二个循环使用三个循环变量分别处理每条数据。
请注意,在循环内部,你可以按需使用变量。你不需要使用所有变量,也不必按相同顺序使用它们。然而,当你有一个未使用的变量时,应使用下划线 _ 命名,以表明它是一个被丢弃的变量。
因此,你可以将循环头重写为:for name, phone, _ in contacts:。在这种情况下,下划线 _ 代表你未在循环体中使用的 email 变量。
Python 中的变量命名
到目前为止,你看到的示例都使用了简短的变量名。在实践中,变量名应具有描述性以提高代码的可读性,因此它们也可能较长并包含多个单词。
在以下章节中,你将学习在 Python 中创建有效变量名时应遵循的规则。你还将学习变量命名的最佳实践以及其他与命名相关的做法。
变量命名规则
Python 中的变量名可以是任意长度,可以包含大写字母(A-Z)、小写字母(a-z)、数字(0-9)和下划线字符(_)。唯一的限制是,尽管变量名可以包含数字,但变量名的第一个字符不能是数字。
注意:Python 目前完全支持 Unicode,你可以在变量名中使用许多 Unicode 字符。例如,以下变量名是有效的:
>>> α = 45 >>> β = 90 >>> δ = 180 >>> π = 3.14这些变量名在 Python 代码中可能不常见,但它们是完全有效的。当你希望代码反映目标学科中使用的符号时,可以在执行科学计算的代码中使用它们。
以下所有变量名都是有效的:
>>> name = "Bob"
>>> year_of_birth = 1970
>>> is_adult = True
这些变量遵循了在 Python 中创建有效变量名的规则。它们还遵循了最佳命名实践,你将在下一节中学习这些内容。
以下变量名不符合规则:
>>> 1099_filed = False
File "<input>", line 1
1099_filed = False
^
SyntaxError: invalid decimal literal
此示例中的变量名以数字开头,这在 Python 中是不允许的。因此,你会收到 SyntaxError 异常。
需要知道的是,变量名是区分大小写的。大写和小写字母被视为不同:
>>> age = 1
>>> Age = 2
>>> aGe = 3
>>> AGE = 4
>>> age
1
>>> Age
2
>>> aGe
3
>>> AGE
4
在此示例中,Python 将这些名称解释为不同且独立的变量。因此,在 Python 中创建变量名时,大小写是需要考虑的因素。
没有什么能阻止你在同一个程序中创建两个名为 age 和 Age 的不同变量,或者 agE。然而,这种做法不推荐,因为它可能会让阅读你代码的人(甚至你自己过一段时间后)感到困惑。通常,你应该在创建变量名时使用小写字母。
下划线字符的使用也很重要。你可以使用下划线来分隔变量名中的多个单词:
>>> first_name = "John"
>>> pen_color = "red"
>>> next_node = 123
在这些变量名中,你使用下划线字符作为多个单词的分隔符。这是一种通过用下划线替代空格字符来提高代码可读性的方法。为了说明这一点,考虑一下如果没有下划线,你的变量名会是什么样子:
>>> firstname = "John"
>>> pencolor = "red"
>>> nextnode = 123
尽管这些名称在技术上是有效的,但它们难以快速阅读和理解。缺乏分隔符使得很难一眼就把握每个变量的含义,需要更多精力去解读。使用下划线可以提高代码的清晰度,使其更易于维护。
变量命名最佳实践
你应该始终为变量赋予一个能清楚解释其用途的描述性名称。有时,你可以找到一个单词来命名给定的变量:
>>> temperature = 25
>>> weight = 54.5
>>> message = "Hello, Pythonista!"
变量总是引用具体的对象,因此它们的名称应该是名词。你应该尝试为变量找到能唯一标识所引用对象的具体名称。像 variable、data 或 value 这样的名称可能过于通用。虽然这些名称在简短示例中可以使用,但对于生产代码来说,它们的描述性不够。
通常,你应该避免使用单字母名称:
>>> t = 25 # 不要这样做
>>> temperature = 25 # 应该这样做
单字母名称可能难以破译,使你的代码难以阅读,尤其是在与其他类似名称一起使用表达式时。当然,也有例外。例如,如果你正在处理嵌套列表,那么可以使用单字母名称来标识索引:
>>> matrix = [
... [9, 3, 8],
... [4, 5, 2],
... [6, 4, 3],
... ]
>>> for i in matrix:
... for j in i:
... print(j)
...
9
3
8
4
5
2
6
4
3
使用 i、j 和 k 表示索引是很常见的,因此你可以在适当的上下文中使用它们。使用 x、y 和 z 表示点坐标也很常见,因此这些也是可以接受的。
不鼓励使用缩写来命名变量,而应使用完整名称:
>>> doc = "Freud" # 不要这样做
>>> doctor = "Freud" # 应该这样做
最好使用完整名称而不是缩写名称,因为这样更具可读性和清晰度。然而,有时当缩写被广泛接受和使用时,也是可以接受的:
>>> cmd = "python -m pip list"
>>> msg = "Hello, Pythonista!"
在这些示例中,cmd 是命令(command)的常用缩写,msg 是消息(message)的常用缩写。Python 中一个经典的广泛使用的缩写是 cls,你应该在类方法中使用它来标识当前类对象。
有时,你需要多个单词来构建一个描述性的变量名。当使用多单词名称时,如果没有明显的单词边界,你可能会难以阅读它们:
>>> numberofgraduates = 200
这个变量名很难阅读。你必须仔细分辨单词的边界才能理解变量所代表的含义。
多单词变量名最常见的做法如下:
- 蛇形命名法(Snake case):小写单词以下划线分隔。例如:
number_of_graduates。 - 驼峰命名法(Camel case):第二个及后续单词首字母大写,使单词边界更易识别。例如:
numberOfGraduates。 - 帕斯卡命名法(Pascal case):类似于驼峰命名法,但第一个单词也首字母大写。例如:
NumberOfGraduates。
《Python 代码风格指南》(又称 PEP 8)包含了针对不同对象类型的命名约定建议标准。关于变量,PEP 8 推荐使用蛇形命名法。
当你需要多单词名称时,通常会将形容词作为限定词与名词组合:
>>> initial_temperature = 25
>>> current_file = "marketing_personel.csv"
>>> next_point = (2, 4)
在这些示例中,你通过组合形容词和名词创建了描述性的变量名,这可以显著提高代码的可读性。另一个需要考虑的点是避免使用以 my_ 开头的多单词名称,例如 my_file、my_color 等。my_ 部分并没有为名称增加任何有用的信息。
标志变量是使用多单词变量名的另一个好例子:
>>> is_authenticated = True
>>> has_permission = False
在这些示例中,你使用下划线分隔单词,使它们的边界清晰可见且易于识别。
在命名列表和字典时,大多数情况下应使用复数名词:
>>> fruits = ["apple", "banana", "cherry"]
>>> colors = {
... "Red": (255, 0, 0),
... "Green": (0, 255, 0),
... "Blue": (0, 0, 255),
... "Yellow": (255, 255, 0),
... "Black": (0, 0, 0),
... "White": (255, 255, 255),
... }
在这些示例中,使用复数名词清楚地表明变量引用的是存储多个相似类型对象的容器。
在命名元组时,应考虑到它们通常用于存储不同类型或含义的对象。因此,使用单数名词是可以接受的:
>>> color = (255, 0, 0)
>>> row = ("Jane", 25, "Python Dev", "Canada")
尽管这些元组存储了多个对象,但它们代表一个单一实体。第一个元组代表 RGB(红、绿、蓝)颜色,第二个元组代表数据库表中的一行或其他表格数据。
公共和非公共变量名
Python 中广泛使用的一种命名约定是在需要表明某个变量是 Python 定义的“非公共”变量时使用前导下划线。非公共变量是指不应在其定义模块外部使用的变量。这些变量仅用于内部使用:
# timeout.py
_timeout = 30
def get_timeout():
return _timeout
def set_timeout(seconds):
global _timeout
_timeout = seconds
在此模块中,你有一个名为 _timeout 的非公共变量。然后,你有几个处理此变量的函数。然而,该变量本身不打算在包含模块外部使用。
受限和不推荐的名称
Python 保留了一小组称为关键字的词,这些词是语言语法的一部分。要获取 Python 关键字列表,请运行以下代码:
>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'async', ..., 'yield']
在大多数情况下,你无法将这些词用作变量名而不引发错误:
>>> class = "Business"
File "<input>", line 1
class = "Business"
^
SyntaxError: invalid syntax
对于大多数关键字都是如此。如果你需要使用与关键字相同的名称,则可以遵循 PEP 8 的建议,即在名称末尾添加一个尾随下划线:
>>> class_ = "Business"
在此示例中,你在关键字末尾添加了一个下划线字符,从而可以将其用作变量名。尽管这种约定有效,但有时做以下事情更为优雅:
>>> passenger_class = "Business"
现在,你的变量名更具描述性和具体性,从而提高了代码的可读性。
还有软关键字(soft keywords),它们仅在特定上下文中被视为关键字。例如,match 关键字仅在结构模式匹配中被视为关键字。
由于 match 是软关键字,你可以将其用作变量名:
>>> import re
>>> text = "Some text containing a number: 123"
>>> match = re.search("123", text)
>>> if match:
... print("Found a match 😃")
... else:
... print("No match found 😔")
...
Found a match 😃
在此示例中,你导入 re 模块以使用正则表达式。此示例仅在目标文本中搜索一个基本表达式。这里的想法是展示即使 match 是关键字,也可以将其用作有效的变量名。
你应该避免的另一种做法是使用内置名称来命名变量。例如,假设你正在学习 Python 列表并运行以下代码:
>>> list = [1, 2, 3, 4]
>>> list
[1, 2, 3, 4]
在此示例中,你使用 list 作为包含数值的列表对象的名称。这会遮蔽名称背后的原始对象,从而阻止你在代码中使用它:
>>> list(range(10))
Traceback (most recent call last):
...
TypeError: 'list' object is not callable
现在,调用 list() 失败了,因为你已在代码中覆盖了内置名称。因此,最好避免使用内置名称来定义变量。这种做法可能会以不同方式导致代码失败。
注意:要获取完整的内置名称列表,请在交互式会话中运行以下代码:
>>> import builtins >>> dir(builtins) ['ArithmeticError', 'AssertionError', 'AttributeError', ..., 'tuple', 'type', 'vars', 'zip']内置名称列表相当长。为了确保你的变量名不会遮蔽此列表中的某个名称,你可以执行类似
"name" in dir(builtins)的检查。如果此检查返回True,则最好找一个不同的名称。请注意,此建议也适用于你代码中使用的第三方库中定义的对象名称。
探索变量的核心特性
当你开始深入研究 Python 变量在内部的工作原理时,会发现一些值得研究的有趣特性。在以下章节中,你将探索变量的一些核心特性,以便更好地理解它们。
变量持有对对象的引用
当你使用赋值创建变量时会发生什么?这是 Python 中的一个重要问题,因为答案与你在许多其他编程语言中看到的不同。
Python 是一种面向对象的编程语言。Python 程序中的每一段数据都是特定类型或类的对象。考虑以下代码:
>>> 300
300
当遇到语句 300 时,Python 会执行以下操作:
- 创建一个整数对象
- 赋予它值
300 - 在屏幕上显示它
你可以使用内置的 type() 函数查看创建的整数对象:
>>> type(300)
<class 'int'>
Python 变量是一个符号名称,它引用或指向像 300 这样的对象。一旦对象被赋给变量,你就可以通过变量名引用它,但数据本身仍然包含在对象中。
例如,考虑以下变量定义:
>>> n = 300
此赋值创建了一个值为 300 的整数对象,并使变量 n 指向该对象。下图展示了这一过程:
(变量引用图示)
变量赋值
在 Python 中,变量不存储对象。它们指向或引用对象。每次在 Python 中创建对象时,都会为其分配一个唯一编号,然后将其与变量关联。
内置的 id() 函数返回对象的标识符或身份:
>>> n = 300
>>> id(n)
4399012816
在 CPython(标准 Python 发行版)中,对象的身份与其内存地址一致。因此,CPython 变量存储内存地址。通过这些内存地址,变量可以访问存储在内存中的具体对象。
你可以创建多个指向同一对象的变量。换句话说,持有相同内存地址的变量:
>>> m = n
>>> id(n) == id(m)
True
在此示例中,Python 没有创建新对象。它创建了一个新的变量名或引用 m,它指向 n 所指向的同一对象:
(Python 变量引用同一对象的图示)
对单个对象的多重引用
接下来,假设你执行如下操作:
>>> m = 400
现在,Python 创建了一个值为 400 的新整数对象,并使 m 成为对该对象的引用:
(Python 中对分离对象的引用图示)
对分离对象的引用
最后,假设你运行以下语句:
>>> n = "foo"
现在,Python 创建了一个值为 "foo" 的字符串对象,并使 n 成为对该对象的引用:
(Python 变量引用图示)
孤儿对象
由于 n 和 m 的重新赋值,你不再有对整数对象 300 的引用。它变成了孤儿对象,你无法再次访问它。
当对对象的引用降至零时,该对象将无法再被访问。此时,其生命周期结束。Python 会回收分配的内存,以便用于其他用途。在编程术语中,此过程称为垃圾回收。
变量具有动态类型
在许多编程语言中,变量是静态类型的,这意味着它们在声明时就被指定为具有特定数据类型,并在其生命周期内保持该类型。分配给该变量的任何值都必须是该指定类型。
Python 变量并非如此。在 Python 中,你可以在不同时刻为变量分配不同类型的数据值:
>>> value = "A string value"
>>> value
'A string value'
>>> # 稍后
>>> value = 23.5
>>> value
23.5
在此示例中,你使 value 变量引用或指向另一种类型的对象。由于此特性,Python 是一种动态类型语言。
需要注意的是,变量数据类型的更改可能导致运行时错误。例如,如果变量的数据类型意外更改,你可能会遇到类型相关的 bug,甚至引发异常:
>>> value = "A string value"
>>> value.upper()
'A STRING VALUE'
>>> # 稍后
>>> value = 23.5
>>> # 再次尝试使用 .upper()
>>> value.upper()
Traceback (most recent call last):
...
AttributeError: 'float' object has no attribute 'upper'
在此示例中,变量类型在代码执行期间发生了变化。当 value 指向字符串时,你可以使用 .upper() 方法将字母转换为大写。然而,当类型变为浮点数时,.upper() 方法不可用,你会收到 AttributeError 异常。
变量可以使用类型提示
你可以使用**类型提示(type hints)**为变量添加显式的类型信息。为此,可以使用以下 Python 语法:
variable: data_type [= value]
方括号 不是 语法的一部分,它们仅表示括号内的部分是可选的。是的,你可以在不为其赋值的情况下声明一个 Python 变量:
>>> number: int
>>> number
Traceback (most recent call last):
...
NameError: name 'number' is not defined
第一行的变量声明是有效的 Python 语法。然而,这种声明实际上并没有为你创建一个新变量。这就是为什么当你尝试访问 number 变量时会收到 NameError 异常的原因。尽管 number 尚未定义,但 Python 已经记录了该类型提示:
>>> __annotations__
{'number': <class 'int'>}
对于基本数据类型(如数字和字符串),类型提示可能看起来是多余的:
>>> language: str = "Python"
>>> number: int = 42
>>> coefficient: float = 2.87
如果你熟悉 Python 的内置类型,那么你就不需要为这些变量添加类型提示,因为你很快就能知道它们分别是一个字符串、整数和浮点数值。因此,在这种情况下,你可以放心地省略类型提示。此外,你的静态类型检查器或代码检查工具(linter)也不会对此提出警告。
然而,当你使用容器类型(如列表、元组、字典和集合)时,情况就不同了。对于这些类型,为其中包含的数据提供类型提示是有意义的。
举个例子,假设你有以下颜色字典:
>>> colors = {
... "red": "#FF0000",
... "green": "#00FF00",
... "blue": "#0000FF",
... "yellow": "#FFFF00",
... "black": "#000000",
... "white": "#FFFFFF",
... }
在这种情况下,如果能知道键和值的数据类型,将会非常有帮助。你可以使用以下类型提示来提供该信息:
>>> colors: dict[str, str] = {
... "red": "#FF0000",
... "green": "#00FF00",
... "blue": "#0000FF",
... "yellow": "#FFFF00",
... "black": "#000000",
... "white": "#FFFFFF",
... }
在这段更新后的代码中,你明确指出 colors 字典将使用字符串作为键和值。
为什么这个类型提示很重要?假设在代码的另一部分,你还有另一个颜色字典,如下所示:
>>> colors = {
... "Red": (255, 0, 0),
... "Green": (0, 255, 0),
... "Blue": (0, 0, 255),
... "Yellow": (255, 255, 0),
... "Black": (0, 0, 0),
... "White": (255, 255, 255),
... }
在某个时刻,你可能会同时处理这两个字典,却无法明确判断当前需要的是哪一个。为了解决这个问题,你也可以为第二个字典添加类型提示:
>>> colors: dict[str, tuple[int, int, int]] = {
... "Red": (255, 0, 0),
... "Green": (0, 255, 0),
... "Blue": (0, 0, 255),
... "Yellow": (255, 255, 0),
... "Black": (0, 0, 0),
... "White": (255, 255, 255),
... }
此示例中的类型提示稍微复杂一些,但它清楚地表明:该字典的键是字符串,而值是由三个整数组成的元组。
需要注意的是,当用空容器初始化变量时,为引用容器数据类型的变量添加类型提示尤其有用:
>>> fruits: list[str] = []
>>> rows: list[tuple[str, int, str]] = []
在这些示例中,你有两个空列表。由于它们带有类型提示信息,你可以快速判断其内容的数据类型。现在你知道第一个列表将包含字符串,而第二个列表将包含元组,每个元组由三项组成:一个字符串、一个整数和另一个字符串值。
请注意,在此示例中,如果你不提供任何类型信息,你的静态类型检查器或 linter 将明确对列表的类型提示提出警告。
稍后在代码中,你可以使用正确的数据类型向每个列表追加元素:
>>> fruits.append("apple")
>>> fruits.append("orange")
>>> rows.append(("Jane", 25, "Python Dev"))
>>> rows.append(("John", 30, "Web Dev"))
首先,你使用 .append() 方法将新的水果(字符串)添加到 fruits 列表末尾,然后将新的行数据添加到 rows 列表末尾。
使用补充方式创建变量
在 Python 中,你还会发现一些创建新变量的替代方法。有时,你需要或希望同时为多个变量赋予相同的初始值。为此,你可以使用并行赋值(parallel assignment)。
在其他情况下,你可能需要用序列类型(如列表或元组)中的值来初始化多个变量。这时,你可以使用一种称为**可迭代解包(iterable unpacking)**的技术。
还有一些场景下,你需要保留某个表达式的结果值。此时,你可以使用赋值表达式(assignment expression)。
在接下来的部分中,你将学习所有这些创建 Python 变量的替代或补充方法。
并行赋值
Python 还允许你在一行代码中执行多个赋值操作。这一特性使得你可以同时将相同的值赋给多个变量:
>>> is_authenticated = is_active = is_admin = False
>>> is_authenticated
False
>>> is_active
False
>>> is_admin
False
此示例中的并行赋值同时将三个不同但相关的变量初始化为 False。这种方式比下面这种逐个赋值的方式更简洁、更少重复:
>>> is_authenticated = False
>>> is_active = False
>>> is_admin = False
>>> is_authenticated
False
>>> is_active
False
>>> is_admin
False
通过使用并行赋值而非单独赋值,你可以使代码更加简洁且减少重复。
可迭代解包
可迭代解包(也称为元组解包)是 Python 中一项很酷的功能。它指的是将可迭代对象中的值分配给一系列变量。在大多数情况下,变量的数量应与可迭代对象中的项数相匹配。不过,你也可以使用 *variable 语法来捕获列表中的多个项。
你可以使用可迭代解包,通过一个包含值的可迭代对象一次性创建多个变量。例如,假设你有一些关于某个人的数据,并希望为每条数据创建专用变量:
>>> person = ("Jane", 25, "Python Dev")
如果你还不了解可迭代解包,那么你可能会手动将数据分配给不同的变量,如下所示:
>>> name = person[0]
>>> age = person[1]
>>> job = person[2]
>>> name
'Jane'
>>> age
25
>>> job
'Python Dev'
这段代码可以正常工作。然而,使用索引来提取数据可能导致代码容易出错且难以阅读。与其使用这种方法,不如利用可迭代解包,最终得到如下代码:
>>> name, age, job = person
>>> name
'Jane'
>>> age
25
>>> job
'Python Dev'
现在,你的代码看起来更干净、更易读。因此,当你发现自己正在使用索引从可迭代对象中创建变量时,请考虑改用解包。
解包的一个绝佳用例是在两个变量之间交换值:
>>> a = 5
>>> b = 10
>>> a, b = b, a
>>> a
10
>>> b
5
在高亮显示的这一行中,你无需使用临时变量(如前所述)就完成了 a 和 b 的值交换。在此示例中,需要注意等号右侧的可迭代对象实际上是一个由变量组成的元组。
赋值表达式
赋值表达式允许你在条件语句或 while 循环中一步完成表达式结果的赋值和使用。例如,考虑以下循环:它会不断从键盘读取输入,直到你输入单词 "stop" 为止:
>>> line = input("Type some text: ")
>>> while line != "stop":
... print(line)
... line = input("Type some text: ")
...
Type some text: Python
Python
Type some text: Walrus
Walrus
Type some text: stop
这个循环按预期工作。然而,这段代码的缺点是不必要地重复调用了 input()。
你可以使用赋值表达式重写该循环,得到如下代码:
>>> while (line := input("Type some text: ")) != "stop":
... print(line)
...
Type some text: Python
Python
Type some text: Walrus
Walrus
Type some text: stop
在表达式 (line := input("Type some text: ")) 中,你创建了一个名为 line 的新变量,用于保存输入数据的引用。该数据同时也作为表达式的结果返回,并最终与 "stop" 进行比较以结束循环。因此,赋值表达式是创建变量的另一种方式。
理解变量作用域
作用域(scope) 的概念定义了代码中变量和名称的查找方式。它决定了变量在代码中的可见性。变量的作用域取决于你在代码中创建该变量的位置。
在 Python 中,你最多会遇到四种不同的作用域,可以用 LEGB 缩写来表示。该缩写中的字母分别代表:局部(Local)、封闭(Enclosing)、全局(Global) 和 内置(Built-in) 作用域。
在接下来的部分中,你将学习 Python 中变量作用域的基础知识,以及这一概念如何影响你使用变量的方式。
全局变量、局部变量与非局部变量
全局变量 是在模块级别创建的变量。这些变量在包含它们的模块内可见,也可以被导入它们的其他模块所访问。例如,如果你正在使用 Python REPL,那么当前的全局作用域是在一个名为 __main__ 的模块中定义的。
该模块定义了你当前的全局作用域。在此模块中定义的所有变量都是全局的,因此你可以在交互式会话的任何时候使用它们:
>>> value = 42
>>> dir()
[
'__builtins__',
'__doc__',
'__loader__',
'__name__',
...
'value'
]
在此代码片段中,你定义了 value 变量,并调用内置函数 dir() 来查看当前全局作用域中定义的名称列表。在列表末尾,你会找到 'value' 条目,它对应于你的变量。
现在,假设你在同一交互式会话中运行多段代码,以尝试 Python 的一些酷炫功能。在所有这些示例之后,你需要再次使用 value 变量:
>>> value
42
你会注意到这些变量仍然可供你使用。这是因为这个 value 变量对你的代码来说是全局的。
你还可以使用 import 语句导入当前模块外部定义的变量。这种做法的一个典型示例是:你有一个模块用于定义保存某些配置参数的变量。你可以将该变量导入到你的全局作用域中,并像使用任何其他全局变量一样使用它。
举个简单的例子,假设你有以下 config.py 模块:
# config.py
settings = {
"background": "black",
"foreground": "white",
"logo": "logo.png",
"font": "Helvetica",
}
该文件定义了一个字典,其中包含一个假设应用程序的多个配置参数。你可以将此变量导入到你的应用程序的全局作用域中,并按需使用这些设置参数:
>>> from config import settings
>>> settings["background"]
'black'
>>> settings["font"]
'Helvetica'
通过第一行的导入语句,你已将 settings 变量引入当前的全局作用域。你可以从代码的任何部分使用该变量。
局部变量 是在函数内部定义的变量。这些变量可以帮助你将中间计算的结果存储在一个具有描述性的名称下,从而使代码更具可读性和明确性。
考虑以下示例:
>>> def function():
... integer = 42
... return integer
...
>>> function()
42
>>> integer
Traceback (most recent call last):
...
NameError: name 'integer' is not defined
局部变量仅在其定义的函数内部可见。一旦函数返回,这些变量就会消失。这就是为什么你无法在全局作用域中访问 integer 的原因。
类似地,非局部变量(non-local variables) 是在定义了内部函数的外层函数中创建的变量。对外层函数而言是局部的变量,对内部函数而言则是非局部的。当你创建闭包函数和装饰器时,非局部变量非常有用。
以下是一个玩具示例,说明了全局变量、局部变量和非局部变量的工作方式,以及如何在 Python 中识别不同的作用域:
# scopes.py
# 全局作用域
global_variable = "global"
def outer_func():
# 非局部作用域
nonlocal_variable = "nonlocal"
def inner_func():
# 局部作用域
local_variable = "local"
print(f"Hi from the '{local_variable}' scope!")
print(f"Hi from the '{nonlocal_variable}' scope!")
print(f"Hi from the '{global_variable}' scope!")
inner_func()
在此示例中,你首先在模块级别创建了一个全局变量。然后,你定义了一个名为 outer_func() 的函数。在该函数内部,你有一个 nonlocal_variable,它对 outer_func() 是局部的,但对 inner_func() 是非局部的。
在 inner_func() 中,你又创建了一个名为 local_variable 的变量,它对该函数本身是局部的。
注意:你可以在内部函数中访问全局变量和非局部变量。但是,若要在内部函数中更新全局或非局部变量,则需要显式使用
global和nonlocal语句。
这些 print() 调用旨在展示你如何在函数内部访问来自不同作用域的变量。
以下是该函数的运行方式:
>>> from scopes import outer_func
>>> outer_func()
Hi from the 'local' scope!
Hi from the 'nonlocal' scope!
Hi from the 'global' scope!
总之,全局变量在代码的任何位置都可访问。局部变量仅在其定义的函数内可见。非局部变量在其定义(或封闭)函数及其内部函数中可见。
类变量与实例变量(属性)
在使用面向对象编程工具时,你可以在自定义的 Python 类中创建变量。这些变量被称为属性(attributes)。在实践中,你可以拥有类属性和实例属性。
类属性 是在类级别创建的变量,而实例属性 是附加到特定类实例的变量。这些属性只能从类或其实例内部访问。因此,类定义了一个命名空间(namespace),这与作用域类似。
为了说明类属性和实例属性的工作方式,假设你需要一个类来表示公司员工。该类应保存当前员工的信息,你可以将其存储在 .name、.position 等实例属性中。该类还应记录公司当前有多少名员工。要实现此功能,你可以使用一个类属性。
以下是该类的一种可能实现:
# employees.py
class Employee:
count = 0
def __init__(self, name, position, salary):
self.name = name
self.position = position
self.salary = salary
Employee.count += 1
def display_profile(self):
print(f"Name: {self.name}")
print(f"Position: {self.position}")
print(f"Salary: ${self.salary}")
在这个 Employee 类中,你定义了一个名为 count 的类属性,并将其初始化为 0。你将使用该属性作为计数器,跟踪 Employee 实例的数量。像 .count 这样的类属性对类及其所有实例都是共享的。
在初始化方法 .__init__() 中,你定义了三个实例属性,用于保存员工的姓名、职位和薪资。.__init__() 中的最后一行代码每次创建新员工时都会将计数器加 1。为此,你直接在类对象上访问类属性。
注意:你不能使用
self对象来更改类属性的值。例如,如果你写成self.count += 1而不是Employee.count += 1,那么你实际上会创建一个新的实例属性,从而遮蔽类属性。你也可以使用type(self).count代替Employee.count来访问类属性。
最后,你有一个方法用于根据员工的当前信息显示其个人资料。该方法表明,在类内部访问实例属性时需要使用 self 参数。
以下是如何在代码中使用该类的示例:
>>> from employees import Employee
>>> jane = Employee("Jane Doe", "Software Engineer", 90000)
>>> john = Employee("John Doe", "Product Manager", 120000)
>>> jane.display_profile()
Name: Jane Doe
Position: Software Engineer
Salary: $90000
>>> john.display_profile()
Name: John Doe
Position: Product Manager
Salary: $120000
>>> f"Total employees: {Employee.count}"
'Total employees: 2'
在这段代码中,你首先通过使用适当的参数调用类构造函数创建了两个 Employee 实例。然后,你在这两名员工上调用显示方法,并获取相应的信息。最后,你通过 Employee 访问 .count 属性,以获取当前员工总数。
需要注意的是,你可以使用点号表示法在目标实例上访问实例属性,如 .name 或 .position:
>>> jane.name
'Jane Doe'
>>> john.name
'John Doe'
>>> Employee.name
Traceback (most recent call last):
...
AttributeError: type object 'Employee' has no attribute 'name'
实例属性是特定于某个实例的,因此你无法通过类来访问它们。相比之下,类属性对类及其所有实例都是共享的:
>>> jane.count
2
>>> john.count
2
>>> Employee.count
2
要访问类属性,你可以使用实例或类本身。但是,要直接更改类属性,你需要使用类本身。你可以尝试以下示例:
>>> john.count = 100
>>> john.count
100
>>> Employee.count
2
>>> Employee.count = 100
>>> Employee.count
100
在此示例中,你尝试使用实例 john 更新 count 属性。这一操作实际上是在 john 上附加了一个新的实例属性,而不是更新 Employee.count 的值。要更新类属性,你必须使用类本身。
从作用域中删除变量
在 Python 中,你可以使用 del 语句显式地从给定作用域中移除变量(或更一般地说,名称):
>>> city = "New York"
>>> city
'New York'
>>> del city
>>> city
Traceback (most recent call last):
...
NameError: name 'city' is not defined
在此代码片段中,你首先在当前全局作用域中创建了一个名为 city 的新变量。然后,你使用 del 语句将其从所在作用域中移除。当你再次尝试访问该变量时,会收到 NameError 异常。
注意:在实践中,你可以用多种方式使用
del语句从作用域和容器中移除名称。
你应该知道,虽然 del 会移除对对象的引用,但它并不一定会立即释放内存。Python 的垃圾回收器会在对象不再有任何引用时回收其占用的内存。
结论
你现在已掌握在 Python 中使用变量的基础知识,包括如何创建和在代码中使用它们。你了解到 Python 变量可以在不同时间指向不同数据类型的对象,这使得 Python 成为一种动态类型语言。
你还学会了在表达式中使用变量,以及计数器、累加器和布尔标志等常见用例。此外,你还探索了变量命名的最佳实践。
在本教程中,你已经:
- 创建变量并向其赋值
- 动态更改变量的数据类型
- 在表达式、计数器、累加器和布尔标志中使用变量
- 遵循变量命名的最佳实践
- 在特定作用域中创建、访问和使用变量
凭借这些技能,你现在可以自信地管理 Python 程序中的数据,编写可读且易于维护的代码,并应用最佳实践来确保代码质量。