Skip to content

Latest commit

 

History

History
962 lines (602 loc) · 32.3 KB

File metadata and controls

962 lines (602 loc) · 32.3 KB

四、内置类型和对象模型

Python 语言最基本的设计元素之一是对象的使用。对象不仅是用户级结构的中心数据结构,而且是语言本身的许多内部工作。在本章中,我们将开始了解这在原则上和实践中的意义,希望您将开始了解对象在 Python 中的普及程度。

我们将了解对象是什么,如何使用它们,以及如何管理对它们的引用。我们还将开始探索 Python 中的类型的概念,我们将看到 Python 的类型与许多其他流行语言中的类型既相似又不同。作为本次探索的一部分,我们将更深入地了解我们已经遇到的一些集合类型,并介绍更多的集合类型。

Python 对象引用的性质

在前面的章节中,我们已经讨论并使用了 Python 中的“变量”,但变量到底是什么?考虑将一个整数赋给变量的简单方法:

>>> x = 1000

我们这样做的时候到底发生了什么?首先,Python 创建一个 int对象,其值为1000。这个对象是匿名的,因为它本身没有名称(x或其他名称)。它只是 Python 运行时系统分配和跟踪的对象。

创建完对象后,Python 会创建一个名为x对象引用,并安排x引用int(1000)对象:

Figure 4.1: Assign the name 'x' to an integer object with the value 1000

重新分配引用

现在我们将使用另一个赋值来修改x的值:

>>> x = 500

这不会导致对我们之前构建的int(1000)对象进行任何形式的更改。Python 中的整数对象是不可变的,不能更改。事实上,这里发生的事情是 Python 首先创建一个新的不可变的integer对象,其值为500,然后重定向x引用以指向新对象:

Figure 4.2: Reassign the name 'x' to a new integer object with the value 500

由于我们没有对原始int(1000)对象的其他引用,我们现在无法从代码中访问它。因此,Python 垃圾收集器可以在选择的时候自由地收集垃圾。

将一个引用分配给另一个引用

当我们从一个变量赋值到另一个变量时,我们真正做的是从一个对象引用赋值到另一个对象引用,这样两个引用都引用同一个对象。例如,让我们将现有变量x分配给一个新变量y

>>> y = x

这为我们提供了这个结果参考对象图:

Figure 4.3: Assign the existing name 'x' to the name 'y'

现在两个引用引用同一个对象。我们现在将x重新分配给另一个新的integer

>>> x = 3000

这样做会给我们一个参考对象图,显示我们的两个参考和两个对象:

Figure 4.4: Assign a new integer 3000 to 'x'

在这种情况下,垃圾收集器没有工作要做,因为所有对象都可以从活动引用访问。

使用 id()探索价值与身份

让我们使用内置的id()函数更深入地挖掘对象和引用之间的关系。id()接受任何对象作为参数,并返回一个整数标识符,该标识符在对象的生命周期内是唯一且恒定的。让我们使用id()重新运行上一个实验:

>>> a = 496
>>> id(a)
4302202064
>>> b = 1729
>>> id(b)
4298456016
>>> b = a
>>> id(b)
4302202064
>>> id(a) == id(b)
True

这里我们看到最初的ab指的是不同的对象,因此id()为每个变量提供了不同的值。然而,当我们随后将a分配给b时,两个名称都引用同一个对象,因此id()为这两个名称提供了相同的值。这里的主要教训是,id()可以用来建立一个对象的身份,而不依赖于对它的任何特定引用。

与 is 的身份平等性测试

实际上,id()函数很少用于生产 Python 代码。它主要用于对象模型教程(如本教程!)和调试工具。比id()函数更常用的是 is 运算符,它测试身份的相等性。也就是说,is 测试两个引用是否引用同一对象:

>>> a is b
True

我们在前面的第 1 章入门中已经见过 is 运营商,当时我们测试了None

>>> a is None
False

重要的是要记住,is 总是在测试身份平等,也就是说,两个引用是否引用了完全相同的对象。稍后,我们将深入研究另一种主要类型的等式,值等式

突变而不突变

即使是看似自然变异的操作也未必如此。考虑增广赋值算子:

>>> t = 5
>>> id(t)
4297261280
>>> t += 2
>>> id(t)
4297261344

乍一看,我们似乎要求 Python 将整数值t增加 2。但是这里的id()结果清楚地表明t指的是强化赋值前后的两个不同对象。

这里不是修改整数对象,而是描述实际发生的情况。最初,我们的名称t指的是int(5)对象:

Figure 4.5: 'x' refers to the integer 5

接下来,为了执行2t的增广赋值,Python 在幕后创建一个int(2)对象。注意,我们从来没有对这个对象的命名引用;它完全由 Python 代表我们进行管理:

Figure 4.6: Python creates an integer 2 behind the scenes

Python 然后在t和匿名int(2)之间执行加法操作,给我们——你猜对了!-另一个整数对象,这次是一个int(7)

Figure 4.7: Python creates a new integer as the result of the addition

最后,Python 的增广赋值运算符只需将名称t重新分配给 对象new int(7)对象,其余整数对象由垃圾收集器处理:

Figure 4.8: Python reassigned the name 't' to the result of the addition

对可变对象的引用

Python 对象为所有类型显示此名称绑定行为。赋值运算符仅将对象绑定到名称,从不按值复制对象。为了让这一点非常清楚,让我们来看另一个使用可变对象的示例:列表。与我们刚才看到的不可变的ints不同,列表对象具有可变状态,这意味着list对象的值可以随时间变化。

为了说明这一点,我们首先创建一个包含三个元素的list对象,将 list 对象绑定到一个名为r的引用:

>>> r = [2, 4, 6]
>>> r
[2, 4, 6]

然后,我们将参考r分配给一个新的参考s

>>> s = r
>>> s
[2, 4, 6]

这种情况下的参考对象图清楚地表明,我们有两个名称引用单个列表实例:

Figure 4.9: 's' and 'r' refer to the same list object

当我们通过修改中间元素来修改s引用的列表时,我们看到r引用的列表也发生了变化:

>>> s[1] = 17
>>> s
[2, 17, 6]
>>> r
[2, 17, 6]

同样,这是因为名称sr指的是同一个可变对象,我们可以使用前面了解的is关键字来验证这一事实:

>>> s is r
True

本次讨论的要点是,Python 实际上并没有暗喻意义上的包含值的框的变量。它只有对对象的命名引用,这些引用的行为更像是允许我们检索对象的标签。也就是说,在 Python 中谈论变量仍然很常见,因为它很方便。在本书中,我们将继续这样做,因为您现在了解了幕后真正发生的事情。

价值平等(等价)与身份平等

让我们将该行为与值相等或等价性测试进行对比。我们将创建两个相同的列表:

>>> p = [4, 7, 11]
>>> q = [4, 7, 11]
>>> p == q
True
>>> p is q
False

这里我们看到pq指的是不同的对象,但它们所指的对象具有相同的值:

Figure 4.10: 'p' and 'q' different list objects with identical values

正如您在测试值相等性时所期望的,对象应始终与自身相等:

>>> p == p
True

价值平等和身份认同是“平等”的两个根本不同的概念,在你的头脑中把它们分开是很重要的。

值得注意的是,价值比较是通过编程定义的。定义类型时,可以控制该类如何确定值相等。相反,身份比较是由语言定义的,你不能改变这种行为。

参数传递语义–按对象引用传递

现在让我们看看所有这些与函数参数和返回值的关系。调用函数时,我们会创建新的名称绑定(函数定义中声明的绑定)到现有对象(调用时传入的绑定)。因此,如果您想知道函数是如何工作的,那么真正理解 Python 引用语义是很重要的。

修改函数中的外部对象

为了演示 Python 的参数传递语义,我们将在 REPL 处定义一个函数,该函数将值附加到列表并打印修改后的列表。首先,我们将创建一个列表,并将其命名为m

>>> m = [9, 15, 24]

然后我们将定义一个函数modify(),它将附加并打印传递给它的列表。函数接受一个名为k的形式参数:

>>> def modify(k):
...     k.append(39)
...     print("k =", k)
...

然后我们调用modify(),将列表m作为实际参数传递:

>>> modify(m)
k = [9, 15, 24, 39]

这确实会打印包含四个元素的修改列表。但是,我们的列表引用的m现在在函数之外指的是什么?

>>> m
[9, 15, 24, 39]

m引用的列表已被修改,因为它是函数内部k引用的同一列表。正如我们在本节开头提到的,当我们将一个对象引用传递给一个函数时,我们本质上是从实际参数引用(在本例中为 m)赋值到正式参数引用(在本例中为k

Figure 4.11: Referring to the same list in and out of a function

正如我们所看到的,赋值使赋值对象引用与赋值对象引用相同的对象。这正是这里发生的事情。如果您希望函数修改对象的副本,则该函数负责进行复制。

在函数中绑定新对象

让我们看另一个有启发性的例子。首先,我们将创建一个新列表f

>>> f = [14, 23, 37]

然后我们将创建一个新函数replace()。顾名思义,与其修改其参数replace(),不如更改其参数所引用的对象:

>>> def replace(g):
...     g = [17, 28, 45]
...     print("g =", g)
...

我们现在用实际参数f调用replace()

>>> replace(f)
g = [17, 28, 45]

这正是我们所期望的。但是现在外部引用f 的值是多少?

>>> f
[14, 23, 37]

对象引用f仍然引用原始的、未修改的列表。这一次,函数没有修改传入的对象。发生什么事?

答案是:对象引用f被分配给名为g的形式参数,因此gf确实引用了同一个对象,就像前面的示例中一样:

Figure 4.12 : Initially 'f' and 'g' refer to the same list object

然而,在函数的第一行,我们重新分配了引用g以指向新构造的列表**[17,28,45],因此在函数中,对原始[14,23,37]**列表的引用被覆盖,尽管未修改的对象本身仍然被f指向功能外的引用:

Figure 4.13 : After reassignment, 'f' and 'g' refer to different objects

参数传递是引用绑定

我们已经看到,通过函数参数引用修改对象是完全可能的,但也可以将参数引用重新绑定到新值。如果要更改列表参数的内容,并在函数外部看到这些更改,可以修改列表的内容,如下所示:

>>> def replace_contents(g):
...     g[0] = 17
...     g[1] = 28
...     g[2] = 45
...     print("g =", g)
...
>>> f
[14, 23, 37]
>>> replace_contents(f)
g = [17, 28, 45]

事实上,如果您检查f的内容,您将看到它们已被修改:

>>> f
[17, 28, 45]

函数参数通过所谓的“对象引用传递”进行传递。这意味着引用的值被复制到函数参数中,而不是被引用对象的值;不复制任何对象。

Python 返回语义

Python 的 return 语句使用与函数参数相同的对象引用传递语义。在 Python 中,当您从函数返回对象时,您真正做的是将对象引用传递回调用方。如果调用者将返回值分配给引用,那么他们所做的只是将新引用分配给返回的对象。这使用了与我们在显式引用赋值和参数传递中看到的完全相同的语义和机制。

我们可以通过编写一个只返回其唯一参数的函数来证明这一点:

>>> def f(d):
...     return d
...

如果我们创建一个对象(如列表)并通过这个简单函数传递它,我们会看到它返回与我们传入的完全相同的对象:

>>> c = [6, 10, 16]
>>> e = f(c)
>>> c is e
True

请记住,当两个名称引用完全相同的对象时,is 只返回True,所以这个示例显示没有制作列表的副本。

函数参数的详细信息

现在我们了解了对象引用和对象之间的区别,接下来我们将了解函数参数的更多功能。

默认参数值

使用def关键字定义函数时指定的正式函数参数是以逗号分隔的参数名称列表。通过提供默认值,这些参数可以成为可选参数。考虑一个函数,它向控制台打印一个简单的横幅:

>>> def banner(message, border='-'):
...     line = border * len(message)
...     print(line)
...     print(message)
...     print(line)
...

这个函数接受两个参数,我们在文本字符串中提供一个默认值——在本例中为'-'。当我们使用默认参数定义函数时,带有默认参数的参数必须在没有默认参数的参数之后,否则我们将得到一个SyntaxError

在函数的第 2 行,我们将边框字符串乘以消息字符串的长度。这一行显示了两个有趣的特性。首先,它演示了如何使用built-in len()函数确定 Python 集合中的项数。其次,它展示了将一个字符串(在本例中为单字符串边框)乘以一个整数如何生成一个新字符串,其中包含重复多次的原始字符串。我们在这里使用该特性使字符串的长度与消息的长度相等。

在第 3 行到第 5 行,我们再次打印全宽边框、消息和边框。

调用banner()函数时,我们不需要提供边框字符串,因为我们提供了一个默认值:

>>> banner("Norwegian Blue")
--------------
Norwegian Blue
--------------

但是,如果我们提供了可选参数,则会使用:

>>> banner("Sun, Moon and Stars", "*")
*******************
Sun, Moon and Stars
*******************

关键字参数

在生产代码中,此函数调用不是特别的自文档化。我们可以通过在调用站点命名 border 参数来改善这种情况:

>>> banner("Sun, Moon and Stars", border="*")
*******************
Sun, Moon and Stars
*******************

在这种情况下,消息字符串称为“位置参数”,边框字符串称为“关键字参数”。在调用中,位置参数按顺序与函数定义中声明的形式参数匹配。另一方面,关键字参数是按名称匹配的。如果我们对两个参数都使用关键字参数,我们可以自由地按任意顺序提供它们:

>>> banner(border=".", message="Hello from Earth")
................
Hello from Earth
................

但请记住,所有关键字参数必须在任何位置参数之后指定。

什么时候计算默认参数?

为函数提供默认参数值时,可以通过提供 一个表达式来实现。此表达式可以是简单的文字值,也可以是更复杂的函数调用。为了实际使用您提供的默认值,Python 必须在某个时候对该表达式求值。

因此,当 Python 计算默认值表达式时,正确理解是至关重要的。这将帮助您避免一个常见的陷阱,这个陷阱经常会诱捕 Python 的新手。让我们使用 Python 标准库时间模块仔细研究这个问题:

>>> import time

通过time模块的ctime()功能,我们可以很容易地以可读字符串的形式获取当前时间:

>>> time.ctime()
'Sat Feb 13 16:06:29 2016'

让我们编写一个函数,它使用从ctime()检索到的值作为默认参数值:

>>> def show_default(arg=time.ctime()):
...     print(arg)
...
>>> show_default()
Sat Feb 13 16:07:11 2016

到目前为止还不错,但请注意几秒钟后再次呼叫show_default()时会发生什么:

>>> show_default()
Sat Feb 13 16:07:11 2016

再说一遍:

>>> show_default()
Sat Feb 13 16:07:11 2016

正如您所看到的,显示的时间从不前进。

回想一下我们是怎么说的,def是一条语句,执行时将函数定义绑定到函数名?那么,在执行def语句时,默认参数表达式只计算一次。在许多情况下,默认值是一个简单的不可变常量,如 and integer 或字符串,因此这不会导致任何问题。但对于粗心的人来说,这可能是一个令人困惑的陷阱,当您使用列表等可变集合作为参数默认值时,通常会出现这种陷阱。

让我们仔细看一看。考虑这个函数,它使用一个空列表作为默认参数。它接受一个菜单作为字符串列表,将项目"spam"附加到列表中,并返回修改后的菜单:

>>> def add_spam(menu=[]):
...     menu.append("spam")
...     return menu
...

让我们创建一个简单的早餐baconeggs

>>> breakfast = ['bacon', 'eggs']

当然,我们会添加垃圾邮件:

>>> add_spam(breakfast)
['bacon', 'eggs', 'spam']

我们午餐也会做类似的事情:

>>> lunch = ['baked beans']
>>> add_spam(lunch)
['baked beans', 'spam']

到目前为止没有什么意外。但是,看看当您依赖默认参数而不传递现有菜单时会发生什么:

>>> add_spam()
['spam']

当我们将“垃圾邮件”附加到一个空菜单时,我们得到的只是垃圾邮件。这可能仍然是您所期望的,但如果我们再次这样做,我们会在菜单中添加两个垃圾邮件:

>>> add_spam()
['spam', 'spam']

三:

>>> add_spam()
['spam', 'spam', 'spam']

四:

>>> add_spam()
['spam', 'spam', 'spam', 'spam']

这里发生的事情是这样的。首先,在执行def语句时,用于默认参数的空列表只创建一次。这是一个普通的列表,就像我们到目前为止看到的任何其他列表一样,Python 将在整个程序执行过程中使用这个精确的列表。

第一次实际使用默认值时,我们就直接将垃圾邮件添加到默认的list对象中。当我们第二次使用默认列表对象时,我们使用的是同一个默认列表对象——我们刚刚添加了垃圾邮件的对象——最后我们向其添加了第二个“垃圾邮件”实例。第三个呼叫添加了第三个垃圾邮件,无限期。或者可能是

解决方法很简单,但可能并不明显:始终使用不可变对象,例如整数或字符串作为默认值。按照这个建议,我们可以使用不可变的None对象作为哨兵来解决这个特殊情况:

>>> def add_spam(menu=None):
...     if menu is None:
...         menu = []
...     menu.append('spam')
...     return menu
...
>>> add_spam()
['spam']
>>> add_spam()
['spam']
>>> add_spam()
['spam']

我们的add_spam()功能按预期工作。

Python 类型系统

编程语言可以通过几个特征来区分,但最重要的特征之一是其类型系统的性质。Python 可以被描述为具有动态类型的系统。让我们调查一下这意味着什么。

Python 中的动态类型

动态类型意味着直到程序运行时才解析对象引用的类型,并且在编写程序时不需要预先指定对象引用的类型。看看这个简单的添加两个对象的函数:

>>> def add(a, b):
...     return a + b
...

在这个定义中,我们没有提到任何类型。我们可以将add()与整数一起使用:

>>> add(5, 7):
12

我们可以将其用于floats

>>> add(3.1, 2.4)
5.5

您可能会惊讶地发现它甚至适用于字符串:

>>> add("news", "paper")
'newspaper'

实际上,此函数适用于任何类型,如已定义加法运算符的列表:

>>> add([1, 6], [21, 107])
[1, 6, 21, 107]

这些示例说明了类型系统的动态性:add()函数的两个参数ab可以引用任何类型的对象。

Python 中的强类型

另一方面,类型系统的强度可以通过尝试添加尚未定义的add()类型来证明,例如stringsfloats

>>> add("The answer is", 42)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 2, in add
TypeError: Can't convert 'int' object to str implicitly

尝试这样做会导致TypeError,因为 Python 通常不会在对象类型之间执行隐式转换,或者试图强制一种类型转换为另一种类型。这方面的主要例外是转换为 bool,用于if语句和while循环谓词。

变量声明和作用域

正如我们所看到的,Python 中不需要类型声明,变量本质上只是对象的非类型化名称绑定。因此,它们可以根据需要被反弹——或重新分配——甚至可以被分配到不同类型的对象。

但是,当我们将名称绑定到对象时,绑定存储在哪里?要回答这个问题,我们必须了解 Python 中的作用域和作用域规则。

立法局规则

Python 中有四种类型的作用域,它们被安排在一个层次结构中。每个作用域都是一个上下文,在其中存储名称并可以在其中查找名称。从最窄到最宽的四个范围是:

  • 本地-在当前函数中定义的名称。

  • 封闭-在任何和所有封闭函数中定义的名称。(这个范围 对于本书的内容并不重要。)

  • 全局-在模块顶层定义的名称。每个模块都带来了 一个新的全球范围。

  • 内置-通过特殊的 内置模块将名称内置到 Python 语言中。

这些范围共同构成了 LEGB 规则:

立法局规则

在最狭窄的相关上下文中查找名称。

需要注意的是,Python 中的作用域通常不对应于缩进所划分的源代码块。For-循环、带块等不会引入新的嵌套范围。

作用域

考虑我们的 T0 模块。它包含以下全局名称:

  • main-受def main()约束
  • sys-受import sys约束
  • __name__-由 Python 运行时提供
  • urlopen-受来自urllib.request import urlopen的约束
  • fetch_words-受def fetch_words()约束
  • print_items-受def print_items()约束

模块作用域名称绑定通常由import语句和函数或类定义引入。可以在模块范围内使用其他对象,这通常用于常量,但也可以用于变量。

fetch_words()函数中,我们有六个本地名称:

  • word-由内部for 环绑定
  • line_words-受转让约束
  • line-被外部for绑定-循环
  • story_words-受转让约束
  • url-受形式函数参数约束
  • story-受 with 语句约束

这些绑定中的每一个都在第一次使用时就存在,并在函数完成之前继续存在于函数范围内,此时引用将被销毁。

全局和局部范围中的名称相同

有时,我们需要从函数中重新绑定模块范围内的全局名称。考虑下面的简单模块:

count = 0

def show_count():
 print(count)

def set_count(c):
 count = c

如果我们将此模块保存在scopes.py中,我们可以将其导入 REPL 中进行实验:

$ python3
Python 3.5.0 (default, Nov  3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from scopes import *
>>> show_count()
count =  0

调用show_count()时,Python 会在本地名称空间(L)中查找名称计数。它没有找到它,所以在下一个最外层的名称空间中查找,在本例中是全局模块名称空间(G),它在其中查找名称计数并打印引用的对象。

现在我们用一个新值调用set_count()

>>> set_count(5)

然后我们再次致电show_count()

>>> show_count()
count =  0

您可能会感到惊讶,show_count()在调用set_count(5)之后显示0,所以让我们来看看发生了什么。

调用set_count()时,赋值count = c本地范围内的名称计数创建一个新的绑定。当然,这个新绑定引用了作为c传入的对象。关键是,没有对模块范围中定义的全局计数执行任何查找。我们已经创建了一个新变量,该变量会隐藏并阻止访问同名的全局变量。

全局关键字

为了避免全局范围中的名称阴影,我们需要指示 Python 将set_count()函数中的名称计数解析为模块名称空间中定义的计数。我们可以使用 global 关键字来实现这一点。让我们modify set_count()这样做:

def set_count(c):
 global count
 count = c

global关键字只是将本地范围中的绑定引入全局范围中的名称。

退出并重新启动 Python 解释器以练习我们修订的模块:

>>> from scopes import *
>>> show_count()
count =  0
>>> set_count(5)
>>> show_count()
count =  5

它现在演示了所需的行为。

禅宗时刻

特殊情况不足以打破规则——我们遵循模式不是为了消除复杂性,而是为了掌握复杂性:

Figure 4.14: Moment of zen

正如我们所展示的,Python 中的所有变量都是对对象的引用,即使是在基本类型(如整数)的情况下也是如此。这种彻底的面向对象方法是 Python 中的一个重要主题,实际上 Python 中的所有内容都是对象,包括函数和模块。

一切都是物体

让我们回到我们的单词模块,在 REPL 上进一步进行实验。在这种情况下,我们将只导入模块:

$ python3
Python 3.5.0 (default, Nov  3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import words

import语句将module对象绑定到当前命名空间中的名称words。我们可以通过type()内置功能来确定任何对象的类型:

>>> type(words)
<class 'module'>

如果我们想查看对象的属性,可以在 Python 交互会话中使用dir()内置函数来内省对象:

>>> dir(words)
['__builtins__', '__cached__', '__doc__', '__file__', '__initializing__',
'__loader__', '__name__', '__package__', 'fetch_words', 'main',
'print_items', 'sys', 'urlopen']

dir()函数返回模块属性名称的排序列表,包括:

  • 我们定义的函数,比如函数fetch_words()
  • 任何导入的名称,如sysurlopen
  • 各种特殊的dunder属性,如__name____doc__,它们 揭示了 Python 的内部工作原理

检查功能

我们可以对这些属性中的任何一个使用type()函数来了解更多信息。例如,我们可以看到fetch_words是一个函数对象:

>>> type(words.fetch_words)
<class 'function'>

我们可以依次在函数上使用dir()来显示其属性:

>>> dir(words.fetch_words)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']

我们可以在这里看到,函数对象有许多特殊属性,与 Python 函数在幕后的实现方式有关。现在,我们只看几个简单的属性。

正如您所期望的,它的__name__属性是函数对象的名称,作为字符串:

>>> words.fetch_words.__name__
'fetch_words'

同样,__doc__是我们提供的文档字符串,为我们提供了一些关于如何实现内置help()功能的线索:

>>> words.fetch_words.__doc__
'Fetch a list of words from a URL.\n\n    Args:\n        url: The URL of a
UTF-8 text document.\n\n    Returns:\n        A list of strings containing
the words from\n        the document.\n    '

这只是一个小例子,说明了如何在运行时内省 Python 对象,还有许多更强大的工具可用于了解更多有关正在使用的对象的信息。也许这个例子中最有启发性的部分是我们处理的是一个函数对象,这表明 Python 无处不在的面向对象包含了在其他语言中可能根本无法访问的语言元素。

总结

  • Python 对象引用

    • 想象一下 Python 是根据对象的命名引用而不是变量和值来工作的。

    • 赋值不会将值放入框中。它在 对象上附加一个名称标签。

    • 从一个引用分配到另一个引用会在同一对象上放置两个名称标记。

    • Python 垃圾收集器将回收不可访问的对象——那些没有名称标记的 对象。

  • 对象同一性与等价性

    • id()函数返回一个唯一且恒定的标识符,但在生产中很少使用。

    • is 运算符确定标识的相等性。即两个 名称是否指同一对象。

    • 我们可以使用 double equals 操作符来测试等价性。

  • 函数参数和返回值

    • 函数参数是通过对象引用传递的,因此如果函数是可变对象,则可以修改它们的参数。

    • 如果通过赋值重新获得形式函数参数,则对传入对象的引用将丢失。要更改可变参数,您应该替换其内容,而不是替换整个对象。

    • return 语句也按对象引用传递。没有复印件。

    • 可以使用默认值指定函数参数。

    • 当执行def 语句时,默认参数表达式只计算一次。

  • Python 类型系统

    • Python 使用动态类型,所以我们不需要预先指定引用类型。

    • Python 使用强类型。类型不强制匹配。

  • 范围

    • 根据 LEGB 规则,Python 引用名称在四个嵌套作用域 中的一个中查找:函数本地、封闭函数、全局(或模块)命名空间和内置。

    • 全局引用可以从本地范围读取

    • 从本地作用域分配到全局引用需要使用 global 关键字将 引用声明为全局引用。

  • 对象与内省

    • Python 中的所有内容都是对象,包括模块和函数。它们可以像其他对象一样处理。

    • 导入和def关键字导致绑定到命名的 引用。

    • 内置的type()功能可用于确定对象的类型。

    • 内置的dir()函数可用于内省对象并 返回其属性名称列表。

    • 函数或模块对象的名称可以通过其 __name__属性访问。

    • 函数或模块对象的 docstring 可以通过其__doc__属性访问。

  • 混杂的

    • 我们可以用len()来测量绳子的长度。

    • 如果我们将一个字符串与一个整数“相乘”,我们将得到一个新字符串,其中包含操作数字符串的多个副本。这称为“重复”操作。

  1. 您会注意到,这里我们引用了名为x对象引用作为x。这当然有点草率,因为x通常意味着对象引用所引用的对象名为x。但这是一口之多,有点过于迂腐。一般来说,使用引用名称的上下文足以告诉您我们指的是对象还是引用。

  2. 垃圾收集是本书中不涉及的高级主题。不过,简而言之,它是 Python 释放和回收其确定不再使用的资源(即对象)的系统。

  3. 由于将列表引用指定给另一个名称并不会复制该列表,因此您可能想知道,如果需要,您如何复制该列表。这需要其他技术,稍后我们将在更详细地介绍列表时介绍这些技术。

  4. 但是请注意,Python 并不强制执行此行为。完全可以创建一个对象,该对象报告它的值与它本身不相同。在后面的章节中,我们将讨论如何做到这一点——如果你出于某种原因感到了这种冲动。

  5. 虽然没有公认的术语,但您经常会看到术语参数形式参数用于表示函数定义中声明的名称。类似地,术语参数通常用于表示传递到函数中的实际对象(因此绑定到参数)。在本书中,我们将根据需要使用这个术语。

  6. 这种行为是语法实现的一部分,而不是类型系统。