廖雪峰的python教程(1)

教程网址: https://www.liaoxuefeng.com/wiki/1016959663602400

空值

空值是 Python 里一个特殊的值,用 None 表示。None 不能理解为 0,因为 0 是有意义的,而 None 是一个特殊的空值。

字符串编码

计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。

Unicode 把所有语言都统一到一套编码里,这样就不会再有乱码问题了。Unicode 标准也在不断发展,但最常用的是 UCS-16 编码,用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要 4 个字节)。现代操作系统和大多数编程语言都直接支持 Unicode。

如果你写的文本基本上全部是英文的话,用 Unicode 编码比 ASCII 编码需要多一倍的存储空间,在存储和传输上就十分不划算。

所以,本着节约的精神,又出现了把 Unicode 编码转化为“可变长编码”的 UTF- 8 编码。UTF- 8 编码把一个 Unicode 字符根据不同的数字大小编码成 1 - 6 个字节,常用的英文字母被编码成 1 个字节,汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4 - 6 个字节。如果你要传输的文本包含大量英文字符,用 UTF- 8 编码就能节省空间。

字符 ASCII Unicode UTF-8
A 01000001 0000000001000001 01000001
x 0100111000101101 111001001011100010101101

总结一下现在计算机系统通用的字符编码工作方式:

  • 在计算机内存中,统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为 UTF- 8 编码。
  • 用记事本编辑的时候,从文件读取的 UTF- 8 字符被转换为 Unicode 字符到内存里,编辑完成后,保存的时候再把 Unicode 转换为 UTF- 8 保存到文件
  • 浏览网页的时候,服务器会把动态生成的 Unicode 内容转换为 UTF- 8 再传输到浏览器

内存中用 Unicode, 文件中或者网络传输中用 UTF-8, 节省空间。

在最新的 Python 3 版本中,字符串是以 Unicode 编码的。

之前提到计算机只能处理数字, 字符和数字之间需要响应的转换。

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

  • chr(): 数字转字符。built-in function chr(), which is an abbreviation of the word "character".
  • ord(): 字符转数字。 The word that built-in function ord() abbreviate for"ordinal".It's an abbreviation for"ordinal". Ordinal numbers are counting numbers -- i.e., 1, 2, 3. ord()converts the character into its (countable) position in the character set.
  • 如果知道字符的整数编码,还可以用十六进制写文本, 和 chr() 字符拼接等价:
    • '\u4e2d\u6587'
    • chr(20013) +chr(25991)
>>> print('包含中文的 str')
包含中文的 str
>>> ord(" 中 ")
20013
>>> ord(" 中国 ")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ord() expected a character, but string of length 2 found
>>> chr(20013)
'中'
>>> chr(25991)
'文'
>>> s = chr(20013) +chr(25991)
>>> s
'中文'
>>> t = '\u4e2d\u6587'
>>> s==t
True

Python 的字符串类型是 str,在内存中以 Unicode 表示,一个字符对应若干个字节。

Python 对 bytes 类型的数据用带 b 前缀的单引号或双引号表示, 要注意区分 'ABC' 和 b'ABC',前者是 str,后者虽然内容显示得和前者一样,但bytes 的每个字符都只占用一个字节, 所能转换的字符是有限的。

  • 以 Unicode 表示的 str 通过 encode()方法可以编码为指定的 bytes
    • 纯英文的 str 可以用 ASCII 编码为 bytes,内容是一样的
    • 含有中文的 str 可以用 UTF- 8 编码为 bytes。
    • 含有中文的 str 无法用 ASCII 编码,因为中文编码的范围超过了 ASCII 编码的范围,Python 会报错。
    • 在 bytes 中,无法显示为 ASCII 字符的字节,用 \x## 显示
  • 反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是 bytes。要把 bytes 变为 str,就需要用 decode()方法
    • 如果 bytes 中包含无法解码的字节,decode()方法会报错
    • 如果 bytes 中只有一小部分无效的字节,可以传入 errors='ignore' 忽略错误的字节
  • 在操作字符串时,我们经常遇到 str 和 bytes 的互相转换。为了避免乱码问题,应当始终坚持使用 UTF- 8 编码对 str 和 bytes 进行转换。
  • Python 当然也支持其他编码方式,比如把 Unicode 编码成 GB2312. 但这种方式纯属自找麻烦,如果没有特殊业务要求,请牢记仅使用 UTF- 8 编码。
  • len()函数计算的是 str 的字符数,如果换成 bytes,len()函数就计算字节数
    • 1 个中文字符经过 UTF- 8 编码后通常会占用 3 个字节,而 1 个英文字符只占用 1 个字节。
>>> 'ABC'.encode('ascii')
b'ABC'

>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'

>>> '中文'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

>>> b'ABC'.decode('ascii')
'ABC'

>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

>>> b'\xe4\xb8\xad\xff'.decode('utf-8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte

>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'

>>> s= '中文'.encode('gb2312')
>>> s
b'\xd6\xd0\xce\xc4'

>>> s.decode('utf-8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte
>>> s.decode('gb2312')
'中文'

>>> len(b'ABC')
3

>>> len('中文')
2

>>> len('ABC')
3

>>> len('中文'.encode('utf-8'))
6

由于 Python 源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为 UTF- 8 编码。

申明了 UTF- 8 编码并不意味着你的。py 文件就是 UTF- 8 编码的,必须并且要确保文本编辑器正在使用 UTF-8 without BOM 编码

BOM 是 Byte Order Mark 的缩写,就是字节序标记。UTF- 8 不存在字节序的问题,UTF- 8 不需要 BOM 来表明字节顺序。

当 Python 解释器读取源代码时,为了让它按 UTF- 8 编码读取,我们通常在文件开头写上如下的行:

# -*- coding: utf-8 -*-

字符串格式化

  • c 语言风格的格式化 %
    • 有些时候,字符串里面的 % 是一个普通字符, 这个时候就需要转义,用 %% 来表示一个%
  • 使用字符串的 format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}, 占位符编号可以省略, 有格式定义时冒号不能省略, 如 .2f. 可以使用 c 语言风格定义输出格式
  • f-string: 变量名替代风格, 使用以 f 开头的字符串,称之为 f -string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换。 这里依然可以使用 c 语言风格定义输出格式

c 语言风格的格式化常见的占位符:

占位符 替换内容
%d 整数
%f 浮点数
%s 字符串
%x 十六进制整数
>>> 'growth rate: %.2f %%' % 7
'growth rate: 7.00 %'

>>> 'Hello, {}, 成绩提升了 {:.2f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.12%'

>>> 'Hello, {:}, 成绩提升了 {:.2f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.12%'

>>> 'Hello, {0:}, 成绩提升了 {1:.2f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.12%'

>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62

元组 turple

不可变的 tuple 有什么意义?因为 tuple 不可变,所以代码更安全。如果可能,能用 tuple 代替 list 就尽量用 tuple。

tuple 的陷阱:当你定义一个 tuple 时,在定义的时候,tuple 的元素就必须被确定下来。

空的元组和一个元素的元组的定义:

turple_empty = ()
turple_one_item = (1,)

字典 dict

字典的 get 方法存在的问题是, 对应的值本身有可能就是 None 或者指定的值
保险的做法是:

  • 用 in 判断是否存在 key
  • 如果存在, 再获取值: 用 get 方法或者 d[key]

教程中用 get 方法判断是否存在 key 是错误的。

# 定义字典
d = {}
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
d2 =dict()

# 判断 key 是否存在, 用 in
print('Bob' in d)  # True
print('Bob' in d.keys())   # True
print('bob' in d.keys())   # False

# 获取 key 对应的值: get()方法
# 如果存在, 返回的是 key 对应的 value
# 如果 key 不存在,可以返回 None,或者自己指定的 value
print(d.get('Bob'))     # 75
print(d.get('Bob',-1))  # 75
print(d.get('bob'))     # None
print(d.get('bob',-1))  # -1

# 删除 key 和对应的值
# 方法 1: d.pop(key): key 如果不存在会报错
# 方法 2: del d[key]

try:
    d.pop('Bob')
    d.pop('Bob')
except KeyError as e:
    print('KeyError')
    print(e)   # 仅打印错误提示信息, 不会打印错误类型

print(d) # {'Michael': 95, 'Tracy': 85}

try:    
    del d['Michael']
    del d['Michael']
except KeyError as e:
    print('KeyError')
    print(e)   # 仅打印错误提示信息, 不会打印错误类型

print(d) # {'Tracy': 85}

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

  • 查找和插入的速度极快,不会随着 key 的增加而变慢。 貌似数据量很大时, 插入会变慢?
  • 需要占用大量的内存,内存浪费多。dict 是用空间来换取时间的一种方法

而 list 相反:

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

这是因为 dict 根据 key 来计算 value 的存储位置, 这个通过 key 计算位置的算法称为哈希算法(Hash)。

dict 的 key 必须是不可变对象。在 Python 中,字符串、整数等都是不可变的,因此,可以放心地作为 key。

集合

set 和 dict 的唯一区别仅在于没有存储对应的 value,但是,set 的原理和 dict 一样,所以,同样 不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证 set 内部“不会有重复元素”。

s = set([1, 2, 3])
print(s)

# 通过 add(key)方法可以添加元素到 set 中
s.add(100)
print(s)

# 通过 remove(key)方法可以删除元素
s.remove(100)
print(s)

'''
{1, 2, 3}
{1, 2, 3, 100}
{1, 2, 3}
'''

不可变对象

对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。

>>> s = 'abc'
>>> id(s)
1562739064432
>>> t = s.replace('a',"A")
>>> s
'abc'
>>> t
'Abc'
>>> id(s)
1562739064432
>>> id(t)
1562740560176

>>> i = 5
>>> id(i)
140713442805552
>>> i += 1
>>> i
6
>>> id(i)
140713442805584

>>> f = 5.0
>>> id(f)
1562740291920
>>> f += 1
>>> f
6.0
>>> id(f)
1562740292048

>>> aList = [1,2,3]
>>> id(aList)
1562740576832
>>> aList.append(5)
>>> aList
[1, 2, 3, 5]
>>> id(aList)
1562740576832

函数

1. 函数别名

i = -3
print(abs(i)) # 3

# 函数名其实就是指向一个函数对象的引用
#完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”f = abs
print(f(i)) # 3

2. 默认参数

  • Python 函数在定义的时候,默认参数的值就被计算出来了, 因为默认参数也是一个变量,它指向该参数对应的对象,每次调用该函数,如果改变了参数的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时最初的参数了。定义默认参数要牢记一点:默认参数必须指向不变对象!
  • 为什么要设计 str、None 这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
import traceback
# 数据类型检查可以用内置函数 isinstance()实现
def my_abs(x):
    if not isinstance(x, (int, float)):
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x

try:
    my_abs('a')
except Exception as e:
    traceback.print_exc() # 打印完整的错误信息

print(my_abs(-5.5)) # 5.5

def return_multi(a,b):
    return a,b # 实际上这是定义了一个元组类型的返回值

print(return_multi(2,3)) # (2, 3)

# 函数默认参数有个最大的坑

def func(aList=[]):
    aList.append('hello')
    return aList

a=func()
print(a)
a=func()
print(a)
a=func()
print(a)
a=func()
print(a)
'''['hello']
['hello', 'hello']
['hello', 'hello', 'hello']
['hello', 'hello', 'hello', 'hello']
'''a=func(['hello'])
print(a) # ['hello', 'hello']

# 默认参数修改为不可变参数
def func2(aList=None):
    if aList is None:
        aList = []

    aList.append('hello')
    return aList

a=func2()
print(a)
a=func2()
print(a)
a=func2()
print(a)
a=func2()
print(a)
'''['hello']
['hello']
['hello']
['hello']
'''

3. 可变参数, 关键字参数

  • 可变参数就是传入的参数个数是可变的,可以是 1 个、2 个到任意个,还可以是 0 个。
  • *args表示把 args 这个 list 的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
  • 可变参数允许你传入 0 个或任意个参数,这些 可变参数在函数调用时自动组装为一个 tuple
  • 关键字参数允许你传入 0 个或任意个含参数名的参数,这些 关键字参数在函数内部自动组装为一个 dict
  • 关键字参数可以扩展函数的功能。比如,在 person 函数里,我们保证能接收到 name 和 age 这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。
  • 可以解包 dict, 传入关键字参数
def func_sum(*args): # *args 是惯用法, 也可以用 * 其他名字 替代
    s = 0
    for item in args:
        s +=  item

    return s


print(func_sum())         # 0
print(func_sum(1,2,3))    # 6
print(func_sum(1,2,3,4))  # 10

def person(name, age, **kw):
    return ('name:', name, 'age:', age, 'other:', kw)

print(person('Tom',39)) 
# ('name:', 'Tom', 'age:', 39, 'other:', {})

print(person('Adam', 45, gender='M', job='Engineer'))
# ('name:', 'Adam', 'age:', 45, 'other:', {'gender': 'M', 'job': 'Engineer'})

# 可以解包 dict, 传入关键字参数
extra = {'city': 'Beijing', 'job': 'Engineer'}
print(person('Jack', 24, **extra))
# ('name:', 'Jack', 'age:', 24, 'other:', {'city': 'Beijing', 'job': 'Engineer'})
'''
**extra 表示把 extra 这个 dict 的所有 key-value 用关键字参数传入到
函数的 **kw 参数,kw 将获得一个 dict,注意 kw 获得的 dict 是 extra 的一份拷贝,对 kw 的改动不会影响到函数外的 extra
'''

4. 命名关键字参数

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

def person(name, age, *, city, job):
    print(name, age, city, job)
  • 和关键字参数 **kw 不同,命名关键字参数需要一个特殊分隔符 **后面的参数被视为命名关键字参数。
  • 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符 *
def person2(name, age, *, city, job):
    return (name, age, city, job)

print(person2('Jack', 24, city='Beijing', job='Engineer'))
# ('Jack', 24, 'Beijing', 'Engineer')

def person3(name, age, *args, city, job):
    return (name, age, args, city, job)

print(person3('Sandy', 55, city='Nanjing', job='Teacher'))
# ('Sandy', 55, (), 'Nanjing', 'Teacher')
print(person3('Sandy', 55, 'test1','test2',city='Nanjing', job='Teacher'))
# ('Sandy', 55, ('test1', 'test2'), 'Nanjing', 'Teacher')

5. 参数组合

在 Python 中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这 5 种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。所以,对于任意函数,都可以通过类似 func(*args, **kw) 的形式调用它,无论它的参数是如何定义的。

  • *args是可变参数,args 接收的是一个 tuple;
  • `**kw是关键字参数,kw 接收的是一个 dict。
  • 使用 *args 和 **kw 是 Python 的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
  • 命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
  • 定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

递归函数

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

  • 优点: 理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
  • 缺点: 太耗费内存; 容易溢出

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

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

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

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

解决方法 1: 可以用 Y-Combinator 构造解决: https://zhuanlan.zhihu.com/p/37060182

解决方法 2: 修改 python 的默认递归深度

import sys
# 计算阶乘
def fact(n):
    if n==1:
        return 1

    return n * fact(n - 1)

print(fact(10)) # 3628800
try:
    print(fact(1000))
except Exception as e:
    print(e)

# maximum recursion depth exceeded in comparison

print(sys.getrecursionlimit()) # 1000, 有些情况下默认是其他值, 如 3000
sys.setrecursionlimit(10**6)
print(fact(1000))
'''402387260077093773543702433923003985719374864210714632543799910429938512398629020592044208486969404800479988610197196058631666872994808558901323829669944590997424504087073759918823627727188732519779505950995276120874975462497043601418278094646496291056393887437886487337119181045825783647849977012476632889835955735432513185323958463075557409114262417474349347553428646576611667797396668820291207379143853719588249808126867838374559731746136085379534524221586593201928090878297308431392844403281231558611036976801357304216168747609675871348312025478589320767169132448426236131412508780208000261683151027341827977704784635868170164365024153691398281264810213092761244896359928705114964975419909342221566832572080821333186116811553615836546984046708975602900950537616475847728421889679646244945160765353408198901385442487984959953319101723355556602139450399736280750137837615307127761926849034352625200015888535147331611702103968175921510907788019393178114194545257223865541461062892187960223838971476088506276862967146674697562911234082439208160153780889893964518263243671616762179168909779911903754031274622289988005195444414282012187361745992642956581746628302955570299024324153181617210465832036786906117260158783520751516284225540265170483304226143974286933061690897968482590125458327168226458066526769958652682272807075781391858178889652208164348344825993266043367660176999612831860788386150279465955131156552036093988180612138558600301435694527224206344631797460594682573103790084024432438465657245014402821885252470935190620929023136493273497565513958720559654228749774011413346962715422845862377387538230483865688976461927383814900140767310446640259899490222221765904339901886018566526485061799702356193897017860040811889729918311021171229845901641921068884387121855646124960798722908519296819372388642614839657382291123125024186649353143970137428531926649875337218940694281434118520158014123344828015051399694290153483077644569099073152433278288269864602789864321139083506217095002597389863554277196742822248757586765752344220207573630569498825087968928162753848863396909959826280956121450994871701244516461260379029309120889086942028510640182154399457156805941872748998094254742173582401063677404595741785160829230135358081840096996372524230560855903700624271243416909004153690105933983835777939410970027753472000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'''

列表生成式

import os

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

# 使用两层循环,可以生成全排列
print([a + b for a in 'hello' for b in 'world!'])
# ['hw', 'ho', 'hr', 'hl', 'hd', 'h!', 'ew', 'eo', 'er', 'el', 'ed', 'e!', 'lw', 'lo', 'lr', 'll', 'ld', 'l!', 'lw', 'lo', 'lr', 'll', 'ld', 'l!', 'ow', 'oo', 'or', 'ol', 'od', 'o!']

# 列出当前目录下的所有文件和目录名
[d for d in os.listdir('.')] # os.listdir 可以列出文件和目录

# for 后的 if 不能带 else
print([x for x in range(10) if x%2==0]) # [0, 2, 4, 6, 8]

# if else 可以单独用
print([x if x%2==0 else -x for x in range(10)]) 
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]

生成器

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

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素。

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

  • 要创建一个 generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [] 改成(),就创建了一个 generator
    • 可以通过 next()函数获得 generator 的下一个返回值
    • generator 保存的是算法,每次调用 next(g),就计算出 g 的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出 StopIteration 的错误
  • 第二种方法: 如果一个函数定义中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个 generator
    • 最难理解的就是 generator 和函数的执行流程不一样。函数是顺序执行,遇到 return 语句或者最后一行函数语句就返回。而变成 generator 的函数,在每次调用 next()的时候执行,遇到 yield 语句返回,再次执行时从上次返回的 yield 语句处继续执行。
# 创建 L 和 g 的区别仅在于最外层的 [] 和()
# L 是一个 list,而 g 是一个 generator。L = [x for x in range(10)]
g = (x for x in range(10))

# 可以通过 next()函数获得 generator 的下一个返回值
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
'''
0
1
2
3
4
5
6
7
8
'''

for i in g:
    print(i)

# 9
# 之前已经循环过 0~8, 这里只打印 9 

# 斐波拉契数列(Fibonacci),# 除第一个和第二个数外,任意一个数都可由前两个数相加得到:# 1, 1, 2, 3, 5, 8, 13, 21, 34, ……
# 斐波拉契数列用列表生成式写不出来,# 但是,用函数把它打印出来却很容易

print('\n')

def fib(max):
    # 输出斐波那契数列的前 N 个数
    a,b,n = 0,1,0
    while n < max:
        n += 1
        print(n,b)
        a, b = b, a + b
    return 'done'

print(fib(10))
'''
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21
9 34
10 55
done
'''print('\n')

# fib 函数实际上是定义了斐波拉契数列的推算规则
# 可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似 generator

def fib2(max):
    # 输出斐波那契数列的前 N 个数
    a,b,n = 0,1,0
    while n < max:
        n += 1
        yield b
        a, b = b, a + b

for b in fib2(10):
    print(b)
'''
1
1
2
3
5
8
13
21
34
55
'''

杨辉三角

'''
杨辉三角定义如下:1
         / \
        1   1
       / \ / \
      1   2   1
     / \ / \ / \
    1   3   3   1
   / \ / \ / \ / \
  1   4   6   4   1
 / \ / \ / \ / \ / \
1   5   10  10  5   1
把每一行看做一个 list,试写一个 generator,不断输出下一行的 list
# 期待输出:
# [1]
# [1, 1]
# [1, 2, 1]
# [1, 3, 3, 1]
# [1, 4, 6, 4, 1]
# [1, 5, 10, 10, 5, 1]
# [1, 6, 15, 20, 15, 6, 1]
# [1, 7, 21, 35, 35, 21, 7, 1]
# [1, 8, 28, 56, 70, 56, 28, 8, 1]
# [1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
'''

def yhsj(num_line):
    # num_line >=1
    theList = [1]
    temp_num_line = 1
    while temp_num_line<=num_line:
        yield theList
        temp_num_line += 1
        theList.append(1) # 长度加 1, 最后一个值为 1

        # 从倒数第 2 个开始更新数值
        if len(theList)>2:
            for i in range(-2,-len(theList),-1):
                theList[i] = theList[i] + theList[i-1]

for sj in yhsj(10):
    print(sj)
'''
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
'''

g = yhsj(10)
print(g) # <generator object yhsj at 0x000001D7183F6AC0>

区分普通函数和 generator 函数:

  • 普通函数调用直接返回结果
  • generator 函数的“调用”实际返回一个 generator 对象: <generator object yhsj at 0x000001D7183F6AC0>

可迭代对象, 迭代器

我们已经知道,可以直接作用于 for 循环的数据类型有以下几种:

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

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

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

from collections.abc import Iterable

print(isinstance([], Iterable))#True
print(isinstance({}, Iterable))#True
print(isinstance('abc', Iterable))#True
print(isinstance((x for x in range(10)), Iterable))#True
print(isinstance(100, Iterable))# False
  • 生成器不但可以作用于 for 循环,还可以被 next()函数不断调用并返回下一个值,直到最后抛出 StopIteration 错误表示无法继续返回下一个值了。
  • 可以被 next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
  • 可以使用 isinstance()判断一个对象是否是 Iterator 对象:
  • 为什么 list、dict、str 等数据类型不是 Iterator?这是因为 Python 的 Iterator 对象表示的是一个数据流,Iterator 对象可以被 next()函数调用并不断返回下一个数据,直到没有数据时抛出 StopIteration 错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next()函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。Iterator 甚至可以表示一个无限大的数据流,例如全体自然数。而使用 list 是永远不可能存储全体自然数的。
  • 凡是可作用于 for 循环的对象都是 Iterable 类型;
  • 凡是可作用于 next()函数的对象都是 Iterator 类型,它们表示一个惰性计算的序列;
  • 集合数据类型如 list、dict、str 等是 Iterable 但不是 Iterator,不过可以通过 iter()函数获得一个 Iterator 对象。
from collections.abc import Iterator
print(isinstance((x for x in range(10)), Iterator))
# True
print(isinstance([], Iterator))
# False
print(isinstance({}, Iterator))
# False
print(isinstance('abc', Iterator))
# False

# 生成器都是 Iterator 对象,但 list、dict、str 虽然是 Iterable,却不是 Iterator
# 把 list、dict、str 等 Iterable 变成 Iterator 可以使用 iter()函数
print(isinstance(iter([]), Iterator))
# True
print(isinstance(iter({}), Iterator))
# True
print(isinstance(iter('abc'), Iterator))
# True

# Python 的 for 循环本质上就是通过不断调用 next()函数实现的

for x in [1, 2, 3, 4, 5]:
    print(x)
'''
1
2
3
4
5
'''

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

'''
1
2
3
4
5
'''

迭代器一般并不会改变执行结果, 只是在内存空间上做了优化。

评论(没有评论)