【python】读书笔记之编程术语(七)

第 7 章 编程术语

7.1.1 作为语言的Python和作为解释器的Python

Python这个词有很多含义。Python编程语言的名字来自于英国喜剧团体Monty Python,而不是“蟒蛇”(尽管市面上的Python教程和文档有用“蟒蛇”来进行解释的)。同样,Python在计算机编程方面也有两种含义。

我们说“Python运行一个程序”或者“Python将抛出一个异常”时,我们是在谈论Python解释器——它是真实存在的软件,用来读取.py文件的文本并执行指令。当我们说“Python解释器”时,绝大多数情况下是指CPython,它是由Python软件基金会维护的Python解释器,可以在Python官网上找到。CPython是Python语言的一个实现,换言之,它是按照Python语言规范实现的软件,但Python并非仅此一种实现。CPython是用C语言编写的,Jython则是用Java编写的,用于运行和Java程序交互的Python脚本。PyPy是用Python语言编写的1,它是在程序执行时进行即时编译的编译器。

1用一种语言本身编写该语言的解释器或编译器称为自举。——译者注

所有这些实现都能运行用Python编程语言编写的源代码。当提及“这是一个Python程序”或者“我正在学习Python”时,其中的Python就是指Python编程语言。理想情况下,用Python语言编写的任何源代码都应该能被任何Python解释器运行,但实际上,解释器之间会有一些小的不兼容问题和细微差异。CPython被称为Python语言的参考实现,如果其他解释器在解释Python代码时跟CPython存在差异,那么一般认为CPython的行为是合规和正确的。

7.1.2 垃圾回收

Python使用垃圾回收机制避免此类错误,这是一种内存自动管理方式,它持续跟踪和判断内存应该何时分配和释放,程序员不再为此费力。垃圾回收就是内存的回收,它的作用是释放内存,让新数据使用。

7.1.5 对象、值、实例和身份

 对象是一条数据的呈现,比如数字、文本,或者更复杂的数据结构,比如列表或字典。所有对象都可以存储在变量中,作为参数传递给函数调用并作为函数调用的返回内容。

  每个对象都有自己的值、身份和数据类型。是对象所代表的数据,比如整数42或者字符串'hello'。虽然这有些令人困惑,但有些程序员使用术语“值”当作“对象”的同义词,特别是对于简单的数据类型,比如整数或字符串。例如,一个对应值为42的变量,既可以被解释为包含整数值42的变量,也可以被解释为包含一个值为42的整数对象的变量。

变量的比喻:盒子还是标签

许多入门书将变量比喻为一个盒子,这种比喻实际上过分简化了问题。把变量看作存储数值的盒子很容易理解,但这种比喻并不能阐述清楚变量和值的引用关系。以前面的spameggs为例,它们并不存储不同的字典,而是计算机内存中同一个字典的引用。

许多书认为可以把变量视为包含数值的盒子

在Python中,无论哪种数据类型的变量从技术角度上讲都是对值的引用,而非值的容器。盒子的比喻很简单易懂,但不完美。与其把变量想象成盒子,不如把它想象成内存中对象的标签。

变量可以被视为值的标签

多个变量可以引用同一个对象,反过来说,这个对象被“存储”在多个变量中。而用盒子做比喻,没法表现多个盒子存储同一个对象的情况。因此,标签的比喻可能更容易理解

如果不理解=赋值运算符是在复制引用而非对象本身,你可能会因此而犯错。幸运的是,整数、字符串和元组不存在这个问题,具体原因详见7.1.7节。

  你可以使用is运算符比较两个对象的身份是否相同,而==运算符只检查它们的值是否相同。x is y可被视为id(x) == id(y)的简写。在交互式shell中,可以输入以下内容查看is==的不同之处:

7.1.6 项

  在Python中,容器对象(比如列表或字典)中的子对象被称为“项”或者“元素”。比如列表['dog', 'cat', 'moose']中的字符串对象,也被称作“项”。

7.1.7 可变和不可变

尽管元组中的对象是不能改变的,对象本身却是可以改变的

几乎所有Python专家和我都把元组视为不可变的。但实际上,有些元组是否可以算是可变的取决于你的定义

7.1.8 索引、键和哈希值

Python的列表和字典是一类可以包含多个其他值的值。为了访问这些子值,你可以使用索引运算符,它由一对中括号([ ])和一个被称为索引的整数组成。索引用于指定要访问哪个值。在交互式shell中输入以下内容,查看索引是如何在列表中工作的:

1
2
3
4
5
>>> spam = ['cat', 'dog', 'moose']
>>> spam[0]
'cat'
>>> spam[-2]
'dog'

在这个例子中,0是索引。最小的索引是0而不是1,因为Python(以及绝大多数编程语言)使用基于0的索引机制。使用基于1的索引机制的语言很少,其中最著名的是Lua和R语言。Python还支持负数索引,比如-1指的是列表的最后一项,-2指的是列表的倒数第二项,以此类推。你可以把负数索引spam[-n]看作spam[len(spam)-n]

注意 计算机科学家、歌手、词曲作者Stan Kelly-Bootle曾开玩笑说:“数组索引应该从0开始还是从1开始呢?我提议的妥协方案是0.5,可惜他们未予认真考虑便一口回绝了。”

  你也可以在列表字面量上使用索引运算符,不过这些中括号在实际的代码中令人困惑,看起来也没有必要:

1
2
>>> ['cat', 'dog', 'moose'][2]
'moose'

  索引也可以被用于除列表之外的其他值,比如在字符串上获取某个字符:

1
2
>>> 'Hello, world'[0]
'H'

  Python的字典是键−值对,如下所示:

1
2
3
>>> spam = {'name': 'Zophie'}
>>> spam['name']
'Zophie'

  虽然Python的列表索引只能是一个整数,但字典的索引运算符是一个,可以是任何可哈希的对象。哈希值是一个整数,它就像是某个值的“指纹”。一个对象的哈希值在这个对象的生命周期内永远不会改变,且具有相同值的对象必须有相同的哈希值。在上述示例中,字符串'name'是值'Zophie'对应的键。hash()函数可以返回可哈希对象的哈希值。字符串、整数、浮点数和元组等不可变对象都是可哈希的,而列表等可变对象是不可哈希的。在交互式shell中输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
>>> hash('hello')
-1734230105925061914
>>> hash(42)
42
>>> hash(3.14)
322818021289917443
>>> hash((1, 2, 3))
2528502973977326415
>>> hash([1, 2, 3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

  键的哈希值会被用来寻找存储在字典和集合数据类型中的项,具体细节不在本书讲述范围之内。因为列表是可变类型,不能被哈希,所以不能被当作字典的键:

1
2
3
4
5
>>> d = {}
>>> d[[1, 2, 3]] = 'some value'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

  哈希值不同于身份。两个具有相同值的不同对象的身份是不同的,但它们的哈希值是相同的。在交互式shell中输入以下内容:

1
2
3
4
5
6
7
8
9
10
>>> a = ('cat', 'dog', 'moose')
>>> b = ('cat', 'dog', 'moose')
>>> id(a), id(b)
(37111992, 37112136)
>>> id(a) == id(b) ❶
False
>>> hash(a), hash(b) ❷
(-3478972040190420094, -3478972040190420094)
>>> hash(a) == hash(b)
True

ab所指向的元组有不同的身份❶,但它们的值相同,所以有相同的哈希值❷。注意,如果元组只包含可哈希的项,这个元组就是可哈希的,否则就是不可哈希的。因为字典中的键必须是可哈希的,所以不能将包含不可哈希列表的元组当作键。在交互式shell中输入以下内容:

1
2
3
4
5
6
7
8
>>> tuple1 = ('cat', 'dog')
>>> tuple2 = ('cat', ['apple', 'orange'])
>>> spam = {}
>>> spam[tuple1] = 'a value'
>>> spam[tuple2] = 'another value'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

  注意,tuple1是可哈希的❶,但tuple2含有不可哈希的列表❷,所以它是不可哈希的。

7.1.9 容器、序列、映射和集合类型

  容器序列映射这几个词在Python中的含义不一定等同于在其他编程语言中的含义。在Python中,容器是可以包含多个任意类型对象的对象。常见的容器类型包括列表和字典。

  序列是容器数据类型的对象,它的元素有序且可以通过整数索引访问。字符串、元组、列表和字节对象都属于序列。这几种类型的对象可以通过索引访问对应数值,也可以作为参数传递给len()函数。有序指的是序列中有第一个值、第二个值,以此类推。比如,下面两个列表被认为是不相等的,因为它们的值的顺序不同:

1
2
>>> [1, 2, 3] == [3, 2, 1]
False

  映射也是容器数据类型的对象,它使用键而非索引访问。映射既可以是有序的,也可以是无序的。在Python 3.4和更早的版本中,字典是无序的,因为它没有所谓的第一个键−值对和最后一个键−值对:

1
2
3
4
5
6
>>> spam = {'a': 1, 'b': 2, 'c': 3, 'd': 4} # 这是在CPython 3.5中运行的
>>> list(spam.keys())
['a', 'c', 'd', 'b']
>>> spam['e'] = 5
>>> list(spam.keys())
['e', 'a', 'c', 'd', 'b']

  Python的早期版本不能保证从字典中获取项的顺序。由于字典的无序性,两个键−值对顺序不同的字典字面量仍然被认为是相等的:

1
2
>>> {'a': 1, 'b': 2, 'c': 3} == {'c': 3, 'a': 1, 'b': 2}
True

  但是从Cpython 3.6开始,字典保留了键−值对的插入顺序:

1
2
3
4
5
6
>>> spam = {'a': 1, 'b': 2, 'c': 3, 'd': 4} # 该代码需要在Cpython 3.6中运行
>>> list(spam)
['a', 'b', 'c', 'd']
>>> spam['e'] = 5
>>> list(spam)
['a', 'b', 'c', 'd', 'e']

这是CPython 3.6解释器的一个特性,它不存在于其他Python 3.6解释器中。但从Python 3.7开始,这一特性成为Python语言标准,所有Python 3.7解释器都支持了有序字典。但字典的有序性并不意味着它的项可以通过数字索引获取:spam[0]并不会返回有序字典中的第一项(除非凑巧第一项的键正好为0)。如果两个有序字典包含相同的键−值对,但键−值对的顺序不同,那么也会被认为是相等的。

  collections模块包含很多其他映射类型,比如OrderedDictChainMapCounterUserDict。在Python官网上可以找到以上映射类型的介绍。

7.1.10 特殊方法

  特殊方法(dunder method)也被称为魔术方法,是指Python中名称以两个下划线开头和结尾的特殊方法。这些方法用于运算符重载。dunder是double underscore(双下划线)的缩写。最为人熟知的特殊方法是__init__(读作dunder init dunder,也可以直接读作init),其作用是初始化对象。Python有几十个特殊方法,第17章将详细解释。

7.1.11 模块和包

  模块是可以被其他Python程序导入,将代码提供给导入者使用的程序。Python自带的模块被统称为Python标准库,你也可以创建自己的模块。如果一个程序被保存为spam.py,那么其他程序可以运行import spam来访问spam.py程序的函数、类和最上层的变量。

  是模块的集合,通过在文件夹中放置一个名为__init__.py的文件就可以形成一个包。这个文件夹的名称就是包的名称。包可以包含多个模块(也就是.py文件)或其他包(包含__init__.py文件的其他文件夹)。

  关于模块和包的更多解释和细节,请查看Python官网上的相关文档。

7.1.12 可调用对象和头等对象

  在Python中可调用的不只有函数和方法。可调用对象是指实现了可调用运算符(一对括号())的对象。假设你有一条def hello():语句,可以把这段代码视为有一个名为hello的变量,它的值为一个函数对象,在这个变量上使用可调用运算符会调用hello变量对应的函数。

  类是一个OOP概念。类就是可调用对象的例子,它并非函数或方法。例如,datetime模块中的date类,在datetime.date(2020, 1, 1)中通过可调用运算符调用。当类对象被调用时,这个类的__init__方法中的代码会被运行。第15章将介绍类的更多细节。

  函数是Python中的头等对象,这意味着你可以把它们存储在变量中,在函数调用中作为参数传递,作为函数调用的返回值,也可以做任何能对对象做的事情。可以把def语句看作将一个函数对象赋值给变量。比如,可以创建一个spam()函数,然后调用它:

1
2
3
4
5
>>> def spam():
... print('Spam! Spam! Spam!')
...
>>> spam()
Spam! Spam! Spam!

  也可以将spam()函数赋值给其他变量。当调用这个变量时,Python将执行spam()函数:

1
2
3
>>> eggs = spam
>>> eggs()
Spam! Spam! Spam!

这些被称为别名,是为已存在的函数起的其他名称。通常在重命名函数时会用到别名。但如果旧的名字被大量已有代码使用,那么修改名字的工作量很大。

  头等对象最常见的用途是把函数作为参数传递给其他函数。比如,我们可以定义一个callTwice()函数,它可以接受一个需要被调用两次的函数作为参数:

1
2
3
4
5
6
7
>>> def callTwice(func):
... func()
... func()
...
>>> callTwice(spam)
Spam! Spam! Spam!
Spam! Spam! Spam!

  当然,你也可以直接在代码中写两次spam()。使用这种方式的好处是可以将callTwice()函数传递给任何一个在运行的函数,而不必在源代码中预先输入两次函数调用语句。

7.2 经常被混淆的术语

  术语本就够让人困惑的了,特别是那些含义上有关联但又不同的定义。更糟糕的是,编程语言、操作系统、计算机领域这三者可能会使用不同的术语来表示同一个事物,或者用相同的术语表示不同的事物。为了能与其他专业人员清楚地交流,你需要学习以下术语的区别。

7.2.1 语句和表达式

  表达式是由运算符和值组成的指令,可以推导出单一的值。这个值可以是变量(包含值)或者函数调用(返回值)。按照这个定义,2 + 2是一个表达式,它的结果是4len(myName) > 4myName.isupper()myName == 'Zophie'也是表达式。一个值本身也是一个表达式,它的结果就是它自己。

  语句实际上是Python中的所有其他指令,包括if语句、for语句、def语句和return语句等。语句并不能推导得到一个值。一些语句可能包含表达式,比如赋值语句spam = 2 + 2,或者if语句if myName == 'Zophie':

  Python 3使用print()函数,Python 2则使用print语句。看起来只是加了一对括号,但更重要的区别在于Python 3的print()函数具有返回值(虽然总是None),可以作为参数传递给其他函数,也可以被赋值给一个变量。语句是不能做这些操作的。不过在Python 2中也可以在print后添加一对括号,就像下面这个交互式shell示例:

1
2
3
4
>>> print 'Hello, world!' # 在Python 2中运行
Hello, world!
>>> print('Hello, world!') # 在Python 2中运行 ❶
Hello, world!

尽管它看起来像是函数调用❶,但实际上还是一个用括号包裹着字符串值的print语句,就像赋值语句spam = (2 + 2)实际上等效于spam = 2 + 2一样。在Python 2和Python 3中,可以将多个值传递给print语句或者print()函数,就像下面这样:

1
2
>>> print('Hello', 'world') # 在Python 3中运行
Hello world

  但在Python 2中使用同样的代码会被解释为在print语句中传递一个包含两个字符串值的元组,输出如下:

1
2
>>> print('Hello', 'world') # 在Python 2中运行
('Hello', 'world')

  语句和由函数调用组成的表达式之间的差异虽然微妙,但切实存在。

7.2.2 块、子句和主体

  术语子句主体经常混用,用来指代一组Python指令。块以缩进开始,当缩进回退时结束,比如在if语句或for语句后的代码被称为该语句的块。以冒号结尾的语句后会跟着一个新块,比如ifelseforwhiledefclass等。

  Python也支持单行块,虽然是有效的,但并不推荐。语法如下:

1
if name == 'Zophie': print('Hello, kitty!')

通过使用分号,以下这条if语句的块中还可以有多条指令:

1
if name == 'Zophie': print('Hello, kitty!'); print('Do you want a treat?')

但在单行块中不能出现其他需要新块的语句。以下是无效的Python代码:

1
if name == 'Zophie': if age < 2: print('Hello, kitten!')

这之所以是无效的,是因为下一行的else语句无法准确地对应到if语句。Python官方文档更倾向使用术语“子句”而非“块”,下面的代码就是一个子句:

1
2
3
if name == 'Zophie':
print('Hello, kitty!')
print('Do you want a treat?')

if语句是子句的头,if语句内的两个print()调用是子句的内容,也被称为“主体”。Python官方文档使用“块”来指以单元形式执行的一段代码,比如一个模块、一个函数,或者一个类的定义。

7.2.3 变量和特性

  变量是指向对象的一个名字。援引官方文档,特性则是指“.(点)后面的任何名字”。特性是跟对象(点之前的名字)相关联的。例如,在交互式shell中输入以下内容:

1
2
3
4
5
6
>>> import datetime
>>> spam = datetime.datetime.now()
>>> spam.year
2018
>>> spam.month
1

在这个代码示例中,spam是一个值为datetime对象(datetime.datetime.now()的返回值)的变量,yearmonth都是这个对象的特性。再举个例子,sys.exit()中的exit()函数也被视为是sys模块对象的一个特性。

  在其他语言中,特性也被叫作字段、属性或成员变量。

7.2.4 函数和方法

  函数是在调用时被执行的代码的集合。方法是与类相关联的函数(或者叫作一个可调用的函数,第8章将对此说明),就像特性是与对象相关联的变量。函数包括内置函数和模块内的函数。在交互式shell中输入以下内容:

1
2
3
4
5
6
7
>>> len('Hello')
5
>>> 'Hello'.upper()
'HELLO'
>>> import math
>>> math.sqrt(25)
5.0

在这个示例中,len()是一个函数,而upper()是一个字符串方法。方法也被视为它们所绑定的对象的一个特性。注意,出现句点并不意味着使用的就一定是方法而非函数。sqrt()函数处于math模块中,而math并不是一个类,所以sqrt()自然也不是类的方法。

7.2.5 可迭代对象和迭代器

  Python的for循环是个多面手。语句for i in range(3):会将块内的代码执行3次。range(3)调用不仅仅是Python告诉for循环“重复执行代码3次”。调用range(3)会返回一个范围对象,就像调用list('cat')会返回一个列表对象一样,两者都是可迭代对象

  for循环会使用可迭代对象。在交互式shell中输入以下内容,查看for循环如何对范围对象和列表对象进行迭代:

1
2
3
4
5
6
7
8
9
10
11
12
>>> for i in range(3):
... print(i) # for循环的内容
...
0
1
2
>>> for i in ['c', 'a', 't']:
... print(i) # for循环的内容
...
c
a
t

  所有的序列类型都是迭代器,比如范围、列表、元组、字符串对象。一些容器对象也是迭代器,比如字典、集合和文件对象。

  for循环的背后实际上有很多技术细节。在幕后,for循环会调用Python内置的iter()函数和next()函数。当for循环运行时,可迭代对象被传递给内置的iter()函数,作为迭代器对象被返回。迭代器对象会一直跟踪循环中使用的可迭代对象的下一项。在循环的每一次迭代中,将迭代器对象传递给内置的next()就可以返回迭代器中的下一项。我们可以手动调用iter()函数和next()函数直观地查看for循环的工作原理。在交互式shell中输入以下内容,执行与上个循环示例相同的指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> iterableObj = range(3)
>>> iterableObj
range(0, 3)
>>> iteratorObj = iter(iterableObj)
>>> i = next(iteratorObj)
>>> print(i) # 循环的内容
0
>>> i = next(iteratorObj)
>>> print(i) # 循环的内容
1
>>> i = next(iteratorObj)
>>> print(i) # 循环的内容
2
>>> i = next(iteratorObj)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration ❶

注意,当迭代器返回了最后一项后还继续调用next(),Python会抛出StopIteration异常❶。不过程序并不会崩溃并显示错误信息。它是Python内置用来使for循环判断何时停止循环的条件。

  一个迭代器只能在一个可迭代项上迭代一次。类似于只能用open()readlines()读取一次文件的内容,如果想再次读取,需要重新打开文件。如果想再次遍历可迭代对象,必须再次调用iter(),以创建一个新的迭代器。只要你愿意,可以创建多个迭代器对象,每个迭代器对象会独立地追踪下一个返回的项。在交互式shell中输入以下内容,观察运行情况:

1
2
3
4
5
6
7
8
9
10
11
>>> iterableObj = list('cat')
>>> iterableObj
['c', 'a', 't']
>>> iteratorObj1 = iter(iterableObj)
>>> iteratorObj2 = iter(iterableObj)
>>> next(iteratorObj1)
'c'
>>> next(iteratorObj1)
'a'
>>> next(iteratorObj2)
'c'

要点在于可迭代对象作为参数传递给iter()函数,并返回一个迭代器对象。迭代器对象作为参数传递给next()函数。当你使用class语句创建自定义的数据类型时,只要实现__iter____next__这两个特殊方法,就能使用for循环遍历对象。

7.2.6 语法错误、运行时错误和语义错误

  错误的分类方式数不胜数。但大体上看,所有的编程错误都可以被分为3类:语法错误、运行时错误和语义错误。

  语法是某种编程语言中有效指令的规则集。缺少小括号、用句点错误地代替了逗号或者拼写错误都是语法错误,程序会在运行时即刻抛出SyntaxError

  语法错误也被称为解析错误,当Python指令不能将源代码的文本解析成有效的指令时,就会发生语法错误。拿人类语言打比方的话,语法错误相当于语法不正确或者将一连串没有意义的词组拼凑成句子,比如“未受污染的奶酪肯定它是”。3

3正确的语句是“它肯定是未受污染的奶酪”。——译者注

  计算机不能读懂程序员的内心想法,它需要具体的指令,而存在语法错误时,计算机无法明确要做什么,所以程序根本不会运行。

 运行时错误是指运行中的程序不能执行某些任务,比如试图打开一个不存在的文件或者将一个数字除以0。拿人类语言打比方的话,运行时错误相当于给出一个不可能的指令,像是“画一个有三条边的正方形”。运行时错误如果没有被处理,程序就会崩溃并显示回溯信息。可以使用try-except语句捕获运行时错误并进行错误处理。在交互式shell中输入以下内容:

1
2
3
>>> slices = 8
>>> eaters = 0
>>> print('Each person eats', slices / eaters, 'slices.')

这段代码在运行时会展示以下回溯信息:

1
2
3
4
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
print('Each person eats', slices / eaters, 'slices.')
ZeroDivisionError: division by zero

  要知道回溯信息中提到的行号只是Python解释器检查到错误的地方,造成错误的真正原因可能是在前一行代码,甚至是更靠前的地方。明白这一点非常重要。

  一般而言,源代码中的语法错误会在程序运行前被解释器捕获,但语法错误也有可能在运行时发生。eval()函数可以接受一串Python代码并运行,这就可能导致运行时产生SyntaxError。例如,eval('print("Hello, world)')缺少了标记字符串结束的双引号,而程序在调用eval()之前不会察觉到错误。

  语义错误(也称为逻辑错误)是一种更为微妙的错误。语义错误并不会显示错误信息或者导致崩溃,但计算机执行指令的方式会跟程序员预想的不同。拿人类语言打比方的话,语义错误相当于告诉计算机:“从商店买一盒牛奶,如果有鸡蛋,买一打。”结果是计算机会买13盒牛奶。(计算机理解的是,先买一盒牛奶,因为商店有鸡蛋,所以又买一打牛奶,一共13盒。)计算机无法判断对错,只会原原本本地按照你的指示去做。在交互式shell中输入以下内容:

1
>>> print('The sum of 4 and 2 is', '4' + '2')

你会得到以下输出:

1
The sum of 4 and 2 is 42

显然42是个错误答案。但请注意,程序并没有崩溃。Python的+运算符既可以计算整数之和,也可以用来连接字符串。错误地使用字符串'4''2'而非整数类型的42导致了这一错误。

7.2.7 形参和实参

  形参def语句中括号内的变量名称。实参是函数调用中实际传递的值,会被赋给形参。在交互式shell中输入以下内容:

1
2
3
4
5
>>> def greeting(name, species): ❶
... print(name + ' is a ' + description)
...
>> greeting('Zophie', 'cat') ❷
Zophie is a cat

def语句中,namespecies都是形参❶。在函数调用语句中,'Zophie''cat'是实参❷。这两个术语经常被混用,统称为参数。在这个示例中,形参对应的就是变量,实参对应的就是值。

7.2.8 显式类型转换和隐式类型转换

  你可以将一个类型的对象转换为另一个类型。比如,int('42')会把字符串'42'转换成整数42。实际上,并非是字符串对象'42'本身被转换了,而是int()函数基于原对象创建了一个新的整数对象。虽然程序员都将这个过程称为对象转换,但实际上这种转换的原理是基于一个模子创建新的对象。

  Python经常隐含地进行类型转换,比如在计算表达式2 + 3.0 = 5.0时,数值23.0被强制转换成了+运算符可以处理的相同数据类型。这种转换被称为隐式转换

  隐式转换有时可能导致意想不到的结果。Python中的布尔值TrueFalse可以分别被强制转换成整数值10。这意味着表达式True + False + True等效于1 + 0 + 1,结果为2,不过实际的代码一般不会这样写。在学到这个知识点后,你可能会有一个有趣的想法:把布尔列表传递给sum()可以计算出列表中真值的数量。不过,调用列表的count()方法速度会更快。

7.2.9 属性和特性

  在许多语言中,术语属性特性是同义词,但在Python中是有区别的。7.2.3节将“特性”解释为与对象绑定的一个名字。特性包括对象的成员变量和方法。

  有些语言(如Java)的类有getter方法和setter方法,程序不能直接对某个属性赋值(为了避免被赋予无效值),而是需要调用setter方法设置属性。setter方法可以确保成员变量不会被分配到无效值。getter方法读取特性的值。如果一个特性的名称为accountBalance,那么它的setter方法和getter方法通常被分别命名为setAcccountBalance()getAccountBalance()

  Python中的属性允许程序员通过更简洁的语法使用getter和setter。第17章将对Python的属性进行详细的探讨。

7.2.10 字节码和机器码

  源代码被编译成一种叫作机器码的指令形式,由CPU直接执行。机器码由CPU指令集(机器内置的指令集合)的指令组成。由机器码组成的编译程序被称为二进制程序。像C这样经典的语言有可以将C源代码编译成几乎所有CPU都可以使用的二进制文件的编译器软件。如果像Python这样的语言也想在同样的CPU上运行,就必须为每一个CPU编写一个Python编译器,而这工作量太大了。

  除了创建直接由CPU硬件执行的机器码,还有另外一种将源代码转化为机器可使用的代码的方法,那就是创建字节码,也叫作可移植代码。字节码由解释器程序执行,而非直接由CPU执行。Python的字节码是由一个指令集中的指令组成的,但它并非是真的CPU的指令集。Python的字节码与.py文件存放在同一个文件夹中,文件扩展名为.pyc。CPython解释器是用C语言编写的,它可以将Python源代码编译成Python字节码,然后执行这些指令。(Java虚拟机也是这样,它执行的是Java字节码。)因为它是用C语言编写的,所以可以为C语言已经适配的任何CPU编译。

  可以看看Scott Sanderson和Joe Jevnik在PyCon 2016上发表的演讲“Playing with Python Bytecode”(和Python字节码的游戏),这是学习这一知识点的优秀资源。

7.2.11 脚本和程序,以及脚本语言和编程语言

  脚本和程序,以及脚本语言和编程语言的区别一向是模糊的,具有随意性。应该说,所有的脚本都是程序,所有的脚本语言都是编程语言。但是脚本语言一般被视为更容易的或者“不太够资格”的编程语言。区分脚本和程序的一个方法是根据代码的执行方式。脚本语言编写的脚本是直接从源代码解释运行的,而编程语言编写的程序则要被编译成二进制文件再执行。尽管Python程序运行时会有编译字节码的步骤,但它通常被认为是一种脚本语言。而Java跟Python一样生成字节码而非机器码二进制文件,但它一般不被视为脚本语言。从技术角度来讲,语言本身并不存在编译型或者解释型的区别,而是有编译器或者解释器的实现。实际上,任何语言都可以有解释器或者编译器。

  这种差异可以拿来争论,但并无太大意义。脚本语言不一定不够强大,编程语言也不一定就更难运用。

7.2.12 库、框架、SDK、引擎、API

  使用别人的代码可以节省大量时间。你可能经常会找打包好的库、框架、SDK、引擎或API来使用。它们的区别很微妙但很重要。

  是一个通用术语,用来指第三方制作的代码集合。库可以包含供开发人员使用的函数、类或者其他代码片段。Python库可能包含一个或一组包,甚至只是一个模块。库通常是限于特定语言的。开发人员不需要知道库的代码如何工作,只需要知道如何调用或者接入库中的代码。标准库(比如Python标准库)是指适用于该语言的所有实现的代码库。

  框架是代码的集合,通过控制反转的方式运行。框架会根据需要调用开发人员创建的函数,而不是开发人员调用框架中的函数。控制反转的一种通俗描述是:“不要给我们打电话,我们会给你打电话。”举例来说,使用Web应用框架编写代码时需要为网页创建函数,以便Web请求进入时框架会调用这些函数。

  软件开发套件(software development kit,SDK)包括代码库、文档和软件工具,它们用来协助为特定的操作系统或平台创建应用程序。比如Android SDK和iOS SDK分别用于为Android和iOS创建移动应用程序,Java开发工具包(JDK)是Java虚拟机创建应用程序的SDK。

  引擎是一个大型、独立的系统,开发人员的软件可以对其进行外部控制。开发人员通常调用引擎中的函数执行大型的复杂任务。引擎包括游戏引擎、物理引擎、推荐引擎、数据库引擎、国际象棋引擎和搜索引擎等。

  应用程序接口(application programming interface,API)是库、SDK、框架或引擎的对外接口。API规定了如何调用函数或向库提出访问资源的请求。库的作者会提供(希望如此)API文档。许多流行的社交类或其他类型的网站提供了HTTP API,允许程序访问它们的服务,而不是由人使用浏览器访问。使用这些API,你可以编写出能够自动发布Facebook信息或者阅读Twitter的最新消息的程序。


【python】读书笔记之编程术语(七)
http://example.com/2024/01/07/617python读书笔记之编程术语(七)/
作者
Wangxiaowang
发布于
2024年1月7日
许可协议