为何程序要从0开始计数

这一篇是《流畅的 python》读书笔记。主要介绍元组、分片、序列赋值以及引用了大师 Edsger W.Dijkstra为何序列从0开始计数的解释。html

元组

在有些python 的介绍中,元组被称为不可变列表,这实际上是不许确的,没有彻底归纳元组的特色。元组除了用做不可变列表,还能够用于没有字段名的记录python

元组和记录

元组实际上是对数据的记录:元组中的每一个元素都存放了记录中一个字段的数据,外加这个数据的位置。git

若是把元组看成一些字段的集合,数量和位置信息会变得很是重要。好比如下几条用元组表示的记录:函数

>>> lax_coordinates = (33.9425, -118.408056) # 洛杉矶国际机场的经纬度
 # 东京的一些信息:市名、年份、人口、人口变化和面积
 >>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)复制代码

以上这两个元组每一个位置都对应一个数据记录。ui

元组拆包

>>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)复制代码

这个例子中,咱们把元组的数据用一条语句分别赋值给 city, year, pop, chg, area,这就是元组拆包的一个具体应用。编码

元组拆包能够应用到任何可迭代对象上,可是被迭代的对象窄的元素的数量必须跟接受这些元素的元组的空档数一致。spa

好比:3d

>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates
>>> latitude
33.9425
>>> longitude
-118.408056复制代码

还能够用 * 运算符把一个可迭代对象拆开做为函数的参数:rest

>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmode(*t)
(2, 4)
>>> quotient, remainder = divmode(*t)
>>> quotient, remainder
(2, 4)复制代码

在进行拆包是,咱们可能对元组的某些值并不感兴趣,这时能够用 _ 占位符处理。好比:code

>>> divmode(20, 8)
(2, 4)
>>> _, remainder = divmode(20, 8)  # 这里咱们只关心第二个值
>>> remainder
4复制代码

在处理函数参数时,咱们常常用*args 来表示不肯定数量的参数。在python3中,这个概念被扩展到了平行赋值中:

# python 3 代码示例
>>> a, b, *rest = range(5)
>> a, b, rest
(0, 1, [2, 3, 4])
# * 前缀只能用在一个变量名前,这个变量能够在其余位置
>>> a, *rest, c, d = range(5) 
>> a, rest, c, d
(0, [1, 2], 3, 4)
>>> a, b, *rest = range(2)
>> a, b, rest
(0, 1, [])复制代码

元组也支持嵌套拆包,好比:

>>> l = (1, 2, 3, (4, 5))
>>> a, b, c, (d, e) = l
>>> d
4
>>> 5
4复制代码

具名元组

元组做为记录除了位置之外还少一个功能,那就是没法给字段命名,namedtuple解决了这个问题。

namedtuple 使用方式实例:

>>> from collecitons import namedtuple
>>> city = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo.population  # 可使用字段名获取字段信息
36.933
>>> tokyo[1] # 也可使用位置获取字段信息
'JP'
>>> City._fields # _fields 属性是一个包含这个类全部字段名的元组 
('name', 'country', 'population', 'coordinates')
>>> tokyo_data = ('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo = City._make(tokyo_data) # _make() 方法接受一个可迭代对象生成这个类的实例,和 City(*tokyo_data) 做用一致
>>>  tokyo._asdict() # _asdict() 把具名元组以 collections.OrderedDict 的形式呈现
OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', 36.933), ('coordinates', (35.689722, 139.691667))])复制代码

collections.namedtuple 是一个工厂函数,它能够用来构建一个带字段名的元组和一个有名字的类。
namedtuple 构建的类的实例锁消耗的内存和元组是同样的,由于字段名都被存放在对应的类里。这个实例和普通的对象实例相比也更小一些,由于 在这个实例中,Python 不须要用 __dict__ 来存放这些实例的属性

切片

Python 中列表、元组、字符串都支持切片操做。

在切片和区间操做里不包含区间范围的最后一个元素是 Python 的风格。这样作的好处以下:

  • 当只有最后一个位置信息时,咱们能够快速看出切片和区间里有几个元素:range(3) 和 mylist[:3] 都只返回三个元素
  • 当气质位置可见时,能够快速计算出切片和区间的长度,用后一个数减去第一个下标(stop-start)便可。
  • 这样还可让咱们利用任意一个下标来把序列分割成不重复的两部分,只要写成 mylist[:x] 和 mylist[x:] 就能够。

切片除了开始和结束的下标以外还能够有第三个参数,好比:s[a:b:c],这里 c 表示取值的间隔,c 还能够为负值,负值意味着反向取值。

>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::2]
'eccb'复制代码

a:b:c 这种用法只能做为索引或者下标在[] 中返回一个切片对象:slice(a, b, c)。对 seq[start:stop:step] 进行求值的时候,Python 会调用 seq.getitem(slice(start:stop:step)]。

给切片赋值

若是把切片放在赋值语句的左边,或者把它做为 del 操做的对象,咱们就能够对序列进行嫁接、切除或修改操做,好比:

>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100
Traceback (most recent call last):
      file "<stdin>", line 1 in <moduld>
TypeError: can only assign an iterable复制代码

若是赋值的对象是一个切片,那么赋值语句的右侧必须是一个可迭代对象。

给切片命名

若是代码中已经出现了大量的没法直视的硬编码切片下标,可使用给切片命名的方式清理代码。好比你有一段代码要从一个记录字符串中几个固定位置提取出特定的数据字段 好比文件或相似格式 :

### 01234567890123456789012345678901234567890123456789012345678901234
record = '............100....513.25........'
cost = int(record[20:23]) * float(record[31:37])
# 这时,能够先给切片命名,以免大量没法理解的硬编码下标,使代码可读性更强
SHARES= slice(20, 23)
PRICE = slice(31, 37)
cost = int(record[SHARES]) * float(record[PRICE])复制代码

slice() 函数建立了一个切片对象,能够被用在任何切片容许使用的地方,好比:

>>> items = [0, 1, 2, 3, 4, 5, 6]
>>> a = slice(2, 4)
>>> items[2:4]
[2, 3]
>>> items[a]
[2, 3]
>>> items[a] = [10, 11]
>>> items
[0, 1, 10, 11, 4, 5, 6]复制代码

若是你有一个切片对象 a,还能够调用 a.start, a.stop, a.step 来获取更多信息,好比:

>>> a = slice(5, 50, 2)
>>> a.start
5
>>> a.step
2复制代码

扩展阅读 为何下标要从0开始

Python 里的范围(range)和切片都不会返回第二个下标所指的元素,计算机科学领域的大师 Edsger W.Dijkstra 在一个很短的备忘录 Why numbering should start at zero 里对这一惯例作了说明。如下是部分关键说明:

为了表示出天然数的子序列,2, 3, ... , 12,不使用省略记号那三个点号,咱们能够选择4种约定方式:

  • a) 2 ≤ i < 13
  • b) 1 < i ≤ 12
  • c) 2 ≤ i ≤ 12
  • d) 1 < i < 13

是否有什么理由,使选择其中一种约定比其它约定要好呢?是的,确实有理由。能够观察到,a) 和 b)有个优势,上下边界的相减获得的差,正好等于子序列的长度。另外,做为推论,下面观察也成立:在 a),b)中,假如两个子序列相邻的话,其中一个序列的上界,就等于另外一个序列的下界。但上面观察,并不能让咱们从a), b)二者中选出更好的一个。让咱们从新开始分析。

必定存在最小的天然数。假如像b)和d)那样,子序列并不包括下界,那么当子序列从最小的天然数开始算起的时候,会使得下界进入非天然数的区域。这就比较丑陋了。因此对于下界来讲,咱们更应该采用≤,正如a)或c)那样。
如今考虑,假如子序列包括上界,那么当子序列从最小的天然数开始算起,而且序列为空的时候,上界也会进入非天然数的区域。这也是丑陋的。因此,对于上界,咱们更应该采用 <, 正如a)或b)那样。所以咱们得出结论,约定a)是更好的选择。

  • 好比要表示 0, 1, 2, 3 若是用 b) d) 的方式,下界就要表示成 -1 < i
  • 若是一个空序列用 c) 实际上是没法表示的,用 a) 则能够表示成 0 ≤ i < 0

总结

这一篇主要介绍元组、分片、序列赋值以及对为何序列从0开始计数作了摘录。

参考连接


最后,感谢女友支持。

欢迎关注(April_Louisa) 请我喝芬达
欢迎关注
欢迎关注
请我喝芬达
请我喝芬达
相关文章
相关标签/搜索