超简单的Python教程系列——第4篇:数据类型和不变性
Posted 飞天程序猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超简单的Python教程系列——第4篇:数据类型和不变性相关的知识,希望对你有一定的参考价值。
现在准备好编写代码了,我认为值得探讨一下Python 里的变量是什么。在讨论的过程中,我们将看看函数、字符串以及所有其他枯燥乏味的东西……这不会像表面那么无聊。这里有很多信息,当把它们放在一起理解时很有意思。
欢迎来到 Python这个奇妙世界。
前言
我在整个系列中都使用术语“变量”,主要是因为这是跨语言的标准术语。在 Python 中使用该术语是有效的,甚至在官方文档中也得到了承认。
但是,Python 变量(variables
)的技术术语实际上是名称(name
); 这与我稍后将提到的“名称绑定”的整个概念有关。
使用你喜欢的任何术语。只需了解 Python“变量”被正式称为“名称”,你很可能会同时听到这两种情况。
数据类型
2018 年夏天,我坐在家里面的沙发上上,登录了 GitHub。我刚刚决定将语言从php 转到 Python,我有一些问题。
我加入#python
专题。
如何在 Python 中声明变量的数据类型?
不一会儿,我收到了回复,我认为这是我第一次真正进入奇异的python编程世界。
<_habnabit> youre a data type
他和其他程序朋友很快就回答了问题。Python 是一种动态类型的语言,这意味着我不必去告诉语言变量中包含什么样的信息。我什至不必使用特殊的“变量声明”关键字。我只需要直接分配值就行,这一点和PHP很像。
netWorth = 52348493767.50
在那一刻,Python 成为了我一直以来很喜欢的语言。
然而,在我们得意忘形之前,我必须指出Python 仍然是一种强类型语言。
嗯,动态类型?强类型?这一切意味着什么?
- 动态类型:变量(对象)的数据类型在运行时确定。与“静态类型”相比,我们最初声明对象的数据类型。(C++ 是静态类型的。)
- 强类型:该语言对你可以对不同数据类型执行的操作有严格的规定,例如将整数和字符串相加。与“弱类型”相比,这种语言几乎可以让你做任何事情,它会为你解决问题。(javascript/PHP 是弱类型的。)
因此,换句话说: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()
现在,事实是,我们很少用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
andis not
进行比较。”
错了,错了,错了。这些条件语句有效,但不是我想的那样。一旦你尝试is
,这个错误的逻辑就会崩溃......
nephews = ["Huey", "Dewey", "Louie"]
if nephews is ["Huey", "Dewey", "Louie"]:
print("Scrooges nephews identified.")
else:
print("Not recognized.")
>>> Not recognized.
你可能会对此稍加研究,甚至确认nephews is nephews
计算结果为True
. 那么令人沮丧的结果到底是怎么回事呢?
麻烦的是,运算is
符检查两个操作数是否是同一个实例,而 Python 里这些有趣的东西,称为不可变类型。
简化一下,当你有一个整数或字符串之类的东西时,程序的内存中实际上只有一个数据存在。之前,当我创建 string 时"Scrooge McDuck"
,只有一个存在(不是一直存在吗?)如果我说......
richestDuck = "Scrooge McDuck"
adventureCapitalist = "Scrooge McDuck"
...我们会说两者richestDuck
和adventureCapitalist
都绑定到内存中的这一实例"Scrooge McDuck"
。它们就像几个路标,都指向只存在一个完全相同的东西。
换句话说,如果你熟悉指针,这有点像。相当于有两个指针指向内存中的同一位置。
如果我们更改其中一个变量,例如richestDuck = "Glomgold"
,我们将重新绑定 richestDuck
以指向内存中不同的位置了。
另一方面,可变类型可以在内存中多次存储相同的数据。列表,如["Huey", "Dewey", "Louie"]
,是这些可变类型之一,这就是用is
比较两个一样的数据时,返回了False。这两个列表虽然包含完全相同的信息,但并不是同一个实例。
技术说明:你应该要知道,不变性实际上与仅共享一个事物的实例无关,这是一种有用的想象方式,但不要指望它总是如此。可以存在多个实例。在不同的交互式终端中,运行结果会存在不一样的结果...
a = 5
b = 5
a is b
>>> True
a = 500
b = 500
a is b
>>> False
a = 500; b = 500; a is b
>>> True
a = "AVeryVeryVeryVeryLongString"
b = "AVeryVeryVeryVeryLongString"
a is b
>>>True
a = "a b"
b = "a b"
a is b
>>>False
不变性背后的原理实际情况要复杂得多。有兴趣可以去了解一下,这里简单说几点:
- 在交互模式下,每行字符串字面量都会申请一个新字符串,但是只含大小写字母、数字和下划线的会被intern,也就是维护了一张dict来使得这些字符串全局唯一;而把这段代码放在py文件执行,第二个也是True了,因为会常量合并
- 单个字符的字符串和空串有内建小对象池,所以测试会是全局只有唯一对象
以上只限cpy实现以及只使用cpy的情况,意思是:一,其他实现不一定保证;二,假如你自己用C扩展了一个py模块,且在C中构造了一个已经被intern的字符串或小对象,则这个字符串对象传回py环境的时候,是一个合法对象,但地址显然和你之前用的不一样,这意味着不要以为在cpy下就可以依赖这种特性。
那么,我们应该使用什么来代替is
? 其实用最原始的==
就可以。
nephews = ["Huey", "Dewey", "Louie"]
if nephews == ["Huey", "Dewey", "Louie"]:
print("Scrooges nephews identified.")
else:
print("Not recognized.")
>>> Scrooges nephews identified.
通常,你应该始终使用==
来比较值和is
比较实例。意思是,虽然它们看起来效果相同,但其实is 是检查两个对象是否指向同一块内存空间,而 == 是检查他们的值是否相等。可以看出,is 是比 == 更严格的检查,is 返回True表明这两个对象指向同一块内存,值也一定相同...
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?")
无论哪种方式都很好,尽管后者被认为是更简单、更合理的方式。
Hungarian Notation
当我还是这门语言的新手时,我有了一个“绝妙”的想法,即使用 Systems Hungarian 表示法来提醒我想要的数据类型。
intFoo = 6
fltBar = 6.5
strBaz = "Hello, world."
事实证明,这个想法既不新颖也不出色。
首先,Systems Hungarian 表示法是对 Apps Hungarian 表示法的一种误解,它本身就是微软开发人员 Charles Simonyi 的聪明想法。
在 Apps Hungarian 中,我们在变量名称的开头使用一个简短的缩写来提醒我们该变量的用途。例如,他在 Microsoft Excel 的开发工作中使用了这一点,其中他将row
在与行相关的任何变量的开头使用,以及col
在与列相关的任何变量的开头使用。这使代码更具可读性,并有助于防止名称冲突(例如rowIndex
vs colIndex
)。时至今日,我在 GUI 开发工作中使用 Apps Hungarian 来区分小部件的类型和用途。
然而,Systems Hungarian 忽略了这一点,并在变量前面加上了数据类型的缩写,例如intFoo
or strBaz
。在静态类型语言中,它是非常多余的,但在 Python 中,它可能是个好主意。
然而,它不是一个好主意的原因是它剥夺了你动态类型语言的优势!我们可以在某个时刻将一个数字存储在一个变量中,然后转身在其中存储一个字符串。只要我们以某种在代码中有意义的方式执行此操作,就可以释放静态类型语言所缺乏的许多潜力。但是,如果我们在精神上将自己锁定在每个变量的一个预先确定的类型中,我们实际上是在将 Python 视为一种静态类型语言,在此过程中使自己步履蹒跚。
尽管如此,Systems Hungarian 在你的 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(...)
语句中,我能够将所有三个部分连接(组合)成一个要打印的字符串,因为所有三个部分都是strings。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("Scrooges \\"money bin\\" is really a huge building.")
>>> Scrooges "money bin" is really a huge building.
就个人而言,在这种情况下,我更喜欢使用(和转义)双引号,因为它们不会像撇号那样逃避我的注意。
但请记住,我们也有那些三重引号 ( """
),我们也可以在这里使用。
print("""Scrooges "money bin" is really a huge building.""")
>>> Scrooges "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 youre not Scrooge McDuck, please dont 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
——但是语言对字符串文字的处理略有不同。
原始字符串以 开头r
,例如...
print(r"I love backslashes: \\ Arent they cool?")
在原始字符串中,反斜杠被视为文字字符。原始字符串中的任何内容都不能“转义”。这对你使用的引号类型有影响,所以要小心。
print("A\\nB")
>>> A
>>> B
print(r"A\\nB")
>>> A\\nB
print(r"\\"")
>>> \\"
这对于正则表达式模式特别有用,在正则表达式模式中,你可能需要大量的反斜杠作为模式的一部分,而不是在它到达那里之前被 Python 解释出来。始终将原始字符串用于正则表达式模式。
注意:如果反斜杠是原始字符串中的最后一个字符,它仍然会转义结束引号,并因此产生语法错误。这与 Python 自己的语言词法规则有关,与字符串无关。
string 的另一种“类型”是格式化的 string或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)
,我们实际上不必按顺序传递参数,只要它们都有一个值。
默认参数
说到这里,你有没有注意到我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
将返回任何值treasure
。(顺便说一句,该代码不起作用,因为我从未定义过treasure
。这只是一个例子。)
这个惯例让我们很容易处理可选的返回:
treasure = landPlane()
if treasure:
storeInMoneyBin(treasure)
NoneType
是一个很好的东西。
注意:你会注意到,本指南中的所有其他函数都缺少return
声明。一个函数如果到达末尾没有找到return
语句,则自动返
以上是关于超简单的Python教程系列——第4篇:数据类型和不变性的主要内容,如果未能解决你的问题,请参考以下文章