非常简单的 Python:数据类型和不变性
迂腐的观点
数据类型在哪里?!?
不变的真理
警告:匈牙利命名法
试镜
悬而未决
功能
审查
喜欢这些文章吗?那就买本书吧! Jason C. McDonald 的《Dead Simple Python》现已由 No Starch Press 出版。
我收到了Damian Rivas对这个系列的精彩评论......
我刚刚读完了目前发布的前两部分。不得不说,Python 差不多是我接触的第五门语言了,我真的很欣赏这种教学风格!很难找到不以“什么是变量”开头的教材,哈哈。
不想让人失望,达米安,但我无法永远避免变数!
好了好了,我就不跟你们解释变量了,第 2,582,596 个解释。你们很聪明,我相信你们现在应该都懂了。
既然我们已经准备好编写代码了,我觉得有必要聊聊Python 中的变量是什么。顺便说一下,我们先来了解一下函数、字符串,以及其他那些枯燥乏味的东西……其实它们在底层可能并没有那么枯燥。这里面有很多信息,但我相信,把它们放在一起理解会更有道理。
欢迎来到 Python。进入兔子洞时请注意家具。
迂腐的观点
我在整个系列中都使用“变量”这个术语,主要是因为它是跨语言的标准术语。在 Python 中使用该术语是有效的,甚至在官方文档中也得到了认可。
然而,Python 变量的技术术语实际上是name
;它与我稍后将提到的“名称绑定”的整个概念有关。
使用任何你觉得舒服的术语就好。只要理解 Python 的“变量”官方说法是“名称”,你很可能听到这两种说法。
数据类型在哪里?!?
2011 年夏天,我坐在华盛顿州西雅图的门廊秋千上,登录了 Freenode IRC 聊天室。我刚刚决定将语言从 Visual Basic .NET 切换到 Python,我有一些问题想问。
我加入#python
并立即投入其中。
如何在 Python 中声明变量的数据类型?
不一会儿,我就收到了回复,我认为这是我第一次真正进入奇妙的编程世界。
<_habnabit> 你是一个数据类型
他和其他常客很快就给我讲解了。Python是一种动态类型语言,这意味着我不需要告诉语言变量中应该包含什么样的信息。我甚至不需要使用特殊的“变量声明”关键字。我只需赋值即可。
netWorth = 52348493767.50
正是在那一刻,Python 成为了我最喜欢的语言。
然而,在我们忘乎所以之前,我必须指出Python 仍然是一种强类型语言。
嗯,动态类型?强类型?这些是什么意思?
-
动态类型:变量(对象)的数据类型在运行时确定。与“静态类型”相对,“静态类型”是指初始时声明对象的数据类型。(C++ 是静态类型的。)
-
强类型:该语言对不同类型的操作有严格的规定,例如将整数和字符串相加。与之相对的是“弱类型”,在弱类型中,该语言几乎允许你执行任何操作,并且会自动帮你完成。(JavaScript 是弱类型的。)
(如果您想要更高级的解释,请参阅为什么 Python 是一种动态语言,也是一种强类型语言)。
换句话说:Python 变量有数据类型,但语言会自动找出该数据类型。
因此,我们可以重新分配一个变量来包含我们想要的任何数据......
netWorth = 52348493767.50
netWorth = "52.3B"
...但我们能做的有限...
netWorth = 52348493767.50
netWorth = netWorth + "1 billion"
>>> Traceback (most recent call last):
>>> File "<stdin>", line 1, in <module>
>>> TypeError: unsupported operand type(s) for +: 'float' and 'str'
如果我们需要知道某个变量的类型,可以使用该type()
函数。它会打印出该变量所属的类。(在 Python 中,一切都是对象,所以要学会面向对象。)
netWorth = 52348493767.50
type(netWorth)
>>> <class 'float'>
在对某个数据类型进行操作之前,我们可能确实需要检查一下它的类型。为此,我们可以将type()
函数与is
运算符配对,如下所示:
if type(netWorth) is float:
swimInMoneyBin()
然而,在很多情况下,使用 可能更好,因为这将考虑子类和继承(面向对象编程,有人吗?)额外isinstance()
的type()
好处是,函数本身返回 True 或 False...
if isinstance(netWorth, float):
swimInMoneyBin()
事实上,我们很少用 来检查。Python 爱好者更喜欢鸭子类型isinstance()
的哲学;也就是说,我们不是检查类型是什么,而是直接寻找对象所需的特性。如果它看起来像鸭子,走起来像鸭子,叫起来也像鸭子,那它肯定就是鸭子。至于它实际上是一只机器鸭子,还是一只穿着鸭子服装的驼鹿,那就无所谓了;如果它具备我们需要的特性,那么其余的通常就没什么意义了。
不变的真理
由于我刚刚介绍了该is
操作符,我们最好澄清一些事情:is
不要==
做同样的事情!
许多 Python 新手发现这很有效......
richestDuck = "Scrooge McDuck"
if richestDuck is "Scrooge McDuck":
print("I am the richest duck in the world!")
if richestDuck is not "Glomgold":
print("Please, Glomgold is only the SECOND richest duck in the world.")
>>> I am the richest duck in the world!
>>> Please, Glomgold is only the SECOND richest duck in the world.
“哦,太酷了!”西雅图一位年轻的开发者说道。“所以,在 Python 中,我只用is
‘和’is not
来比较。”
错,错,错!那些条件语句虽然有效,但原因却与我想象的不一样。is
一旦你尝试这样做,这个错误的逻辑环境就崩溃了……
nephews = ["Huey", "Dewey", "Louie"]
if nephews is ["Huey", "Dewey", "Louie"]:
print("Scrooge's nephews identified.")
else:
print("Not recognized.")
>>> Not recognized.
“等等,什么?”你可能会仔细琢磨一下,甚至确认一下 的nephews is nephews
计算结果是否为True
。那么 dismal downs 到底是怎么回事呢?
问题是,运算is
符检查两个操作数是否是同一个实例,而 Python 中存在称为不可变类型的奇怪东西。
简单来说,当你有一个整数或字符串之类的东西时,程序内存中实际上一次只有一个这样的数据。之前我创建字符串的时候"Scrooge McDuck"
,只有一个存在(难道不是一直都存在吗?)。如果我说……
richestDuck = "Scrooge McDuck"
adventureCapitalist = "Scrooge McDuck"
……我们会说 和 都richestDuck
绑定adventureCapitalist
到了内存中的这个 实例"Scrooge McDuck"
。它们就像一对路标,指向同一个东西,而我们只有一个。
换句话说,如果你熟悉指针,这有点像那样(没有可怕的尖锐边缘)。你可以有两个指针指向内存中的同一个位置。
如果我们修改了其中一个变量,比如说richestDuck = "Glomgold"
,我们就会重新绑定 richestDuck
,使其指向内存中不同的值。(我们也会因为声称 Glomgold 如此富有而感到自豪。)
另一方面,可变类型["Huey", "Dewey", "Louie"]
可以在内存中多次存储相同的数据。像 这样的列表就是这种可变类型之一,这就是为什么该is
运算符会报告它之前所做的事情。这两个列表虽然包含完全相同的信息,但它们并不是同一个 实例。
技术说明:您应该知道,不变性实际上与仅共享一个实例无关,尽管这是一个常见的副作用。这是一种有用的想象方式,但不要指望它总是如此。可以存在多个实例。在交互式终端中运行此代码,以了解我的意思……
a = 5
b = 5
a is b
>>> True
a = 500
b = 500
a is b
>>> False
a = 500; b = 500; a is b
>>> True
不变性背后的真相远比这复杂得多。我的 Freenode#python
朋友 Ned Batchelder ( nedbat
) 就此发表了一篇精彩的演讲,你绝对应该去看看。
那么,我们应该用什么来代替is
?你会很高兴知道,它只是老式的==
。
nephews = ["Huey", "Dewey", "Louie"]
if nephews == ["Huey", "Dewey", "Louie"]:
print("Scrooge's nephews identified.")
else:
print("Not recognized.")
>>> Scrooge's nephews identified.
通常,你应该始终使用==
(etc.) 来比较值,并is
使用 (etc.) 来比较实例。这意味着,尽管它们看起来作用相同,但前面的示例实际上应该这样写……
richestDuck = "Scrooge McDuck"
if richestDuck == "Scrooge McDuck":
print("I am the richest duck in the world!")
if richestDuck != "Glomgold":
print("Please, Glomgold is only the SECOND richest duck in the world.")
>>> I am the richest duck in the world!
>>> Please, Glomgold is only the SECOND richest duck in the world.
有一个半例外……
license = "1245262"
if license is None:
print("Why is Launchpad allowed to drive, again?")
检查非值是很常见的,foo is None
因为只有一个None
非值。当然,我们也可以用更简洁的方式做到这一点……
if not license:
print("Why is Launchpad allowed to drive, again?")
两种方法都可以,但后者被认为是更干净、更“Pythonic”的方法。
警告:匈牙利命名法
当我还不熟悉该语言时,我突然想到使用系统匈牙利表示法来提醒我想要的数据类型。
intFoo = 6
fltBar = 6.5
strBaz = "Hello, world."
事实证明,这个想法既不新颖,也不高明。
首先,系统匈牙利表示法是对应用程序匈牙利表示法的严重误解,而应用程序匈牙利表示法本身是微软开发人员 Charles Simonyi 的聪明想法。
在 Apps Hungarian 中,我们在变量名开头使用简短的缩写来提醒我们该变量的用途。例如,他在开发 Microsoft Excel 时就使用了这种方法,他会row
在任何与行相关的变量开头以及col
与列相关的变量开头使用它。这使得代码更具可读性,并且非常有助于防止名称冲突(例如rowIndex
vs colIndex
)。直到今天,我仍然在 GUI 开发中使用 Apps Hungarian,以区分小部件的类型和用途。
然而,系统匈牙利算法完全忽略了这一点,而是在变量前面加上了数据类型intFoo
的缩写,例如或strBaz
。在静态类型语言中,这显然是多余的,但在 Python 中,这可能是一个好主意。
然而,这并不是一个好主意,因为它剥夺了动态类型语言的优势!我们可以一会儿把数字存入变量,一会儿又把字符串存入变量。只要我们以某种在代码中合理的方式进行操作,就能释放出静态类型语言所缺乏的大量潜力。但如果我们刻意将每个变量锁定在一个预先确定的类型上,我们实际上就是把 Python 当作静态类型语言,从而在这个过程中束缚了我们自己。
总而言之,系统匈牙利算法不适用于你的 Python 代码。坦白说,它不适用于任何代码。立即从你的代码库中移除它,以后再也不要提它了。
试镜
让我们暂时放下关于不变性的脑力劳动,来谈谈一些更容易理解的事情:类型转换。
不,这不是大卫·田纳特为史高治·麦克老鸭配音的那种类型的演员……尽管他在这个角色中表现得非常出色。
我正在谈论将数据从一种数据类型转换为另一种数据类型,而在 Python 中,这非常容易,至少对于我们的标准类型而言。
例如,要将整数或浮点数转换为字符串,我们只需使用该str()
函数。
netWorth = 52348493767.50
richestDuck = "Scrooge McDuck"
print(richestDuck + " has a net worth of $" + str(netWorth))
>>> Scrooge McDuck has a net worth of $52348493767.5
在该print(...)
语句中,我能够将所有三个部分连接(组合)成一个要打印的字符串,因为所有三个部分都是字符串。print(richestDuck + " has a net worth of $" + netWorth)
会失败,TypeError
因为 Python 是强类型的(还记得吗?),并且您不能直接组合浮点数和字符串。
您可能会有点困惑,因为这是有效的......
print(netWorth)
>>> 52348493767.5
这是因为print(...)
函数会在后台自动处理类型转换。但它无法处理那个+
运算符——它在数据传递之前就发生了print(...)
——所以我们必须自己在那里进行转换。
当然,如果你正在编写一个类,你需要自己定义这些函数,但这超出了本文的讨论范围。(提示:__str__()
并__int__()
分别处理将对象转换为字符串或整数。)
悬而未决
既然我们讨论的是字符串,那么有几点需要了解。最让人困惑的,或许是定义字符串字面量的方法有很多种……
housekeeper = "Mrs. Beakley"
housekeeper = 'Mrs. Beakley'
housekeeper = """Mrs. Beakley"""
'...'
我们可以用单引号、双引号"..."
或三引号来包裹字面值"""..."""
,Python 会(大部分情况下)以相同的方式处理它。第三种方式有些特殊,我们稍后再讨论。
Python 风格指南PEP 8解决了单引号和双引号的使用问题:
在 Python 中,单引号字符串和双引号字符串是相同的。本 PEP 对此不做任何建议。请选择一条规则并严格遵守。但是,当字符串包含单引号或双引号字符时,请使用另一个引号,以避免字符串中出现反斜杠。这可以提高可读性。
当我们处理这样的事情时这会很有用......
quote = "\"I am NOT your secretary,\" shouted Mrs. Beakley."
quote = '"I am NOT your secretary," shouted Mrs. Beakley.'
显然,第二种选择可读性更强。引号前的反斜杠表示我们想要的是字面字符,而不是让 Python 将其视为字符串的边界。但是,由于我们包裹字符串的引号必须匹配,如果我们用单引号包裹,Python 会将双引号视为字符串中的字符。
只有在字符串中同时包含两种类型的引号时,我们才真正需要这些反斜杠。
print("Scrooge's \"money bin\" is really a huge building.")
>>> Scrooge's "money bin" is really a huge building.
就我个人而言,在这种情况下,我更喜欢使用(并转义)双引号,因为它们不会像撇号那样引起我的注意。
但请记住,我们还有三重引号 ( """
),我们也可以在这里使用。
print("""Scrooge's "money bin" is really a huge building.""")
>>> Scrooge's "money bin" is really a huge building.
不过,在你为了方便起见把所有字符串都用三重引号括起来之前,请记住我说过它们有一些特殊之处。事实上,有两点。
首先,三重引号是多行的。换句话说,我可以用它们来做这件事……
print("""\
Where do you suppose
Scrooge keeps his
Number One Dime?""")
>>> Where do you suppose
>>> Scrooge keeps his
>>> Number One Dime?
所有内容,包括换行符和前导空格,在三重引号中都是字面意义上的。唯一的例外是,如果我们使用反斜杠 ( \
) 来转义某些内容,就像我在开头对换行符所做的那样。我们通常这样做,只是为了让代码更简洁。
内置textwrap
模块有一些用于处理多行字符串的工具,包括允许您在不包含它的情况下进行“正确”缩进的工具(textwrap.dedent
)。
三重引号的另一个特殊用途是创建文档字符串,它为模块、类和函数提供基本文档。
def swimInMoney():
"""
If you're not Scrooge McDuck, please don't try this.
Gold hurts when you fall into it from forty feet.
"""
pass
这些经常被误认为是注释,但它们实际上是 Python 会执行的有效代码。文档字符串必须出现在其所讨论内容(例如函数)的第一行,并且必须用三重引号括起来。之后,我们可以通过以下两种方式之一访问该文档字符串,如下所示:
# This always works
print(swimInMoney.__doc__)
# This works in the interactive shell only
help(swimInMoney)
特殊字符串类型
我想简单介绍一下 Python 提供的另外两种字符串类型。实际上,它们并不是真正不同的字符串类型——它们都是该类的不可变实例str
——只是 Python 对字符串字面量的处理略有不同。
原始字符串前面带有r
,例如...
print(r"I love backslashes: \ Aren't they cool?")
在原始字符串中,反斜杠被视为文字字符。原始字符串中的内容不能被“转义”。这会对您使用的引号类型产生影响,因此请务必谨慎。
print("A\nB")
>>> A
>>> B
print(r"A\nB")
>>> A\nB
print(r"\"")
>>> \"
这对于正则表达式模式尤其有用,因为你可能会在模式中包含大量的反斜杠,而这些反斜杠在到达模式之前不会被 Python 解释掉。因此,对于正则表达式模式,请始终使用原始字符串。
警告:如果反斜杠是原始字符串的最后一个字符,它仍然会将结尾的引号转义,从而导致语法错误。这与 Python 自身的词法分析规则有关,而非字符串本身。
另一种字符串“类型”是格式化字符串,或称f-string,它是 Python 3.6 中新增的。它允许你以非常优雅的方式将变量的值插入到字符串中,而无需像之前那样进行连接或转换。
我们在字符串前面加上一个f
。在 里面,我们可以用 包裹变量来替换它们{...}
。我们把它们放在一起,像这样……
netWorth = 52348493767.50
richestDuck = "Scrooge McDuck"
print(f"{richestDuck} has a net worth of ${netWorth}.")
>>> Scrooge McDuck has a net worth of $52348493767.5.
你不仅可以在花括号 ( {...}
) 中输入变量!实际上,你可以将任何有效的 Python 代码放入其中,包括数学运算、函数调用、表达式……任何你需要的内容。
与旧的str.format()
方法和%
格式(我这里不会介绍)相比,f 字符串要快得多。这是因为它们在代码运行之前就被求值了。
格式化的字符串由PEP 498定义,因此请前往那里获取更多信息。
功能
趁着讲完基础知识,我们来聊聊 Python 函数。我不会再重新定义“函数”来炫耀你的智商了。举个简单的例子就够了。
def grapplingHook(direction, angle, battleCry):
print(f"Direction = {direction}, Angle = {angle}, Battle Cry = {battleCry}")
grapplingHook(43.7, 90, "")
def
说我们正在定义一个函数,然后我们给出函数名,并在括号中给出参数名。哈欠
让我们让它更有趣一点。(以下内容适用于 Python 3.6 及更高版本。)
def grapplingHook(direction: float, angle: float, battleCry: str = ""):
print(f"Direction = {direction}, Angle = {angle}, Battle Cry = {battleCry}")
grapplingHook(angle=90, direction=43.7)
信不信由你,这才是正宗的 Python!里面有很多精妙的小玩意儿,我们来分解一下。
调用函数
当我们调用一个函数时,我们显然可以按照参数在函数定义中出现的顺序提供参数,就像在第一个例子中一样:grapplingHook(43.7, 90, "")
。
但是,如果我们愿意,我们实际上可以指定要将哪些值传递给哪个参数。这在很多情况下使我们的代码更具可读性:grapplingHook(angle=90, direction=43.7)
。此外,我们实际上不必按顺序传递参数,只要它们都有值即可。
默认参数
说到这里,你有没有注意到,在第二次调用中我省略了 for 的值battleCry
,而它却没有生气?那是因为我在函数定义中为参数提供了一个默认值……
def grapplingHook(direction, angle, battleCry = ""):
在这种情况下,如果没有提供 的值,则使用battleCry
空字符串。实际上,我可以在那里输入任何我想要的值: 、,或者其他任何值。""
"Yaargh"
None
它很常见地用作None
默认值,因此您可以检查参数是否具有指定的值,如下所示......
def grapplingHook(direction, angle, battleCry = None):
if battleCry:
print(battleCry)
但是,如果你只是要做这样的事情......
def grapplingHook(direction, angle, battleCry = None):
if not battleCry:
battleCry = ""
print(battleCry)
...此时,您最好从一开始就给出battleCry
该默认值。""
陷阱警告:默认参数只计算一次,并在所有函数调用之间共享。这对于可变类型(例如空列表)来说,会产生奇怪的影响[]
。不可变类型可以作为默认参数,但应该避免使用可变的默认参数。
警告:您必须在可选参数(具有默认值的参数)之前列出所有必需参数(没有默认值的参数)。这是不行的,因为可选参数在必需参数之前。(direction=0, angle, battleCry = None)
direction
angle
类型提示和函数注释
如果您熟悉 Java 和 C++ 等静态类型语言,这可能会让您有点兴奋……
def grapplingHook(direction: float, angle: float, battleCry: str = "") -> None:
但这并没有达到你想象的效果!
我们可以在 Python 3.6 及更高版本中提供类型提示,它们提供的正是:关于应该传入什么数据类型的提示-> None
。类似地,冒号 ( :
) 之前的部分提示了返回类型。
然而...
- 如果传递错误的类型,Python 不会抛出错误。
- Python 不会尝试转换为该类型。
- Python 实际上会忽略这些提示并继续执行,就好像它们不存在一样。
那么重点是什么呢?类型提示确实有一些优点,但最直接的优点是文档。函数定义现在会显示它需要什么类型的信息,这在 IDE 会在你输入参数时自动显示提示的情况下尤其有用。某些 IDE 和工具甚至会在你执行一些奇怪的操作时发出警告,比如,将一个字符串传递给类型提示为整数的对象;事实上,PyCharm 在这方面非常擅长!像 Mypy 这样的静态类型检查器也会这样做。我不会在这里讨论这些工具,但可以说,它们确实存在。
我应该特别说明一下,上面的类型提示是一种函数注解,它有各种各样的巧妙用例。这些在PEP 3107中有更详细的定义。
使用Python 3.5 中添加的typing
模块,您可以通过多种方式使用类型提示,甚至超越函数定义。
重载函数?
你可能已经猜到了,由于 Python 是动态类型的,我们不太需要重载函数。因此,Python 甚至不提供重载函数!通常只能有一个版本。如果多次定义同名函数,我们定义的最后一个版本将会遮蔽(隐藏)所有其他版本。
因此,如果您希望函数能够处理许多不同的输入,则需要利用 Python 的动态类型特性。
def grapplingHook(direction, angle, battleCry: str = ""):
if isinstance(direction, str):
# Handle direction as a string stating a cardinal direction...
if direction == "north":
pass
elif direction == "south":
pass
elif direction == "east":
pass
elif direction == "west":
pass
else:
# throw some sort of error about an invalid direction
else:
# Handle direction as an angle.
注意,我上面省略了类型提示,因为我要处理多种可能性。说实话,这是一个很糟糕的例子,但你明白我的意思了。
陷阱警报:虽然这完全合理,但它几乎总是一种“代码异味”——糟糕设计的标志。你应该isinstance()
尽量避免,除非它绝对是解决问题的最佳方法……而且你可能一辈子都无法做到这一点!
返回类型
如果您是 Python 新手,您可能还注意到缺少了一些东西:返回类型。我们实际上并没有直接指定返回类型:我们只是在需要时返回一些值。如果我们想在函数执行过程中退出而不返回任何值,我们可以直接使用return
。
def landPlane():
if getPlaneStatus() == "on fire":
return
else:
# attempt landing, and then...
return treasure
这句话的意思return
和说的一样return None
,whilereturn treasure
会返回 is 的值treasure
。(顺便说一句,这段代码不会运行,因为我从来没有定义过 treasure。这只是一个比较简单的示例。)
这种约定让我们可以轻松处理可选的返回值:
treasure = landPlane()
if treasure:
storeInMoneyBin(treasure)
NoneType
真是一件奇妙的事。
提示:您会注意到,本指南中的所有其他函数都缺少return
语句。如果函数None
执行到末尾仍未找到return
语句,则会自动返回;无需在末尾添加语句。
类型提示和默认值
当使用类型提示时,您可能会想这样做......
def addPilot(name: str = None):
if name is not None:
print(name)
else:
print("Who is flying this thing?!?")
这曾经是可以接受的,但现在已不再被认为是正确的。您应该使用Optional[...]
来处理这种情况。
def addPilot(name: Optional[str] = None):
审查
希望你对 Python 的类型系统不再感到困惑,也希望你在探索的过程中没有磕磕绊绊。以下是重点内容:
-
Python 是动态类型的,这意味着它在运行时确定对象的数据类型。
-
Python 是强类型的,这意味着对于对任何给定数据类型的操作都有严格的规则。
-
Python 中的许多数据类型都是不可变的,这意味着内存中只存在数据的副本,并且每个包含该数据的变量都只指向该主副本。而可变类型则不会这样做。
-
is
检查操作数是否是同一个对象的实例==
,而比较值。不要混淆它们。 -
系统匈牙利命名法(例如
intFoo
)是个坏主意。请不要这么做。 -
您可以将字符串括在单引号 (
'...'
) 或双引号 ("..."
) 中。 -
三引号字符串 ("""...""") 用于多行字符串。它们也可以用于文档字符串,用于记录函数、类或模块。
-
原始字符串 (
r"\n"
) 将任何反斜杠视为字面值。这使得它们非常适合正则表达式模式。 -
格式化字符串(
f"1 + 1 = {1+1}"
)让我们可以神奇地将一些代码的结果替换为字符串。 -
可以为函数参数指定默认值,使其成为可选参数。所有可选参数都应位于必需参数之后。
-
类型提示可让您“提示”应将哪种类型的数据传递到函数参数中,但这将被视为建议,而不是规则。
与往常一样,您可以在 Python 文档中找到有关这些主题的更多信息。
感谢deniska
、、grym
和ikanobori
(Freenode IRC #python
)提出的修改建议。