Python语法

转载自:https://blog.csdn.net/qq_33530388/article/details/66476130

运行源代码 正常输出中文

由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
  • 1
  • 2

第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;

第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。

申明了UTF-8编码并不意味着你的.py文件就是UTF-8编码的,必须并且要确保文本编辑器正在使用UTF-8 without BOM编码 
如果.py文件本身使用UTF-8编码,并且也申明了# -*- coding: utf-8 -*-,打开命令提示符测试就可以正常显示中文

基础

Python的语法采用缩进方式,使用4个空格的缩进。 
Python程序是大小写敏感的

直接运行py文件

在Windows上是不能像.exe文件那样直接运行.py文件的,但是,在Mac和Linux上是可以的,方法是在.py文件的第一行加上一个特殊的注释:

#!/usr/bin/env python3
print('hello, world')
  • 1
  • 2

然后,通过命令给hello.py以执行权限: 
$ chmod a+x hello.py 
就可以直接运行hello.py了

输出

输出格式print()也可以接受多个字符串,用逗号“,”隔开,就可以连成一串输出,遇到逗号“,”会输出一个空格

字符串的格式化输出

输出格式化的 类似’亲爱的xxx你好!’之类的字符串,而xxx的内容都是根据变量变化的,%运算符就是用来格式化字符串的

>>> 'Hello, %s' % 'world'
'Hello, world'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'
  • 1
  • 2
  • 3
  • 4

写入

写入格式input(),将输入数据赋予一个变量,变量=input() 
input()返回的数据类型是str,str不能直接和整数比较,必须先把str转换成整数。

注释

以#开头的语句是注释

代码块

语句以冒号 : 结尾时,缩进的语句视为代码块。

转义字符及简化

如果’本身也是一个字符,那就可以用”“括起来 
如果字符串内部既包含’又包含”怎么办?可以用转义字符\来标识,比如: 
'I\'m \"OK\"!' 
转义字符\可以转义很多字符,比如\n表示换行,\t表示制表符,字符\本身也要转义,所以\表示的字符就是\ 
如果字符串里面有很多字符都需要转义,就需要加很多\,为了简化,Python还允许用r”表示”内部的字符串默认不转义,可以自己试试:

>>> print('\\\t\\')
\       \
>>> print(r'\\\t\\')
\\\t\\
  • 1
  • 2
  • 3
  • 4

如果字符串内部有很多换行,用\n写在一行里不好阅读,为了简化,Python允许用”’…”’的格式表示多行内容

>>> print('''line1
... line2
... line3''')
line1
line2
line3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

数据类型

空值

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

字符串 (编码及 传输)

在最新的Python 3版本中,字符串是以Unicode编码的。 
对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符 
由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes。

Python 对bytes类型的数据用带b前缀的单引号或双引号表示

x = b'ABC' 
要注意区分’ABC’和 b’ABC’,前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

以Unicode表示的str通过encode()方法可以编码为指定的bytes,例如:

>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
  • 1
  • 2
  • 3
  • 4

反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:

>>> b'ABC'.decode('ascii')
'ABC'
  • 1
  • 2

有序集合

列表:list

list是一种有序的集合,可以随时添加和删除其中的元素。

>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']
  • 1
  • 2
  • 3

索引来访问list中每一个位置的元素,记得索引是从0开始的: 
如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素 
list是一个可变的有序表,所以,可以往list中追加元素到末尾list变量.append('Adam') 
可以把元素插入到指定的位置,比如索引号为1的位置,list变量.insert(1, 'Jack') 
删除指定位置的元素,用pop(i)方法,其中i是索引位置: 
list里面的元素的数据类型也可以不同,比如:

>>> L = ['Apple', 123, True]
  • 1

list元素也可以是另一个list,比如:

>>> s = ['python', 'java', ['asp', 'php'], 'scheme']
>>> len(s)
4
  • 1
  • 2
  • 3

要注意s只有4个元素,其中s[2]又是一个list,如果拆开写就更容易理解了:

>>> p = ['asp', 'php']
>>> s = ['python', 'java', p, 'scheme']
  • 1
  • 2

要拿到’php’可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。

元组 tuple

另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字: 
>>> classmates = ('Michael', 'Bob', 'Tracy') 
现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。

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

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

>>> t = (1, 2)
>>> t
(1, 2)
  • 1
  • 2
  • 3

如果要定义一个空的tuple,可以写成():

>>> t = ()
>>> t
()
  • 1
  • 2
  • 3

但是,要定义一个只有1个元素的tuple,如果你这么定义:

>>> t = (1)
>>> t
1
  • 1
  • 2
  • 3

定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。

所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:

>>> t = (1,)
>>> t
(1,)
  • 1
  • 2
  • 3

Python在显示只有1个元素的tuple时,也会加一个逗号,,以免你误解成数学计算意义上的括号。

最后来看一个“可变的”tuple:

>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])
  • 1
  • 2
  • 3
  • 4
  • 5

表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。但指向的这个list本身是可变的

无序集合

字典 dict

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。

>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95
  • 1
  • 2
  • 3

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:>>> d['Adam'] = 67 
要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:

>>> 'Thomas' in d 
二是通过dict提供的get方法,如果key不存在,可以返回None,或者自己指定的value:

>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1
  • 1
  • 2
  • 3

注意,dict内部存放的顺序和key放入的顺序是没有关系的。 
dict的key必须是不可变对象。这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。

set

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。 
要创建一个set,需要提供一个list作为输入集合:

>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}
  • 1
  • 2
  • 3

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。

进阶

变量的弱类型、赋值 及常量

可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的数据

这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错

把一个变量a赋值给另一个变量b,这个操作实际上是把变量b指向变量a所指向的数据

常量通常用全部大写的变量名表示

不可变对象

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

变量的数据类型检查

数据类型检查 可以用内置函数isinstance()实现:

def my_abs(x):
    if not isinstance(x, (int, float)):  #可以理解为x变量不是int/float类型的实例的话,进入if
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:

>>> my_abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in my_abs
TypeError: bad operand type
  • 1
  • 2
  • 3
  • 4
  • 5

条件判断

elif是else if的缩写,完全可以有多个elif,所以if语句的完整形式就是:

if <条件判断1>:
    <执行1>
elif <条件判断2>:
    <执行2>
elif <条件判断3>:
    <执行3>
else:
    <执行4>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

记得加冒号:

两种循环

一种是for...in循环,依次把list或tuple中的每个元素迭代出来

names = ['Michael', 'Bob', 'Tracy']
for name in names:
    print(name)
  • 1
  • 2
  • 3

第二种循环是while循环

sum = 0
n = 99
while n > 0:
    sum = sum + n
    n = n - 2
print(sum)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数

定义

定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。

我们以自定义一个求绝对值的my_abs函数为例:

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x
  • 1
  • 2
  • 3
  • 4
  • 5

空函数

定义一个什么也不做的空函数,可以用pass语句

def nop():
    pass
  • 1
  • 2

pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。

pass还可以用在其他语句里,比如:if语句 
缺少了pass,代码运行就会有语法错误。

函数的参数

位置参数

函数有多个参数时,这几个参数都是位置参数,调用函数时,传入的几个值按照位置顺序依次赋给参数。

默认参数

就是给 形参 赋一个默认值

def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

设置默认参数时,有几点要注意:

一是必选参数在前,默认参数在后

二是如何设置默认参数。

当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 7),意思是,除了name,gender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。

可以不按顺序 提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll('Adam', 'M', city='Tianjin'),意思是,city参数用传进去的值,其他默认参数继续使用默认值。

默认参数的 陷阱

如果你用 默认参数指向可变对象的话,可能结果与你预期不太一样,演示如下:

先定义一个函数,传入一个list,添加一个END再返回:

def add_end(L=[]):
    L.append('END')
    return L
  • 1
  • 2
  • 3

当你正常调用时,结果似乎不错:

>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
  • 1
  • 2
  • 3
  • 4

当你使用默认参数调用时,一开始结果也是对的:

>>> add_end()
['END']
  • 1
  • 2

但是,再次调用add_end()时,结果就不对了:

>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
  • 1
  • 2
  • 3
  • 4

很多人很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了’END’后的list。

原因解释如下:

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],(参数L的指向不会改变,而作为默认值的对象就一个,在函数定义的时候得到)每次调用该函数,如果改变了L的内容(L指向对象的内容),则下次调用时,默认参数的内容就变了(因为L还是指向那个对象),不再是函数定义时的[]了。

所以,定义默认参数要牢记一点:默认参数必须指向不变对象!

要修改上面的例子,我们可以用None这个不变对象来实现:

def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L
  • 1
  • 2
  • 3
  • 4
  • 5

现在,无论调用多少次,都不会有问题:

>>> add_end()
['END']
>>> add_end()
['END']
  • 1
  • 2
  • 3
  • 4

为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数

可变参数就是传入的参数个数是可变的 
我们以数学题为例子,给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:

def calc(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum
  • 1
  • 2
  • 3
  • 4
  • 5

但是调用的时候,需要先组装出一个list或tuple:

>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84
  • 1
  • 2
  • 3
  • 4

如果利用可变参数,调用函数的方式可以简化成这样:

>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
  • 1
  • 2
  • 3
  • 4

所以,我们把函数的参数改为可变参数:

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum
  • 1
  • 2
  • 3
  • 4
  • 5

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:

>>> calc(1, 2)
5
>>> calc()
0
  • 1
  • 2
  • 3
  • 4

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:

>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14
  • 1
  • 2
  • 3

这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

>>> nums = [1, 2, 3]
>>> calc(*nums)
14
  • 1
  • 2
  • 3

*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键字参数

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。

命名关键字参数

如果要限制关键字参数的名字,就可以用命名关键字参数 
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

参数组合

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

返回值(返回多个值)

函数可以返回多个值吗?答案是肯定的。

比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的新的坐标:

import math

def move(x, y, step, angle=0):
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

import math语句表示导入math包,并允许后续代码引用math包里的sin、cos等函数。

然后,我们就可以同时获得返回值

>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0
  • 1
  • 2
  • 3

其实这只是一种假象,Python函数返回的仍然是单一值

>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)
  • 1
  • 2
  • 3

原来返回值是一个tuple!但是,在语法上,返回一个tuple可以 省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

高级

切片

切片用于取一个list或tuple的部分元素。比如,一个list如下:

>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
  • 1
  • 2

取前N个元素,也就是索引为0-(N-1)的元素,可以用循环:

>>> r = []
>>> n = 3
>>> for i in range(n):
...     r.append(L[i])
... 
>>> r
['Michael', 'Sarah', 'Tracy']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

对这种经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。

对应上面的问题,取前3个元素,用一行代码就可以完成切片:

>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
  • 1
  • 2
  • 3

L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3。即索引0,1,2,正好是3个元素。

如果第一个索引是0,还可以省略:

>>> L[:3]
['Michael', 'Sarah', 'Tracy']
  • 1
  • 2
  • 3

也可以从索引1开始,取出2个元素出来:

>>> L[1:3]
['Sarah', 'Tracy']
  • 1
  • 2
  • 3

类似的,既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片,试试:

>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']
  • 1
  • 2
  • 3
  • 4
  • 5

记住倒数第一个元素的索引是-1。 
前10个数,每两个取一个:

>>> L[:10:2]
[0, 2, 4, 6, 8]
  • 1
  • 2
  • 3

所有数,每5个取一个:

>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
  • 1
  • 2
  • 3

甚至什么都不写,只写[:]就可以原样复制一个list:

>>> L[:]
[0, 1, 2, 3, ..., 99]
  • 1
  • 2
  • 3

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:

>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
  • 1
  • 2
  • 3

字符串’xxx’也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:

>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
  • 1
  • 2
  • 3
  • 4
  • 5

迭代

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

在Python中,迭代是通过for … in来完成的,而很多语言比如C或者Java,迭代list是通过下标完成的,比如Java代码:

for (i=0; i<list.length; i++) {
    n = list[i];
}
  • 1
  • 2
  • 3
  • 4

Python的for循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上

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

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
...     print(key)
...
a
c
b
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

因为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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

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

最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

列表生成式

列表生成式是Python内置的用来创建list的生成式。

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)):

>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • 1
  • 2
  • 3

但如果要生成[1x1, 2x2, 3x3, …, 10x10]怎么做?方法一是循环:

>>> L = []
>>> for x in range(1, 11):
...    L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

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

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

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

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

还可以使用两层循环,可以生成全排列:

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

for循环其实可以同时使用两个甚至多个变量,比如dict的items()可以同时迭代key和value:

>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
...     print(k, '=', v)
...
y = B
x = A
z = C
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

因此,列表生成式也可以使用两个变量来生成list:

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

如果list中既包含字符串,又包含整数,由于非字符串类型没有lower()方法,所以列表生成式会报错:

>>> L = ['Hello', 'World', 18, 'Apple', None]
>>> [s.lower() for s in L]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
AttributeError: 'int' object has no attribute 'lower'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

使用内建的isinstance函数可以判断一个变量是不是字符串:

>>> x = 'abc'
>>> y = 123
>>> isinstance(x, str)
True
>>> isinstance(y, str)
False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

生成器

列表容量是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

我们可以直接打印出list的每一个元素,但我们怎么打印出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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1
4
```
64
81
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

所以,我们创建了一个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'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意,赋值语句:

a, b = b, a + b 
相当于:

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

但不必显式写出临时变量t就可以赋值。

上面的函数可以输出斐波那契数列的前N个数:

>>> fib(6)
1
1
2
3
5
8
'done'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

仔细观察,可以看出,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'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
  • 1
  • 2
  • 3
  • 4

这里,最难理解的就是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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

调用该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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以看到,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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

>>> 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

迭代器

可以直接作用于for循环的数据类型有以下几种:

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

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

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

>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

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

>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

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

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

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

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

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

实际上完全等价于:

# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值