文章目录

  • 1. 迭代
  • 1.1 索引迭代
  • 1.2 并行迭代
  • 1.3 反向迭代和排序后再迭代
  • 2. 列表生成式
  • 2.1 列表生成器
  • 2.2 字典推导
  • 2.3 集合推导式
  • 3. 生成器 generator
  • 4. 迭代器 Iterator
  • 5. 易错知识点
  • 6. 练习


【Python 基础教程(第3版) [挪] Magnus Lie Hetland 著】

1. 迭代

通过for循环来遍历,这种遍历称为迭代(Iteration)。
在Python中,迭代是通过for ... in来完成的。只要是可迭代对象,无论有无下标,都可以迭代,比如 dict 就可以迭代。

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
...     print(key)
...
a
c
b

因为 dict 的存储不是按照 list 的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
默认情况下,dict 迭代的是 key。如果要迭代 value,可以用for value in d.values(),如果要同时迭代 key 和 value,可以用for k, v in d.items()
由于字符串也是可迭代对象,因此,也可以作用于for循环:

>>> for ch in 'ABC':
...     print(ch)
...
A
B
C

通过collections模块的Iterable类型判断一个对象是否为可迭代对象:

>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

1.1 索引迭代

通过 Python 内置的enumerate 函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C

1.2 并行迭代

通过内置函数 zip,可将两个序列“缝合”起来,并返回一个由元组组成的序列。返回值是一个适合迭代的对象,要查看其内容,可使用 list 将其转换为列表。

>>> list(zip(names, ages))
[('anne', 12), ('beth', 45), ('george', 32), ('damon', 102)]

可在循环中将元组解包。

for name, age in zip(names, ages):
	print(name, 'is', age, 'years old')

函数 zip 可用于“缝合”任意数量的序列。当序列的长度不同时,函数 zip 将在最短的序列用完后停止“缝合”。

1.3 反向迭代和排序后再迭代

内置函数 reversedsorted,类似于列表方法的 reversesort。可用于任何序列或可迭代的对象,且不就地修改对象,而是返回反转和排序后的版本。

>>> sorted([4, 3, 6, 8, 3])
[3, 3, 4, 6, 8]
>>> sorted('Hello, world!')
[' ', '!', ',', 'H', 'd', 'e', 'l', 'l', 'l', 'o', 'o', 'r', 'w']
>>> list(reversed('Hello, world!'))
['!', 'd', 'l', 'r', 'o', 'w', ' ', ',', 'o', 'l', 'l', 'e', 'H']
>>> ''.join(reversed('Hello, world!'))
'!dlrow ,olleH'

sorted 返回一个列表,而 reversedzip 那样返回一个可迭代对象。

2. 列表生成式

2.1 列表生成器

列表生成式(List Comprehensions),是Python内置的非常简单却强大的可以用来创建 list 的生成式。使用 [] 方括号。

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来

for循环后面还可以加上if判断,这样就可以筛选出仅偶数的平方:

>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列,优先运行后面的 for 循环:

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

for循环可以同时使用两个甚至多个变量,比如dictitems()可以同时迭代keyvalue

>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']

在一个列表生成式中,for 前面的 if ... else 是表达式,必须一起使用,而 for 后面的 if 是过滤条件,不能带 else

>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]

示例代码,将名字的首字母相同的男孩和女孩配对:

>>> girls = ['alice', 'bernice', 'clarice']
>>> boys = ['chris', 'arnold', 'bob']
>>> [b+'+'+g for b in boys for g in girls if b[0] == g[0]]
['chris+clarice', 'arnold+alice', 'bob+bernice']

以上代码由于需要检查每种可能的配对,因此效率不太高。可使用以下代码:

girls = ['alice', 'bernice', 'clarice']
boys = ['chris', 'arnold', 'bob']
letterGirls = {}
for girl in girls:
	# setdefault 返回的是空 list,因此能直接使用 append 方法
	letterGirls.setdefault(girl[0], []).append(girl)	
print([b+'+'+g for b in boys for g in letterGirls[b[0]]])

这个程序创建一个名为 letterGirls 的字典,其中每项的键都是一个字母,而值为以这个字母打头的女孩名字组成的列表。创建这个字典后,列表推导遍历所有的男孩,并查找名字首字母与当前男孩相同的所有女孩。这样,这个列表推导就无需尝试所有的男孩和女孩组合并检查他们的名字首字母是否相同了。

2.2 字典推导

使用 {} 花括号来执行字典推导for 前面有两个用冒号分隔的表达式。这两个表达式分别为键及其对应的值:

>>> squares = {i:"{} squared is {}".format(i, i**2) for i in range(3)}
>>> squares
{0: '0 squared is 0', 1: '1 squared is 1', 2: '2 squared is 4'}
>>> squares[1]
'1 squared is 1'

2.3 集合推导式

>>>a = {x for x in 'abracadabra' if x not in 'abc'} #集合推导式,同列表推导式
>>> a
{'r', 'd'}

3. 生成器 generator

如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

创建Lg的区别仅在于最外层的[]()L是一个list,而g是一个generator。

如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

>>> next(g)
0
>>> next(g)
1
......
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。
使用for循环,因为generator也是可迭代对象:

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1
4
9
16
25
36
49
64
81

所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ... 斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

注意,赋值语句:

a, b = b, a + b

相当于:

t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]

但不必显式写出临时变量t就可以赋值。
上面的函数可以输出斐波那契数列的前N个数:

>>> fib(6)
1
1
2
3
5
8
'done'

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
举个简单的例子,定义一个generator,依次返回数字1,3,5:

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)

调用该generator时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值:

>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

可以看到,odd不是普通函数,而是generator,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。
回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:

>>> for n in fib(6):
...     print(n)
...
1
1
2
3
5
8

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIterationvalue中:

>>> g = fib(6)
>>> while True:
...     try:
...         x = next(g)
...         print('g:', x)
...     except StopIteration as e:
...         print('Generator return value:', e.value)
...         break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

4. 迭代器 Iterator

可以直接作用于for循环的对象统称为可迭代对象:Iterable

  • 集合数据类型,如list、tuple、dict、set、str等
  • generator,包括生成器和带yield的generator function

生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator

把list、dict、str等Iterable变成Iterator可以使用iter()函数:

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

你可能会问,为什么list、dict、str等数据类型不是Iterator
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用 list 是永远不可能存储全体自然数的。

5. 易错知识点

  1. 对于序列(包括 list,tuple 和 string)均可以进行切片(slice)操作。若切片中的两个索引值相同,则会返回空序列。而对于可修改的序列—— list, 此时可进行赋值和删除操作。示例如下:
>>> t = (1,2,3)
>>> t[1:1]	# 此时返回空 tuple
()
>>> s = "abc"
>>> s[1:1] 	# 此时返回空 string
''
>>> l = ["a","d","e"]
>>> l[1:1]	# 此时返回空 list
[]
>>> l[1:1] = ["b","c"] # 可在此处插入新 list
>>> l
['a', 'b', 'c', 'd', 'e']
>>> l[1:3] = [] # 可删除指定位置的对象
>>> l
['a', 'd', 'e']
  1. s[0] 表示索引(index),s[:1] 表示切片(slice),这两个操作在 python 内部的实现方式是不同的。索引返回单个项目,但切片返回项目的子序列。
    示例如下:
>>> l = [1,2,3]
>>> l[0]	# 返回单个 item
1
>>> l[:1]	# 返回子序列
[1]
>>> t = (1,2,3)
>>> t[0]	# 返回单个 item
1
>>> t[:1]	# 返回子序列
(1,)
>>> s = "abc"
>>> s[0]	# 返回单个 item
'a'
>>> s[:1]	# 返回子序列,两者结果一样
'a'

为什么在字符串不为空时,s[0]s[:1]的表现结果“恰好”一致?
这是由于在 python 中,字符串外没有单个字符之类的东西。一个字符只是一个单字符的字符串。因此才让 s[0] 的结果与 s[:1] 的结果“恰好”一致。

  1. 如果取 s[0],代表这个序列必须有该对应元素。若序列为空,则取 s[0] 会报错IndexError: string/tuple/list index out of range
    如果取 s[:1],即使是一个空字符串,取 s[:1] 只会返回空字符串,不会报错。

这是由于 Python 中切片操作不受内置类型的边界检查,因此允许切片越界索引,而索引却无法越界。

对于切片表达式 s[i:j:k] 而言,其处理逻辑如下:

  • ij 为负数,则会被转换成 len(s) + ilen(s) + j 进行计算。
  • k 为正数时:
  • i 省略时,则使用 0 计算。当 j 省略时,则使用 len(s) 计算。
  • i 大于等于 j 时,直接返回空序列。
  • ij 大于 len(s) 时, 则均会使用 len(s) 计算。
  • k 为负数时:
  • ij 省略时,均当作序列的 end(末尾) 计算。
  • i 小于等于 j 时,直接返回空序列。
  • ij 大于 len(s) 时, 则会使用 len(s) - 1 计算。

示例如下:

>>> "abc"[:999]	# 相当于 [0:len(s)],即 [0:3]
'abc'
>>> "abc"[999:9999]	# 相当于 [len(s):len(s)],即 [3:3]
''
>>> "abc"[-3:]	# 相当于 [len(s)-3:len(s)],即 [0:3]
'abc'
>>> "abc"[:-1]	# 相当于 [0:len(3)-1],即 [0:2]
'ab'

参考链接:
Python String index out of range, difference between s[0] and s[:1] [duplicate]Why does substring slicing with index out of range work?5.6. Sequence Types — str, unicode, list, tuple, bytearray, buffer, xrange

  1. for 循环中,若迭代字符串变量,则在循环内部对变量重新赋值,不影响循环。若迭代列表变量,在循环内部改变此变量时,会影响到循环本身。

字符串示例如下:

s = "abcde"
for i,a in enumerate(s):
    s = s[2:]
    print(i,a,s)

运行结果如下,即使最后 s 指向了空字符串,原循环仍然迭代了 5 次,不会因为 s 的改变受影响:

0 a cde
1 b e
2 c
3 d
4 e

列表示例如下:

l = [1,2,3,4,5,]
for i,a in enumerate(l):
    l.pop()
    print(i,a,l)

运行结果如下,由于 pop() 操作不断在改变原列表,造成本该迭代 5 次,结果只迭代了 3 次:

0 1 [1, 2, 3, 4]
1 2 [1, 2, 3]
2 3 [1, 2]

6. 练习

  1. 利用切片操作,实现一个trim()函数,去除字符串首尾的空格,注意不要调用str的strip()方法:
# -*- coding: utf-8 -*-
def trim(s):
	return s
# 补充

# 测试:
if trim('hello  ') != 'hello':
    print('测试失败!')
elif trim('  hello') != 'hello':
    print('测试失败!')
elif trim('  hello  ') != 'hello':
    print('测试失败!')
elif trim('  hello  world  ') != 'hello  world':
    print('测试失败!')
elif trim('') != '':
    print('测试失败!')
elif trim('    ') != '':
    print('测试失败!')
else:
    print('测试成功!')

解答一,使用两个 for/while 循环,分别去掉字符串首尾的空格:

def trim(s):
    while s[-1:] == ' ':
        s = s[:-1]
    while s[:1] == ' ':
        s = s[1:]
    return s

解法二,仅使用一次 while 循环,将字符串转成列表,利用列表翻转的方法实现:

def trim(s):
    if s:
        l = list(s)
        n = 0
        while n < 2:
            if l[0] == " ":
                del l[0]
                if not l:
                    return ""
                continue
            l.reverse()
            n += 1
        return "".join(l)
    else:
        return ""

解法三,直接获取首尾非空格的索引,截断字符串:

def trim(s):
    s_len = len(s) 
    begin = end = s_len
    for i, a in enumerate(s):
        if a != " " and begin == s_len: 
            begin = i
        if a == " " and begin != s_len and s[i-1] != " ":
            end = i
    s = s[begin:end]
    return s
  1. 请使用迭代查找一个list中最小和最大值,并返回一个tuple:
# -*- coding: utf-8 -*-
def findMinAndMax(L):
    return (None, None)
# 补充


# 测试
if findMinAndMax([]) != (None, None):
    print('测试失败!')
elif findMinAndMax([7]) != (7, 7):
    print('测试失败!')
elif findMinAndMax([7, 1]) != (1, 7):
    print('测试失败!')
elif findMinAndMax([7, 1, 3, 9, 5]) != (1, 9):
    print('测试失败!')
else:
    print('测试成功!')

解法一,若不采用 minmax 函数,则使用列表的 sort 方法是最快的:

def findMinAndMax(L):
    if L:
        L.sort()
        return (L[0],L[-1])
    else:
        return (None, None)

解法二,采用冒泡排序法:

def findMinAndMax(L):
    if L:
        if len(L) == 1:
            return (L[0],L[-1])
        for i in range(len(L)-1):
            for j in range(len(L)-1-i):
                if L[j] > L[j+1]:
                    L[j], L[j+1] = L[j+1], L[j]
        return (L[0],L[-1])
    else:
        return (None, None)

解法三,常规找最大值最小值:

def findMinAndMax(L):
    if L==[]:
        return (None,None)
    else:
        min=L[0]
        max=L[0]
        for i in L:
            if i<min:
                min=i
            if i>max:
                max=i
        return (min,max)
  1. 如果list中既包含字符串,又包含整数,由于非字符串类型没有 lower() 方法,所以列表生成式会报错。使用内建的isinstance函数可以判断一个变量是不是字符串:
>>> x = 'abc'
>>> y = 123
>>> isinstance(x, str)
True
>>> isinstance(y, str)
False

请修改列表生成式,通过添加if语句保证列表生成式能正确地执行:

# -*- coding: utf-8 -*-
L1 = ['Hello', 'World', 18, 'Apple', None]
L2 = ???	# 补充

# 测试:
print(L2)
if L2 == ['hello', 'world', 'apple']:
    print('测试通过!')
else:
    print('测试失败!')

解答:

L1 = ['Hello', 'World', 18, 'Apple', None]
L2 = [s.lower() for s in L1 if isinstance(s,str)]