python教程 -- 廖雪峰

Python简介

  • python语言缺点:①运行速度慢,和C程序相比非常慢,因为Python是解释型语言,你的代码在执行时会一行一行地翻译成CPU能理解的机器码,这个翻译过程非常耗时,所以很慢。而C程序是运行前直接编译成CPU能执行的机器码,所以非常快。②代码不能加密。如果要发布你的Python程序,实际上就是发布源代码,这一点跟C语言不同,C语言不用发布源代码,只需要把编译后的机器码(也就是你在Windows上常见的xxx.exe文件)发布出去。要从机器码反推出C代码是不可能的,所以,凡是编译型的语言,都没有这个问题,而解释型的语言,则必须把源码发布出去。
  • 在Python交互模式下,可以输入代码,然后执行,并立刻得到结果。在命令行模式下,可以直接运行.py文件。
    在这里插入图片描述
    在这里插入图片描述
  • input() – 可以让用户输入字符串,并存放到一个变量里
    输入/输出 = Input/Output 简写为 I/O
name = input('please enter your name:')

Python基础

数据类型and编码

  • python语言是区分大小写的
  • python的除法分两种:① / ② //

/:除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数; // 还有一种除法是//,称为地板除,两个整数的除法仍然是整数: 所以要做精确的除法使用 / 就可以了,还可以使用求取余数的操作,使用 % 就行。

字符集的演变进程:
在这里插入图片描述
在这里插入图片描述

  • 关于字符集的操作:① ord() 函数获取字符的整数表示;② chr() 函数把编码转换为对应的字符;③ 如果知道字符的整数编码,还可以使用十六进制来书写str;
>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
  • 在python当中,格式化方式和C语言其实是一致的,都是用 % 来实现
>>> 'Hello, %s' %'World'
'Hello, World'
>>> 'Hi, %s, you have %d' %('HK', 10000)
'Hi, HK, you have 10000'

在这里插入图片描述

占位符替换内容
%d整数
%f浮点数
%s字符串
%x十六进制整数

格式化小数还可以指定小数的位数:

>>> '%.2f' % 3.1415926
'3.14'
  • 使用format关键字来格式化
>>> 'Hello, {0}成绩提升了{1}'.format('HK',17.125)
'Hello, HK成绩提升了17.125

List

  • List – python内置的一种数据类型是列表,list是一种有序的集合,可以随时添加和删除其中的元素。

①列出班上几个同学的名字,用一个list来表示

>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']

②使用len() 函数来获取list元素的个数

>>> len(classmates)
3

③ 使用索引来访问list中每一个位置的元素,索引是从0开始的

>>> classmates[0]
'Michael'
>>> classmates[1]
'Bob'
>>> classmates[2]
'Tracy'
>>> classmates[3]
报错了,索引出现了越界
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

④ list是一个可变的有序表,可以往list中追加元素到末尾
append

>>> classmates.append('Adam')
>>> classmates
['Michael', 'Bob', 'Tracy', 'Adam']

⑤ 也可以insert将元素插入到指定的位置

>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']

⑥ 删除list末尾的元素 用pop()

>>> classmates.pop()
'Adam'
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy']

⑦ 使用pop(i) 可以删除指定位置的元素

>>> classmates.pop(1)
'Jack'
>>> classmates
['Michael', 'Bob', 'Tracy']

⑧ 可以将list中的某个元素替换成别的元素,可以直接赋值给相应的索引位置

>>> classmates[1] = 'Sarah'
>>> classmates
['Michael', 'Sarah', 'Tracy']

⑨ list当中的元素的数据类型可以不同

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

⑩ list当中也可以包含另一个list

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

理解:s只要4个元素,其中s[2]又是一个list,拆开书写更容易理解

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

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

如果一个list中一个元素也没有,就是一个空的list,它的长度为0:

>>> L = []
>>> len(L)
0

元组(Tuple)

另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:

>>> classmates = ('Michael', 'Bob', 'Tracy')

现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。

不可变tuple的意义:就是因为其不可变所以代码会更安全,如果可以,能用tuple代替list就尽量使用tuple。

注意

  • ① 定义一个tuple时,在定义的时候,tuple元素就必须确定下来。
>>> t = (1, 2)
>>> t
(1, 2)
  • ② 定义一个只有一个元素的tuple ,可能会产生歧义
>>> t = (1)
>>> t
1
--- 所以只有一个元素的tuple定义时,必须加一个逗号,以免误解成数学计算意义上的括号
>>> t = (1,)
>>> t
(1,)
  • ③ “可变的”的tuple
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

这个tuple定义的时候有3个元素,分别是’a’,'b’和一个list。不是说tuple一旦定义后就不可变了吗?怎么后来又变了?
理解:
先看看定义的时候tuple包含的3个元素:
在这里插入图片描述
当我们把list的元素’A’和’B’修改为’X’和’Y’后,tuple变为:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
list和tuple是Python内置的有序集合,一个可变,一个不可变。根据需要来选择使用它们。

条件判断

python使用的是缩进原则来实现具体的条件判断:
根据Python的缩进规则,如果if语句判断是True,就把缩进的两行print语句执行了,否则,什么也不做。

age = 3
if age >= 18:
    print('adult')
elif age >= 6:
    print('teenager')
else:
    print('kid')
  • if判断条件还可以进行简写操作,例如:
if x:
    print('True')

只要x是非零数值,非空字符串,非空list等,就可以判断为True,否则就是False

  • 再次回顾上次提到的input函数
    在这里插入图片描述
  • 条件判断的形象体现:条件判断从上向下匹配,当满足条件时执行对应的块内语句,后续的elif和else都不再执行。
    在这里插入图片描述

循环

python循环分为两种:
① 一种是for…in循环,依次把list或tuple中的每个元素迭代出来

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

执行上述代码,会依次打印names中的每一个元素:

Michael
Bob
Tracy

解释:for x in …循环就是把每个元素代入变量x,然后执行缩进块的语句。
实例:计算1-10的整数之和,可以用一个sum变量做累加:

sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
    sum = sum + x
print(sum)

range() 函数,可以生成一个整数序列,在通过list() 函数可以转换为list

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

② 另一种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。
实例:计算100以内所有奇数之和,可以用while循环实现:

sum = 0
n = 99
while n > 0:
    sum = sum + n
    n = n - 2
print(sum)

break
使用break 语句可以提前退出循环

n = 1
while n <= 100:
    if n > 10: # 当n = 11时,条件满足,执行break语句
        break # break语句会结束当前循环
    print(n)
    n = n + 1
print('END')

解释:打印出1~10后,紧接着打印END,程序结束。可见break的作用是提前结束循环。

continue
使用continue 可以跳过当前这次循环,直接开始下一次循环
实例:在打印过程中,只想打印奇数,可以使用continue语句跳过某些循环

n = 0
while n < 10:
    n = n + 1
    if n % 2 == 0: # 如果n是偶数,执行continue语句
        continue # continue语句会直接继续下一轮循环,后续的print()语句不会执行
    print(n)

解释:打印的不再是1~10,而是1,3,5,7,9。
可见continue的作用是提前结束本轮循环,并直接开始下一轮循环
break和continue会造成代码执行逻辑分叉过多,容易出错。大多数循环并不需要用到break和continue语句。即:break和continue可以少用尽量少用!

dict 和 set

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
举例:
假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:

names = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]

很明显,给定一个名字要查找对应的成绩,就先要在names中找到对应的位置,在从scores取出对应的成绩,list越长,耗时越长

使用dict实现,只需要一个**“名字”-“成绩”**的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:

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

为什么dict查找速度比list快这么多呢 ?
① 因为dict的实现原理和查字典是一样的。假设字典包含了1万个汉字,我们要查某一个字,一个办法是把字典从第一页往后翻,直到找到我们想要的字为止,这种方法就是在list中查找元素的方法,list越大,查找越慢。
② 第二种方法是先在字典的索引表里(比如部首表)查这个字对应的页码,然后直接翻到该页,找到这个字。无论找哪个字,这种查找速度都非常快,不会随着字典大小的增加而变慢。

dict就是第二种实现方式, 给定一个名字,比如’Michael’,dict在内部就可以直接计算出Michael对应的存放成绩的“页码”,也就是95这个数字存放的内存地址,直接取出来,所以速度非常快。

key-value存储方式
在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value。

  • 把数据放入dict当中,除了初始化指定之外,还可以通过key放入:
>>> d['Adam'] = 67
>>> d['Adam']
67
  • 多次对一个key放入value,后面的值会对前面的值进行覆盖
>>> d['Jack'] = 90
>>> d['Jack']
90
>>> d['Jack'] = 88
>>> d['Jack']
88
  • 当键(key)不存在的时候,dict就会报错
>>> d['Thomas']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Thomas'
  • 避免key不存在错误的两种方法,① 通过in 判断key是否存在
>>> 'Thomas' in d
False

② 通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:

>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1
  • 删除key – pop(key)方法,对应的value也会从dict中删除
>>> d.pop('Bob')
75
>>> d
{'Michael': 95, 'Tracy': 85}
  • list 与 dict 之间的对比
    在这里插入图片描述
    牢记:dict的key必须是不可变对象。因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
    要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key
>>> key = [1, 2, 3]
>>> d[key] = 'a list'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

  • 要创建一个set,需要提供一个list作为输入集合
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

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

  • 重复元素在set中自动被过滤
>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}
  • add(key) 方法可以添加元素到set中,可以重复添加,但不会有效果:
>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}
  • 通过remove(key) 方法可以删除元素
>>> s.remove(4)
>>> s
{1, 2, 3}
  • set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}
  • set和dict之间的区别:
    set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。

不可变对象与可变对象

str是不变对象,而list是可变对象。对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:

>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']

而对于不可变对象,比如str,对str进行操作呢:

>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'

虽然字符串有个replace()方法,也确实变出了’Abc’,但变量a最后仍是’abc’,应该怎么理解呢?

>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'

解释:
在这里插入图片描述

  • 使用key-value存储结构的dict在Python中非常有用,选择不可变对象作为key很重要,最常用的key是字符串

函数

调用函数

函数文档

  • 可以 在交互式命令行中通过help(abs)来查看abs函数的帮助信息
>>> help(abs)
Help on built-in function abs in module builtins:

abs(x, /)
    Return the absolute value of the argument.
  • 调用abs函数
>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34
  • abs函数使用可能出现的错误:① 传入参数数量不对 ② 传入的参数类型不能被函数所接受
>>> abs(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (2 given)
>>> abs('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
  • max函数可以接收任意多个参数,并返回最大的那一个
>>> max(1, 2)
2
>>> max(2, 3, 1, -5)
3

数据类型转换

  • int()函数可以把其他数据类型转换为整数
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False
  • 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1
  • 练习题
    在这里插入图片描述

定义函数

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

  • 自定义一个求绝对值的my_abs函数为例:
def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

print(my_abs(-99))
# 99

注意点:

  • 函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。函数内部通过条件判断和循环可以实现非常复杂的逻辑。
  • 没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。

空函数

  • 定义一个什么事也不做的空函数,可以用pass语句:
def nop():
    pass
  • pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来
if age >= 18:
    pass

缺少了pass,代码运行就会有语法错误。

参数检查

  • 调用函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError:
>>> my_abs(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: my_abs() takes 1 positional argument but 2 were given
  • 如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs和内置函数abs的差别:
>>> my_abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in my_abs
TypeError: unorderable types: str() >= int()
>>> abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'

解释:当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,会导致if语句出错,出错信息和abs不一样。所以,这个函数定义不够完善。

  • 修改函数my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数,数据类型检查可以用内置函数isinstance() 实现:
def my_abs(x):
    if not isinstance(x, (int, float)):
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x
  • 添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
>>> 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

后续还会接着复习到 错误和异常

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

import math

def move(x, y, step, angle=0):
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny
  • import math语句表示导入math包,并允许后续代码引用math包里的sin、cos等函数。
  • 我们可以同时获取返回值
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0
  • 但是,Python函数返回的仍然是单一值
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

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

  • 定义函数时,需要确定函数名和参数个数;
  • 如果有必要,可以先对参数的数据类型做检查;
  • 函数体内部可以用return随时返回函数结果;
  • 函数执行完毕也没有return语句时,自动return None。
  • 函数可以同时返回多个值,但其实就是一个tuple。

函数的参数

  • 定义函数的时候,我们把参数的名字位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
  • Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。

位置参数

  • 先写一个计算x2的函数
def power(x):
    return x * x
  • 调用power函数时,必须传入有且仅有的一个参数x:
>>> power(5)
25
>>> power(15)
225
  • 把power(x)修改为power(x, n),用来计算xn,说干就干:
def power(x, n):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s

这个修改后的power(x, n)函数,可以计算任意n次方:

>>> power(5, 2)
25
>>> power(5, 3)
125

修改后的power(x, n)函数有两个参数:x和n,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数x和n。

默认参数

  • 由于我们经常计算x2,所以,完全可以把第二个参数n的默认值设定为2:
def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s
  • 当我们调用power(5)时,相当于调用power(5, 2):
>>> power(5)
25
>>> power(5, 2)
25

设置默认参数注意点

  • 必选参数在前,默认参数在后,否则Python的解释器会报错
  • 如何设置默认参数:当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数
    使用默认参数的好处
    使用默认参数最大的好处就是:能降低调用函数的难度。因为在有些情境当中,有一些参数是固定的,不用每次在调用函数的时候都进行传入。
    例子:在注册的时候,大多数学生在注册时不需要提供年龄和城市,只提供必须的两个参数:
def enroll(name, gender, age=6, city='Beijing'):
    print('name:', name)
    print('gender:', gender)
    print('age:', age)
    print('city:', city)

默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。

  • 有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll(‘Bob’, ‘M’, 7),意思是,除了name,gender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。
  • 不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll(‘Adam’, ‘M’, city=‘Tianjin’),意思是,city参数用传进去的值,其他默认参数继续使用默认值.

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

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

None是不变对象,无论调用多少次,都不会有问题:

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

可变参数

  • 可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
  • 实例:给定一组数字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
  • 调用的时候,需要先组装出一个list或tuple
>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84
  • 利用可变参数,调用函数的方法可以简化这样:
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
  • 把函数的参数改为可变参数:
def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum
  • 定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
>>> calc(1, 2)
5
>>> calc()
0
  • Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
>>> nums = [1, 2, 3]
>>> calc(*nums)
14

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

关键字参数

  • 可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)
  • 函数person除了必选参数name和age外,还接受关键字参数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函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

  • 和可变参数类似,也可以先组装出一个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'}
  • 将上面的写法进行简化
>>> 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获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。

命名关键字参数

  • 对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查
  • 以person()函数为例,我们希望检查是否有city和job参数:
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)

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

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() missing 2 required keyword-only arguments: 'city' and 'job'

解释:由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。

  • 命名关键字可以有缺省值,从而来简化调用
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种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
  • 比如定义一个函数,包含上述若干种参数
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}
  • 最神奇的是通过一个tuple和dict,你也可以调用上述函数:
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'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种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。

练习
在这里插入图片描述
小结
在这里插入图片描述

递归函数

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

实例:我们来计算阶乘n! = 1 x 2 x 3 x … x n,用函数fact(n)表示,
可以看出:
fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n

fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。
fact(n)用递归的方式写出来

def fact(n):
    if n==1:
        return 1
    return n * fact(n - 1)

实验结果

>>> fact(1)
1
>>> fact(5)
120

计算过程
在这里插入图片描述
递归函数的优点:定义简单,逻辑清晰。其实所有递归函数都可以写成循环方式,但是循环逻辑没有递归那么清晰。
在这里插入图片描述

>>> fact(1000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in fact
  ...
  File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison

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

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

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

def fact(n):
    return fact_iter(n, 1)

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

解释:return fact_iter(num - 1, num * product)仅返回递归函数本身,num - 1和num * product在函数调用前就会被计算,不影响函数调用。

递归函数调用过程:
在这里插入图片描述
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

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

小结:

  1. 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
  2. 针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
  3. Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

练习
在这里插入图片描述

高级特性

切片

  • 取一个list或tuple的部分元素是非常常见的操作, 经常取指定索引范围的操作,用循环十分繁琐,Python提供了切片(Slice)操作符,能大大简化这种操作。取前3个元素,用一行代码就可以完成切片:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']

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

  • 如果第一个索引是0,还可以省略:
>>> L[:3]
['Michael', 'Sarah', 'Tracy']
  • 也可以从索引1开始,取出2个元素出来:
>>> L[1:3]
['Sarah', 'Tracy']
  • 既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片,倒数第一个元素的索引是-1
>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']
  • 切片操作十分有用,先创建一个0-99的数列:
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
  • 可以通过切片轻松取出某一段数列,比如前10个数;后10个数;前11-20个数;前10个数,每两个取一个;所有数,每5个取一个 只写[:]就可以原样复制一个list;
>>> L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
>>> L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> L[:10:2]
[0, 2, 4, 6, 8]
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
>>> L[:]
[0, 1, 2, 3, ..., 99]
  • tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:-- tuple切片操作的结果仍然是tuple
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
  • 字符串可以看一种list,每个元素就是一个字符,因此字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

在这里插入图片描述

迭代

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

  • Python的for循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上。list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如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循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。

>>> for ch in 'ABC':
...     print(ch)
...
A
B
C
  • 如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:
>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
  • 如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C
  • for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码。
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
...     print(x, y)
...
1 1
2 4
3 9

练习
在这里插入图片描述

  • 小总结:在使用遍历求取最小或者最大值的时候,可以用变量来保存数组中的第一个值,往后依次做对比即可。

列表生成式

  • 列表生成式是Python内置的非常简单却强大的可以用来创建list的生成式。
  • 例子1:要生成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]
  • 例子2:要生成[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]

方法二:列表生成式:一行语句代替循环生成上面的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]
  • 还可以使用两层循环,可以生成全排列
>>> [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', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
  • 前面讲到:for循环其实可以同时使用两个甚至多个变量,比如dict的items可以同时迭代key和value;dict是用{}符号来表示:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
...     print(k, '=', v)
...
y = B
x = A
z = C
  • 换一种写法,其实就是列表生成式也可以使用两个变量来生成list:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
  • 把一个list中所有的字符串变成小写:
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
  • 在列表生成式当中进行条件的筛选,使用if, 但是不能在if后面加上else,因为只是用来条件的筛选
>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]
  • 但是在有些时候,必须在if后面加上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前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else:

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

小总结:在一个列表生成式中,for前面的if … else是表达式,而for后面的if是过滤条件,不能带else。

练习
在这里插入图片描述
小总结:运用列表生成式,可以快速生成list,可以通过一个list推导出另一个list,而代码却十分简洁。

生成器

  • 通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
  • 如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器: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>
  • 创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generato;
  • 可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
    可以使用next() 函数来获取生成器的下一个返回值
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> 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后,通过for循环来迭代它,并且不需要关心StopIteration的错误。generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现

用函数来进行实现

  • 实例1:斐波拉契数列
    著名的斐波拉契数列(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就可以赋值

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

上面的函数可以输出斐波那契数列的前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函数,调用一个generator函数将返回一个generator
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

对于generator函数的解释: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)就报错。
  • 注意点:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。
  • 对生成器函数进行调用的正确写法:创建一个generator对象,然后不断对这一个generator对象调用next():
>>> g = odd()
>>> next(g)
step 1
1
>>> next(g)
step 2
3
>>> next(g)
step 3
5
  • 在fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
  • 把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代
>>> for n in fib(6):
...     print(n)
...
1
1
2
3
5
8
  • 用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

– 如何捕获错误,后面还会复习到

练习 – 试写一个generator,来打印出一个杨辉三角,见 迭代器打印杨辉三角

小总结

  • generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
  • 要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。对于函数改成的generator来说**,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束**。
  • 普通函数和generator函数的区分
    1、普通函数调用直接返回结果:
>>> r = abs(6)
>>> r
6

2、generator函数的调用实际返回一个generator对象:

>>> g = fib(6)
>>> g
<generator object fib at 0x1022ef948>

迭代器

  • 直接作用于for循环的数据类型
    1、一类是集合数据类型,如list、tuple、dict、set、str等
    2、一类是generator,包括生成器和带yield的generator function。
  • 上面这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。
  • 可以使用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)
True
>>> isinstance(100, Iterable)
False
  • 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是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是永远不可能存储全体自然数的。

小总结:

  • 凡是可作用于for循环的对象都是Iterable类型;
  • 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
  • 集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。
  • Python的for循环本质上就是通过不断调用next()函数实现的,例如:
for x 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

函数式编程

高阶函数

map/reduce
1、map()

  • Python内建了map()和reduce()函数。
  • map – map()函数接收两个参数,一个是函数一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
  • 举个实际例子来进行说明

在这里插入图片描述

  • 使用Python代码来进行实现
>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

解释:map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。

  • 不需要map() 函数,写一个循环,也可以计算出结果
L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
    L.append(f(n))
print(L)
  • 虽然可以,但是从上面的循环代码,不能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”
  • map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

2、reduce()

  • reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
  • 实例:对一个序列进行求和操作,可以直接使用reduce来实现
>>> from functools import reduce
>>> def add(x, y):
...     return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
  • 求和运算当然可以直接使用Python内建函数sum(), 没有必要使用reduce
>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
  • map和reduce用处甚少,这里就不过多赘述。

filter
Python内建的filter()函数用于过滤序列。

  • 和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
  • 例如:在一个list中,删掉偶数, 只保留奇数
def is_odd(n):
    return n % 2 == 1

list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]

– 只有函数最终结果为True的时候,才会保留下来

  • 例如:把一个序列中的空字符串删掉:可以这么写:
def not_empty(s):
    return s and s.strip()

list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
# 结果: ['A', 'B', 'C']
  • Python当中的strip方法
    在这里插入图片描述
  • filter()这个高阶函数,关键在于正确实现一个**“筛选”**函数。
  • filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

使用filter来求解素数的实例:
在这里插入图片描述

  • 使用Python来实现这个算法,可以先构造一个从3开始的奇数序列,
def _odd_iter():
    n = 1
    while True:
        n = n + 2
        yield n
  • 上面是一个生成器,并且是一个无限序列,使用filter来定义一个筛选函数:
def _not_divisible(n):
    return lambda x: x % n > 0
  • 定义一个生成器,不断返回下一个素数:
def primes():
    yield 2
    it = _odd_iter() # 初始序列
    while True:
        n = next(it) # 返回序列的第一个数
        yield n
        it = filter(_not_divisible(n), it) # 构造新序列

解释:这个生成器先返回第一个素数2, 然后利用filter() 不断产生筛选后的新的序列

  • 而上述primes() 也是一个无限序列,所以调用时需设置一个退出循环的条件:
# 打印1000以内的素数:
for n in primes():
    if n < 1000:
        print(n)
    else:
        break

练习
回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()筛选出回数:
思路:利用回数的性质, 从左向右读和从右向左读都是一样的数,我们可以利用字符串的性质 外加 切片来进行比较,因为filter是需要写出一个判断函数。
在这里插入图片描述
小总结:filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素。

排序算法(sorted)
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

  • Python内置的sorted()函数就可以对list进行排序:
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
  • sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
  • key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list:
list = [36, 5, -12, 9, -21]
keys = [36, 5,  12, 9,  21]

不需要过多解读,其实就是list上的每一个元素都进行了abs运算。
在这里插入图片描述
以上是对sorted([36, 5, -12, 9, -21], key=abs)的解释

  • 字符串排序的例子
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
  • 默认情况下,对字符串排序,是按照ASCII的大小比较的,由于’Z’ < ‘a’,结果,大写字母Z会排在小写字母a的前面。
  • 我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
  • 直接给sorted函数传入key函数即可,即可实现忽略大小写的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
  • 要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
  • 高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

小总结:sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数(key函数实现即可)

练习:
在这里插入图片描述
解释:跟常规的list表达不一样,sorted函数运行时,参数L是每次传其中一个元素进去,例子中就是把L的元素每一个tuple() 传到key里,所以可以用t[n]表示每个tuple里的元素。

但是我们在对一般list进行取值的时候,一般情况下是下面这种形式:有点类似于二维数组:

>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
>>> L[0]
('Bob', 75)
>>> L[1]
('Adam', 92)
>>> L[0][0]
'Bob'

对字典中的值如何取:

>>> T = {'kang':1999, 'lei':2000, 'YY':1998}
>>> T['kang']
1999

返回函数

即函数作为返回值

  • 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
  • 实现一个可变参数的求和,通常情况下,可变参数求和函数的定义:
def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax
  • 如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum
  • 当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
  • 调用函数f时,才真正计算求和的结果:
>>> f()
25
  • 在这个例子中,我们在函数lazy_sum又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
  • 再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

但是:f1() 和 f2() 调用结果互不影响!

闭包

  • 返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
  • 需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()
  • 上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
  • 你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9

全部都是9!原因就在于返回的函数引用了变量 i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9

返回闭包时必须牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量!!!

如果一定要引用循环变量怎么办?
方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
    return fs

再看看结果就是:

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
  • 缺点是代码较长,可利用lambda函数缩短代码。

nonlocal(非局部的)
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:

def inc():
    x = 0
    def fn():
        # 仅读取x的值:
        return x + 1
    return fn

f = inc()
print(f()) # 1
print(f()) # 1
  • 但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错
def inc():
    x = 0
    def fn():
        x = x + 1  # 报错
        return x
    return fn

f = inc()
print(f()) # 1
print(f()) # 2

解释
x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()的x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1。

所以只要加上一个简单的声明即可:

def inc():
    x = 0
    def fn():
        # nonlocal x
        x = x + 1
        return x
    return fn

f = inc()
print(f()) # 1
print(f()) # 2

!!! 使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。

练习
问题描述:利用闭包返回一个计数器函数,每次调用它返回递增整数:
方法一:
在这里插入图片描述
方法二:
在这里插入图片描述
小总结:

  • 一个函数可以返回一个计算结果,也可以返回一个函数。
  • 返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。(不要使用循环变量)

匿名函数
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便

在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数

>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
  • 通过对比可以看出,匿名函数lambda x: x * x实际上就是:
def f(x):
    return x * x
  • 关键字lambda表示匿名函数,冒号前面的x表示函数参数
  • 匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
  • 匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
  • 同样,也可以把匿名函数作为返回值返回,比如:
def build(x, y):
    return lambda: x * x + y * y

练习
在这里插入图片描述
小总结:

  • Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。

装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数

>>> def now():
...     print('2015-3-25')
...
>>> f = now
>>> f()
2015-3-25
  • 函数对象有一个__name__属性,可以拿到函数的名字:
>>> now.__name__
'now'
>>> f.__name__
'now'
  • 假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
  • 本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper
  • 观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
    print('2015-3-25')
  • 调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
>>> now()
call now():
2015-3-25

解释:把@log放到now()函数的定义处,相当于执行了语句:

now = log(now)

解释:由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

上面的例子可能很难理解,举一个实际例子来讲解
其实:当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣,
这个时候就可以使用一种叫做装饰器的东西来整理代码

我们一般写判断一个数是不是素数的操作:

import time

def is_prime(num):
    if num < 2:
        return False
    elif num == 2:
        return True
    else:
         for i in range(2, num):
             if num % i == 0:
                 return False
         return True
def prime_nums():
    t1 = time.time()
    for i in range(2, 10000):
        if is_prime(i):
            print(i)
    t2 = time.time()
    print(t2 - t1)
prime_nums()

使用装饰器

'''
装饰器的使用
慢慢开始引入装饰器这个概念
记录时间
当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣,
这个时候就可以使用一种叫做装饰器的东西来整理代码
'''
import time

# 来求取一个函数的总运行时间
# 里面这个参数实际意思就是我等下要运行prime_nums这个函数
def display_time(func):
    # wrapper() 表示这个函数需要运行哪一些内容
    def wrapper():
        # 比如说我们要做的是计时
        # 1、先截取一个时间
        t1 = time.time()
        # 2、运行一下我要走的那个函数
        func()
        # 3、截取另一段时间
        t2 = time.time()
        print(t2 - t1)
    return wrapper

# 判断一个数字是不是素数
def is_prime(num):
    if num < 2:
        return False
    elif num == 2:
        return True
    else:
         for i in range(2, num):
             if num % i == 0:
                 return False
         return True
# 如何使用装饰器,使用 @
@ display_time
def prime_nums():
    for i in range(2, 10000):
        if is_prime(i):
            print(i)
'''
此时运行这个函数的时候会:
1、先运行display_time这个装饰器
2、运行wrapper里面的内容:
3、最后才是prime_nums()这个函数实现运行操作
'''
prime_nums ()

!!! 简单使用装饰器以后和之前那个运行结果其实是一样的。

  • 假设接下来我们想要统计2-10000之间有多少个质数?
  • 先将上面解释器里面的demo进行小小变动即可
@ display_time
def count_prime_nums():
    count = 0
    for i in range(2, 10000):
        if is_prime(i):
            count = count + 1
    return count
count = count_prime_nums()
print(count)

count_prime_nums()

0.46373701095581055
None

但是上述代码在执行以后,并没有打印出统计结果,那是因为返回值并没有传到func里面:
在这里插入图片描述
如果想要返回返回值的话,那么需要在wrapper() 里面将返回值返回出来!!!
即修改以后变成:

def display_time(func):
    # wrapper() 表示这个函数需要运行哪一些内容
    def wrapper():
        # 比如说我们要做的是计时
        # 1、先截取一个时间
        t1 = time.time()
        # 2、运行一下我要走的那个函数
        # 将函数返回值记录到一个变量result当中
        result = func()
        # 3、截取另一段时间
        t2 = time.time()
        print("Total time: {:.4} s".format(t2 - t1))
        return result
    return wrapper

如果装饰器下面那个函数带了参数呢?比如在这个例子当中,我们不是到10000,而是想看2到任意数字,则代码变成这样:

def count_prime_nums(maxnum):
    count = 0
    for i in range(2, maxnum):
        if is_prime(i):
            count = count + 1
    return count
count = count_prime_nums(1999)
print(count)

此时程序会报错:

TypeError: wrapper() takes 0 positional arguments but 1 was given

那么函数带了参数以后装饰器应该怎么进行修改呢?
同样也是在wrapper里面进行修改:

def display_time(func):
    # wrapper() 表示这个函数需要运行哪一些内容
    # 如果不知道有多少个参数的话,可以写得稍微笼统一点
    def wrapper(*args):
        # 比如说我们要做的是计时
        # 1、先截取一个时间
        t1 = time.time()
        # 2、运行一下我要走的那个函数
        # 将函数返回值记录到一个变量result当中
        result = func(*args)
        # 3、截取另一段时间
        t2 = time.time()
        print("Total time: {:.4} s".format(t2 - t1))
        return result
    return wrapper

最后总的例子实现代码是:

'''
装饰器的使用
慢慢开始引入装饰器这个概念
记录时间
当一个函数中,不同逻辑混杂在一起的时候,程序的可读性就会大大折扣,
这个时候就可以使用一种叫做装饰器的东西来整理代码
'''
import time

# 来求取一个函数的总运行时间
# 里面这个参数实际意思就是我等下要运行prime_nums这个函数
def display_time(func):
    # wrapper() 表示这个函数需要运行哪一些内容
    # 如果不知道有多少个参数的话,可以写得稍微笼统一点
    def wrapper(*args):
        # 比如说我们要做的是计时
        # 1、先截取一个时间
        t1 = time.time()
        # 2、运行一下我要走的那个函数
        # 将函数返回值记录到一个变量result当中
        result = func(*args)
        # 3、截取另一段时间
        t2 = time.time()
        print("Total time: {:.4} s".format(t2 - t1))
        return result
    return wrapper

# 判断一个数字是不是素数
def is_prime(num):
    if num < 2:
        return False
    elif num == 2:
        return True
    else:
         for i in range(2, num):
             if num % i == 0:
                 return False
         return True
# 如何使用装饰器,使用 @
@ display_time
# def prime_nums():
#     for i in range(2, 10000):
#         if is_prime(i):
#             print(i)
def count_prime_nums(maxnum):
    count = 0
    for i in range(2, maxnum):
        if is_prime(i):
            count = count + 1
    return count
count = count_prime_nums(5000)
print(count)
'''
此时运行这个函数的时候会:
1、先运行display_time这个装饰器
2、运行wrapper里面的内容:
3、最后才是prime_nums()这个函数实现运行操作
'''
# count_prime_nums()

运行结果是:

Total time: 0.1388 s
669

练习:
请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间:
在这里插入图片描述
小总结:

  • 在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现
  • decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。
  • 了解 + 使用 = YYDS

偏函数

  • Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。Python当中的偏函数和数学意义上的偏函数是不一样的。
  • 介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
  • 举例:int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
>>> int('12345')
12345
  • int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

base是可以省略不写的

  • 假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
def int2(x, base=2):
    return int(x, base)
  • 此时,转换二进制就非常方便了,只要传入自己想要传的字符串即可
>>> int2('1000000')
64
>>> int2('1010101')
85
  • 简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
>>> int2('1000000', base=10)
1000000
  • 创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数,当传入:
int2 = functools.partial(int, base=2)
  • 实际上固定了int()函数的关键字参数base,也就是:
int2('10010')

相当于:

kw = { 'base': 2 }
int('10010', **kw)

当传入:

max2 = functools.partial(max, 10)

实际上会把10作为*args的一部分自动加到左边,也就是:

max2(5, 6, 7)

就相当于变成:

args = (10, 5, 6, 7)
max(*args)

最终上面的函数的结果是 10 。

小总结:

  • 当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

模块

使用模块

  • Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。
  • 我们以内建的sys模块为例,编写一个hello的模块:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

import sys

def test():
    args = sys.argv
    if len(args)==1:
        print('Hello, world!')
    elif len(args)==2:
        print('Hello, %s!' % args[1])
    else:
        print('Too many arguments!')

if __name__=='__main__':
    test()

对上面代码的解释:

  • 第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;

  • 第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;

  • 第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名;

  • 以上就是Python模块的标准文件模板,当然也可以全部删掉不写,但是,按标准办事肯定没错。
    后面开始就是真正的代码部分。

  • 你可能注意到了,使用sys模块的第一步,就是导入该模块:

import sys

导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。 – 关键

sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:

运行python3 hello.py获得的sys.argv就是[‘hello.py’];

  • 最后,注意到这两行代码:
if __name__=='__main__':
    test()

当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

  • 我们可以用命令行运行hello.py看看效果:
$ python3 hello.py
Hello, world!
$ python hello.py Michael
Hello, Michael!
  • 如果启动Python交互环境,再导入hello模块:
$ python3
Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 23 2015, 02:52:03) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>>
  • 导入时,没有打印Hello, word!,因为没有执行test()函数。
  • 调用hello.test()时,才能打印出Hello, word!:
>>> hello.test()
Hello, world!

作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过**_前缀**来实现的。

  • 正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等;
  • 类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,我们自己的变量一般不要用这种变量名
  • 类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;
  • 之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
  • private函数或变量不应该被别人引用,那它们有什么用呢?请看例子:
def _private_1(name):
    return 'Hello, %s' % name

def _private_2(name):
    return 'Hi, %s' % name

def greeting(name):
    if len(name) > 3:
        return _private_1(name)
    else:
        return _private_2(name)

我们在模块里公开greeting()函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法,即:
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。-- 还是非常关键的!!!

安装第三方模块

方法一:

  • 在Python中,安装第三方模块,是通过包管理工具pip完成的。
  • 如果你正在使用Mac或Linux,安装pip本身这个步骤就可以跳过了。
  • 在命令提示符窗口下(cmd)尝试运行pip,如果Windows提示未找到命令,可以重新运行安装程序添加pip。
  • 注意:Mac或Linux上有可能并存Python 3.x和Python 2.x,因此对应的pip命令是pip3。
  • 例如,我们要安装一个第三方库——Python Imaging Library,这是Python下非常强大的处理图像的工具库。不过,PIL目前只支持到Python 2.7,并且有年头没有更新了,因此,基于PIL的Pillow项目开发非常活跃,并且支持最新的Python 3。
  • 一般来说,第三方库都会在Python官方的pypi.python.org网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者pypi上搜索,比如Pillow的名称叫Pillow,因此,安装Pillow的命令就是:
pip install Pillow

方法二:
安装常用模块

  • 在使用Python时,我们经常需要用到很多第三方库,例如,上面提到的Pillow,以及MySQL驱动程序,Web框架Flask,科学计算Numpy等。
  • 用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。
  • 安装好Anaconda后,重新打开命令行窗口,输入python,可以看到Anaconda的信息:
    在这里插入图片描述
    可以尝试直接import numpy等已安装的第三方模块。

模块搜索路径
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错:

>>> import mymodule
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named mymodule
  • 默认情况下,Python解释器会搜索当前目录所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:
>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', ..., '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']

如果我们要添加自己的搜索目录,有两种方法:

  1. 一是直接修改sys.path,添加要搜索的目录:这种方法是在运行时修改,运行结束后失效
>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')
  1. 第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。-- 自行百度即可。

面向对象编程

类和实例

  • 面向对象最重要的概念就是类(Class)和实例(Instance)
  • 必须牢记类是抽象的模板,比如Student类
  • 而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

仍以Student类为例,在Python中,定义类是通过class关键字

class Student(object):
    pass

解释:class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:

>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

解释:可以看到,变量bart指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身则是一个类。

  • 可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
  • 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__即为初始化方法,在创建实例的时候,就把name,score等属性绑上去:
class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

!!!注意:特殊方法“init”前后分别有两个下划线!!!

  • 注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
  • 有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
  • 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

>>> def print_score(std):
...     print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59

但是,既然Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入

>>> bart.print_score()
Bart Simpson: 59

这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。

封装的另一个好处是可以给Student类增加新的方法,比如get_grade:

class Student(object):
    ...

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'

同样的,get_grade方法可以直接在实例变量上调用,不需要知道内部实现细节

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'
// 测试
lisa = Student('Lisa', 99)
bart = Student('Bart', 59)
print(lisa.name, lisa.get_grade())
print(bart.name, bart.get_grade())
// run
Lisa A 
Bart C 

小总结:

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
  • 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
  • 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'

访问限制

  • 在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
  • 但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性:
>>> bart = Student('Bart Simpson', 59)
>>> bart.score
59
>>> bart.score = 99
>>> bart.score
99
  • 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:
class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))
  • 改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name和实例变量.__score了:
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
  • 这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
  • 但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:
class Student(object):
    ...

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score
  • 如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
class Student(object):
    ...

    def set_score(self, score):
        self.__score = score

外部代码想要获取或者修改属性的话,设置相应的get和set方法即可

  • 你也许会问,原先那种直接通过bart.score = 99也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数: – 直接写了一个方法里面对参数进行了判断,选择出符合自己的
class Student(object):
    ...

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')
  • 需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__、__score__这样的变量名。
  • 有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
  • 双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:
>>> bart._Student__name
'Bart Simpson'
  • 但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。
  • 总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
  • 最后注意下面的错误写法:
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'

表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:

>>> bart.get_name() # get_name()内部返回self.__name
'Bart Simpson'

练习:
请把下面的Student对象的gender字段对外隐藏起来,用get_gender()和set_gender()代替,并检查参数有效性:
在这里插入图片描述

继承和多态

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

class Animal(object):
    def run(self):
        print('Animal is running...')
  • 当我们需要编写Dog和Cat类时,就可以直接从Animal类继承
class Dog(Animal):
    pass

class Cat(Animal):
    pass

解释:对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。Cat和Dog类似。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:

// 先创建相应的对象
dog = Dog()
dog.run()

cat = Cat()
cat.run()

运行结果如下:

Animal is running...
Animal is running...

当然,也可以对子类增加一些方法,比如Dog类:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

    def eat(self):
        print('Eating meat...')

继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running…,符合逻辑的做法是分别显示Dog is running…和Cat is running…,因此,对Dog和Cat类改进如下:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

class Cat(Animal):

    def run(self):
        print('Cat is running...')

再次运行,运行结果为:

Dog is running...
Cat is running...

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态

理解多态:
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型

判断一个变量是否是某个类型可以用**isinstance()**判断:

>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

看来a、b、c确实对应着list、Animal、Dog这3种类型。但是等等,试试:

>>> isinstance(c, Animal)
True

看来c不仅仅是Dog,c还是Animal!
不过仔细想想,这是有道理的,因为**Dog是从Animal继承下来的,**当我们创建了一个Dog的实例c时,我们认为c的数据类型是Dog没错,但c同时也是Animal也没错,Dog本来就是Animal的一种!

  • 所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行
>>> b = Animal()
>>> isinstance(b, Dog)
False
  • Dog可以看成Animal,但Animal不可以看成Dog。
  • 要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
def run_twice(animal):
    animal.run()
    animal.run()
  • 当我们传入Animal的实例时,run_twice()就打印出:
>>> run_twice(Animal())
Animal is running...
Animal is running...
  • 当我们传入Dog的实例时,run_twice()就打印出:
>>> run_twice(Dog())
Dog is running...
Dog is running...
  • 当我们传入Cat的实例时,run_twice()就打印出:
>>> run_twice(Cat())
Cat is running...
Cat is running...
  • 看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:
class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')
  • 当我们调用run_twice()时,传入Tortoise的实例:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
  • 你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
  • 多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思
  • 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:① 对扩展开放:允许新增Animal子类;② 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
  • 继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:
    在这里插入图片描述
    静态语言 vs 动态语言
  • 对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法
  • 对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:
class Timer(object):
    def run(self):
        print('Start...')

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
即要求没有那么严格

**Python的“file-like object“**就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

小总结:

  • 继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写
  • 动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。

获取对象信息

  • 当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
    可以使用type()
  • 首先,我们来判断对象类型,使用type()函数:
  • 基本类型都可以用type()判断:
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
  • 如果一个变量指向函数或者是类,也可以使用 type() 判断
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
  • 但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
  • 判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
>>> import types
>>> def fn():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

使用isinstance()

  • 对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
  • 我们回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky
  • 那么,isinstance()就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
  • 然后,使用函数isinstance() 来进行判断即可
>>> isinstance(h, Husky)
True

没有问题,因为h变量指向的就是Husky对象。
接着判断:

>>> isinstance(h, Dog)
True

解释:
h虽然自身是Husky类型,但由于Husky是从Dog继承下来的,所以,h也还是Dog类型。换句话说,isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上

  • 因此,我们可以确信,h还是Animal类型:
>>> isinstance(h, Animal)
True
  • 同理,实际类型是Dog的d也是Animal类型:
>>> isinstance(d, Dog) and isinstance(d, Animal)
True
  • 但是,d不是Husky类型:
>>> isinstance(d, Husky)
False
  • 而且, 能用type() 判断的基本类型也可以用isinstance()判断:
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
  • 并且还可以判断一个变量是否是某些类型中的一种(是否是某种类型中的一种),比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True

总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。

使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
  • 类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
  • 我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:
>>> class MyDog(object):
...     def __len__(self):
...         return 100
...
>>> dog = MyDog()
>>> len(dog)
100
  • 剩下的都是普通属性或方法,比如lower()返回小写的字符串:
>>> 'ABC'.lower()
'abc'
  • 仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
>>> class MyObject(object):
...     def __init__(self):
...         self.x = 9
...     def power(self):
...         return self.x * self.x
...
>>> obj = MyObject()
  • 紧接着,可以测试该对象的属性:
>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19
  • 如果试图获取不存在的属性,会抛出AttributeError的错误:
>>> getattr(obj, 'z') # 获取属性'z'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
  • 可以传入一个default参数,如果属性不存在,就返回默认值:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404

  • 也可以获得对象的方法:
>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

小总结:

  • 通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:
sum = obj.x + obj.y

就不要写:

sum = getattr(obj, 'x') + getattr(obj, 'y')
  • 一个正确的用法的例子是:
def readImage(fp):
    if hasattr(fp, 'read'):
        return readData(fp)
    return None
  • 假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
  • 请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能

实例属性和类属性

  • 由于Python是动态语言,根据类创建的实例可以任意绑定属性。
  • 给实例绑定属性的方法是通过实例变量,或者通过self变量:
class Student(object):
    def __init__(self, name):
        self.name = name

s = Student('Bob')
s.score = 90
  • 但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
class Student(object):
    name = 'Student'
  • 当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
>>> class Student(object):
...     name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

解释:从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

练习:
为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加:
在这里插入图片描述
可以通过类名来调用属性
小总结

  • 实例属性属于各个实例所有,互不干扰;
  • 类属性属于类所有,所有实例共享一个属性;
  • 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。

面向对象高级编程

略,后续有需要再学

错误、调试和测试

错误处理

在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就可以知道是否有错,以及出错的原因。在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数open(),成功时返回文件描述符(就是一个整数),出错时返回-1。

def foo():
    r = some_function()
    if r==(-1):
        return (-1)
    # do something
    return r

def bar():
    r = foo()
    if r==(-1):
        print('Error')
    else:
        pass
  • 一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。
  • 所以高级语言通常都内置了一套try…except…finally…的错误处理机制,Python也不例外。

try
让我们用一个例子来看看try的机制:

try:
    print('try...')
    r = 10 / 0
    print('result:', r)
except ZeroDivisionError as e:
    print('except:', e)
finally:
    print('finally...')
print('END')

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

  • 上面的代码在计算10 / 0时会产生一个除法运算错误:
try...
except: division by zero
finally...
END
  • 从输出可以看到,当错误发生时,后续语句print(‘result:’,
    r)不会被执行
    ,except由于捕获到ZeroDivisionError,因此被执行。最后,finally语句被执行。然后,程序继续按照流程往下走。
  • 如果把除数0改成2,则执行结果如下:
try...
result: 5
finally...
END
  • 由于没有错误发生,所以except语句块不会被执行,但是finally如果有,则一定会被执行(可以没有finally语句)。
  • 你还可以猜测,错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理。没错,可以有多个except来捕获不同类型的错误:
try:
    print('try...')
    r = 10 / int('a')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')
print('END')
  • int()函数可能会抛出ValueError,所以我们用一个except捕获ValueError,用另一个except捕获ZeroDivisionError。
  • 此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句
try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
else:
    print('no error!')
finally:
    print('finally...')
print('END')
  • Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:、
try:
    foo()
except ValueError as e:
    print('ValueError')
except UnicodeError as e:
    print('UnicodeError')
  • 第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了。
  • Python所有的错误都是从BaseException类派生的。
  • 使用try…except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用bar(),bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')
  • 也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try…except…finally的麻烦。

调用栈

  • 如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看err.py:
# err.py:
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    bar('0')

main()

执行结果如下:

$ python3 err.py
Traceback (most recent call last):
  File "err.py", line 11, in <module>
    main()
  File "err.py", line 9, in main
    bar('0')
  File "err.py", line 6, in bar
    return foo(s) * 2
  File "err.py", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
  • 出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
    在这里插入图片描述
    出错的时候一定要分析错误的调用栈信息, 才能定位错误的位置

记录错误
如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。

# err_logging.py

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

同样是出错,但程序打印完错误信息后会继续执行,并正常退出:

$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
  File "err_logging.py", line 13, in main
    bar('0')
  File "err_logging.py", line 9, in bar
    return foo(s) * 2
  File "err_logging.py", line 6, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END

通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

抛出错误
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。

  • 如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例
# err_raise.py
class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

foo('0')

执行,可以最后跟踪到我们自己定义的错误:

$ python3 err_raise.py 
Traceback (most recent call last):
  File "err_throw.py", line 11, in <module>
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
  • 只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError,TypeError),尽量使用Python内置的错误类型。
  • 最后我们再来看另外一种错误处理方式:
# err_reraise.py

def foo(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n

def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise

bar()
  • 在bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了,这不有病么?
  • 其实这种错误处理方式不但没病,而且相当常见。**捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,**让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。
  • raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型
try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError。

练习:
运行下面的代码,根据异常信息进行分析,定位出错误源头,并修复:
在这里插入图片描述

小总结:

  • Python内置的try…except…finally用来处理错误十分方便。出错时,会分析错误信息并定位错误发生的代码位置才是最关键的。
  • 程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。

调试

程序能一次写完并正常运行的概率很小,基本不超过1%。总会有各种各样的bug需要修正。有的bug很简单,看看错误信息就知道,有的bug很复杂,我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是错误的,因此,需要一整套调试程序的手段来修复bug。

  • 第一种方法简单直接粗暴有效,就是**用print()**把可能有问题的变量打印出来看看:
def foo(s):
    n = int(s)
    print('>>> n = %d' % n)
    return 10 / n

def main():
    foo('0')

main()
  • 执行后在输出中查找打印的变量值:
$ python err.py
>>> n = 0
Traceback (most recent call last):
  ...
ZeroDivisionError: integer division or modulo by zero
  • 用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。所以,我们又有第二种方法。

断言
凡是用print()来辅助查看的地方,都可以用断言(assert)来替代

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。

如果断言失败,assert语句本身就会抛出AssertionError

$ python err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!
  • 程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:
$ python -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

注意:断言的开关“-O”是英文大写字母O,不是数字0。
关闭后,你可以把所有的assert语句当成pass来看。

logging

  • 把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件
import logging

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
  • logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。怎么回事?
  • 别急,在import logging之后添加一行配置再试试:
import logging
logging.basicConfig(level=logging.INFO)
  • 可以看到输出了:
$ python err.py
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print(10 / n)
ZeroDivisionError: division by zero
  • 这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
  • logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

小总结:

  • 写程序最痛苦的事情莫过于调试,程序往往会以你意想不到的流程来运行,你期待执行的语句其实根本没有执行,这时候,就需要调试了。
  • 虽然用IDE调试起来比较方便,但是最后你会发现,logging才是终极武器。

单元测试

文档测试

IO编程

进程和线程

正则表达式

  • 6
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值