python基础 学习笔记

廖雪峰python 学习笔记
https://www.liaoxuefeng.com/wiki/1016959663602400/1017063413904832


一、python基础

1. 数据类型和变量

整数
Python可以处理任意大小的整数。

十六进制用0x前缀和0-9,a-f表示,例如:0xff00,0xa5b4c3d2,等等。

对于很大的数,Python允许在数字中间以_分隔,因此,写成10_000_000_000和10000000000是完全一样的。十六进制数也可以写成0xa1b2_c3d4。

浮点数
浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,比如,1.23x10^9 和 12.3x10^8是完全相等的。
浮点数可以用数学写法,如1.23,3.14,-9.01,等等。
但是对于很大或很小的浮点数,就必须用科学计数法表示,把10用e替代,1.23x10^9就是1.23e9,或者12.3e8,0.000012可以写成1.2e-5,等等。

整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的(除法难道也是精确的?是的!),而浮点数运算则可能会有四舍五入的误差。

字符串

字符串是以单引号’或双引号"括起来的任意文本。
如果’本身也是一个字符,那就可以用""括起来,比如"I’m OK"包含的字符是I,’,m,空格,O,K这6个字符。
如果字符串内部既包含’又包含"怎么办?可以用转义字符\来标识,比如:

'I\'m \"OK\"!'

表示的字符串内容是:I’m “OK”!

转义字符\可以转义很多字符,比如\n表示换行,\t表示制表符,字符\本身也要转义,所以\\表示的字符就是\

如果字符串里面有很多字符都需要转义,就需要加很多\,为了简化,Python还允许用r’‘表示’'内部的字符串默认不转义,可以自己试试:

>>> print('\\\t\\')
\       \
>>> print(r'\\\t\\')
\\\t\\

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

print('''a
b
c''')
结果:
a
b
c

布尔值
一个布尔值只有True、False两种值,要么是True,要么是False,在Python中,可以直接用True、False表示布尔值(请注意大小写),也可以通过布尔运算计算出来:

>>> True
True
>>> 3 > 5
False

可以用and、or和not运算。

and运算是与运算,只有所有都为True,and运算结果才是True:

>>> 5 > 3 or 1 > 3
True
>>> not True
False
>>> not 1 > 2
True

2. 字符串和编码

3. list和tuple

3.1 list(“[]”可修改)

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

比如:

>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']
获取个数

变量classmates就是一个list。
用len()函数可以获得list元素的个数:

>>> len(classmates)
3

一个空的list,它的长度为0:

>>> L = []
>>> len(L)
0
索引访问

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

>>> classmates[0]
'Michael'
>>> classmates[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

当索引超出了范围时,Python会报一个IndexError错误。

如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引:

>>> classmates[-1]
'Tracy'
>>> classmates[-2]
'Bob'
>>> classmates[-3]
'Michael'
>>> classmates[-4]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

当然,倒数第4个就越界了。

追加元素append、insert

list是一个可变的有序表,所以,可以往list中追加元素到末尾;也可以把元素插入到指定的位置,比如索引号为1的位置:

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

>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']
删除元素pop

要删除list末尾的元素,用pop();要删除指定位置的元素,用pop(i):

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

>>> classmates.pop(1)
'Jack'
>>> classmates
['Michael', 'Bob', 'Tracy']
替换元素(用“=”赋值)

要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:

>>> classmates[1] = 'Sarah'
>>> classmates
['Michael', 'Sarah', 'Tracy']
多种类型的元素混合

list里面的元素的数据类型也可以不同,比如:

>>> L = ['Apple', 123, True]
多维数组

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

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

如果拆开写:

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

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

3.2 tuple(“()”初始化后不能修改)

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

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

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

因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple。

tuple的陷阱

当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来,如果要定义一个空的tuple,可以写成():

>>> t = ()
>>> t
()

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

>>> t = (1)
>>> t
1

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

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

>>> t = (1,)
>>> t
(1,)

Python在显示只有1个元素的tuple时,也会加一个逗号。

“可变”的tuple
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

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

4. 条件判断

age = 3
if age >= 18:
    print('adult')
elif age >= 6:
    print('teenager')
else:
    print('kid')

elif是else if的缩写。

if语句执行有个特点,它是从上往下判断,如果在某个判断上是True,把该判断对应的语句执行后,就忽略掉剩下的elif和else

if判断条件还可以简写,比如写:
if x:
print(‘True’)
只要x是非零数值、非空字符串、非空list等,就判断为True,否则为False。

再议 input
用input()读取用户的输入,这样可以自己输入,程序运行得更有意思。但要注意input()返回的数据类型是str,str不能直接和整数比较,必须先把str转换成整数。Python提供了int()函数来完成这件事情:

s = input('birth: ')
birth = int(s)
if birth < 2000:
    print('00前')
else:
    print('00后')

但是,如果输入abc呢?又会得到一个错误信息。
原来int()函数发现一个字符串并不是合法的数字时就会报错,程序就退出了。

5. 循环

for…in循环

依次把list或tuple中的每个元素迭代出来,看例子:

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

所以for x in …循环就是把每个元素代入变量x,然后执行缩进块的语句。

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]

while循环

第二种循环是while循环

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

6. dict和set

6.1 dict(“{}”)

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

举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:
names = [‘Michael’, ‘Bob’, ‘Tracy’]
scores = [95, 75, 85]
给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,list越长,耗时越长。

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

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

——为什么dict查找速度这么快?
给定一个名字,比如’Michael’,dict在内部就可以直接计算出Michael对应的存放成绩的内存地址,直接取出来,所以速度非常快。
这种key-value存储方式,在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value。
请务必注意,dict内部存放的顺序和key放入的顺序是没有关系的。

——和list比较,dict有以下几个特点:
查找和插入的速度极快,不会随着key的增加而变慢;
需要占用大量的内存,内存浪费多。why?
——而list相反:
查找和插入的时间随着元素的增加而增加;
占用空间小,浪费内存很少。
所以,dict是用空间来换取时间的一种方法。

dict可以用在需要高速查找的很多地方,正确使用dict需要牢记的第一条就是dict的key必须是不可变对象,最常用的key是字符串。

这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:

>>> key = [1, 2, 3]
>>> d[key] = 'a list' # 在d这个dict中放入新元素
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
放入新元素

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

>>> d['Adam'] = 67
>>> d['Adam']
67
修改value

由于一个key只能对应一个value,所以,多次对一个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,如-1:

>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1

注意:返回None的时候Python的交互环境不显示结果。print(lll.get(‘m’))会显示None。

删除pop

要删除一个key,用pop(key)方法,对应的value也会从dict中删除:

>>> d.pop('Bob')
75
>>> d
{'Michael': 95, 'Tracy': 85}

6.2 set

set可以看成数学意义上的无序和无重复元素的集合。
set和dict类似,也是一组key的集合,但不存储value。

要创建一个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

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:

>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}
删除元素remove

通过remove(key)方法可以删除元素:

>>> s.remove(4)
>>> s
{1, 2, 3}
交集&并集|
>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

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

6. 不可变对象的理解

上面我们讲了,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'

要始终牢记的是,a是变量,而’abc’才是字符串对象!有些时候,我们经常说,对象a的内容是’abc’,但其实是指,a本身是一个变量,它指向的对象的内容才是’abc’:

┌───┐ ┌───────┐
│ a │─────────────────>│ ‘abc’ │
└───┘ └───────┘
当我们调用a.replace(‘a’, ‘A’)时,实际上调用方法replace是作用在字符串对象’abc’上的,而这个方法虽然名字叫replace,但却没有改变字符串’abc’的内容。相反,replace方法创建了一个新字符串’Abc’并返回,如果我们用变量b指向该新字符串,就容易理解了,变量a仍指向原有的字符串’abc’,但变量b却指向新字符串’Abc’了:

┌───┐ ┌───────┐
│ a │─────────────────>│ ‘abc’ │
└───┘ └───────┘
┌───┐ ┌───────┐
│ b │─────────────────>│ ‘Abc’ │
└───┘ └───────┘
所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的

tuple虽然是不变对象,但试试把(1, 2, 3)和(1, [2, 3])放入dict或set中,并解释结果。

lll = {(1,2,3):10,'a':20}
print(lll)
(结果为){(1, 2, 3): 10, 'a': 20}
lll[(1,[2,3])]=40
print(lll)
(报错)TypeError: unhashable type: 'list'

二、函数

函数的参数

https://www.zhihu.com/question/57726430
python中没有类型声明的语句,所以经常搞不清要给函数传什么类型的参数
没有具体的参数类型这种写法能起到类似多态的作用,使得函数不必指定具体的对象类型,增加了灵活性。但是,如果希望自己写的函数参数类型明确,应该怎么做呢?

(1) 使用__annotations__注解
在函数形参的名字后面加上“:注解”,可以实现函数注解【1】功能:

def add(x:int, y:int):   
    return x+y

对于较新的python版本可以直接使用函数的__annotations__属性查看注解内容:
print(add.annotations)
将会得到一个包含add的参数名称和对应类型说明的字典:
{‘x’: <class ‘int’>, ‘y’: <class ‘int’>}

(2) 使用isinstance进行强制类型检查
python提供的isinstance(obj, class)函数可以检查obj是不是class的对象,比如强制检查add函数中x是不是int类型:

def add(x, y):
    # 强制类型检查
    if not isinstance(x, int):
        raise ValueError("x is not int")
    if not isinstance(y, int):
        raise ValueError("y is not int")
    return x+y

现在,add函数传入的参数x和y就必须是int类型,否则就会报错。

位置参数

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

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)
25
>>> power(5, 2)
25

注意:
(1) 必选参数在前,默认参数在后
(2) 多个参数时把变化大的放前面,变化小的放后面
(3) 若不按顺序提供默认参数,需要把参数名写上。比如:
def enroll(name, gender, age=6, city=‘Beijing’):
调用enroll(‘Adam’, ‘M’, city=‘Tianjin’),意思是,city参数用传进去的值,其他默认参数继续使用默认值。
(4) 大坑:默认参数必须指向不变对象。否则调用该函数时,如果改变了默认参数的内容,那么下次调用时默认参数的内容仍然是变化后的内容而非定义时的内容。
——为什么要设计str、None这样的不变对象呢?
1) 因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。
2) 由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。
我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数(可变n个参数,前面加*)

可变参数就是传入的参数个数是可变的
定义可变参数时,仅仅在参数前面加了一个*号。如:

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

在函数内部,参数numbers接收到的是一个tuple。调用该函数时,可以传入任意个参数,包括0个参数。

若已有一个list或tuple,要调用一个可变参数,可以在其前面加*,把其中的元素变为可变参数传进去。

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

关键字参数(可变n个含参数名的参数,前面加**)

对比:
可变参数(前加*):可以传入任意个参数,函数调用时自动组装为一个tuple
关键字参数(前加**):可以传入任意个含参数名的参数,函数调用时自动组装为一个dict

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)
>>> person('Michael', 30)
name: Michael age: 30 other: {}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

作用是可以保证接收到name和age两个参数,其他参数若有提供则亦可以接收到。

若已有一个dict,可以在其前面加**,来传入。

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

命名关键字参数(*分隔)

命名关键字参数/限定关键字形参,当然就是为了限制后面几个参数只能按关键字传递,这往往是因为后面几个形参名具有十分明显的含义,显式写出有利于可读性;或者后面几个形参随着版本更迭很可能发生变化,强制关键字形式有利于保证跨版本兼容性。

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

def person(name, age, *, city, job):
    print(name, age, city, job)
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
>>> person('Jack', 24, city='Beijing')
TypeError: person() missing 1 required keyword-only argument: 'job'
>>>person('Jack', 24, city='Beijing',job='Engineer', hobby='tennis')
TypeError: person() got an unexpected keyword argument 'hobby'

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

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

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

def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

参数组合

参数定义的顺序必须是:
必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

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

f1(1, 2)
f1(1, 2, 'a')
f1(1, 2, 3, 'a', 'b')
f1(1, 2, 3, 'a', 'b', x=99)
f1(1, 2, d=99, ext=None)

结果:
a = 1 b = 2 c = 0 args = () kw = {}
a = 1 b = 2 c = a args = () kw = {}
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
a = 1 b = 2 c = 0 args = () kw = {'d': 99, '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': '#'}
def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

f2(1, 2, d=99, ext=None)
args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)
结果:
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

对于任意函数,都可以通过类似func(*args, kw)的形式调用它,无论它的参数是如何定义的。
args是可变参数,args接收的是一个tuple;
**kw是关键字参数,kw接收的是一个dict。
使用
args和
kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

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

递归函数

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

针对尾递归优化的语言可以通过尾递归防止栈溢出。(大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化。)尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。

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

举例:汉诺塔
编写move(n, a, b, c)函数,接收参数n表示3个柱子A、B、C中第1个柱子A的盘子数量,然后打印出把所有盘子从A借助B移动到C的方法。

def move(n, a, b, c):
    if n == 1:
        print(a, '-->', c)
    else:
        move(n-1, a, c, b)
        move(1, a, b, c)
        move(n-1, b, a, c)

三、高级特性

python中有很多高级特性可以使代码更精简。


1.切片(list、tuple、字符串)

一个list如下:

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

取前3个元素,应该怎么做?

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

>>> r = []
>>> n = 3
>>> for i in range(n):
...     r.append(L[i])
... 
>>> r
['Michael', 'Sarah', 'Tracy']

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

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

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

如果索引是0,还可以省略;支持倒数切片;甚至什么都不写,只写[:]就可以原样复制一个list:

>>> L[:3]
['Michael', 'Sarah', 'Tracy']
>>> L[1:3]
['Sarah', 'Tracy']
>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']

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

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

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

>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

list[start : end : step]
start:起始位置(不写为0)
end:结束位置(不写为0)
step:步长

在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。

练习:
利用切片操作,实现一个trim()函数,去除字符串首尾的空格,注意不要调用str的strip()方法:

def trim(s):
    if s=='': #或者len(s)==0:  这里易漏
        return s
    else:
        while s[0] == ' ':
            s=s[1:]
            if s=='':   #这里易漏
                return s
        while s[-1] == ' ':
            s=s[:-1]
            if s=='':
                return s
    return s

递归方法:

def trim(s):
    if s=='':  #或者len(s)==0:
        return s
    else:
        if s[0] == ' ':
            s=s[1:]
            return(trim(s))
        if s[-1] == ' ':
            s=s[:-1]
            return(trim(s))
    return s

2.迭代

如果给定一个list或tuple(或其他可迭代对象),我们可以通过for循环来遍历,这种遍历我们称为迭代(Iteration)。
在Python中,迭代是通过for … in来完成的。
而很多语言比如C语言,迭代list是通过下标完成的。
可以看出,Python的for循环抽象程度要高于C的for循环,因为Python的for循环不仅可以用在list或tuple上,还可以作用在其他没有下标的可迭代对象上。

2.1 list的迭代

for k in ['a', 'b', 'c']:
    print(k)

如果要对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

2.2 字符串的迭代

由于字符串也是可迭代对象,因此,也可以作用于for循环:

>>> for ch in 'ABC':
...     print(ch)
...
A
B
C

2.3 dict的迭代

默认情况下,dict迭代的是key。
因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。

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

如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。

d = {'a': 1, 'b': 2, 'c': 3}
for value in d.values():
    print(value)
结果为:
1
2
3
d = {'a': 1, 'b': 2, 'c': 3}
for k,v in d.items():
    print(k,v)
结果为:
a 1
b 2
c 3

2.4 可迭代对象

只要作用于一个可迭代对象,for循环就可以正常运行。
那么,如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:

from collections.abc import Iterable
print(isinstance('abc', Iterable)) # str是否可迭代
print(isinstance([1,2,3], Iterable)) # list是否可迭代
print(isinstance(123, Iterable)) # 整数是否可迭代
结果:
True
True
False

3.列表生成式List Comprehensions

列表生成式即List Comprehensions,是Python内置的,可以快速生成list,也可以通过一个list推导出另一个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]

但如果要生成[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]

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

>>> [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']

列表生成式也可以使用两个变量来生成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']

在一个列表生成式中,
for后面的if是过滤条件,不能带else:

>>> L=[x+3 for x in range(10) if x%2==0]
[3, 5, 7, 9, 11]

for前面若有if必须带else,这里的if … else是表达式:

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

4. 生成器generator

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

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

举个简单的例子,定义一个generator,依次返回数字1,3,5:

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)

generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

调用该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)就报错。

生成方法

类似生成list的方法(for语句)

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

>>> L = [x * x for x in range(4)]
>>> L
[0, 1, 4, 9]
>>> g = (x * x for x in range(4))
>>> g
<generator object <genexpr> at 0x1022ef630>

普通函数调用直接返回结果;generator函数的“调用”实际返回一个generator对象。
我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
可以通过next()函数获得generator的下一个返回值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。:

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

当然,更好的方法是使用for循环,因为generator也是可迭代对象:

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

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

用函数实现(包含yield)

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

比如,著名的斐波拉契数列(Fibonacci):
1, 1, 2, 3, 5, 8, 13, 21, 34, …

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

def fib(max):
    n, a, b = 0, 1, 1  # n用来记录打印出前几个数
    while n < max:
        print(a)
        a, b = b, a+b  # 不必显式写出临时变量就可以赋值,相当于temp=a+b, a=b, b=temp
        # 也相当于t = (b, a + b) , a = t[0], b = t[1]  # t是一个tuple
        n = n + 1
    return ‘done’

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
要把fib函数变成generator,只需要把print(b)改为yield b就可以了:

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

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

直接print(fib(6))不会打印出数列,得到<generator object fib at 0x00000147C313AAC0>
用next逐个打印:

o = fib(3)
print(next(o))
print(next(o))
print(next(o))

同样的,我们基本上从来不会用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

关于如何捕获错误,后面的错误处理还会详细讲解。

练习:
杨辉三角:

def yanghui():
    t = [1]
    while True:
        yield t
        temp=[]
        for i in range(len(t)-1): # 一开始len-1为0,range(0)为空,不执行循环
            temp.append(t[i]+t[i+1] )
        temp.insert(0,1)
        temp.append(1)
        t=temp
o = yanghui()
for i in range(5):
    print(next(o)) # 打印得到前5行

可以用列表生成式进行简化:

def triangles():
	L = [1]
	while True:
		yield L
		L = [L[i] + L[i + 1 ] for i in range(len(L) - 1 ) ] 
		L.insert(0, 1)        
		L.append(1)             

5. 迭代器iterator

区别iterable和iterator:
可迭代的 和 迭代器:

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

Iterable

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

可以使用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

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象:

>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

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

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

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

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

三、函数式编程

函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

我们首先要搞明白计算机(Computer)和计算(Compute)的概念:
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。

1. 高阶函数

>>> abs(-10)
10
>>> abs
<built-in function abs>

>>> f = abs
>>> f
<built-in function abs>
>>> f(-10)
10

可见:
①abs(-10)是函数调用,而abs是函数本身。
②函数本身也可以赋值给变量,即:变量可以指向函数。可通过该变量来调用这个函数,直接调用abs()函数和调用变量f()完全相同。
③函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

如果把abs指向其他对象,会有什么情况发生?

>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

把abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!当然实际代码绝对不能这么写。注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10。

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:

def add(x, y, f):
    return f(x) + f(y)
print(add(-5, 6, abs))

当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,结果为11。
编写高阶函数,就是让函数的参数能够接收别的函数。

map/reduce

Python内建了map()和reduce()函数。

map

map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
如:

>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r) # 直接打印得到的是map object,用list()把序列转换为列表
[1, 4, 9, 16, 25, 36, 49, 64, 81]

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

map()作为高阶函数,把运算规则抽象了,因此,我们还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:

>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce

使用前要:

from functools import 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]) # add直接写x+y会报错,add要先行定义

当然求和运算可以直接用Python内建函数sum(),没必要动用reduce。

但是如果要把序列[1, 3, 5, 7, 9]变换成整数13579,reduce就可以派上用场:

>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579

这个例子本身没多大用处,但是,如果考虑到字符串str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换为int的str2int函数:

from functools import reduce

DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def str2int(s):
    def fn(x, y):
        return x * 10 + y
    def char2num(s):
        return DIGITS[s]
    return reduce(fn, map(char2num, s))

还可以用lambda函数进一步简化成:

from functools import reduce

DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def char2num(s):
    return DIGITS[s]

def str2int(s):
    return reduce(lambda x, y: x * 10 + y, map(char2num, s))

也就是说,假设Python没有提供int()函数,你完全可以自己写一个把字符串转化为整数的函数。
lambda函数的用法在后面介绍。

练习:
(1) 利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:[‘adam’, ‘LISA’, ‘barT’],输出:[‘Adam’, ‘Lisa’, ‘Bart’]:

def normalize(name):
   name=name[0].upper()+name[1:].lower()   # 注意upper和lower用法
   return name
L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)

错误写法: name[0] = name[0].upper()
报错:TypeError: ‘str’ object does not support item assignment
在python中,字符串是不可变对象,不能通过下标的方式直接赋值修改。
(2) 利用map和reduce编写一个str2float函数,把字符串’123.456’转换成浮点数123.456:

from functools import reduce

def str2float(s):
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def fn1(x, y):
return x * 10 + y
def fn2(i,j):
return 0.1*i+j
def char2num(s):
return DIGITS[s]
m= s.split('.')[0] # 用split对字符串进行分割
n= s.split('.')[1]
m1=list(map(char2num,m))
n1=list(map(char2num,n))[::-1] # 用[::-1]将list逆置
print(n1)
return reduce(fn1, m1)+reduce(fn2,n1)*0.1 # 注意要*0.1

注意:
①用split对字符串进行分割
②用[::-1]将list逆置
③*0.1

filter

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

和map()类似,filter()也接收一个函数和一个序列。filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

注意filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list

例如:
(1) 在一个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]

把一个序列中的空字符串删掉,可以这么写:

def not_empty(s):
    return s and s.strip() # 为什么不只写s.strip()

list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
# 结果: ['A', 'B', 'C']

注意:
strip() 方法用于移除字符串头尾指定的字符(默认为空格)或字符序列
②在布尔上下文中从左到右演算表达式的值,如果布尔上下文中的所有值都为真,那么 and 返回最后一个值。
如果布尔上下文中的某个值为假,则 and 返回第一个假值。
如print(None and ‘A’)结果为None,print(‘B’ and ‘A’)结果为A。
为什么不只写s.strip()

(2) 计算素数的一个方法是埃氏筛法:
首先,列出从2开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
不断筛下去,就可以得到所有的素数。

用Python来实现这个算法,可以先构造一个从3开始的奇数序列(为什么不列出所有的数?因为2后面的偶数肯定不是素数,可以不列):

def _odd_iter():
    n = 1
    while True:
        n = n + 2
        yield n

注意这是一个生成器,并且是一个无限序列。

然后定义一个筛选函数:

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

注意到Iterator是惰性计算的序列,所以我们可以用Python表示“全体自然数”,“全体素数”这样的序列,而代码非常简洁。

练习
回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()筛选出回数:

def is_palindrome(n):
    return str(n) == str(n)[::-1]
     
# 测试代码
if list(filter(is_palindrome, range(1, 200))) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]:
    print('测试成功!')
else:
    print('测试失败!')

str() 函数将指定的值转换为字符串。

sorted

Python内置的sorted()函数就可以对list进行排序。此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

我们再看一个字符串排序的例子。默认情况下,对字符串排序,是按照ASCII的大小比较的,由于’Z’ < ‘a’,结果,大写字母Z会排在小写字母a的前面。现在,我们提出排序应该忽略大小写,按照字母序排序。

>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
>>> 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']

练习:
假设我们用一组tuple表示学生名字和成绩:
L = [(‘Bob’, 75), (‘Adam’, 92), (‘Bart’, 66), (‘Lisa’, 88)]
请用sorted()对上述列表分别按名字排序,再按成绩从高到低排序:

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

def by_name(t):
    return t[0]
L2 = sorted(L, key=by_name)
print(L2)

def by_score(t):
    return t[1]
L2 = sorted(L, key=by_score,reverse=True)
print(L2)

2. 返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

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时,才真正计算求和的结果:

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
>>> 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()的调用结果互不影响。

闭包

需要注意的是,返回的函数并没有立刻执行,而是直到调用了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

练习:
利用闭包返回一个计数器函数,每次调用它返回递增整数:

def createCounter():
    n = 0
    def counter():
        nonlocal n
        n+=1
        return n
    return counter

# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('测试通过!')
else:
    print('测试失败!')

3. 匿名函数

以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

print(build(3,4))
print(build(3,4)())
# 打印结果:
<function build.<locals>.<lambda> at 0x000001C7F0A2E280>
25

4. 装饰器

???
https://www.liaoxuefeng.com/wiki/1016959663602400/1017451662295584

5. 偏函数

通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换;但如果传入base参数(默认为10),就可以做N进制的转换:

>>> int('12345')
12345
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假设要转换大量的二进制字符串,可以定义一个int2()的函数,默认把base=2传进去:

def int2(x, base=2):
    return int(x, base)
    
>>> int2('1000000')
64

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>
>>> int2('1000000')
64
>>> int2('1000000', base=10) #把base参数重新设定默认值为2,但也可以在函数调用时传入其他值
1000000 

所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

最后,创建偏函数时,实际上可以接收函数对象、*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。

附:
*args是非关键字参数,用于元组,**kw关键字参数,用于字典。
这两个作为python的可变参数,也就是说args表示任何多个无名参数,kwags表示一个一个有着对应关系的关键字参数。
在使用的时候需要注意,*args要在**kwags之前,不然会发生语法错误。如:

def foo(*args,**kwargs):
    print 'args is',args
    print 'kwargs is',kwargs
foo(1,2)
foo(k=1,w=2,a=3,r=4,g=5,s=6)
foo(1,2,a=1,b=2,c=2)
foo('a',1,None,a=1,b='2',c=3)
# 结果:
args is (1, 2)
kwargs is {}
args is ()
kwargs is {'a': 3, 'g': 5, 'k': 1, 's': 6, 'r': 4, 'w': 2}
args is (1, 2)
kwargs is {'a': 1, 'c': 2, 'b': 2}
args is ('a', 1, None)
kwargs is {'a': 1, 'c': 3, 'b': '2'}

四、模块

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少。在Python中,一个.py文件就称之为一个模块(Module)。

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。点这里查看Python的所有内置函数。

如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。
现在,假设我们的abc和xyz这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany,按照如下目录存放:

mycompany
├─ init.py
├─ abc.py
└─ xyz.py
引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变成了mycompany.abc,类似的,xyz.py的模块名变成了mycompany.xyz。

请注意,每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。init.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany。

类似的,可以有多级目录,组成多级层次的包结构。比如如下的目录结构:

mycompany
├─ web
│ ├─ init.py
│ ├─ utils.py
│ └─ www.py
├─ init.py
├─ abc.py
└─ utils.py
文件www.py的模块名就是mycompany.web.www,两个文件utils.py的模块名分别是mycompany.utils和mycompany.web.utils。

创建自己的模块时,要注意:
模块名要遵循Python变量命名规范,不要使用中文、特殊字符;
模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块。

使用模块

https://www.liaoxuefeng.com/wiki/1016959663602400/1017455068170048

安装第三方模块

https://www.liaoxuefeng.com/wiki/1016959663602400/1017493741106496

五、面向对象编程

面向对象的设计思想是抽象出Class,根据Class创建Instance。

一个Class既包含数据,又包含操作数据的方法。

数据封装、继承和多态是面向对象的三大特点。

类和实例

以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本身则是一个类。

和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。可以自由地给一个实例变量绑定属性,比如,给实例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
 
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59

注意到__init__方法的第一个参数永远是self,表示创建的实例本身。有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。

方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据。通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。这就是数据封装
和普通的函数相比,在类中定义的函数(方法)只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递self参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

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

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'
            
lisa = Student('Lisa', 99)
lisa.print_score()
print(lisa.get_grade())

访问限制

私有变量:
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在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))

    def get_name(self): # 外部代码可以通过get_name获取name
        return self.__name

    def get_score(self):
        return self.__score

    def set_score(self, score): # 直接通过bart.score = 99也可以修改,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad 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是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

>>> bart._Student__name
'Bart Simpson'

但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。

注意下面的这种错误写法:

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

特殊变量
需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__、__score__这样的变量名。

有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

继承和多态

继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印。当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:

class Animal(object):
    def run(self):
        print('Animal is running...')
        
class Dog(Animal):
    pass

class Cat(Animal):
    pass

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

dog = Dog()
dog.run()

cat = Cat()
cat.run()

# 运行结果如下:
Animal is running...
Animal is running...

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

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

当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样。判断一个变量是否是某个类型可以用isinstance()判断:

a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
>>> isinstance(c, Animal)
True
>>> b = Animal()
>>> isinstance(b, Dog)
False

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的开闭原则

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的函数。

继承还可以一级一级地继承下来。

静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:

class Dog(Animal):
    def run(self):
        print('Dog is running')
class Timer(object):
    def run(self):
        print('Start')

def run_twice(Animal):
    Animal.run()
    Animal.run()
    
run_twice(Animal())
run_twice(Dog())
run_twice(Timer()) # Timer不是Animal类型,但是也可以作为参数传入到run_twice中

#结果
Animal is running
Animal is running
Dog is running
Dog is running
Start
Start

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

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

实例属性和类属性

当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到:

>>> class Student(object):
...     name = 'Student' # 类属性name
...
>>> s = Student() # 创建实例s
>>> print(s.name) 
Student
>>> print(Student.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类增加一个类属性,每创建一个实例,该属性自动增加:

class Student(object):
    count = 0

    def __init__(self, name):
        self.name = name
        Student.count+=1

面向对象高级编程

数据封装、继承和多态只是面向对象程序设计中最基础的3个概念。在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。

使用__slots__

除了可以给一个实例绑定属性,还可以绑定方法,也可以给class绑定方法。
使用__slots__可以限制实例的属性。但这仅对当前类的实例起作用,对继承的子类是不起作用的。除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。
https://www.liaoxuefeng.com/wiki/1016959663602400/1017501655757856

使用@property

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值