Python项目的代码结构大致由 包(文件夹) -> 模块(文件) ->类(class) -> 方法(类中的函数)组成。
模块
如果将代码按照不同的功能拆分放到不同的.py文件中,每个.py文件就是一个模块。不同模块中的变量名可以相同,他们不会受影响。
包
在创建许许多多模块后,我们可能希望将某些功能相近的文件组织在同一文件夹下,这里就需要运用包的概念了。
包是带有__init__.py文件的文件夹。包必须有__init__.py文件,不然他就是个文件夹。
包是用来区分不同类型的模块,不同包中的模块名可以相同,他们不会相互受影响。当然包下可以还有包(子包),如同子文件夹。
com/ 顶层包
__init__.py 初始化 com包
requirements.txt requirements 文件
comtest.py com 下comtest模块
woodman/ com下的子包
__init__.py
woodtest.py
...
gif/ 图片处理包
__init__.py
gifecho.py
giftest.py 注意添加代码
...
modle/ modle子包
__init__.py
model.py
类
python的class(类)相当于一个多个函数组成的家族,如果在这个Myclass大家族里有一个人叫f,假如这个f具有print天气的作用,那么如果有一天我需要这个f来print一下今天的天气,那么我必须叫他的全名MyClass.f才可以让他给我print,即在调用他的时候需要带上他的家族名称+他的名称。
- 方法:类中定义的函数。
- 类的构造方法__init__():类有一个名为 init() 的特殊方法(构造方法),该方法在类实例化时会自动调用。
- 实例变量:在类的声明中,属性是用变量来表示的,这种变量就称为实例变量,实例变量就是一个用 self 修饰的变量。
from ... import ...
from ... import ... 这种引入方式使用一个点号来标识引入类库的精确位置。
这里的.
可以访问同级目录下的包(Package)或者模块(Module)。
这里的..
可以访问上一级目录下的包(Package)或者模块(Module)。
单引号和双引号
使用单引号和双引号表示字符串的方式是等价的,使用单引号时,字符串中可以包含双引号,使用双引号时,字符串中可以包含单引号。
原始字符串是Python中一类比较特殊的字符串,以大写字母R或者小写字母r开始。在原始字符串中,字符“\” 不再表示转义字符的含义。
format函数
fotmat作为Python的的格式字符串函数,主要通过字符串中的花括号{},来识别替换字段,从而完成字符串的格式化。
signs = ['+', '-']
numbers = [1, 2]
ascii = ["{sign}{number}".format(sign=sign, number=number)
for sign in signs for number in numbers]
花括号的对数跟参数数量需要一致。
魔法命令
Python提供了许多魔法命令,使得在IPython环境中的操作更加得心应手。魔法命令都以%或者%%开头,以%开头的成为行命令,%%开头的称为单元命令。行命令只对命令所在的行有效,而单元命令则必须出现在单元的第一行,对整个单元的代码进行处理。
%matplotlib inline
数据结构
数组
print函数
print()函数打印的字符串中包含一个或多个变量,则%后的变量需要被入圆括号中。
print("a=%d b=%d"%(a,b))
print函数默认会换行,如想不换行可以指定结束符,如下:
print('dic:', end='')
列表(list)
相当于数组,用方括号表示。
array = [1, 2, 3, 4, 5, 6]
small = [n * 2 for n in array if n < 4]
print(small)
元组(tuple)
元组用花括号表示,元组的一级元素不可被修改增加删除但可以修改二级后的。
import collections
People = collections.namedtuple('People', ['name', 'age'])
p = People('bestswifter', '22')
print(p.name) # bestswifter
print(p.age) # 22
namedtuple又名具名元组,因为普通元组的局限性,不能为元组的数据进行命名,所以我们并不知道一个元组所要表达的意义,所以在这里引入了collections.namedtuple这个工厂函数,来构造一个带字段名的元组。具名元组的实例和普通元组消耗的内存一样多,因为字段名都被存在对应的类里面。
namedtuple对象的定义如以下格式:
collections.namedtuple(typename, field_names, verbose=False, rename=False)
返回一个具名元组子类typename,其中参数的意义如下
# typename:元组名称
# field_names:元组中元素的名称
# rename:如果元素名称中含有python的关键字,则必须设置为rename=True
一般只需要关注前两个参数
任意无符号的对象,以逗号隔开,默认为元组;元组的一个小技巧是可以避免用临时变量来交换两个数的值:
a = 1
b = 2
a, b = b, a
# a = 2, b = 1
数组切片
切片的基本格式如下,表示对 array 在 start 到 end 之前以 step 为间隔取切片。
array[start:end:step]
s = 'hello'
s[0:5:2]
# 表示取 s 的第 0、2、4 个字符,结果是 'hlo'
开区间与闭区间
满足 a ≤ x ≤ b 的实数 x 的集合,表示为 [ a,b ],叫做闭区间;
满足 a < x <b 的实数 x 的集合,表示为 (a,b),叫做开区间;
强调一下 array[a:b] 表示的区间是 [a, b)
循环与遍历
python中循环常用的语法为
for ... in ...
举例如下:
for i in [1, 2, 3]:
print(i)
name = 'bs'
if name in ('hello', 'hi', 'bs', 'admin'):
print('Valid')
或者使用range函数:
a = [1, 2, 3, 4, 5]
for i in range(0, len(a), 2):
print(a[i])
range 的语法和切片类似,为range(start,end,step)
在这种写法中,我们不仅能获得元素,还能知道元素的下标,这与使用 enumerate(sequence ,[start=0]) 函数类似:
a = [1, 2, 3, 4, 5]
for i, n in enumerate(a):
print(i, n)
enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。
enumerate(sequence, [start=0])
参数:sequence -- 一个序列、迭代器或其他支持迭代对象。
start -- 下标起始位置的值。
返回值: enumerate(枚举) 对象。
魔术方法
目前看来Python的各种数据结构都有操作数据的魔术方法,后续可以持续关注。
也许你已经注意到了,数组和字符串都支持切片,而且语法高度统一。事实上,Python 能够支持这样统一的语法,并非巧合,而是因为所有用中括号进行下标访问的操作,其实都是调用这个类的 __getitem__ 方法。
比如我们完全可以让自己的类也支持通过下标访问:
class Book:
def __init__(self):
self.chapters = [1, 2, 3]
def __getitem__(self, n):
return self.chapters[n]
b = Book()
print(b[1])
需要注意的是,这段代码几乎不会出问题(除非数组越界),这是因为我们直接把下标传到了内部的 self.chapters 数组上。但如果要自己处理下标,需要牢记它不一定是数字,也可以是切片,因此更完整的逻辑应该是:
def __getitem__(self, n):
if isinstance(n, int): # n是索引
# 处理索引
if isinstance(n, slice): # n是切片
# 通过 n.start,n.stop 和 n.step 来处理切片
例如:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
#print("a=%d b=%d" %(a,b))
#print("a=%d" %(a))
return a
if isinstance(n, slice):
start = n.start
stop = n.stop
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
print("a=%d" %(a))
L.append(a)
a, b = b, a + b
print("a=%d b=%d" %(a,b))
return L
fib = Fib()
print(fib[10])
print(fib[0:10])
后续的章节还会介绍更多 Python 中的魔术方法,这种方法的名称前后都有两个下划线,如果读作 “下划线-下划线-getitem” 会比较拗口,因此可以读作 “dunder-getitem” 或者 “双下-getitem”,类似的,我想每个人都能猜到 __setitem__ 的作用和用法。
字典
初始化字典
字典字面量由大括号包住(注意区别于数组的中括号),键值对之间由逗号分割,每个键值对内部用冒号分割键和值。
{'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65}
如果数组的每个元素都是二元的元组,这个数组可以直接转成字典:
dict([('a', 61), ('b', 62), ('c', 63), ('d', 64), ('e', 65)])
就像数组可以推导一样,字典也可以推导,只要记得外面还是大括号就行了:
a = [('a', 61), ('b', 62), ('c', 63), ('d', 64), ('e', 65)]
d = {letter: number for letter, number in a} # 这里用到了元组拆包
两个独立的数组可以被压缩成一个字典:
numbers = [61, 62, 63, 64, 65]
letters = ['a', 'b', 'c', 'd', 'e']
d = dict(zip(letters, numbers))
print(d)
正如 zip 的意思所表示的,超出长处的那部分数组会被抛弃。
查询字典
最简单方法是直接写键名,但如果键名不存在会抛出 KeyError:
d = {'a': 61}
d['a'] # 值是 61
d['b'] # KeyError: 'b'
但更加优雅简洁一些的写法是用 get(k, default) 方法来提供默认值
d = {'a': 61}
d.get('a', 62) # 得到 61
d.get('b', 62) # 得到 62
get(k, default),当找不到键值时,返回default;
不过有时候,我们可能不仅仅要读出默认属性,更希望能把这个默认属性能写入到字典中,这种情况下,setdefault(key, default) 函数更合适,下面例子。
d = {}
d.setdefault('key', []).append(1)#{'key': [1]}
append() 函数可以向列表末尾添加元素
遍历字典
直接遍历字典实际上是遍历了字典的键,因此也可以通过键获取值:
d = {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65}
for i in d:
print(i, d[i])
我们也可以用字典的 keys() 或者 values() 方法显式的获取键和值。字典还有一个 items() 方法,它返回一个数组,每个元素都是由键和值组成的二元元组:
d = {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65}
for (k, v) in d.items():
print(k, v)
可见 items() 方法和字典的构造方法互为逆操作,因为这个公式总是成立的:
dict(d.items()) == d
字典的魔术方法
在数组中介绍过,通过下标访问最终都会由 __getitem__ 这个魔术方法处理,因此字典的 d[key] 这种写法也不例外。 如果键不存在,则会走到 __missing__ 方法,再给一次挽救的机会。比如我们可以实现一个字典, 自动忽略键的大小写:
class MyDict(dict):
def __missing__(self, key):
if key.islower():
raise KeyError(key)
else:
return self[key.lower()]
d = MyDict({'a': 61})
d['A'] # 返回 61
'A' in d # False
KeyError()函数,当没有该键时抛出错误,有找到该键,则返回键值;
这个字典比较简陋,比如 key 可能不是字符串,不过我没有处理太多情况,因为它主要是用来演示 __missing__ 的用法,如果想要最后一行的 in 语法正确工作,需要重写 __contains__ 这个魔术方法,如下。
global false, null, true
false = null = true = ''
class MyDict(dict):
def __missing__(self, key):
if key.islower():
raise KeyError(key)
else:
return self[key.lower()]
def __contains__(self, key):
if key.islower():
return true
else:
return false
d = MyDict({'a': 61})
print(d['A'])
a = 'A' in d
if (true == a):
print("true")
else:
print("false")
出现NameError: name 'false' is not defined这个错误时,是因为在转化为字典时,中间的false,null等区分大小写导致。
global false, null, true
false = null = true = ''
在使用true/false之前出现这个问题,应该这样处理。
虽然通过自定义的函数也能实现相似的效果,不过这个自定义字典对用户更加透明,如果不在文档中说明,调用方很难察觉到字典的内部逻辑被修改了。 Python 有很多强大的功能,可以具备这种内部进行修改,但是对外保持透明的能力。这可能是我们第一次体会到,后续还会不断的经历。(待消化)
集合
集合更像是不会有重复元素的数组,但它的本质是以元素的哈希值作为 Key,从而实现去重的逻辑。因此,集合也可以推导,不过得用字典的语法:
a = [1,2,3,4,5,4,3,2,1]
d = {i for i in a if i < 5}
# d = {1, 2, 3, 4},注意这里的大括号
回忆一下,二进制逻辑运算一共有三个运算符,按位或 |,按位与 & 和异或 ^,这三个运算符也可以用在集合之间,而且含义变化不大。比如:
a = {1, 2, 3}
b = {3, 4, 5}
c = a | b
# c = {1, 2, 3, 4, 5}
这里的 | 运算表示并集,也就是 c 中的任意元素,要么在 a,要么在 b 集合中。类似的,按位与 & 运算求的就是交集:
a = {1, 2, 3}
b = {3, 4, 5}
c = a & b
# c = {3}
而异或则表示那些只在 a 不在 b 或者只在 b 不在 a 的元素。或者换个说法,表示那些在集合 a 和 b 中出现了且仅出现了一次的元素:
a = {1, 2, 3}
b = {3, 4, 5}
c = a ^ b
# c = {1, 2, 4, 5}
还有一个差集运算 -,表示在集合 a 中但不在集合 b 中的元素:
a = {1, 2, 3}
b = {3, 4, 5}
c = a - b
# c = {1, 2}
字符串
字符串编码
首先,编码的函数是 encode,它是字符串的方法:
s = 'hello'
s.encode() # 得到 b'hello'
s.encode('utf16') # 得到 b'\xff\xfeh\x00e\x00l\x00l\x00o\x00'
encode 函数有两个参数,第一个参数不写表示使用默认的 utf8 编码,理论上会输出二进制格式的编码结果,但在终端打印时,被自动还原回字符串了。如果用 utf16 进行编码,则会看到编码以后的二进制结果。
有时候在某个编码规范中,并没有指定某个字符是如何编码的,也就是找不到对应的数字,这时候编码就会报错:
city = 'São Paulo'
b_city = city.encode('cp437')
# UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
# position 1: character maps to <undefined>
此时需要用到 encode 函数的第二个参数,用来指定遇到错误时的行为。它的值可以是 'ignore',表示忽略这个不能编码的字符,也可以是 'replace',表示用默认字符代替:
b_city = city.encode('cp437', errors='ignore')
# b'So Paulo'
b_city = city.encode('cp437', errors='replace')
# b'S?o Paulo'
decode 完全是 encode 的逆操作,只有二进制类型才有这个函数。它的两个参数含义和 encode 函数完全一致,就不再详细介绍了。
raw_bin = b'\xff\xfeh\x00e\x00l\x00l\x00o\x00'
str = raw_bin.decode('utf16')
print(str)# hello
从理论上来说,仅从编码后的内容上来看,是无法确定编码方式的,也无法解码出原来的字符。但不同的编码有各自的特点,虽然无法完全倒推,但可以从概率上来猜测,如果发现某个二进制内容,有 99% 的可能性是 utf8 编码生成的,我们就可以用 utf8 进行解码。Python 提供了一个强大的工具包 Chardet 来完成这一任务,需要安装Chardet库。
octets = b'Montr\xe9al'
chardet.detect(octets)
# {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''}
octets.decode('ISO-8859-1')
# Montréal
返回结果中包含了猜测的编码方式,以及可信度。可信度越高,说明是这种编码方式的可能性越大。
有时候,我们拿到的是二进制的字符串字面量,比如 68 65 6c 6c 6f,前文说过只有二进制类型才有 decode 函数,所以需要通过二进制的字面量生成二进制变量:
s = '68 65 6c 6c 6f'
b = bytearray.fromhex(s)
b.decode() # hello
classmethod fromhex(string)
bytearray是可变的二进制数据(byte),这里bytearray.fromhex表示:
bytearray 类方法返回一个解码给定字符串的 bytearray 对象。 字符串必须由表示每个字节的两个十六进制数码构成,其中的 ASCII 空白符会被忽略。
字符串的常用方法
字符串的 split(sep, maxsplit) 方法可以以指定的分隔符进行分割,有点类似于 Shell 中的 awk -F ' '',第一个 sep 参数表示分隔符,不填则为空格:
s = 'a b c d e'
a = s.split()
# a = ['a', 'b', 'c', 'd', 'e']
第二个参数 maxsplit 表示最多分割多少次,因此返回数组的长度是 maxsplit + 1。举个例子说明下:
s = 'a;b;c;d;e'
a = s.split(';')
# a = ['a', 'b', 'c', 'd', 'e']
b = s.split(';', 2)
# b = ['a', 'b', 'c;d;e']
如果想批量替换,则可以用 replace(old, new[, count]) 方法,由中括号括起来的参数表示选填。
old = 'a;b;c;d;e'
new = old.replace(';', ' ', 3)
# new = 'a b c d;e'
strip[chars] 用于移除指定的字符们:
old = "*****!!!Hello!!!*****"
new = old.strip('*') # 得到 '!!!Hello!!!'
new = old.strip('*!') # 得到 'Hello'
new = old.strip('*!Hel') # 得到 'o'
new = old.strip('*!ol') # 得到 'He'
strip函数分别从字符串的首尾两端移除指定的字符直到找不到字符为止。
最后一个常用方法是 join,其实这个可以理解为字符串的构造方法,它可以把数组转换成字符串:
array = 'a b c d e'.split() # 之前说过,结果是 ['a', 'b', 'c', 'd', 'e']
s = ';'.join(array) # 以分号为连接符,把数组中的元素连接起来
# s = 'a;b;c;d;e'
所以 join 可以理解为 split 的逆操作,这个公式始终是成立的:
c.join(string.split(c)) = string
上面这些字符串处理的函数,大多返回的还是字符串,因此可以链式调用,避免使用临时变量和多行代码,但也要避免过长(超过 3 个)的链式调用,以免影响可读性。
字符串格式化
最初级的字符串格式化方法是使用 + 来拼接:
class Person:
def __init__(self):
self.name = 'bestswifter'
self.age = 22
self.sex = 'm'
p = Person()
print('Name: ' + p.name + ', Age: ' + str(p.age) + ', Sex: ' + p.sex)
# 输出:Name: bestswifter, Age: 22, Sex: m
除了把对象传给 format 函数并在字符串中展开以外, 也可以传入多个参数,并且通过下标访问他们
print('{0}, {1}, {0}'.format(1, 2))
# 输出:1, 2, 1
花括号中的数字表示参数的序号,例如{0}表示第一个参数,{1}表示第二个参数;
HereDoc
Heredoc 不是 Python 特有的概念, 命令行和各种脚本中都会见到,它表示一种所见即所得的文本。
假设我们在写一个 HTML 的模板,绝大多数字符串都是常量,只有有限的几个地方会用变量去替换,那这个字符串该如何表示呢?一种写法是直接用单引号去定义:
s = '<HTML><HEAD><TITLE>\nFriends CGI Demo</TITLE></HEAD>\n<BODY><H3>ERROR</H3>\n<B>%s</B><P>\n<FORM><INPUT TYPE=button VALUE=Back\nONCLICK=\'window.history.back()\'></FORM>\n</BODY></HTML>'
这段代码是自动生成的还好,如果是手动维护的,那么可读性就非常差,因为换行符和转义后的引号增加了理解的难度。如果用 heredoc 来写,就非常简单了:
s = '''<HTML><HEAD><TITLE>
Friends CGI Demo</TITLE></HEAD>
<BODY><H3>ERROR</H3>
<B>%s</B><P>
<FORM><INPUT TYPE=button VALUE=Back
ONCLICK='window.history.back()'></FORM>
</BODY></HTML>
'''
Heredoc 主要是用来书写大段的字符串常量,比如 HTML 模板,SQL语句等等。
函数
函数是一等公民
一等公民指的是 Python 的函数能够动态创建,能赋值给别的变量,能作为参传给函数,也能作为函数的返回值。总而言之,函数和普通变量并没有什么区别。
Python 中基本上不会使用 lambda 表达式。
如果某个类实现了 __call__ 这个魔术方法,这个类的实例就都可以像函数一样被调用:
class Person:
def __init__(self):
self.name = 'bestswifter'
self.age = 22
self.sex = 'm'
def __call__(self):
print(self)
def __str__(self):
return 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}'.format(user=self)
p = Person()
p() # 等价于 print(p)
通过自定义__str__()函数就可以帮助我们打印对象中相关的内容。
函数参数
函数传参
Python 的传值方式可以被理解为混合传值。对于那些不可变的对象(比如 1.1.2 节中介绍过的元组,还有数字、字符串类型),传值方式是拷贝传值;对于那些可变对象(比如数组和字典)则是引用传值。
def foo(arg):
arg.append(1)
print(arg)
a = [1]
foo(a)
print(a) # 输出两个 [1, 1]
参数a为数组,是可以被改变的对象,函数中调用arg.append(1)改变了参数的值。
默认参数
Python 的函数可以有默认值,这个功能很好用:
def foo(a, l=[]):
l.append(a)
return l
foo(2,[1]) # 给数组 [1] 添加一个元素 2,得到 [1,2]
foo(2) # 没有传入数组,使用默认的空数组,得到 [2]
然而如果这样调用:
foo(2) # 利用默认参数,得到 [2]
foo(3) # 竟然得到了 [2, 3]
函数调用了两次以后,默认参数被改变了,也就是说函数调用产生了副作用。这是因为默认参数的存储并不像函数里的临时变量一样存储在栈上、随着函数调用结束而释放,而是存储在函数这个对象的内部:
foo.__defaults__ # 一开始确实是空数组
foo(2) # 利用默认参数,得到 [2]
foo.__defaults__ # 如果打印出来看,已经变成 [2] 了
foo(3) # 再添加一个元素就得到了 [2, 3]
因为函数 foo 作为一个对象,不会被释放,因此这个对象内部的属性也不会随着多次调用而自动重置,会一直保持上次发生的变化。基于这个前提,我们得出一个结论:函数的默认参数不允许是可变对象,比如这里的 foo 函数需要这么写:
def foo(a, l=None):
if l is None:
l = []
l.append(a)
return l
print(foo(2)) # 得到 [2]
print(foo(3)) # 得到 [3]
现在,给参数添加默认值的行为在函数体中完成,不会随着函数的多次调用而累积。
对于 Python 的默认参数来说:
如果默认值是不可变的,可以直接设置默认值,否则要设置为 None 并在函数体中设置默认值。
多参数传递
当参数个数不确定时,可以在参数名前加一个 *:
def foo(*args):
print(args)
foo(1, 2, 3) # 输出 [1, 2, 3]
这里的单个 * 只能接收非关键字参数,也就是仅有参数值的哪些参数。如果想接受关键字参数,需要用 ** 来表示:
def foo(*args, **kwargs):
print(args)
print(kwargs)
foo(1,2,3, a=61, b=62)
# 第一行输出:(1, 2, 3)
# 第二行输出:{'a': 61, 'b': 62}
类似的,字典变量传入函数只能作为单个参数,如果要想展开并被 **kwargs 识别,需要在字典前面加上两个星号 **:
a = [1, 2, 3]
d = {'a': 61, 'b': 62}
foo(*a, **d)
参数分类
Python 中函数的参数可以分为两大类:
-
定位参数(Positional):表示参数的位置是固定的。比如对于函数 foo(a, b) 来说,foo(1, 2) 和 foo(2, 1) 就是截然不同的,a 和 b 的位置是固定的,不可随意调换。
-
关键词参数(Keyword):表示参数的位置不重要,但是参数名称很重要。比如 foo(a = 1, b = 2) 和 foo(b = 2, a = 1) 的含义相同。
有一种参数叫做仅限关键字(Keyword-Only)参数,比如考虑这个函数:
def foo(*args, n=1, **kwargs):
print(n)
这个函数在调用时,如果参数 n 不指定名字,就会被前面的 *args 处理掉,如果指定的名字不是 n,又会被后面的 **kwargs 处理掉,所以参数 n 必须精确的以 (n = xxx) 的形式出现,也就是 Keyworld-Only。
函数内省
在 前面,我们查看了函数变量的 __defaults__ 属性,其实这就是一种内省,也就是在运行时动态的查看变量的信息。
前文说过,函数也是对象,因此函数的变量个数,变量类型都应该有办法获取到,如果你需要开发一个框架,也许会对函数有各种奇葩的检查和校验。
以下面这个函数为例:
g = 1
def foo(m, *args, n, **kwargs):
a = 1
b = 2
首先可以获取函数名,函数所在模块的全局变量等:
foo.__globals__ # 全局变量,包含了 g = 1
foo.__name__ # foo
我们还可以看到函数的参数,函数内部的局部变量:
foo.__code__.co_varnames # ('m', 'n', 'args', 'kwargs', 'a', 'b')
foo.__code__.co_argcount # 只计算参数个数,不考虑可变参数和仅限关键字参数,所以得到 1
或者用 inspect 模块来查看更详细的信息:
import inspect
sig = inspect.signature(foo) # 获取函数签名
sig.parameters['m'].kind # POSITIONAL_OR_KEYWORD 表示可以是定位参数或关键字参数
sig.parameters['args'].kind # VAR_POSITIONAL 定位参数构成的数组
sig.parameters['n'].kind # KEYWORD_ONLY 仅限关键字参数
sig.parameters['kwargs'].kind # VAR_KEYWORD 关键字参数构成的字典
inspect.getfullargspec(foo)
# 得到:ArgSpec(args=['m', 'n'], varargs='args', keywords='kwargs', defaults=None)
装饰器
装饰器的基本原理
装饰器的基本写法如下:
def decorator_name(origin_func): # 这个参数是被装饰的函数
print(1) # 先输出点东西
return origin_func # 把原函数直接返回
首先,装饰器是个函数,它的参数是被装饰的函数,返回值也是一个函数:
def decorate(origin_func): # 这个参数是被装饰的函数
print(1) # 先输出点东西
return origin_func # 把原函数直接返回
@decorate # 注意这里不是函数调用,所以不用加括号,也不用加被修饰的函数名
def sayHello():
print('Hello')
sayHello() # 如果没有装饰器,只会打印 'Hello',实际结果是打印 1 再打印 'Hello'
因此,使用装饰器的这种写法:
@decorate
def foo():
pass
和下面这种写法是完全等价的, 初学者可以把装饰器在心中默默的转换成下一种写法,以方便理解:
def foo():
pass
foo = decorate(foo)
需要注意的是,装饰器函数 decorate 在模块被导入时就会执行,而被装饰的函数只在被调用时才会执行,也就是说即使不调用 sayHello 函数也会输出 1,但这样就不会输出 Hello 了。
有了装饰器,配合前面介绍的函数对象,函数内省,我们可以做很多有意思的事,至少判断上一节中某个函数是否是策略是非常容易的。在装饰器中,我们还可以把策略函数都保存到数组中, 然后提供一个“推荐最佳策略”的功能, 其实就是遍历执行所有的策略,然后选择最好的结果。(待更深入理解)
装饰器进阶
上一节中的装饰器主要是为了介绍工作原理,它的功能非常简单,并不会改变被装饰函数的运行结果,仅仅是在导入时装饰函数,然后输出一些内容。换句话说,即使不执行函数,也要执行装饰器中的 print 语句,而且因为直接返回函数的缘故,其实没有真正的起到装饰的效果。
如何做到装饰时不输出任何内容,仅在函数执行最初输出一些东西呢?这是常见的 AOP(面向切片编程) 的需求。这就要求我们不能再直接返回被装饰的函数,而是应该返回一个新的函数,所以新的装饰器需要这么写:
def decorate1(origin_func):
def new_func():
print(1)
origin_func()
return new_func
@decorate1
def sayHello():
print('Hello')
sayHello() # 运行结果不变,但是仅在调用函数 sayHello 时才会输出 1
这是很自然的,因为本质上其实执行的是:
new_func = decorate(sayHello)
为了处理不定参数,并且不改变被装饰函数的外观(比如函数名),我们需要做一些细微的修补工作。这些工作都是模板代码,所以 Python 早就提供了封装:
import functools
def decorate(origin_func):
@functools.wraps(origin_func) # 这是 Python 内置的装饰器
def new_func(*args, **kwargs):
print(1)
origin_func(*args, **kwargs)
return new_func
这个封装我们称之为装饰器工厂,下一节接着介绍。
装饰器工厂
在前面代码注释中我解释过,装饰器后面不要加括号,被装饰的函数自动作为参数,传递到装饰器函数中。如果加了括号和参数,就变成手动调用装饰器函数了,大多数时候这与预期不符(因为装饰器的参数一般都是被装饰的函数)。
不过装饰器可以接受自定义的参数,然后返回另一个装饰器,这样外面的装饰器实际上就是一个装饰器工厂,可以根据用户的参数,生成不同的装饰器。还是以上面的装饰器为例,我希望输出的内容不是固定的 1,而是用户可以指定的,代码就应该这么写:
import functools
def decorate(content): # 这其实是一个装饰器工厂
def real_decorator(origin_func): # 这才是刚刚的装饰器
@functools.wraps(origin_func)
def new_func():
print('You said ' + str(content)) # 现在输出内容可以由用户指定
origin_func()
return new_func # 在装饰器里,返回的是新的函数
return real_decorator # 装饰器工厂返回的是装饰器
装饰器工厂和装饰器的区别在于它可以接受参数,返回一个装饰器:
@decorate(2017)
def sayHello():
print('Hello')
sayHello()
其实等价于:
real_decorator = decorate(2017) # 通过装饰器工厂生成装饰器
new_func = real_decorator(sayHello) # 正常的装饰器工作逻辑
new_func() # 调用的是装饰过的函数
装饰器在log系统中的应用非常典型,引用一个分析如下:
先来看一个简单例子,虽然实际代码可能比这复杂很多:
def foo():
print('i am foo')
现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:
def foo():
print('i am foo')
logging.info("foo is running")
如果函数 bar()、bar2() 也有类似的需求,怎么做?再写一个 logging 在 bar 函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志 ,日志处理完之后再执行真正的业务代码
def use_logging(func):
logging.warn("%s is running" % func.__name__)
func()
def foo():
print('i am foo')
use_logging(foo)
这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。
简单装饰器
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func() # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
return wrapper
def foo():
print('i am foo')
foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo() # 执行foo()就相当于执行 wrapper()
use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程。
@ 语法糖
如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。
import logging
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper
@use_logging
def foo():
print("i am foo")
foo()
如上所示,有了 @ ,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。你们看到了没有,foo() 函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。
装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。
*args、**kwargs
可能有人问,如果我的业务逻辑函数 foo 需要参数怎么办?比如:
def foo(name):
print("i am %s" % name)
我们可以在定义 wrapper 函数的时候指定参数:
def wrapper(name):
logging.warn("%s is running" % func.__name__)
return func(name)
return wrapper
这样 foo 函数定义的参数就可以定义在 wrapper 函数中。这时,又有人要问了,如果 foo 函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道 foo 到底有多少个参数时,我们可以用 *args 来代替:
def wrapper(*args):
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper
如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:
def foo(name, age=None, height=None):
print("I am %s, age %s, height %s" % (name, age, height))
这时,你就可以把 wrapper 函数指定关键字函数:
def wrapper(*args, **kwargs):
# args是一个数组,kwargs一个字典
logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper
带参数的装饰器
装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。
import logging
logging.basicConfig(level=logging.DEBUG)
def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator
@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()
上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。
@use_logging(level="warn") 等价于 @decorator
类装饰器
装饰器还可以通过类来实现,其实主要是利用类的以下特点来变相实现函数装饰器功能:
函数调用语语法f()等同于类的实例化,即调用类的__init__函数创建对象。
对象的调用obj()等同于运行对象的__call__魔法函数。
通过类实现装饰器,可以避免函数装饰器超过2层的嵌套情况,因为如果有三层的话,最外层函数可以认为是在调用类的__init__函数,这样可以让代码更易读和维护。
本质,只要实现类的__init__和__call__魔法函数,并在__init__函数内接受装饰器参数,在__call__函数内实现具体装饰器结构即可。
下面举例,用类实现带参装饰器,可以观察下不同
from functools import wraps
#定义一个装饰器名称的类
class with_para_decorator:
#在类的__init__函数内接受装饰器参数,并赋值给类的实例参数,这样可以让其他函数随时使用
#当然,如果装饰器没有参数,此处不转a,b即可,相当于类无参实例化
def __init__(self,a,b):
self.a=a
self.b=b
#在类的__call__函数内接受被装饰函数,并具体定义装饰器
def __call__(self,func):
@wraps(func)
def wrap_function(arg1,arg2):
print('装饰带参数的函数,函数传的参数为:{0}, {1}'.format(arg1,arg2))
print('带参数的装饰器,装饰器传的参数为:{0}, {1}'.format(self.a,self.b))
return func(arg1,arg2)
return wrap_function
#使用装饰器
@with_para_decorator(1,2)
def need_decorate(a,b):
pass
need_decorate(4,5)
以上代码具体原理解析如下:
@with_para_decorator(1,2),因为是类的名称,相当于使用(1,2)参数创建并返回该类的一个实例对象,比如是 obj。
此时,语法变为@obj,相当于need_decorate=obj(need_decorate),此时会调用obj.__call__魔法函数,而我们在该魔法函数具体实现了装饰器功能。
可以看到,其本质的运行原理,和函数装饰器没区别,只是将三层函数嵌套,变成了一个__init__函数和__call__函数的两层嵌套。
对比下来,可以看到,类装饰器,代码更加直观
使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、__name__、参数列表,先看例子:
from functools import wraps
def logged(func):
# @wraps(func)
def with_logging(*args, **kwargs):
"""dacorator"""
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
a = f(2)
print(a) # 6
print(f.__name__ ) # with_logging
print(f.__doc__) # dacorator
不难发现,函数 f 被with_logging取代了,当然它的docstring,__name__就是变成了with_logging函数的信息了。好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,当我们使用@wraps(func)来装饰func,这使得装饰器里面的 func 函数也有和原函数 f 一样的元信息了。运行结果如下:
6
f
does some math
PS:__doc__方法是python内置方法之一,该方法通常会输入指定对象中的注释部分
装饰器顺序
一个函数还可以同时定义多个装饰器,比如:
@a
@b
@c
def f ():
pass
它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于
f = a(b(c(f)))
面向对象
对象内存管理
对象不是盒子
在 Python 中,变量没有类型,值才有,或者说只有对象才有类型。这里与C语言有很大的差别,例如在C语言中:
int a = 1;
a = 2;
我们改变 a 的值:a = 2;,可以打印 a 的地址来证明它并没有发生变化。所以只是盒子里装的内容(指针指向的位置)发生了改变:

但是在 Python 中,变量不是盒子。比如同样的定义变量跟赋值:
a = 1;
a = 2;
相当于把标签 a 贴在另一个对象上:

基于这个认知,我们现在应该更容易理解前面中所说的函数传参规则了。如果传入的是不可变类型,比如 int,改变它的值实际上就是把标签挂在新的对象上,自然不会改变原来的参数。如果是可变类型,并且做了修改,那么函数中的变量和外面的变量都是指向同一个对象的标签,所以会共享变化。
默认浅复制
根据上一节的描述,直接把变量赋值给另一个变量, 还算不上复制:
a = [1, 2, 3]
b = a
b == a # True,等同性校验,会调用 __eq__ 函数,这里只判断内容是否相等
b is a # True,一致性校验,会检查是否是同一个对象,调用 hash() 函数,可以理解为比较指针
可见不仅仅数组相同,就连变量也是相同的,可以把 b 理解为 a 的别名。
如果用切片,或者数组的构造函数来创建新的数组,得到的是原数组的浅拷贝:
a = [1, 2, 3]
b = list(a)
b == a # True,因为数组内容相同
b is a # False,现在 a 和 b 是两个变量,恰好指向同一个数组对象
但如果数组中的元素是可变的,可以看到这些元素并没有被完全拷贝:
a = [[1], [2], [3]]
b = list(a)
b[0].append(2)
a # 得到 [[1, 2], [2], [3]],因为 a[0] 和 b[0] 其实还是挂在相同对象上的不同标签
print(a) # 得到 [[1, 2], [2], [3]]
print(b) # 得到 [[1, 2], [2], [3]]
print(b is a) # False
print(b[0] is a[0]) #True
如果想要深拷贝,需要使用 copy 模块的 deepcopy 函数:
import copy
b = copy.deepcopy(a)
b[0].append(2)
b # 变成了 [[1, 2], [2], [3]]
a # 还是 [[1], [2], [3]]
此时,不仅仅是每个元素的引用被拷贝,就连每个元素自己也被拷贝。所以现在的 a[0] 和 b[0] 是指向两个不同对象的两个不同变量(标签),自然就互不干扰了。
如果要实现自定义对象的深复制,只要实现 __deepcopy__ 函数即可。这个概念在几乎所有面向对象的语言中都会存在,就不详细介绍了。
弱引用
Python 内存管理使用垃圾回收的方式,当没有指向对象的引用时,对象就会被回收。然而对象一直被持有也并非什么好事,比如我们要实现一个缓存,预期目标是缓存中的内容随着真正对象的存在而存在,随着真正对象的消失而消失。如果因为缓存的存在,导致被缓存的对象无法释放,就会导致内存泄漏。
Python 提供了语言级别的支持,我们可以使用 weakref 模块,它提供了 weakref.WeakValueDictionary 这个弱引用字典来确保字典中的值不会被引用。如果想要获取某个对象的弱引用,可以使用 weakref.ref(obj) 函数。
Python 风格的对象
静态函数与类方法
静态函数其实和类的方法没什么关系,它只是恰好定义在类的内部而已,所以这里我用函数(function) 来形容它。它可以没有参数:
class Person:
@staticmethod # 用 staticmethod 这个修饰器来表明函数是静态的
def sayHello():
print('Hello')
Person.sayHello() # 输出 'Hello`
静态函数的调用方式是类名加上函数名。类方法的调用方式也是这样,唯一的不同是需要用 @staticmethod 修饰器,而且方法的第一个参数必须是类:
class Person:
@classmethod # 用 classmethod 这个修饰器来表明这是一个类方法
def sayHi(cls):
print('Hi: ' + cls.__name__)
Person.sayHi() # 输出 'Hi: Person`
类方法和静态函数的调用方法一致,在定义时除了修饰器不一样,唯一的区别就是类方法需要多声明一个参数sayHi(cls)。这样看起来比较麻烦,但静态函数无法引用到类对象,自然就无法访问类的任何属性。
于是问题来了,静态函数有何意义呢?有的人说类名可以提供命名空间的概念,但在我看来这种解释并不成立,因为每个 Python 文件都可以作为模块被别的模块引用,把静态函数从类里抽取出来,定义成全局函数,也是有命名空间的:
# 在 module1.py 文件中:
def global():
pass
class Util:
@staticmethod
def helper():
pass
# 在 module2.py 文件中:
import module1
module1.global() # 调用全局函数
module1.Util.helper() # 调用静态函数
从这个角度看,定义在类中的静态函数不仅不具备命名空间的优点,甚至调用语法还更加啰嗦。对此,我的理解是:静态函数可以被继承、重写,但全局函数不行,由于 Python 中的函数是一等公民,因此很多时候用函数替代类都会使代码更加简洁,但缺点就是无法继承,后面还会有更多这样的例子。
属性 attribute
Python (等多数动态语言)中的类并不像 C/OC/Java 这些静态语言一样,需要预先定义属性。我们可以直接在初始化函数中创建属性:
class Person:
def __init__(self, name):
self.name = name
bs = Person('bestswifter')
bs.name # 值是 'bestswifter'
#{'name': 'bestswifter'}
由于 __init__ 函数是运行时调用的,所以我们可以直接给对象添加属性:
bs.age = 22
bs.age # 因为刚刚赋值了,所以现在取到的值是 22
如果访问一个不存在的属性,将会抛出异常。从以上特性来看,对象其实和字典非常相似,但这种过于灵活的特性其实蕴含了潜在的风险。比如某个封装好的父类中定义了许多属性, 但是子类的使用者并不一定清楚这一点,他们很可能会不小心就重写了父类的属性。一种隐藏并保护属性的方式是在属性前面加上两个下划线:
class Person:
def __init__(self):
self.__name = 'bestswifter'
bs = Person()
bs.__name # 这样是无法获取属性的
bs._Person__name # 这样还是可以读取属性
这是因为 Python 会自动处理以双下划线开头的属性,把他们重名为 _Classname__attrname 的格式。self.__name会被自动处理为_Person__name。由于 Python 对象的所有属性都保存在实例的 __dict__ 属性中,我们可以验证一下:
bs = Person()
bs.__dict__
# 得到 {'_Person__name': 'bestswifter'}
但很多人并不认可通过名称改写(name mangling) 的方式来存储私有属性,原因很简单,只要知道改写规则,依然很容易的就能读写私有属性。与其自欺欺人,不如采用更简单,更通用的方法,比如给私有属性前面加上单个下划线 _。
注意,以单个下划线开头的属性不会触发任何操作,完全靠自觉与共识。任何稍有追求的 Python 程序员,都不应该读写这些属性。
特性 property
这里读原文不太理解,先读Python的类基础。
创建类
使用 class 语句来创建一个新类,class 之后为类的名称并以冒号结尾:
class ClassName:
'类的帮助信息' #类文档字符串
class_suite #类体
类的帮助信息可以通过ClassName.__doc__查看。
class_suite 由类成员,方法,数据属性组成。
下面是一个类的实例:
class Employee:
'base class'
empCount = 0
def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
def displayCount(self):
print("Total Employee %d" % Employee.empCount)
def displayEmployee(self):
print("Name : ", self.name, ", Salary: ", self.salary)
-
empCount 变量是一个类变量,它的值将在这个类的所有实例之间共享。你可以在内部类或外部类使用 Employee.empCount 访问。
-
第一种方法__init__()方法是一种特殊的方法,被称为类的构造函数或初始化方法,当创建了这个类的实例时就会调用该方法
-
self 代表类的实例,self 在定义类的方法时是必须有的,虽然在调用时不必传入相应的参数。
self代表类的实例,而非类
类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。
self 不是 python 关键字,我们把他换成 runoob 也是可以正常执行的:
创建实例对象
实例化类其他编程语言中一般用关键字 new,但是在 Python 中并没有这个关键字,类的实例化类似函数调用方式。
以下使用类的名称 Employee 来实例化,并通过 __init__ 方法接收参数。
"创建 Employee 类的第一个对象"
emp1 = Employee("Zara", 2000)
"创建 Employee 类的第二个对象"
emp2 = Employee("Manni", 5000)
访问属性
您可以使用点号 . 来访问对象的属性。使用如下类的名称访问类变量:
emp1.displayEmployee()
emp2.displayEmployee()
print "Total Employee %d" % Employee.empCount
你可以添加,删除,修改类的属性,如下所示:
emp1.age = 7 # 添加一个 'age' 属性
emp1.age = 8 # 修改 'age' 属性
del emp1.age # 删除 'age' 属性
你也可以使用以下函数的方式来访问属性:
-
getattr(obj, name[, default]) : 访问对象的属性。
-
hasattr(obj,name) : 检查是否存在一个属性。
-
setattr(obj,name,value) : 设置一个属性。如果属性不存在,会创建一个新属性。
-
delattr(obj, name) : 删除属性。
hasattr(emp1, 'age') # 如果存在 'age' 属性返回 True。
getattr(emp1, 'age') # 返回 'age' 属性的值
setattr(emp1, 'age', 8) # 添加属性 'age' 值为 8
delattr(emp1, 'age') # 删除属性 'age'
Python内置类属性
-
__dict__ : 类的属性(包含一个字典,由类的数据属性组成)
-
__doc__ :类的文档字符串
-
__name__: 类名
-
__module__: 类定义所在的模块(类的全名是'__main__.className',如果类位于一个导入模块mymod中,那么className.__module__ 等于 mymod)
-
__bases__ : 类的所有父类构成元素(包含了一个由所有父类组成的元组)
python对象销毁(垃圾回收)
Python 使用了引用计数这一简单技术来跟踪和回收垃圾。
在 Python 内部记录着所有使用中的对象各有多少引用。
一个内部跟踪变量,称为一个引用计数器。
当对象被创建时, 就创建了一个引用计数, 当这个对象不再需要时, 也就是说, 这个对象的引用计数变为0 时, 它被垃圾回收。但是回收不是"立即"的, 由解释器在适当的时机,将垃圾对象占用的内存空间回收。
a = 40 # 创建对象 <40>
b = a # 增加引用, <40> 的计数
c = [b] # 增加引用. <40> 的计数
del a # 减少引用 <40> 的计数
b = 100 # 减少引用 <40> 的计数
c[0] = -1 # 减少引用 <40> 的计数
垃圾回收机制不仅针对引用计数为0的对象,同样也可以处理循环引用的情况。循环引用指的是,两个对象相互引用,但是没有其他变量引用他们。这种情况下,仅使用引用计数是不够的。Python 的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器。作为引用计数的补充, 垃圾收集器也会留心被分配的总量很大(即未通过引用计数销毁的那些)的对象。 在这种情况下, 解释器会暂停下来, 试图清理所有未引用的循环。
注意:通常你需要在单独的文件中定义一个类
类的继承
面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。
通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。
继承语法
class 派生类名(基类名)
...
在python中继承中的一些特点:
-
1、如果在子类中需要父类的构造方法就需要显式的调用父类的构造方法,或者不重写父类的构造方法。详细说明可查看: python 子类继承父类构造函数说明。
-
2、在调用基类的方法时,需要加上基类的类名前缀,且需要带上 self 参数变量。区别在于类中调用普通函数时并不需要带上 self 参数
-
3、Python 总是首先查找对应类型的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找。(先在本类中查找调用的方法,找不到才去基类中找)。
如果在继承元组中列了一个以上的类,那么它就被称作"多重继承" 。
语法:
派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后,如下所示:
class SubClassName (ParentClass1[, ParentClass2, ...]):
...
实例:
class Parent:
parentAttr = 100
def __init__(self):
print("parent init")
def parentMethod(self):
print('parant method')
def setAttr(self, attr):
Parent.parentAttr = attr
def getAttr(self):
print("parent attr:", Parent.parentAttr)
class Child(Parent):
def __init__(self):
print("child init")
def childMethod(self):
print('child method')
p = Parent() #parent init,实例化父类
c = Child() #child init,实例化子类
c.childMethod() #child method
c.parentMethod() #parant method
c.setAttr(200) #set parent attr
c.getAttr() #parent attr: 200
print(issubclass(Child,Parent)) #True
print(isinstance(c,Parent)) #True
print(isinstance(p,Parent)) #True
print(isinstance(p,Child)) #False
你可以使用issubclass()或者isinstance()方法来检测。
-
issubclass() - 布尔函数判断一个类是另一个类的子类或者子孙类,语法:issubclass(sub,sup)
-
isinstance(obj, Class) 布尔函数如果obj是Class类的实例对象或者是一个Class子类的实例对象则返回true。
可以看到子类实例c也属于父类的实例,但父类实例p却不是子类的实例。
方法重写
如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法:
实例:
class Parent: # 定义父类
def myMethod(self):
print '调用父类方法'
class Child(Parent): # 定义子类
def myMethod(self):
print '调用子类方法'
c = Child() # 子类实例
c.myMethod() # 子类调用重写方法
基础重载方法
下表列出了一些通用的功能,你可以在自己的类重写:
序号 | 方法, 描述 & 简单的调用 |
1 | __init__ ( self [,args...] ) 构造函数 简单的调用方法: obj = className(args) |
2 | __del__( self ) 析构方法, 删除一个对象 简单的调用方法 : del obj |
3 | __repr__( self ) 转化为供解释器读取的形式 简单的调用方法 : repr(obj) |
4 | __str__( self ) 用于将值转化为适于人阅读的形式 简单的调用方法 : str(obj) |
5 | __cmp__ ( self, x ) 对象比较 简单的调用方法 : cmp(obj, x) |
运算符重载
Python同样支持运算符重载,“+”重载,实例如下:
class Vector:
def __init__(self, a, b):
self.a = a
self.b = b
def __str__(self):
return 'Vector (%d, %d)' % (self.a, self.b)
def __add__(self,other):
return Vector(self.a + other.a, self.b + other.b)
v1 = Vector(2,10)
v2 = Vector(5,-2)
print(v1 + v2)
类属性与方法
类的私有属性
__private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs。
类的方法
在类的内部,使用 def 关键字可以为类定义一个方法,与一般函数定义不同,类方法必须包含参数 self,且为第一个参数
类的私有方法
__private_method:两个下划线开头,声明该方法为私有方法,不能在类的外部调用。在类的内部调用 self.__private_methods
class JustCounter:
__secretCount = 0 # 私有变量
publicCount = 0 # 公开变量
def count(self):
self.__secretCount += 1
self.publicCount += 1
print self.__secretCount
counter = JustCounter()
counter.count()
counter.count()
print counter.publicCount
print counter.__secretCount # 报错,实例不能访问私有变量
Python不允许实例化的类访问私有数据,但你可以使用 object._className__attrName( 对象名._类名__私有属性名 )访问属性(但请不要这样做)。
单下划线、双下划线、头尾双下划线说明:
-
__foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 __init__() 之类的。
-
_foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
-
__foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。
Python 正则表达式
先回顾一下正则表达式的基础知识:
正则表达式中小括号()的作用:和算术运算中的小括号是一样的,那就是进行简单的分组。表示括弧中的部分是一个整体。
正则表达式字符替换规则
RE 字符 | 意义与范例 |
^word | 意义:待搜寻的字符串(word)在行首! 范例:搜寻行首为 # 开始的那一行,并列出行号 grep -n '^#' regular_express.txt |
word$ | 意义:待搜寻的字符串(word)在行尾! 范例:将行尾为 ! 的那一行打印出来,并列出行号 grep -n '!$' regular_express.txt |
. | 意义:代表『一定有一个任意字符』的字符! 范例:搜寻的字符串可以是 (eve) (eae) (eee) (e e), 但不能仅有 (ee) !亦即 e 与 e 中间『一定』仅有一个字符,而空格符也是字符! grep -n 'e.e' regular_express.txt |
\ | 意义:透过 shell 的跳脱字符,将特殊符号的特殊意义去除! 范例:搜寻含有单引号 ' 的那一行! grep -n \' regular_express.txt |
* | 意义:重复零个到无穷多个的前一个 RE 字符 范例:找出含有 (es) (ess) (esss) 等等的字符串,注意,因为 * 可以是 0 个,所以 es 也是符合带搜寻字符串。另外,因为 * 为重复『前一个 RE 字符』的符号, 因此,在 * 之前必须要紧接着一个 RE 字符喔!例如任意字符则为 『.*』 ! grep -n 'ess*' regular_express.txt |
[list] | 意义:字符集合的 RE 字符,里面列出想要撷取的字符! 范例:搜寻含有 (gl) 或 (gd) 的那一行,需要特别留意的是,在 [] 当中『谨代表一个待搜寻的字符』, 例如『 a[afl]y 』代表搜寻的字符串可以是 aay, afy, aly 即 [afl] 代表 a 或 f 或 l 的意思! grep -n 'g[ld]' regular_express.txt |
[n1-n2] | 意义:字符集合的 RE 字符,里面列出想要撷取的字符范围! 范例:搜寻含有任意数字的那一行!需特别留意,在字符集合 [] 中的减号 - 是有特殊意义的,他代表两个字符之间的所有连续字符!但这个连续与否与 ASCII 编码有关,因此,你的编码需要设定正确(在 bash 当中,需要确定 LANG 与 LANGUAGE 的变量是否正确!) 例如所有大写字符则为 [A-Z] grep -n '[A-Z]' regular_express.txt |
[^list] | 意义:字符集合的 RE 字符,里面列出不要的字符串或范围! 范例:搜寻的字符串可以是 (oog) (ood) 但不能是 (oot) ,那个 ^ 在 [] 内时,代表的意义是『反向选择』的意思。 例如,我不要大写字符,则为 [^A-Z]。但是,需要特别注意的是,如果以 grep -n [^A-Z] regular_express.txt 来搜寻,却发现该档案内的所有行都被列出,为什么?因为这个 [^A-Z] 是『非大写字符』的意思, 因为每一行均有非大写字符,例如第一行的 "Open Source" 就有 p,e,n,o.... 等等的小写字 grep -n 'oo[^t]' regular_express.txt |
\{n,m\} | 意义:连续 n 到 m 个的『前一个 RE 字符』 意义:若为 \{n\} 则是连续 n 个的前一个 RE 字符, 意义:若是 \{n,\} 则是连续 n 个以上的前一个 RE 字符! 范例:在 g 与 g 之间有 2 个到 3 个的 o 存在的字符串,亦即 (goog)(gooog) grep -n 'go\{2,3\}g' regular_express.txt |
正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。
Python 自1.5版本起增加了re 模块,它提供 Perl 风格的正则表达式模式。
re 模块使 Python 语言拥有全部的正则表达式功能。
compile 函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。该对象拥有一系列方法用于正则表达式匹配和替换。
re 模块也提供了与这些方法功能完全一致的函数,这些函数使用一个模式字符串做为它们的第一个参数。
本章节主要介绍Python中常用的正则表达式处理函数。
re.match函数
re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match() 就返回 none。
函数语法:
re.match(pattern, string, flags=0)
函数参数说明:
pattern:匹配的正则表达式
string:要匹配的字符串。
flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志
返回值:匹配成功 re.match 方法返回一个匹配的对象,否则返回 None。
我们可以使用 group(num) 或 groups() 匹配对象函数来获取匹配表达式。
group的参数,num=0返回匹配的整个表达式的字符串;
num为从 1 到 所含的小组号时,返回对应的小组字符串;
实例:
import re
line = "Cats are smarter than dogs"
matchObj = re.match( r'(.*) are (.*?) (.*) (.*)', line, re.M|re.I)
print(matchObj)
if matchObj:
print("matchObj.group() : ", matchObj.group())
print("matchObj.group(1) : ", matchObj.group(1))
print("matchObj.group(2) : ", matchObj.group(2))
print("matchObj.group(3) : ", matchObj.group(3))
print("matchObj.group(4) : ", matchObj.group(4))
else:
print("No match!!")
运行结果如下:
matchObj.group() : Cats are smarter than dogs
matchObj.group(1) : Cats
matchObj.group(2) : smarter
matchObj.group(3) : than
matchObj.group(4) : dogs
re.search方法
re.search 扫描整个字符串并返回第一个成功的匹配。
函数语法:
re.search(pattern, string, flags=0)
参数跟返回值与re.match一样,这里我们看一个实例就好:
import re
line = "Cats are smarter than dogs";
searchObj = re.search( r'(.*) are (.*?) .*', line, re.M|re.I)
if searchObj:
print("searchObj.group() : ", searchObj.group())
print("searchObj.group(1) : ", searchObj.group(1))
print("searchObj.group(2) : ", searchObj.group(2))
else:
print("Nothing found!!" )
运行结果为:
searchObj.group() : Cats are smarter than dogs
searchObj.group(1) : Cats
searchObj.group(2) : smarter
re.match与re.search的区别
re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。
import re
line = "Cats are smarter than dogs";
matchObj = re.match( r'dogs', line, re.M|re.I)
if matchObj:
print("match --> matchObj.group() : ", matchObj.group())
else:
print("No match!!")
matchObj = re.search( r'dogs', line, re.M|re.I)
if matchObj:
print("search --> searchObj.group() : ", matchObj.group())
else:
print("No match!!")
运行结果为:
No match!!
search --> searchObj.group() : dogs
检索和替换
Python 的 re 模块提供了re.sub用于替换字符串中的匹配项(替换为指定字符串或者指定的函数返回的字符串)。
语法:
re.sub(pattern, repl, string, count=0, flags=0)
-
pattern : 正则中的模式字符串。
-
repl : 替换的字符串,也可为一个函数。
-
string : 要被查找替换的原始字符串。
-
count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
实例:
import re
phone = "2004-959-559 # this is a phone number"
num = re.sub(r'#.*$', "", phone) #替换#后面字串为空
print("phone number is ", num)
num = re.sub(r'\D', "", phone) # 删除非数字(-)为空
print("pure phone number is ", num)
运行结果
phone number is 2004-959-559
pure phone number is 2004959559
PS:\D 匹配 [^0-9],与\d相反
当repl 参数是一个函数(double)时,示例如下:
import re
# 将匹配的数字乘以 2
def double(matched):
value = int(matched.group('value'))
return str(value * 2)
s = 'A23G4HFD567'
print(re.sub('(?P<value>\d+)', double, s)) #A46G8HFD1134
re.compile 函数
compile 函数用于编译正则表达式,生成一个正则表达式( Pattern )对象,供 match() 和 search() 这两个函数使用。
语法格式为:
re.compile(pattern[, flags])
-
pattern : 一个字符串形式的正则表达式
-
flags : 可选,表示匹配模式,比如忽略大小写,多行模式等,具体参数为:
-
re.I 忽略大小写
-
re.L 表示特殊字符集 \w, \W, \b, \B, \s, \S 依赖于当前环境
-
re.M 多行模式
-
re.S 即为 . 并且包括换行符在内的任意字符(. 不包括换行符)
-
re.U 表示特殊字符集 \w, \W, \b, \B, \d, \D, \s, \S 依赖于 Unicode 字符属性数据库
-
re.X 为了增加可读性,忽略空格和 # 后面的注释
实例:
import re
pattern = re.compile(r'\d+')
m = pattern.match('one12twothree34four')
print(m) # None,从字符串头没有匹配到
m = pattern.match('one12twothree34four', 2, 10)
print(m) # None,从字符串offset=2开始没有匹配到
m = pattern.match('one12twothree34four', 3, 10)
print(m) # <_sre.SRE_Match object; span=(3, 5), match='12'>
print(m.group(0)) #12
print(m.start(0)) # 3
print(m.end(0)) # 5
print(m.span(0)) # (3, 5)
-
group前面已经有介绍。
-
start([group]) 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;
-
end([group]) 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;
-
span([group]) 方法返回 (start(group), end(group))。
再看一个实例:
import re
pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I)
m = pattern.match('Hello World Wide Web')
print(m) # Hello World,匹配两个整体分组这里意味着空格符分割的组员
print(m.group(0)) # Hello World,整个group
print(m.group(1)) # Hello
print(m.group(2)) # World
print(m.span(2)) #(6, 11)
print(m.groups()) #('Hello', 'World')
print(m.group(3)) # error,没有第三个组员
findall
在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果有多个匹配模式,则返回元组列表,如果没有找到匹配的,则返回空列表。
注意: match 和 search 是匹配一次 findall 匹配所有。
语法格式为:
findall(string[, pos[, endpos]])
-
string : 待匹配的字符串。
-
pos : 可选参数,指定字符串的起始位置,默认为 0。
-
endpos : 可选参数,指定字符串的结束位置,默认为字符串的长度。
import re
pattern = re.compile(r'\d+') # find digital number
result1 = pattern.findall('runoob 123 google 456')
result2 = pattern.findall('run88oob123google456', 0, 10)
result3 = pattern.findall('run88oob123google456', 0, 2)
print(result1) # ['123', '456']
print(result2) # ['88', '12']
print(result3) # []
多个匹配模式,返回元组列表:
import re
result = re.findall(r'(\w+)=(\d+)', 'set width=20 and height=10')
print(result) # [('width', '20'), ('height', '10')]
re.finditer
和 findall 类似,在字符串中找到正则表达式所匹配的所有子串,并把它们作为一个迭代器返回。
re.finditer(pattern, string, flags=0)
参数与re.match一致。
pattern: 匹配的正则表达式。
string: 要匹配的字符串。
flags: 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志
import re
it = re.finditer(r"\d+","12a32bc43jf3")
for match in it:
print (match.group() )
运行结果为:
12
32
43
3
re.split
split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下:
re.split(pattern, string[, maxsplit=0, flags=0])
参数:
pattern: 匹配的正则表达式。
string: 要匹配的字符串。
maxsplit:分隔次数,maxsplit=1 分隔一次,默认为 0,不限制次数。
flags: 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志
import re
print(re.split('\W+', 'runoob, runoob, runoob.'))
print(re.split('(\W+)', ' runoob, runoob, runoob.'))
print(re.split('\W+', ' runoob, runoob, runoob.', 1))
print(re.split('a*', 'hello world'))
输出结果如下:
['runoob', 'runoob', 'runoob', '']
['', ' ', 'runoob', ', ', 'runoob', ', ', 'runoob', '.', '']
['', 'runoob, runoob, runoob.']
/usr/local/python-3.6.7/lib/python3.6/re.py:212: FutureWarning: split() requires a non-empty pattern match.
return _compile(pattern, flags).split(string, maxsplit)
['hello world']
ord()函数与chr()函数
chr( {需要转换的Unicode编码} ),返回值是对应的字符;
ord( {需要转换的字符} ),返回值是对应的Unicode编码;