Python笔记1(Python基础、函数、高级特性)

原文连接:廖雪峰老师的Python教程。笔者在学习了廖老师的教程之后做的笔记,一些细节之处有笔者自己见解。如有侵权,联系必删

1. Python基础

1. 数据类型和变量

Python中可以直接处理的数据类型有以下几种:

整数、浮点数、字符串、布尔值、空值、变量、常量

  • 整数
    1000 和 1_000、1000000 和 1_000_000 是完全一样的。
    整数比较
    整数运算永远是精确的,整数的除法也是。

  • 浮点数
    浮点数运算可能会有四舍五入的误差。

  • 字符串
    如果字符串中既包含 ’ 又包含 " 可以使用转义字符 \ 来标识:

    ‘I’m “OK”!’ 表示的内容是 I’m “OK”!
    反斜杠转义

  • 转义字符的用途:

    • \n 换行
    • \t 制表符
    • \本身也要转义
    • \表示\
  • 换行的一个demo:

    >>>print('''abc
    ... bcd''')
    abc
    bcd
    
  • 布尔值
    空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。

  • 变量
    变量名必须是大小写英文、数字和_的组合,且不能用数字开头。

2. 字符编码

Unicode把所有语言都统一到一套编码里,解决乱码问题。

在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。

1. Python中的字符编码

对单个字符的编码,Python提供 ord() 函数获取字符的整数表示,chr() 函数把编码转换为对应的字符:

>>> ord('A')
65
>>> ord('a')
97
>>> ord('中')
20013
>>> chr(63)
'?'
>>> chr(69)
'E'
>>> chr(25932)
'敌'

知识点:

  • 1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
2. 格式化

1. Python用 % 运算符格式化数据:
格式化
demo:
转换demo
注意

  • %s永远起作用,它会把任何数据类型转换为字符串
  • 如果 % 是一个普通字符串,就需要对 % 转义, %% 输出 %

2. format()
字符串的format()方法

3. list 和 tuple

list
  • 添加元素:
    list添加元素
  • 删除元素:
    list删除元素
tuple

不能pop和insert,可以根据切片获取,也可以直接删掉整个元组。
定义一个只包含一个元素的元组需要加个逗号:

tuple_demo = (1,)

元组的几个类型:

a = ()        b = (3,)        c = (2, 3, 4)

4. dict 和 set

dict

和list比较,dict有以下几个特点:

  • 查找和插入的速度极快,不会随着key的增加而变慢;
  • 需要占用大量的内存,内存浪费多。

而list相反:

  • 查找和插入的时间随着元素的增加而增加;
  • 占用空间小,浪费内存很少。

所以,dict是用空间来换取时间的一种方法。

dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象

这是因为 dict根据 key来计算 value的存储位置,如果每次计算相同的key得出的结果不同,那 dict内部就完全混乱了。 这个通过key计算位置的算法称为哈希算法(Hash)。

要保证 hash的正确性,作为 key的对象就不能变

Python中的可变和不可变类型:

  • 不可变类型:字符串、整数、元组;
  • 可变类型:列表、集合
set

set = {1, 2, 3, 4} 不包含重复元素,set 与list 和tuple三者可以相互转换。
set 可以使用 remove删除指定元素。

5. if-elif-else 条件判断

形式:

if <条件判断1>:
<执行1>
elif <条件判断2>:
    <执行2>
elif <条件判断3>:
    <执行3>
else:
    <执行4>

规则:

	if 成立,eilf 和 else 都不执行;如果 if 下还是 if 则会每一个都判断。

6. 循环

for循环

列表生成式简单点的demo:

# 生成一个0~10的列表
>>> list(range(11))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

就不要使用下边这个了:

>>> [x for x in range(11)]

当然,如果有限制条件,下边这种写法还是不能忽视的。。

2. 函数

1. 函数的参数

1. 位置参数
def func(x):
	return x + 2
# x 就是一个位置参数
2. 默认参数
def func(x=3):
	return x
# x=3就是一个默认参数

注意

  • 必选参数在前,默认参数在后
  • 设置默认参数:当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数可以作为默认参数
  • 默认参数必须指向不变类型
    • 比如默认参数是列表时:
>>> def func(L=[]):
...     L.append('a')
...     return L
...
>>> func()
['a']
>>> func()
['a', 'a']
>>> func()
['a', 'a', 'a']
  • 第一次调用看起来是正确的,但之后每次调用函数func()时都会追加一个’a’。因为默认参数 L=[ ] 列表时一个可变类型,它指向对象 [ ],每次调用该函数,如果改变了 L的内容,下次调用时,默认参数的内容就变了,不再是函数定义时的 [ ] 了。

设置默认参数的好处:

  • 降低调用函数的难度。

调用默认参数:

  • 按顺序提供默认参数;
  • 不按顺序提供部分默认参数时,需要把参数名写上。
3. 可变参数(不定长参数)

定义:

  • 参数前加 * 号。参数args接收到的是一个tuple元组,包含0到多个的任意个参数。

形式:

def func(*args):
	pass

知识点

  • 如果需要把一个列表或元组或集合传入含可变参数的函数内,可以这样写:

     def func(*args):
     	pass
     	
     num = [1, 2, 3]
     func(*num)
    
  • 代码如下:

>>> def func(*nums):
...     sum = 0
...     for n in nums:
...             sum = n * n
...     return sum
...
>>> num_list = [1, 2, 3, 4, 5]
>>> func(*num_list)
25
>>> num_tuple = (1, 2, 3, 4, 5)
>>> func(*num_tuple)
25
>>> num_set = {1, 2, 3, 4, 5}
>>> func(*num_set)
25
  • 当然也可以这样写,但是太繁琐:
>>> def func(*nums):
	...     sum = 0
	...     for n in nums:
	...             sum = n * n
	...     return sum
	...
>>> num = [1, 2, 3]
>>> func(num[0], num[1], num[2])
9
4. 关键字参数

定义:

  • 参数前加 ** 号。允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict

形式:

def func(name, age, **kwargs):
	pass

可以传入必传参数,也可以传入任意个参数:

>>> def person(name, age, **kwargs):
...     print("name:",name,"age:",age,"other:",kwargs)
...
>>> person("王二狗", 25)
name: 王二狗 age: 25 other: {}
>>> person("王二狗", 25, city="北京")
name: 王二狗 age: 25 other: {'city': '北京'}

关键字参数作用:

关键字参数可以扩展函数的功能

比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

**自动拆包字典的数据:person(info)

>>> def person(name,age,**kw):
...     print('name:',name,'age:',age,'other:',kw)
...
>>> info={'name':'jason','gender':'male','age':25,'city':'beijing','job':'coder'}
>>> person(**info)
name: jason age: 25 other: {'gender': 'male', 'city': 'beijing', 'job': 'coder'}

注意:

  • **kw表示把info这个字典以 k-v 关键字的形式传入到函数的 **kw 参数,kw将获取到一个字典,这个字典是对info的一份拷贝,对kw的改动不会影响info的信息。
5. 命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。

仍以person()函数为例,我们希望检查是否有city和job参数:

>>> def person(name, age, **kw):
...     if 'city' in kw:
...         pass
...     if 'job' in kw:
...         pass
...     print('name:', name, 'age:', age, 'other:', kw)
...
>>> person('jason', 25, city='beijing', addr='chaoyang', gender='male')
name: jason age: 25 other: {'city': 'beijing', 'addr': 'chaoyang', 'gender': 'male'}

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job):
    print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符 *,星号后面的参数被视为命名关键字参数。

调用方式如下:

>>> def person(name,age,*,city,job):
...     print(name,age,city,job)
...
>>> person('jason',25,city='beijing',job='coder')
jason 25 beijing coder

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

>>> person('jack',25,'beijing','coder')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given

由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。

命名关键字参数可以有缺省值,从而简化调用:

>>> def person(name,age,*,city='beijing',job):
...     print(name,age,city,job)
...
>>> person('jason',25,job='coder')
jason 25 beijing coder

由于命名关键字参数city具有默认值,调用时,可不传入city参数:

>>> person('jason',25,job='coder')
jason 25 beijing coder

也可以给默认值city传入参数:

>>> person('jason',25,city='shanghai',job='coder')
jason 25 shanghai coder

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个 * 作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:

def person(name, age, city, job):
    # 缺少 *,city和job被视为位置参数
    pass
6. 参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数

比如定义一个函数,包含上述若干种参数:

>>> def f1(a,b,c=0,*args,**kwargs):
...     print('a=',a,'b=',b,'c=',c,'args=',args,'keargs=',kwargs)
...
>>> def f2(a,b,c=2,*,d,**kw):
...     print('a=',a,'b=',b,'c=',c,'d=',d,'kw=',kw)
...

在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去:

>>> f1(1,2)
a= 1 b= 2 c= 0 args= () keargs= {}
>>>
>>> f1(1,2,c=3)
a= 1 b= 2 c= 3 args= () keargs= {}
>>>
>>> f1(1,2,3,'a','b')
a= 1 b= 2 c= 3 args= ('a', 'b') keargs= {}
>>>
>>> f1(1,2,3,'a','b',x=99)
a= 1 b= 2 c= 3 args= ('a', 'b') keargs= {'x': 99}
>>>
>>> f2(1,2,d=90,ex=None)
a= 1 b= 2 c= 2 d= 90 kw= {'ex': None}

最神奇的是通过一个tuple和dict,你也可以调用上述函数:

>>> args={1,2,3,4}
>>> kw={'d':99,'x':'#'}
>>> f1(*args,**kw)
a= 1 b= 2 c= 3 args= (4,) keargs= {'d': 99, 'x': '#'}
>>>
>>> args = (1,2,3)
>>> kw={'d':88,'x':'@'}
>>> f2(*args, **kw)
a= 1 b= 2 c= 3 d= 88 kw= {'x': '@'}

所以,对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

说明

  • 虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。

demo:创建一个函数接收一个或多个数字,打印这些数字的乘积。

>>> def func(*args):
...     result = 1
...     for x in args:
...             result *= x
...     print(result)
...
>>> func(1)
1
>>> func(1,2,3)
6
>>> func(1,2,3,4)
24
>>> func(1,2,3,4,5)
120

小结:

  • Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。

  • 默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

  • 要注意定义可变参数和关键字参数的语法:

    • *args是可变参数,args接收的是一个tuple;

    • **kw是关键字参数,kw接收的是一个dict。

  • 调用函数时如何传入可变参数和关键字参数的语法:

    • 可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过args传入:func((1, 2, 3));

    • 关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过 ** kw 传入:func(**{‘a’: 1, ‘b’: 2})。

  • 使用*args和**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

  • 命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。

  • 定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

2. 递归函数

1. 概念

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

2. 举例

demo:计算阶乘n! = 1 x 2 x 3 x … x n,用函数func(n)表示:

>>> def func(n):
...     if n == 1:
...             return 1
...     return n * func(n-1)
...

上边就是一个递归函数,测试如下:

>>> func(1)
1
>>> func(2)
2
>>> func(3)
6
>>> func(4)
24
>>> func(5)
120
>>> func(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
3. 优点

定义简单,逻辑清晰。

理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

4. 特点

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试func(1000):

>>> func(1000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in func
  File "<stdin>", line 4, in func
  File "<stdin>", line 4, in func
  [Previous line repeated 995 more times]
  File "<stdin>", line 2, in func
RecursionError: maximum recursion depth exceeded in comparison

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的func(n)函数由于return n * func(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

def func(n):
    return func_iter(n, 1)

def func_iter(num, product):
    if num == 1:
        return product
    return func_iter(num - 1, num * product)

可以看到,return func_iter(num - 1, num * product)仅返回递归函数本身,num - 1和num * product在函数调用前就会被计算,不影响函数调用。

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。

小结:

  • 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出

  • 针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。

  • Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

3. 高级特性

1. 切片

  • list
>>> name_list = ['Bob','Jason','Tom','Mark','Sarah']
>>> name_list[:3]
['Bob', 'Jason', 'Tom']
>>> name_list[1:3]
['Jason', 'Tom']
>>> name_list[-1]
'Sarah'
  • tuple
>>> tuple_num = (1,2,3,4,5,6,7,8)
>>> tuple_num[:5]
(1, 2, 3, 4, 5)
>>> tuple_num[3:]
(4, 5, 6, 7, 8)
>>> tuple_num[:5:2]
(1, 3, 5)
  • string
>>> str_text = 'hello world'
>>> str_text[1:5]
'ello'
>>> str_text[1:8]
'ello wo'
>>> str_text[-1:-3]
''
>>> str_text[-3:-1]
'rl'
>>> str_text[-3]
'r'
  • set 不支持
>>> set_num = {1,2,3,4,5,6}
>>> set_num[:3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable
  • dict 不支持
>>> info = {'name':'Jason','age':25,'gender':'male'}
>>> info[1:2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'slice'
  • int 不支持
>>> inin = 12345
>>> inin[1,2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not subscriptable

支持切片的数据类型:列表、元组、字符串

2. 迭代

如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。

在Python中,迭代是通过for … in来完成的。

list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:

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

所以,当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。

那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:

>>> from collections import Iterable
>>> isinstance('abc',Iterable)
True
>>> isinstance([1,2,3],Iterable)
True
>>> isinstance({'name':'Jason'}, Iterable)
True
>>> isinstance(123,Iterable)
False

如果要list或tuple打印出带有下标的信息,可以使用 enumerate模块:

>>> for i,v in enumerate(['a','b','c']):
...     print(i,v)
...
0 a
1 b
2 c

如果列表中嵌套了列表,每个嵌套的列表含有相同数量的元素,可以这样输出:

>>> for x,y in [(1,2),(3,4),(5,6)]:
...     print(x,y)
...
1 2
3 4
5 6
>>> for x,y,z in [(1,2,3),(2,3,4),(3,4,5),('a','b','c')]:
...     print(x,y,z)
...
1 2 3
2 3 4
3 4 5
a b c

如果只指定一个变量就跟简单的 for…i…in…list:…print(i)一样:

>>> for x in [(1,2,3),(2,3,4),(3,4,5),('a','b','c')]:
...     print(x)
...
(1, 2, 3)
(2, 3, 4)
(3, 4, 5)
('a', 'b', 'c')

3. 列表生成式

  • demo1:生成1~11的数字列表:
>>> list(range(1,11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • dem2:生成1~11的平方的数字列表:
>>> [x**2 for x in range(1,11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
  • demo3:生成 [AX,AY,AZ,BX,BY,BZ,CX,CY,CZ] 的列表:
>>> [a + b for a in 'ABC' for b in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
  • demo4:生成1~10之间偶数的列表:
>>> [x for x in range(1,11) if x % 2 == 0]
[2, 4, 6, 8, 10]
# 因为range()区间,后边的数是不包含的,所以需要写到11才能包含到10
  • demo5:列出当前目录下的所有文件和目录名:
>>> import os
>>> [d for d in os.listdir('.')]
['DLLs', 'Doc', 'include', 'Lib', 'libs', 'LICENSE.txt', 'NEWS.txt', 'python.exe', 'python3.dll', 'python37.dll', 'pythonw.exe', 'Scripts', 'tcl', 'Tools', 'vcruntime140.dll']
  • demo6:for循环同时使用两个甚至多个变量,比如dict的items()可以同时迭代key和value:
>>> info = {'name':'Jason','address':'beijing'}
>>> for k,v in info.items():
...     print(k + '=' + v)
...
name=Jason
address=beijing
  • demo7:把一个list中所有的字符串变成小写:
>>> name_list = ['jason','bob','tom','lili']
>>> [name.capitalize() for name in name_list]
['Jason', 'Bob', 'Tom', 'Lili']

if-else:

  • error1:不能在最后的if加上else:
[x for x in range(1, 11) if x % 2 == 0 else 0]
  File "<stdin>", line 1
    [x for x in range(1, 11) if x % 2 == 0 else 0]
                                              ^
SyntaxError: invalid syntax
  • error2:if写在for前面,但是不加else会报错:
[x if x % 2 == 0 for x in range(1, 11)]
  File "<stdin>", line 1
    [x if x % 2 == 0 for x in range(1, 11)]
                       ^
SyntaxError: invalid syntax

其他有意思的事:

  • 小计:一行列表表达式中包含 for…if…else的格式:

    • [x if 表达式 else 表达式 for x的一个范围]
  • demo1:输出1~10的列表,是2的倍数的则正常输出,不是2的倍数的输出False

>>> [x if x % 2 == 0 else x == False for x in range(1, 11)]
[True, 2, False, 4, False, 6, False, 8, False, 10]
>>>
# 也可以在是2的倍数的数字中选出一个放到else中:
>>> [x if x % 2 == 0 else x == 2 for x in range(1, 11)]
[False, 2, False, 4, False, 6, False, 8, False, 10]
  • demo2:还是输出1~11的列表,是2的倍数的则正常输出,是3输出True,其他输出False:
>>> [x if x % 2 == 0 else x == 3 for x in range(1, 11)]
[False, 2, True, 4, False, 6, False, 8, False, 10]
  • demo3:输出1~10的列表,是2的倍数正常输出,不是2的倍数则输出它的相反数:
>>> [x if x % 2 == 0 else -x for x in range(1,11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
  • demo4:输出1~10的列表,是2的倍数的正常输出,不是2的倍数的输出指定数字88:
>>> [x if x % 2 == 0 else 88 for x in range(1,11)]
[88, 2, 88, 4, 88, 6, 88, 8, 88, 10]

4. 生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

在Python中,一边循环一边计算的机制,称为生成器:generator

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

>>> l = [x for x in range(10)]
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x000001434C9345C8>

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

list直接打印出所有数据,而generator需要使用 next() 函数获得generator的下一个返回值:

>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
4
>>> next(g)
5
>>> next(g)
6
>>> next(g)
7
>>> next(g)
8
>>> next(g)
9
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出 StopIteration 的错误。

上面这种不断调用next(g)实在是太变态了,正确的方法是使用 for循环,因为generator也是可迭代对象:

>>> g = (x for x in range(10))
>>> for i in g:
...     print(i)
...
0
1
2
3
4
5
6
7
8
9

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

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。

比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, …

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

Fibonacci.py

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

fib(6)

运行结果:
斐波那契数列运行结果
仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。

也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:

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

print(fib2(5))

运行结果:

<generator object fib2 at 0x7fc6f8995ba0>

注意:最难理解的就是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)
steo3
5
>>> next(o)
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    next(o)
StopIteration

可以看到,odd不是普通函数,而是generator,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。

回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:

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

>>> for i in fib(6):
		print(i)

1
1
2
3
5
8

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

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

>>> 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

杨辉三角

def triangles():
    Y = [1]  # 初始化为 [1],杨辉三角的每一行都是一个list
    while True:
        yield Y  # yield 实现记录功能,没有下一个next将跳出循环
        H = Y[:]  # 将 Y 赋值给 H, 通过 H 计算每一行
        H.append(0)  # 将list添加 0 作为最后一个元素,长度增加 1
        Y = [H[i - 1] + H[i] for i in range(len(H))]  # 通过 H 计算出 Y

测试:

>>> def triangles():
    Y = [1]
    while True:
        yield Y
        H = Y[:]
        H.append(0)
        Y = [H[i - 1] + H[i] for i in range(len(H))]

>>> g = triangles()
>>> g
<generator object triangles at 0x7fbc0fbea190>
>>> next(g)
[1]
>>> next(g)
[1, 1]
>>> next(g)
[1, 2, 1]
>>> next(g)
[1, 3, 3, 1]
>>> next(g)
[1, 4, 6, 4, 1]
>>> next(g)
[1, 5, 10, 10, 5, 1]

小结

  • generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。

  • 要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。

  • 对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束。

请注意区分普通函数和generator函数:

  • 普通函数调用直接返回结果:
>>> r = abs(6)
>>> r
6
  • generator函数的“调用”实际返回一个generator对象:
>>> g = fib(6)
>>> g
<generator object fib at 0x1022ef948>

5. 迭代器

直接作用于for循环的数据类型有:

  • 一类是集合数据类型:list、tuple、dict、set、string

  • 一类是generator,包括生成器和带yield带generator function

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

可使用 isinstance() 判断一个对象是否是 Iterable 对象:

>>> from collections.abc import Iterable
>>> isinstance([], Iterable)  # 列表
True
>>> isinstance({}, Iterable)  # 字典
True
>>> isinstance('abc', Iterable)  # 字符串
True
>>> isinstance((x for x in range(10)), Iterable)  # generator
True
>>> isinstance(list(range(10)), Iterable)  # 列表生成式
True
>>> isinstance(100, Iterable)  # 整形 不可迭代
False

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

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

迭代器有两个基本的方法:iter()next()

可以使用isinstance()判断一个对象是否是Iterator对象:

>>> from collections.abc import Iterator
>>> isinstance([], Iterator)  # 列表不是迭代器
False
>>> isinstance(list(range(10)), Iterator)  # 列表生成器不是迭代器
False
>>> isinstance((x for x in range(10)), Iterator)  # generator是迭代器
# a = (x for x in range(10))
# next(a)可以不断生成下一个数字
True
>>> isinstance({}, Iterator)  # 字典不是迭代器
False
>>> isinstance('abc', Iterator)  # 字符串不是迭代器
False

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。

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

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

小结

数据类型生成器可迭代对象迭代器
list
tuple
dict
set
str
  • 可迭代对象可通过 iter()转换成迭代器

为什么list、dict、str等数据类型不是Iterator?

因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

小结

  • 凡是可作用于for循环的对象都是Iterable类型

  • 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列

  • 集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象

  • Python等for循环本质上就是通过不断调用next()函数实现的,例如:

for i in [1, 2, 3, 4, 5]:
	pass

完全等价于:

# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环
while True:
try:
	# 获得下一个值
	x = next(it)
except StopIteration:
	# 遇到StopIteration就退出循环
	break

6. 迭代器和生成器总结

1. 迭代器和迭代过程

生成器和迭代器

在Python中,迭代器是遵循迭代协议的对象

迭代器有两种形式:

  • 使用 iter() 从任何序列对象中得到迭代器(如 list、tuple、dict、set等);
  • 输入迭代器是 generator (生成器)。

很多容器诸如列表、字符串,可以用for循环遍历对象。for语句会调用容器对象中的 iter() 函数,该函数返回一个定义了 __next__() 方法的迭代器对象,该方法将逐一访问容器中的元素。

Python中的任意对象,只要定义了__next__()方法,它就是一个迭代器。因此,python中的容器如列表、元组、字典、集合、字符串都可以用于创建迭代器。

迭代器和可迭代对象的关系
迭代是从迭代器中取元素的过程

  • demo: 比如用for循环从列表 [1,2,3] 中取元素,这种遍历过程就被称为 迭代。
 # 列表、元组、字典、字符串都是迭代器
 for i in [1,2,3]:  # 列表
 	print(i)
 for i in (1,2,3):  # 元组
 	print(i)
 for key in {'one':1,'two':2}:  # 字典
 	print(key)
 for i in '123':  # 字符串
 	print(i)

 for i in open("myfile.txt"):  # 打开的text同样是迭代器
 	print(i, end='')

如果不想用for循环迭代,可用如下方式:

  1. 先调用容器的 iter() 函数;
  2. 再使用 next() 内置函数来调用 __next__() 方法
  3. 当元素用尽时, __next__() 将引发 StopIteration 异常
    iter
>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x000002D6514E1788>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

2. 生成器

在Python中,生成器(Generator) 是一边循环一边计算的机制

生成器也是一种迭代器,但只能迭代一次。因为它们并没有把所有的值存在内存中,而是在运行时生成值。

生成器可通过遍历去使用,用for循环,或将其传递给任意可进行迭代的函数和结构。大多数生成器是以函数来实现的。然而,它们并不返回一个值,而是 yield (暂且译作 “生出”) 一个值。

每次对生成器调用 next() 时它都会从上次离开位置恢复执行(它会记住上次执行语句时的所有数据值)。

demo:创建生成器

>>> def func(data):
...     for i in range(len(data) - 1, -1, -1):
...             yield data[i]
...
>>> for char in func('golf'):
...     print(char)
...
f
l
o
g

可以用生成器来完成的操作同样可以用基于类的迭代器来完成。但生成器的写法更为紧凑,因为它会自动建 __iter__()__next__() 方法。

3. 生成器表达式

生成器不一定要用复杂的函数表示,Python提供了简洁的生成器表达式。

生成器表达式跟列表推导式很像,仅仅是将列表推导式中的 [] 替换为 () , 但是两者差别挺大,生成器表达式可以说组合了迭代功能和列表解析功能。

生成器表达式可认为是一种特殊的生成器函数,类似于 lambda表达式普通函数。但是和生成器一样,生成器表达式也是返回生成器generator对象,一次只返回一个值

>>> sum(i*i for i in range(10))  # 平方和
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x, y in zip(xvec, yvec))  # 点积
260
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员老五

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值