10553 字

《廖雪峰 Python 教程》笔记 1:基础语法、函数

Python; Python;

原文地址http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000

Python 简介

Python解释器

  • CPython: 官方版本的解释器。这个解释器是用 C 语言开发的,所以叫 CPython。在命令行下运行 Python 就是启动 CPython 解释器。
  • IPython: CPython 之上的一个交互式解释器,也就是说,IPython 只是在交互方式上有所增强,但是执行 Python 代码的功能和 CPython 是完全一样的。好比很多国产浏览器虽然外观不同,但内核其实都是调用了 IE。
  • CPython>>> 作为提示符,而 IPython 用 In [序号]: 作为提示符。
  • Jython: 运行在 Java 平台上的 Python 解释器,可以直接把 Python 代码编译成 Java 字节码执行。
  • IronPython: 和 Jython 类似,只不过 IronPython 是运行在微软 .Net 平台上的 Python 解释器,可以直接把 Python 代码编译成 .Net 的字节码。
  • Jupyter Notebook: 此前被称为IPython notebook,是一个交互式笔记本,支持运行 40 多种编程语言。本质是一个 Web 应用程序,便于创建和共享文学化程序文档,支持实时代码,数学方程,可视化和 markdown。 用途包括数据清理和转换、数值模拟、统计建模、机器学习等等。

第一个 Python 程序

exit()退出 Python。

python hello.py执行程序。

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

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

然后,通过命令给hello.py以执行权限:

$ chmod a+x hello.py

Python代码运行助手

print()会依次打印每个字符串,遇到逗号,会输出一个空格

name = input()
name = input('please enter your name: ')
print('hello,', name)

input()返回的数据类型是strstr不能直接和整数比较,必须先把str转换成整数。Python 提供了int()函数来完成这件事情。

Python 基础语法

预备知识

  • 注释:用#开头,行内最后的注释也有用//的;
  • Python 大小写敏感;
  • 代码段由缩进控制,因此复制可能导致意外发生,为了尽量减少意外,可能需要少用多重缩进,并且习惯上用将 1 个tab对应为 4 个空格;
  • 注意ifelse后面的:,这表明接下来是一个block
  • 转义\用于字符的转义,而%%则用于占位符情形下%的转义;r''用于简化转义;如果字符串内部有很多换行,用\n写在一行里不好阅读,因此可以用'''...'''定义多行文本,类似的,用r'''...'''定义不必转义的多行文本,关于字符串中的'",如果只有一种,则用另一种引括即可,如果两种都有,就需要用\'\"进行转义;
  • 占位符%d对应整数,%f对应浮点数,%s对应字符串,%x对应十六进制数,其中%s是万金油,而%f还可以指定显示格式,如%.1f对应只显示 1 位小数的浮点数;

基础语法

  • 常见运算符与常量+, -, *, **, /, //, True, False, and, or, not, &, |, !=, >=, <=, None, PI(常量经常用大写), /, //(地板除), %(余数)
  • Python 的整数没有大小限制,而某些语言的整数根据其存储长度是有大小限制的,例如 Java 对 32 位整数的范围限制在-2147483648-2147483647
  • Python 的浮点数也没有大小限制,但是超出一定范围就直接表示为inf(无限大)。

数据类型

  • 基础的类型有字符串整数浮点数布尔值
  • 复杂的类型有list, tuple,其中的list类似于 Matlab 中的cell类型,用[]进行定义,tuple中的内容一旦定义,不能再修改,用()定义1
  • 其它还有dict, set,其中dict对应key-value对,如d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}所示,用{:,}的方式定义,包含get()pop()方法,如'Thomas' in d所示,还可以用in这一操作;setdict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key,用set([,])方式定义,可以使用add()remove()方法,参考下面的例子:
>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}

字符串

  • 对于字符串而言,在内存中统一为 Unicode,但是具体到终端时,可能是各种类型的编码;
  • 对于单个字符的编码,Python 提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符;
  • 如果知道字符的整数编码,还可以用十六进制这么写 str:'\u4e2d\u6587',这其实对应于字符串中文
  • 由于 Python 的字符串类型是str,在内存中以 Unicode 表示,一个字符对应若干个字节(英文字母是 1 个字节,汉字是 3 个字节)。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes。Python 对bytes类型的数据用带b前缀的单引号或双引号表示:x = b'ABC',要注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节2

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

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

纯英文的str可以用 ASCII 编码为bytes,内容是一样的,含有中文的str可以用 UTF-8 编码为bytes。含有中文的str无法用 ASCII 编码,因为中文编码的范围超过了 ASCII 编码的范围,Python 会报错。

bytes中,无法显示为 ASCII 字符的字节,用\x##显示。

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

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

要计算 str 包含多少个字符,可以用len()函数:

>>> len('ABC')
3
>>> len('中文')
2

len()函数计算的是str字符数,如果换成byteslen()函数就计算字节数:

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

可见,1 个中文字符经过 UTF-8 编码后通常会占用 3 个字节,而 1 个英文字符只占用 1 个字节。

在操作字符串时,我们经常遇到strbytes的互相转换。为了避免乱码问题,应当始终坚持使用 UTF-8 编码对strbytes进行转换。

List 和 tuple

  • list:是一种有序的集合,可以随时添加[append, insert(i, value)]删除[pop, pop(i)]其中的元素,list中的元素从0号开始,其中-1表示最后一个元素,索引越界时,会抛出IndexError错误;list可以作为另一个list的元素,此时的访问方法是myList[i][j],初始的赋值用classmates = ['Michael', 'Bob', 'Tracy']完成,注意其中用的是[],之后操纵元素时用的是函数的()
  • tuple:和list非常类似,但是tuple一旦初始化就不能修改,初始的赋值用classmates = ('Michael', 'Bob', 'Tracy')完成,注意其中用的是(),并且之后元素的不可改变3tuple所谓的不变是说,tuple的每个元素,指向永远不变。即指向'a',就不能改成指向'b',指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!

dict 和 list

注意dict内部存放的顺序和key放入的顺序没有关系。和list比较,dict有以下几个特点:

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

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

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

循环、判断

if 语句

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

循环语句:for 和 while

  • for...in循环,依次把listtuple中的每个元素迭代出来
sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
    sum = sum + x
print(sum)

如果要计算 1-100 的整数之和,从 1 写到 100 有点困难,幸好 Python 提供一个range()函数,可以生成一个整数序列,再通过list()函数可以转换为list。比如range(5)生成的序列是从 0 开始小于 5 的整数:

>>> list(range(5))
[0, 1, 2, 3, 4]

range(101)就可以生成 0-100 的整数序列,计算如下:

sum = 0
for x in range(101):
    sum = sum + x
print(sum)

从上面的例子看业,range()生成的序列,即使不转换成list,也可以循环操作。

  • while循环,只要条件满足,就不断循环,条件不满足时退出循环。比如我们要计算 100 以内所有奇数之和,可以用 while 循环实现:
sum = 0
n = 99
while n > 0:
    sum = sum + n
    n = n - 2
print(sum)

可以用break以及continue跳出循环,有些时候,如果代码写得有问题,会让程序陷入死循环,也就是永远循环下去。这时可以用Ctrl+C退出程序,或者强制结束 Python 进程。

函数

内置函数列表:https://docs.python.org/3/library/functions.html

  • 类型转换int(), float(), str(), bool(), hex(), enumerate()4, iter()5, list()6
  • 类型检查isinstance(x, str)
  • 是否包含关键字in

定义函数

在 Python 中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。如果没有return语句,函数执行完毕后也会返回结果,只是结果为Nonereturn None可以简写为return

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

如果已经把my_abs()的函数定义保存为abstest.py文件了,那么,可以在该文件的当前目录下启动 Python 解释器,用from abstest import my_abs来导入my_abs()函数,注意abstest是文件名(不含.py扩展名)。

更稳健的函数

  • 空函数:如果想定义一个什么事也不做的空函数,可以用pass语句。
def nop():
    pass
  • 参数检查:调用函数时,如果参数个数不对,Python 解释器会自动检查出来,并抛出TypeError,但是如果参数类型不对,Python 解释器就无法帮我们检查。数据类型检查可以用内置函数isinstance()实现。
def my_abs(x):
    if not isinstance(x, (int, float)):
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x
  • 返回多个值:返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python 的函数返回多值其实就是返回一个tuple,但写起来更方便。
import math

def move(x, y, step, angle=0):
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny

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

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

函数的参数

Python 的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数可变参数关键字参数命名关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。下面是关于函数参数的一些重要规则的总结:

  • 默认参数必须指向不变对象;
  • 可变参数内部是()-tuple,关键字参数内部是{}-dict
  • 可变参数在语法上是*,关键字参数是**,命名关键字参数是单独的 *,如果有可变参数,则命名关键字参数不再需要单独的使用特殊分隔符*
  • 可变参数本身调用时,参数可以是list-()tuple-[]
  • 对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的,但是这种做法容易引起混淆,不推荐;
  • 参数定义的顺序必须是:必选参数默认参数可变参数命名关键字参数关键字参数

默认参数(必须指向不变对象)

def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s

必选参数在前,默认参数在后,否则 Python 的解释器会报错;当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 7),意思是,除了namegender这两个参数外,最后 1 个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll('Adam', 'M', city='Tianjin'),意思是,city参数用传进去的值,其他默认参数继续使用默认值。

警告:默认参数很有用,但使用不当,会有很大的麻烦。

下面的例子先定义一个函数,传入一个list,添加一个'END'再返回:

def add_end(L=[]):
    L.append('END')
    return L

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

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

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

>>> add_end()
['END']

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

>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

默认参数是[],但是函数似乎每次都记住了上次添加了'END'后的list

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

提醒:默认参数必须指向不变对象!

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

def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L

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

>>> add_end()
['END']
>>> add_end()
['END']

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

可变参数(函数内部为 tuple,定义和引用都可以用 *)

要定义出这种函数,必须确定输入的参数。由于参数个数不确定,首先想到可以把abc……作为一个listtuple传进来,但是调用的时候,需要先组装出一个listtuple,如果利用可变参数,调用函数的方式可以简化。

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

定义可变参数和定义一个listtuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括 0 个参数。如果已经有一个list或者tuple,可以将其中的元素一个一个传递给可变参数函数,但是太繁琐,所以 Python 允许在listtuple前面加一个*号,把listtuple的元素变成可变参数传进去,这种写法相当有用,而且很常见。

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

关键字参数(函数内部为 dict,定义和引用都可以用 **)

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

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

函数person除了必选参数nameage外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:

>>> person('Michael', 30)
name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

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

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

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

当然,上面复杂的调用可以用简化的写法7

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dictextra的一份拷贝,对kw的改动不会影响到函数外的extra

命名关键字参数(限制参数名称,单独的 *)

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

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

// 这种做法更灵活
def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)

但是调用者仍可以传入不受限制的关键字参数:

>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

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

// 这种做法更严谨
def person(name, age, *, city, job):
    print(name, age, city, job)

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

调用方式如下:

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

提示:如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

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

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

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given

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

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

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

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

>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

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

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

### 不同类型参数的优先级

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

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

```python
def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

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

>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
    // 这里是直接一个一个参数的传递

>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
    // c 用默认值,d 对应可变参数, ext 对应关键字参数

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

>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
    // 直接用 list 或 tuple 作为参数
    // 注意 args = (4,) 后面有一个逗号,(4) 对应一个数 4,而 (4, ) 才对应只有一个元素 4 的 tuple

>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
    // 这里的 args 和 kw 不是直接对应 * 和 **
    // 而是要先满足必选参数的 1,2,3,之后再用 kw 的第一项满足可变参数
    // 最后用 kw 的第二项满足关键字参数

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

递归

教程上说用尾递归的方法可以避免栈溢出,但 Python 也没有提供针对尾递归的优化,考虑到平时用的很少,并且尾递归代码的写法比较麻烦9,所以干脆不要使用。

高级特性

切片

>>> L[0:3]
>>> L[:3]            // 取前 3 个元素

>>> L[1:3]           // 取 1, 2 号元素,0 号不取
>>> L[:10:2]         // 前 10 个元素,每两个取 1 个
>>> L[::5]           // 所有数,每5个取一个
>>> [:]              // 复制

>>> L[-1]            // 取倒数第一个元素
>>> L[-2:-1]         // 取最后两个元素
>>> L[-10:]          // 取最后 10 个元素

>>> 'ABCDEFG'[:3]    // 字符串也可以视作一个 list
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

迭代

  • 通常用for ... in完成迭代;
  • 默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代keyvalue,可以用for k, v in d.items()
  • 字符串也是可迭代对象,因此,也可以作用于for循环;
  • 通过collections模块的Iterable类型判断一个对象是可迭代对象;
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
  • 可以同时引用了两个变量;
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
...     print(x, y)
...
1 1
2 4
3 9

要对list实现类似 Java 那样的下标循环,Python 内置的enumerate()函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身;

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

list 生成器

静态 list

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

>>> [x * x for x in range(1, 11) if x % 2 == 0]         // 用 if 后,只输出偶数的平方
[4, 16, 36, 64, 100]

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']  // 使用两层循环,可以生成全排列

>>> import os                                           // 导入 os 模块,模块的概念后面讲到
>>> [d for d in os.listdir('.')]                        // os.listdir 可以列出文件和目录
['.emacs.d', '.ssh', '.Trash', 'Desktop', 'Documents', 'Downloads', 'Library', 'VirtualBox VMs']

>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }                 // 使用两个变量来生成 list
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']

>>> L = ['Hello', 'World', 'IBM', 'Apple']              // 把一个 list 中所有的字符串变成小写
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']

>>> L = ['Hello', 'World', 18, 'Apple', None]           // 非字符串类型没有 lower() 方法,列表生成式会报错
>>> [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'

>>> x = 'abc'                                           // 用内建 isinstance 函数可判断变量是否为字符串
>>> y = 123
>>> isinstance(x, str)
True
>>> isinstance(y, str)
False

动态 list:generator

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

所以,如果list中的元素可以按照某种算法推算出来,就不必创建完整的list,从而节省大量的空间。在 Python 中,这种一边循环一边计算的机制,称为生成器:generator

方法1(()):要创建一个generator,把一个列表生成式的[]改成(),就创建了一个generator

>>> L = [x * x for x in range(10)]          // list
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))          // generator
>>> g                                       // 元素无法直接输出
<generator object <genexpr> at 0x1022ef630>

>>> next(g)                                 // 使用 next() 才能依次输出元素
0
>>> next(g)
1

不断调用next(g)并不可行,正确的方法是使用for循环,因为generator也是可迭代对象。

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1
4

方法2(yield):如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,可以用函数来实现。

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就可以赋值,另外这里t只是临时的,所以应该不涉及 tuple 不能修改的问题。可以看出,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'

简单来讲,如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator

警告:用for循环调用generator时,得不到generatorreturn语句的返回值。如果想要得到返回值,必须捕获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

迭代器

  • Iterable:可以直接用for循环的对象统称为可迭代对象listtupledictsetstr等都可以用for遍历;
  • Iterator:可以被next()函数调用并不断返回下一个值的对象称为迭代器,方法 1 和方法 2 中用generator生成的都可以用next()操作;
  • 可以使用isinstance()判断一个对象是否是Iterable对象或者Iterator对象。
  • listdictstr虽然Iterable,却不是Iterator,把listdictstrIterable变成Iterator可以使用iter()函数:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

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


  1. tuple的定义在语法上要用myTuple(1,)这样的方式,否则用myTuple(1)则认为是 一个元素为1的一维tuple,具体可参考教程说明。
  2. 这地方稍有歧义,汉字是 3 个字节,但对应于一个字符,因此这种解释只适用于 ASCII 范围内的符号。
  3. 虽然tuple指向的元素不可改变,但如果其中一个元素是可以改变的list,那么可以通过改变list中元素的内容来达到修改tuple内容的目的,具体参考使用listtuple
  4. 把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身。
  5. iterable转换成iterator
  6. 将一个惰性的iterator转换成一个确定的iterable型的list
  7. 虽然将一个dict作为参数传递给函数也是可行的,但这样做在函数内部就需要再将dict一个一个单独进行额外处理,而**的方法可以省略这个步骤。
  8. 这种做法容易引起混淆,个人不太推荐
  9. 尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。