Python 学习笔记
关于编码
- ASCII编码——只有127个字符被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为
ASCII
编码,比如大写字母A
的编码是65
,小写字母z
的编码是122
。 - 为了适配多种语言,发明了Unicode编码。最常用的是UCS-16编码,用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。现代操作系统和大多数编程语言都直接支持Unicode。
- 为了节约存储空间,把Unicode编码转化为“可变长编码”的
UTF-8
编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:
Python3 字符串使用Unicode编码
对于单个字符的编码,**ord()获取字符的整数表示,chr()**把编码转换为对应的字符
对于字符串,Python的字符串类型是
str
,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str
变为以字节为单位的bytes
。Unicode表示的str通过encode()方法编码为指定的bytes
1
2
3
4
5
6
7
8'ABC'.encode('ascii')
b'ABC'
'中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
'中文'.encode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
- 在
bytes
中,无法显示为ASCII字符的字节,用\x##
显示。
- 反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是
bytes
。要把bytes
变为str
,就需要用decode()
方法:
1 | b'ABC'.decode('ascii') |
- 如果
bytes
中包含无法解码的字节,decode()
方法会报错 - 如果
bytes
中只有一小部分无效的字节,可以传入errors='ignore'
忽略错误的字节:
1 | b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore') |
- 要计算str包含多少个字符,可以用len()函数
1 | len('ABC') |
len()
函数计算的是str
的字符数,如果换成bytes
,len()
函数就计算字节数:
1 | len(b'ABC') |
- 保存源代码保存为UTF-8编码,按照UTF-8读取
1 | #!/usr/bin/env python3 |
- 字符串替换
%
运算符就是用来格式化字符串的。在字符串内部,%s
表示用字符串替换,%d
表示用整数替换,有几个%?
占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?
,括号可以省略。
1 | 'Hello, %s' % 'world' |
- 用
%%
来表示一个%
:
1 | 'growth rate: %d %%' % 7 |
- format()
另一种格式化字符串的方法是使用字符串的format()
方法,它会用传入的参数依次替换字符串内的占位符{0}
、{1}
……,不过这种方式写起来比%要麻烦得多:
1 | 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) |
- F-string
最后一种格式化字符串的方法是使用以f
开头的字符串,称之为f-string
,它和普通字符串不同之处在于,字符串如果包含{xxx}
,就会以对应的变量替换:
1 | >>> r = 2.5 |
- 两种除法
/
除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:
1 | 10 / 3 |
- 还有一种除法是
//
,称为地板除,两个整数的除法仍然是整数:
1 | 10 // 3 |
list & tuple
list
list是一种有序的集合,可以随时添加和删除其中的元素(类似于数组,但不完全一样)
- 索引从0开始
- 有序集合
- len()获得list元素的个数
- 取最后一个元素,可以使用-1作为索引
1 | 'Michael', 'Bob', 'Tracy'] classmates = [ |
- 使用append追加元素
1 | 'Adam') classmates.append( |
- 把元素插入到指定的位置,比如索引号为1的位置
1 | 1, 'Jack') classmates.insert( |
- 删除list末尾的元素,用pop()
1 | classmates.pop() |
- 删除指定位置的元素,用pop(i)
1 | 1) classmates.pop( |
- list里元素数据类型可以不同
1 | L = ['Apple',123,True] |
- list元素可以是另一个list(这个整个list作为另一个list的元素)
- 要拿到’php’得写s1[1](s1 = [‘asp’, ‘php’]) 或者 s[2][1]
1 | 'python', 'java', ['asp', 'php'], 'scheme'] s = [ |
tuple
元组,也是一种有序列表,tuple与list类似,但是tuple一旦初始化之后就不能修改,不能添加、删除和修改,tuple不可变,是指它的内容不能修改,但变量可以指向新的tuple
现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0]
,classmates[-1]
,但不能赋值成另外的元素。
1 | 'Michael', 'Bob', 'Tracy') classmates = ( |
- 当定义一个tuple时,tuple的元素就必须被确定下来
1 | 1, 2) t = ( |
- 定义一个空的tuple
1 | t = () |
- 只有1个元素的tuple定义时必须加一个逗号,,来消除歧义
1 | 1,) t = ( |
- “可变”的tuple
1 | 'a', 'b', ['A', 'B']) t = ( |
当我们把list的元素'A'
和'B'
修改为'X'
和'Y'
后,tuple变为:
条件判断
复杂匹配
1 | age = 15 |
匹配列表
1 | args = ['gcc', 'hello.c', 'world.c'] |
循环
- For … In …
1 | names = ['Michael', 'Bob', 'Tracy'] |
- Range (n),从0开始小于n的整数
1 | list(range(5)) |
- While
1 | sum = 0 |
- Break
1 | n = 1 |
- Continue
1 | n = 0 |
Dict & set
Dict
字典,使用key-value存储,具有极快的查找速度
1 | 'Michael': 95, 'Bob': 75, 'Tracy': 85} d = { |
把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:
1 | 'Adam'] = 67 d[ |
查询key是否存在。通过dict提供的get()
方法,如果key不存在,可以返回None
,或者自己指定的value:
1 | 'Thomas') d.get( |
删除key,使用pop(key)
1 | 'Bob') d.pop( |
Set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key,且set中的值即key值不可以改变
创建一个set,用{x,y,z,...}
列出每个元素:
1 | 1, 2, 3} s = { |
或者提供一个list作为输入集合:
1 | set([1, 2, 3]) s = |
重复元素在set中自动被过滤:
1 | 1, 1, 2, 2, 3, 3} s = { |
通过**add(key)
**方法可以添加元素到set中,可以重复添加,但不会有效果:
1 | 4) s.add( |
通过**remove(key)
**方法可以删除元素:
1 | 4) s.remove( |
set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
1 | 1, 2, 3} s1 = { |
set和dict的唯一区别仅在于没有存储对应的value
不可变对象
上面我们讲了,str是不变对象,而list是可变对象。
对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
1 | 'c', 'b', 'a'] a = [ |
对于不可变对象,比如str,对str进行操作呢:
1 | 'abc' a = |
要始终牢记的是,a
是变量,而'abc'
才是字符串对象!有些时候,我们经常说,对象a
的内容是'abc'
,但其实是指,a
本身是一个变量,它指向的对象的内容才是'abc'
:
当我们调用a.replace('a', 'A')
时,实际上调用方法replace
是作用在字符串对象'abc'
上的,而这个方法虽然名字叫replace
,但却没有改变字符串'abc'
的内容。相反,replace
方法创建了一个新字符串'Abc'
并返回,如果我们用变量b
指向该新字符串,就容易理解了,变量a
仍指向原有的字符串'abc'
,但变量b
却指向新字符串'Abc'
了:——相当于是创建了新的对象
函数
从其他python文件中导入函数
如果你已经把my_abs()
的函数定义保存为abstest.py
文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs
来导入my_abs()
函数,注意abstest
是文件名(不含.py
扩展名):
1 | from abstest import my_abs |
空函数
如果想定义一个什么事也不做的空函数,可以用pass
语句:
1 | def nop(): |
pass
语句什么都不做,那有什么用?实际上pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
pass
还可以用在其他语句里,比如:
1 | if age >= 18: |
缺少了pass
,代码运行就会有语法错误。
参数检查
让我们修改一下my_abs
的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()
实现:
1 | def my_abs(x): |
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
1 | 'A') my_abs( |
isinstance()函数是一个内置函数,用来检查一个对象是否是指定类或类型的实例
1 | isinstance(object, classinfo) |
object:要检查的对象。
classinfo:一个类、类型或元组,它可以是一个单一类型,也可以是多个类型组成的元组。
raise
用于在 Python 中显式地抛出一个异常。可以抛出标准的异常,也可以抛出自定义的异常。语法如下:
1 | raise ExceptionType("Error message") |
ExceptionType:异常的类型,可以是 Python 内置的异常类型(如 ValueError
, TypeError
, IndexError
等),也可以是自定义的异常类。
**”Error message”**:异常的错误信息,它是一个字符串,描述错误的详细信息。
返回多个值
1 | import math |
实际上python返回的仍然是单一值,返回值为一个tuple,语法上,返回一个tuple可以省略括号
1 | 100, 100, 60, math.pi / 6) r = move( |
默认参数
1 | def power(x, n=2): |
这样,当我们调用power(5)
时,相当于调用power(5, 2)
:
而对于n > 2
的其他情况,就必须明确地传入n,比如power(5, 3)
。
1 | 5) power( |
设置默认参数时,有几点要注意:
一是必选参数在前,默认参数在后,否则Python的解释器会报错
二是如何设置默认参数。
当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
默认参数的坑:可变对象作为默认值
默认参数的默认值只会在函数定义时计算一次! 这对于 可变对象(如 list
****, dict
****, **set
**) 可能会导致意外行为。
❌ 错误示例
1 | def add_item(item, item_list=[]): |
原因:
item_list=[]
这个默认值在 函数定义时 只计算一次,而不是每次调用时都创建一个新的列表。- 结果是 所有函数调用都共享同一个 **
item_list
**,导致数据意外累积。
✅ 正确做法:使用 None
作为默认值
1 | def add_item(item, item_list=None): |
特别注意
定义默认参数要牢记一点:默认参数必须指向不变对象!
1 | #我们可以用None这个不变对象来实现: |
为什么要设计str
、None
这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。
可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
我们以数学题为例子,给定一组数字a,b,c……,请计算a(2) + b(2) + c(2) + ……。
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
1 | def calc(numbers): |
但是调用的时候,需要先组装出一个list或tuple:
1 | 1, 2, 3]) calc([ |
如果利用可变参数,调用函数的方式可以简化成这样:
1 | 1, 2, 3) calc( |
所以,我们把函数的参数改为可变参数:
1 | def calc(*numbers): |
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
1 | 1, 2) calc( |
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
1 | 1, 2, 3] nums = [ |
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
1 | 1, 2, 3] nums = [ |
关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
1 | def person(name, age, **kw): |
函数person
除了必选参数name
和age
外,还接受关键字参数kw
。在调用该函数时,可以只传入必选参数:
1 | 'Michael', 30) person( |
也可以传入任意个数的关键字参数:
1 | 'Bob', 35, city='Beijing') person( |
关键字参数有什么用?它可以扩展函数的功能。比如,在person
函数里,我们保证能接收到name
和age
这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
1 | 'city': 'Beijing', 'job': 'Engineer'} extra = { |
简化写法
1 | 'city': 'Beijing', 'job': 'Engineer'} extra = { |
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw
检查。
仍以person()
函数为例,我们希望检查是否有city
和job
参数:
1 | def person(name, age, **kw): |
但是调用者仍可以传入不受限制的关键字参数:
1 | 'Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456) person( |
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
1 | 'Jack', 24, city='Beijing', job='Engineer') person( |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
1 | 'Jack', 24, 'Beijing', 'Engineer') person( |
由于调用时缺少参数名city
和job
,Python解释器把前两个参数视为位置参数,后两个参数传给*args
,但缺少命名关键字参数导致报错。
命名关键字参数可以有缺省值,从而简化调用:
1 | def person(name, age, *, city='Beijing', job): |
由于命名关键字参数city
具有默认值,调用时,可不传入city
参数:
1 | 'Jack', 24, job='Engineer') person( |
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
1 | def person(name, age, city, job): |
参数组合
参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
1 | def f1(a, b, c=0, *args, **kw): |
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
1 | 1, 2) f1( |
最神奇的是通过一个tuple和dict,你也可以调用上述函数:
1 | 1, 2, 3, 4) args = ( |
所以,对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
递归函数
为了防止栈溢出使用尾递归优化
尾递归(Tail Recursion) 是指 递归调用发生在函数的最后一步,并且递归调用的返回值 直接作为当前函数的返回值 (即不需要进一步计算)。
1 | def fact(n): |
可以看到,return fact_iter(num - 1, num * product)
仅返回递归函数本身,num - 1
和num * product
在函数调用前就会被计算,不影响函数调用。
fact(5)
对应的fact_iter(5, 1)
的调用如下:
1 | => fact_iter(5, 1) |
尾递归很好,但大部分编程语言都没有针对尾递归做优化,Python解释器也没有做优化,所以即使把上面的fact(n)函数改为尾递归的方式,也会导致栈溢出
练习-汉诺塔问题
1 | def move(n, a, b, c): |
高级特性
切片
取list或tuple的部分元素
L[0:3]
表示,从索引0
开始取,直到索引3
为止,但不包括索引3
。即索引0
,1
,2
,正好是3个元素。
如果第一个索引是0
,还可以省略:
1 | 'Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] L = [ |
如果第一个索引是0
,还可以省略:
1 | 3] L[: |
也可以从索引1开始,取出2个元素出来:
1 | 1:3] L[ |
前10个数,每两个取一个:
1 | 10:2] L[: |
所有数,每5个取一个:
1 | 5] L[:: |
甚至什么都不写,只写[:]
就可以原样复制一个list:
1 | L[:] |
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
1 | 0, 1, 2, 3, 4, 5)[:3] ( |
字符串'xxx'
也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
1 | 'ABCDEFG'[:3] |
迭代
Python的for
循环不仅可以用在list
或tuple
上,还可以作用在其他可迭代对象上。
1 | 'a': 1, 'b': 2, 'c': 3} d = { |
因为dict
的存储不是按照list
的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
默认情况下,dict
迭代的是key。如果要迭代value,可以用for value in d.values()
,如果要同时迭代key和value,可以用for k, v in d.items()
。
由于字符串也是可迭代对象,因此,也可以作用于for
循环:
1 | for ch in 'ABC': |
通过collections.abc
模块的Iterable
类型判断一个对象是否是可迭代对象
1 | from collections.abc import Iterable |
Python内置的enumerate
函数可以把一个list
变成索引-元素对,这样就可以在for
循环中同时迭代索引和元素本身,对list
实现类似Java那样的下标循环
1 | for i, value in enumerate(['A', 'B', 'C']): |
上面的for
循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:
1 | for x, y in [(1, 1), (2, 4), (3, 9)]: |
列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以用list(range(1, 11))
:
1 | list(range(1, 11)) |
生成[1x1, 2x2, 3x3, ..., 10x10]
方法一是循环:
1 | L = [] |
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
1 | >>> [x * x for x in range(1, 11)] |
写列表生成式时,把要生成的元素x * x
放到前面,后面跟for
循环,就可以把list创建出来
加上if判断,筛选出仅偶数的平方:
1 | for x in range(1, 11) if x % 2 == 0] [x * x |
使用两层循环,可以生成全排列:
1 | for m in 'ABC' for n in 'XYZ'] [m + n |
列出当前目录下的所有文件和目录名,可以通过一行代码实现:
1 | import os # 导入os模块,模块的概念后面讲到 |
for
循环其实可以同时使用两个甚至多个变量,比如dict
的items()
可以同时迭代key和value:
1 | 'x': 'A', 'y': 'B', 'z': 'C' } d = { |
列表生成式也可以使用两个变量来生成list:
1 | 'x': 'A', 'y': 'B', 'z': 'C' } d = { |
最后把一个list中所有的字符串变成小写:
1 | 'Hello', 'World', 'IBM', 'Apple'] L = [ |
if…else
以下代码正常输出偶数:
1 | for x in range(1, 11) if x % 2 == 0] [x |
但是,我们不能在最后的if
加上else
:
1 | for x in range(1, 11) if x % 2 == 0 else 0] [x |
把if
写在for
前面必须加else
,否则报错:
1 | if x % 2 == 0 for x in range(1, 11)] [x |
这是因为for
前面的部分是一个表达式,它必须根据x
计算出一个结果。因此,考察表达式:x if x % 2 == 0
,它无法根据x
计算出结果,因为缺少else
,必须加上else
:
1 | if x % 2 == 0 else -x for x in range(1, 11)] [x |
在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
生成器
生成器——generator:在Python中,一边循环一边计算的机制,用于节省空间。
创建一个generator,把一个列表生成式的[]
改成()
:
1 | for x in range(10)] L = [x * x |
创建L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator。
打印出generator的每一个元素
可以通过next()
函数获得generator的下一个返回值:
1 | next(g) |
generator保存的是算法,每次调用next(g)
,就计算出g
的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。
正常遍历方法,使用for
循环,因为generator也是可迭代对象:
1 | for x in range(10)) g = (x * x |
使用函数实现斐波拉契
1 | def fib(max): |
注:
1 | a, b = b, a + b |
相当于——用tuple是它更轻量级,为只读的数据结构,更安全
1 | t = (b, a + b) # t是一个tuple |
这其实是一个打包-解包的操作
- 避免使用临时变量
- 计算时不会覆盖旧值
要把fib
函数变成generator函数,只需要把print(b)
改为yield b
就可以了:
1 | def fib(max): |
这就是定义generator的另一种方法。如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator:
1 | 6) f = fib( |
generator函数和普通函数的执行流程不一样。普通函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
举个简单的例子,定义一个generator函数,依次返回数字1,3,5:
1 | def odd(): |
调用该generator函数时,首先要生成一个generator对象,然后用next()
函数不断获得下一个返回值:
1 | o = odd() |
例如:这样调用next()
每次都返回1:
1 | next(odd()) |
原因在于odd()
会创建一个新的generator对象,上述代码实际上创建了3个完全独立的generator,对3个generator分别调用next()
当然每个都会返回第一个值。
正确的写法是创建一个generator对象,然后不断对这一个generator对象调用next()
:
1 | g = odd() |
但是用for
循环调用generator时,发现拿不到generator的return
语句的返回值。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:
生成器迭代完成会抛出StopIteration异常,value存储生成器的返回值,即’done’
1 | 6) g = fib( |
Try 和 except 用于处理可能发生的异常
1 | try: |
else
块
else
块中的代码在没有异常时执行。当 try
块中的代码没有抛出异常时,else
块会执行。
1 | try: |
finally
块
finally
块中的代码总是会被执行,不论是否发生异常。常用于释放资源(例如关闭文件、网络连接等)。
1 | try: |
做题的时候发现需要注意的点
yield返回的是引用而非拷贝
生成杨辉三角(主要是zip函数)
1 | def triangles(): |
迭代器
可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
1 | from collections.abc import Iterable |
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
可以使用isinstance()
判断一个对象是否是Iterator
对象:
1 | from collections.abc import Iterator |
生成器都是Iterator
对象,但list
、dict
、str
虽然是Iterable
,却不是Iterator
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用iter()
函数:
1 | isinstance(iter([]), Iterator) |
为什么list
、dict
、str
等数据类型不是Iterator
?
这是因为Python的Iterator
对象表示的是一个数据流,Iterator
对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator
甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
函数式编程
高阶函数
变量可以指向函数
把函数本身赋值给变量
1 | abs f = |
函数名也是变量
函数名就是指向函数的变量
1 | abs = 10 |
注:由于abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个简单的高阶函数
1 | def add(x, y, f): |
当我们调用add(-5, 6, abs)
时,参数x
,y
和f
分别接收-5
,6
和abs
,根据函数定义,我们可以推导计算过程为:
1 | x = -5 |
map/reduce
map
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
1 | def f(x): |
map()
传入的第一个参数是f
,即函数对象本身。由于结果r
是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list。
你可能会想,不需要map()
函数,写一个循环,也可以计算出结果:
1 | L = [] |
map()
作为高阶函数,把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x**2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
1 | list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
Reduce
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
比方说对一个序列求和,就可以用reduce
实现:
1 | from functools import reduce |
把str转化为int
1 | from functools import reduce |
整理成str2int的函数就是
1 | from functools import reduce |
用lambda函数进一步简化成:
1 | from functools import reduce |
filter
filter()
函数用于过滤序列,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
1 | def is_odd(n): |
把一个序列中的空字符串删掉
1 | def not_empty(s): |
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
使用生成器和迭代器以及filter实现100以内的素数
1 | #!/usr/bin/env python3 |
sorted
sorted()
函数就可以对list进行排序
1 | sorted([36, 5, -12, 9, -21]) |
还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
1 | sorted([36, 5, -12, 9, -21], key=abs) |
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs
处理过的list:
1 | list = [36, 5, -12, 9, -21] |
然后sorted()
函数按照keys进行排序,并按照对应关系返回list相应的元素:
1 | keys sort => [5, 9, 12, 21, 36] |
再看一个字符串排序的例子:
1 | sorted(['bob', 'about', 'Zoo', 'Credit']) |
给sorted
传入key函数,即可实现忽略大小写的排序:
1 | sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) |
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
:
1 | sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) |
返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
1 | def calc_sum(*args): |
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
1 | def lazy_sum(*args): |
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
1 | 1, 3, 5, 7, 9) f = lazy_sum( |
调用函数f
时,才真正计算求和的结果:
1 | f() |
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数(类似的情况是生成器):
1 | 1, 3, 5, 7, 9) f1 = lazy_sum( |
闭包
返回的函数在其定义内部引用了局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
注:返回的函数并没有立刻执行,而是直到调用了f()
才执行
1 | def count(): |
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
1 | f1() |
全部都是9
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了3
,因此最终结果为9
。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果要引用循环变量,方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
1 | def count(): |
结果
1 | f1, f2, f3 = count() |
Nonlocal
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:
1 | def inc(): |
但是,如果对外层变量赋值,由于Python解释器会把x
当作函数fn()
的局部变量,它会报错:
1 | def inc(): |
因是x
作为局部变量并没有初始化,直接计算x+1
是不行的。但我们其实是想引用inc()
函数内部的x
,所以需要在fn()
函数内部加一个nonlocal x
的声明。加上这个声明后,解释器把fn()
的x
看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1
。
使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。
练习
方法一
1 | def createCounter(): |
方法二
1 | def createCounter(): |
匿名函数
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。还是以map()
函数为例,计算f(x)=x(2)时,除了定义一个f(x)
的函数外,还可以直接传入匿名函数:
1 | list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
匿名函数lambda x: x * x
实际上就是:
1 | def f(x): |
关键字**lambda
表示匿名函数,冒号前面的x
表示函数参数**。
匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
1 | lambda x: x * x f = |
把匿名函数作为返回值返回
1 | def build(x, y): |
装饰器
函数对象有一个name
属性(注意:是前后各两个下划线),可以拿到函数的名字:
1 | now.__name__ |
假设我们要增强now()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()
函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
1 | def log(func): |
观察上面的log
,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
1 |
|
调用now()
函数,不仅会运行now()
函数本身,还会在运行now()
函数前打印一行日志:
1 | now() |
把@log
放到now()
函数的定义处,相当于执行了语句:
1 | now = log(now) |
由于log()
是一个decorator,返回一个函数,所以,原来的now()
函数仍然存在,只是现在同名的now
变量指向了新的函数,于是调用now()
将执行新函数,即在log()
函数中返回的wrapper()
函数。
wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper()
函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
1 | def log(text): |
这个3层嵌套的decorator用法如下:
1 |
|
执行结果
1 | now() |
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
1 | 'execute')(now) now = log( |
我们来剖析上面的语句,首先执行log('execute')
,返回的是decorator
函数,再调用返回的函数,参数是now
函数,返回值最终是wrapper
函数。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有name
等属性,但你去看经过decorator装饰之后的函数,它们的name
已经从原来的'now'
变成了'wrapper'
:
1 | now.__name__ |
因为返回的那个wrapper()
函数名字就是'wrapper'
,所以,需要把原始函数的name
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的,所以,一个完整的decorator的写法如下:
1 | import functools |
或者针对带参数的decorator:
1 | import functools |
既支持无参也支持有参的@log
的decorator
1 | import time, functools |
输出结果
1 | execute fast() |
偏函数
通过设定参数的默认值,可以降低函数调用的难度。
int()
函数可以把字符串转换为整数,当仅传入字符串时,int()
函数默认按十进制转换:
注:这里的默认十进制是指默认你输入的为10进制
1 | int('12345') |
int()
函数还提供额外的base
参数,默认值为10
。如果传入base
参数,就可以做N进制的转换:
1 | int('12345', base=8) |
假设要转换大量的二进制字符串,每次都传入int(x, base=2)
非常麻烦,于是,我们想到,可以定义一个int2()
的函数,默认把base=2
传进去:
1 | def int2(x, base=2): |
functools.partial
就是帮助我们创建一个偏函数的,不需要我们自己定义int2()
,可以直接使用下面的代码创建一个新的函数int2
:
1 | import functools |
简单总结functools.partial
的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2
函数,仅仅是把base
参数重新设定默认值为2
,但也可以在函数调用时传入其他值:
1 | '1000000', base=10) int2( |
最后,创建偏函数时,实际上可以接收函数对象、*args
和**kw
这3个参数,当传入:
1 | int2 = functools.partial(int, base=2) |
实际上固定了int()函数的关键字参数base
,也就是:
1 | int2('10010') |
相当于:
1 | kw = { 'base': 2 } |
当传入:
1 | max2 = functools.partial(max, 10) |
实际上会把10
作为*args
的一部分自动加到左边,也就是:
1 | max2(5, 6, 7) |
相当于:
1 | args = (10, 5, 6, 7) |
结果为10
模块
每个包目录下面都必须有一个__init__.py
的文件,这个文件可以是空文件,且这个文件本身就是一个模块,模块名就为其所在包目录名称
使用模块
1 | #!/usr/bin/env python3# -*- coding: utf-8 -*-' a test module ' |
- 第1行和第2行是标准注释,第1行注释可以让这个
hello.py
文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码; - 第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
- 第6行使用
author
变量把作者写进去
作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_
前缀来实现的。
正常的函数和变量名是公开的(public),可以被直接引用,比如:abc
,x123
,PI
等;
类似__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊变量,hello
模块定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名;
类似_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc
,__abc
等;
之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
private函数或变量不应该被别人引用,那它们有什么用呢?请看例子:
1 | def _private_1(name): |
面向对象编程
总算看到这儿了,与C++一样的思想
设计一个包含学生姓名&成绩的类,以及打印成绩的函数
1 | class Student(object): |
给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:
1 | bart = Student('Bart Simpson', 59) |
类和实例
创建类的方法
class + 类名 + 继承类(如果没有合适的继承类,可以直接写object)
创建实例
1 | bart = Student() |
可以自由地给一个实例变量绑定属性,比如,给实例bart
绑定一个name
属性:
1 | 'Bart Simpson' bart.name = |
创建类中实例变量的方法,通过定义一个特殊的init
方法,在创建实例的时候,就把name
,score
等属性绑上去:
1 | class Student(object): |
注意到init
方法的第一个参数永远是self
,表示创建的实例本身,因此,在init
方法内部,就可以把各种属性绑定到self
,因为self
就指向创建的实例本身。
有了init
方法,在创建实例的时候,就不能传入空的参数了,必须传入与init
方法匹配的参数,但self
不需要传,Python解释器自己会把实例变量传进去:
1 | 'Bart Simpson', 59) bart = Student( |
数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student
类中,每个实例就拥有各自的name
和score
这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:
1 | def print_score(std): |
但是,既然Student
实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student
类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student
类本身是关联起来的,我们称之为类的方法:
1 | class Student(object): |
和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:
1 | 'Bart Simpson', 59) bart = Student( |
访问限制
在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:
1 | class Student(object): |
此时两个变量变成了私有变量,外部不能直接调用,只有内部可以访问,不过可以在类中定义set和get方法
获取变量的方法
1 | class Student(object): |
修改变量的方法
1 | class Student(object): |
- 需要注意的是,在Python中,变量名类似
xxx
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用name
、score
这样的变量名。 - 有些时候,你会看到以一个下划线开头的实例变量名,比如
_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。 - 双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问
__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量:
1 | bart._Student__name |
但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name
改成不同的变量名。
总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。(奇妙)
错误写法
1 | 'Bart Simpson', 59) bart = Student( |
表面上看,外部代码“成功”地设置了__name
变量,但实际上这个__name
变量和class内部的__name
变量不是一个变量!内部的__name
变量已经被Python解释器自动改成了_Student__name
,而外部代码给bart
新增了一个__name
变量。不信试试:
1 | # get_name()内部返回self.__name bart.get_name() |
继承和多态
当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)
1 | class Animal(object): |
当我们需要编写Dog
和Cat
类时,就可以直接从Animal
类继承:
1 | class Dog(Animal): |
继承,子类(Dog和Cat)获得父类(Animal)全部的功能
1 | dog = Dog() |
运行结果如下:
1 | Animal is running... |
对Dog
和Cat
类改进如下:
1 | class Dog(Animal): |
再次运行,结果如下:
1 | Dog is running... |
当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:
1 | a = list() # a是list类型 |
判断一个变量是否是某个类型可以用isinstance()
判断:
1 | isinstance(a, list) |
Dog
可以看成Animal
,但Animal
不可以看成Dog
。
1 | isinstance(c, Animal) |
当我们传入Animal
的实例时,run_twice()
就打印出:
1 | run_twice(Animal()) |
当我们传入Dog
的实例时,run_twice()
就打印出:
1 | run_twice(Dog()) |
静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run()
方法。
对于Python这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了:
1 | class Animal: |
获取对象信息-优先使用isinstance
使用type()
判断对象类型,使用type()
函数:基本类型都可以用type()
判断:
1 | type(123) |
如果一个变量指向函数或者类,也可以用type()
判断:
1 | type(abs) |
但是type()
函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if
语句中判断,就需要比较两个变量的type类型是否相同:
1 | type(123)==type(456) |
判断基本数据类型可以直接写int
,str
等,但如果要判断一个对象是否是函数怎么办?可以使用types
模块中定义的常量:
1 | import types |
使用isinstance()
对于class的继承关系来说,使用type()
就很不方便。我们要判断class的类型,可以使用isinstance()
函数。
如果继承关系是:
1 | object -> Animal -> Dog -> Husky |
那么,isinstance()
就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
1 | a = Animal() |
然后,判断:
1 | isinstance(h, Husky) |
h
虽然自身是Husky类型,但由于Husky是从Dog继承下来的,所以,h
也还是Dog类型。换句话说,isinstance()
判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
能用type()
判断的基本类型也可以用isinstance()
判断:
1 | isinstance('a', str) |
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
1 | isinstance([1, 2, 3], (list, tuple)) |
使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()
函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
1 | dir('ABC') |
类似xxx
的属性和方法在Python中都是有特殊用途的,比如len
方法返回长度。在Python中,如果你调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的len()
方法,所以,下面的代码是等价的:
1 | len('ABC') |
我们自己写的类,如果也想用len(myObj)
的话,就自己写一个len()
方法:
1 | class MyDog(object): |
仅仅把属性和方法列出来是不够的,配合getattr()
、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态:
1 | class MyObject(object): |
紧接着,可以测试该对象的属性:
1 | hasattr(obj, 'x') # 有属性'x'吗? |
如果试图获取不存在的属性,会抛出AttributeError的错误:
1 | getattr(obj, 'z') # 获取属性'z' |
可以传入一个default参数,如果属性不存在,就返回默认值:
1 | getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404 |
也可以获得对象的方法:
1 | hasattr(obj, 'power') # 有属性'power'吗? |
实例属性和类属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量,或者通过self
变量:
1 | class Student(object): |
但是,如果Student
类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student
类所有,所有实例共享这个name:
1 | class Student(object): |
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
1 | class Student(object): |
面向对象高级编程
使用__slots__
当我们定义了一个class
,创建了一个class
的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。先定义class
:
1 | class Student(object): |
给实例绑定一个属性:
1 | s = Student() |
尝试给实例绑定一个方法:
MethodType(func, obj)
: 它将函数 func
转换为一个绑定到 obj
对象的实例方法。
1 | def set_age(self, age): # 定义一个函数作为实例方法 |
这时只有绑定的实例可以调用这个方法
为了给所有实例都绑定方法,可以给class绑定方法
1 | def set_score(self, score): |
给class
绑定方法后,所有实例均可调用:
1 | 100) s.set_score( |
限制实例的属性,例如只允许对Student实例添加name
和age
属性
为了达到限制的目的,Python允许在定义class
的时候,定义一个特殊的slots
变量,来限制该class
实例能添加的属性:
1 | class Student(object): |
由于'score'
没有被放到slots
中,所以不能绑定score
属性,试图绑定score
将得到AttributeError
的错误。
使用slots
要注意,slots
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
1 | class GraduateStudent(Student): |
除非在子类中也定义slots
,这样,子类实例允许定义的属性就是自身的**slots
加上父类的slots
**。
使用@property
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改:
1 | s = Student() |
为了限制score
的范围,可以通过一个set_score()
方法来设置成绩,再通过一个get_score()
来获取成绩,这样,在set_score()
方法里,就可以检查参数:
1 | class Student(object): |
现在,对任意的Student
实例进行操作,就不能随心所欲地设置score
了:
1 | s = Student() |
但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。
有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!
还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property
装饰器就是负责把一个方法变成属性调用的:
1 | class Student(object): |
@property
的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器@score.setter
,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
1 | s = Student() |
注意到这个神奇的@property
,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:
1 | class Student(object): |
上面的birth
是可读写属性,而age
就是一个只读属性,因为age
可以根据birth
和当前时间计算出来。
要特别注意:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:
1 | class Student(object): |
这是因为调用s.birth
时,首先转换为方法调用,在执行return self.birth
时,又视为访问self
的属性,于是又转换为方法调用self.birth()
,造成无限递归,最终导致栈溢出报错RecursionError
。
多重继承
如果要把上面的两种分类都包含进来,我们就得设计更多的层次:
- 哺乳类:能跑的哺乳类,能飞的哺乳类;
- 鸟类:能跑的鸟类,能飞的鸟类。
这么一来,类的层次就复杂了:
如果要再增加“宠物类”和“非宠物类”,这么搞下去,类的数量会呈指数增长,很明显这样设计是不行的。
正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计:
1 | class Animal(object): |
现在,我们要给动物再加上Runnable
和Flyable
的功能,只需要先定义好Runnable
和Flyable
的类:
1 | class Runnable(object): |
对于需要Runnable
功能的动物,就多继承一个Runnable
,例如Dog
:
1 | class Dog(Mammal, Runnable): |
对于需要Flyable
功能的动物,就多继承一个Flyable
,例如Bat
:
1 | class Bat(Mammal, Flyable): |
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
MixIn
- 在设计类的继承关系时,通常,主线都是单一继承下来的,例如,
Ostrich
继承自Bird
。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich
除了继承自Bird
外,再同时继承Runnable
。这种设计通常称之为MixIn。 - 为了更好地看出继承关系,我们把
Runnable
和Flyable
改为RunnableMixIn
和FlyableMixIn
。类似的,你还可以定义出肉食动物CarnivorousMixIn
和植食动物HerbivoresMixIn
,让某个动物同时拥有好几个MixIn:
1 | class Dog(Mammal, RunnableMixIn, CarnivorousMixIn): |
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
定制类
其实就是修改库里自带的函数,为每个类定制修改函数