函数
函数的参数
位置参数
默认参数
可变参数(传入tuple)
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
我们把函数的参数改为可变参数:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
>>> calc(1, 2)
5
>>> calc()
0
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
*nums
表示把nums
这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
关键字参数(传入dict)
关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
函数person
除了必选参数name
和age
外,还接受关键字参数kw
。在调用该函数时,可以只传入必选参数:
>>> person('Michael', 30)
name: Michael age: 30 other: {}
也可以传入任意个数的关键字参数:
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
关键字参数有什么用?它可以扩展函数的功能。比如,在person
函数里,我们保证能接收到name
和age
这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
当然,上面复杂的调用可以用简化的写法:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
命名关键字参数
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
def person(name, age, *, city, job):
print(name, age, city, job)
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
高级特性
切片
迭代
那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
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
列表生成式
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
写列表生成式时,把要生成的元素x * x
放到前面,后面跟for
循环,就可以把list创建出来
使用内建的isinstance
函数可以判断一个变量是不是字符串:
>>> x = 'abc'
>>> y = 123
>>> isinstance(x, str)
True
>>> isinstance(y, str)
False
if … else
以下代码正常输出偶数:
>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]
因为for
前面的部分是一个表达式,它必须根据x
计算出一个结果。因此,考察表达式:x if x % 2 == 0
,它无法根据x
计算出结果,因为缺少else
,必须加上else
:
>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
可见,在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
生成器
在Python中,这种一边循环一边计算的机制,称为生成器:generator。
创建生成器:把一个列表生成式的[]
改成()
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
要把fib
函数变成generator,只需要把print(b)
改为yield b
就可以了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。
而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
同样的,把函数改成generator后,我们基本上从来不会用next()
来获取下一个返回值,而是直接使用for
循环来迭代:
>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8
但是用for
循环调用generator时,发现拿不到generator的return
语句的返回值。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done
杨辉三角定义如下:
1
/ \
1 1
/ \ / \
1 2 1
/ \ / \ / \
1 3 3 1
/ \ / \ / \ / \
1 4 6 4 1
/ \ / \ / \ / \ / \
1 5 10 10 5 1
把每一行看做一个list,试写一个generator,不断输出下一行的list:
def triangles():
l = [1]
while True:
yield l
l = [0]+l+[0]
l = [l[i]+l[i+1] for i in range(len(l)-1)]
l = [0]+l+[0] #首尾加0,用于边界的计算
l = [l[i]+l[i+1] for i in range(len(l)-1)] #循环生成list,最后赋值给l
l[i]+l[i+1] #前一个值和后一个值相加计算出下一个值
for i in range(len(l)-1) #循环获取生成的索引值
range(len(l)-1) #获得l的全部索引值,因为计算方式是当前值和后一个值相加,避免尾部越界所以要减一
#拆开就是这样的
def triangles():
l = [1]
while True:
yield l
l = [0]+l+[0]
g = []
for i in range(len(l)-1):
g.append(l[i]+l[i+1])
l = g
迭代器
凡是可作用于for
循环的对象都是Iterable
类型;
凡是可作用于next()
函数的对象都是Iterator
类型,它们表示一个惰性计算的序列;
集合数据类型如list
、dict
、str
等是Iterable
但不是Iterator
,不过可以通过iter()
函数获得一个Iterator
对象。
Python的for
循环本质上就是通过不断调用next()
函数实现的,例如:
for x in [1, 2, 3, 4, 5]:
pass
实际上完全等价于:
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
try:
# 获得下一个值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循环
break
函数式编程
高阶函数
传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
当我们调用add(-5, 6, abs)
时,参数x
,y
和f
分别接收-5
,6
和abs
编写高阶函数,就是让函数的参数能够接收别的函数。
map/reduce
map()
函数用法: map接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
reduce
的用法:reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
如果考虑到字符串str
也是一个序列,使用reduce(),配合map()
,我们就可以写出把str
转换为int
的函数:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> def char2num(s):
... digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
... return digits[s]
...
>>> reduce(fn, map(char2num, '13579'))
13579
整理成一个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))
filter
Python内建的filter()
函数用于过滤序列。
filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
在一个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]
用filter求素数
用Python来实现这个算法,可以先构造一个从3
开始的奇数序列:
#用filter求素数
#构造3开始的奇数列
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) # 构造新序列
#由于`primes()`也是一个无限序列,所以调用时需要设置一个退出循环的条件:
#打印1000以内的素数
for n in primes():
if n < 1000:
print(n)
else:
break
filter()
的作用是从一个序列中筛出符合条件的元素。由于filter()
使用了惰性计算,所以只有在取filter()
结果的时候,才会真正筛选并每次返回下一个筛出的元素。
例:回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()筛选出回数:\
#方案一:
def is_palindrome(n):
nn = str(n)
return nn == nn[::-1] #反转字符串并对比原字符串返回true/false
output = filter(is_palindrome, range(1, 1000))
#方案二:
output = filter(lambda n : str(n) == str(n)[::-1], range(1,200)) #用lamba代替函数方法
if list(output) == [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('测试失败!')
sorted (排序算法)
Python内置的sorted()
函数就可以对list进行排序:
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
要进行反向排序,可以传入参数 reverse=True
要忽略大小写,可以传入参数 key=str.lower
返回函数
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中.
※返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
#利用闭包返回一个计数器函数,每次调用它返回递增整数:
def createCounter():
a = 0
def counter():
nonlocal a
a+=1
return a
return counter
#简单版,把一个需序列赋值给s,这样做的目的是方便子函数能够直接使用父函数内的变量值,而不会产生“local variable 'xxx' referenced before assignment”这样的错误。
def createCounter():
s = [0]
def counter():
s[0] = s[0]+1
return s[0]
return counter
补充:关键字global和nonlocal的用法说明
global关键字用来在函数或其他局部作用域中使用全局变量。
如果局部要对全局变量修改,应在局部声明该全局变量。
count = 0
def global_test():
global count
count += 1
print(count)
global_test()
注意:global会对原来的值(全局变量)进行相应的修改
count = 0
def global_test():
global count
count += 1
print(count)
global_test()
print(count)
nonlocal声明的变量不是局部变量,也不是全局变量,而是外部嵌套函数内的变量。*
匿名函数
关键字lambda
表示匿名函数
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
同样,也可以把匿名函数作为返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
装饰器
在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,decorator就是一个返回函数的高阶函数。
假设我们要增强now()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()
函数的定义.
一个完整的decorator的写法如下:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
或者针对带参数的decorator:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
Python内置的functools.wraps
可以把原始函数的__name__
等属性复制到wrapper()
函数中,防止有些依赖函数签名的代码执行出错。
#装饰器练习,请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间:
import time,functools
#三层
def metric(text):
def decorator(func):
t1 = time.time()
@functools.wraps(func)
def wrapper(*args, **kw):
r = func(*args, **kw)
print('%s executed in %s ms:' % (func.__name__, 1000*(time.time()-t1)))
return r
return wrapper
return decorator
# 测试
@metric('Debug')
def fast(x, y):
time.sleep(0.0012)
return x + y;
@metric('Debug')
def slow(x, y, z):
time.sleep(0.1234)
return x * y * z;
#两层~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def metric(fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
t1 = time.time()
print('%s executed in %s ms' % (fn.__name__, time.time()-t1))
return fn(*args, **kw)
return wrapper
# 测试
@metric
def fast(x, y):
time.sleep(0.0012)
return x + y;
@metric
def slow(x, y, z):
time.sleep(0.1234)
return x * y * z;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
f = fast(11, 22)
s = slow(11, 22, 33)
if f != 33:
print('测试失败!')
elif s != 7986:
print('测试失败!')
能否写出一个`@log`的decorator,使它既支持:
@log
def f():
pass
又支持:
@log('execute')
def f():
pass
import functools
import time
def log2(text=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args,**kw):
if isinstance(text,(int,str)):
print(‘%s begin call %s():‘ %(text,func.__name__))
func(*args,**kw)
print(‘%s end call %s():‘ %(text,func.__name__))
else:
print(‘begin call %s():‘ % func.__name__)
func(*args,**kw)
print(‘end call %s():‘ % func.__name__)
return
return wrapper
return decorator if isinstance(text,(int,str)) else decorator(text)
@log2
def now2():
print(‘now is:‘+time.asctime())
now2()
@log2(‘timeshow‘)
def now3():
print(‘now is:‘+‘2017-07-10‘)
now3()
#在调试里面跑了一下,不写参数的时候,text拿到的实际是函数对象,就是本来func应该拿到的那个,结果让text提前吃掉了,截胡了你程序里的now。
#这里不写字符串的话,text不会拿到None,而是直接被赋值成函数。调试里面跑到这一步,text的属性是指向一个function。
#如果把return里面那个decorator(text) 直接改成decorator,执行会提示缺参数。因为内层的func这个量已经没参数可接了。
#decorator(text) 的意思就是,如果发现text不是str类型,那么就是本该传给内层func的函数让外层提前拿走了,并且放在了text里面。这时应该手动把text传给里面那层,于是就return了 decorator(text)
偏函数
functools.partial
可以帮助我们创建一个偏函数,不需要我们自己定义int2()
,可以直接使用下面的代码创建一个新的函数int2
:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
functools.partial
的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
模块
作用域
有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_
前缀来实现
类似__xxx__
这样的变量是特殊变量,我们自己的变量一般不要用这种变量名;
类似_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用
例子:
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。
def _private_1(name):
return 'Hello, %s' % name
def _private_2(name):
return 'Hi, %s' % name
def greeting(name):
if len(name) > 3:
return _private_1(name)
else:
return _private_2(name)
面向对象编程
类和实例
必须牢记类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
访问限制
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。
如果外部代码要获取name和score,可以给Student类增加get_name
和get_score
这样的方法:
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
如果又要允许外部代码修改score,可以再给Student类增加set_score
方法:在方法中,可以对参数做检查,避免传入无效的参数:
class Student(object):
...
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
#请把下面的Student对象的gender字段对外隐藏起来,用get_gender()和set_gender()代替,并检查参数有效性:
class Student(object):
def __init__(self, name, gender):
self.name = name
self.__gender = gender
def get_gender(self):
return self.__gender
def set_gender(self,gender):
if gender == 'male' or gender == 'female':
self.__gender = gender
else:
raise ValueError('bad type')
# 测试:
bart = Student('Bart', 'male')
if bart.get_gender() != 'male':
print('测试失败!')
else:
bart.set_gender('female')
if bart.get_gender() != 'female':
print('测试失败!')
else:
print('测试成功!')
继承和多态
继承: 在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样。
多态: 当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。
对于一个变量,我们只需要知道它是Animal
类型,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal
、Dog
、Cat
还是Tortoise
对象上,由运行时该对象的确切类型决定
“开闭”原则:
对扩展开放:允许新增Animal
子类;
对修改封闭:不需要修改依赖Animal
类型的run_twice()
等函数。
class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
def run_twice(animal):
animal.run()
animal.run()
run_twice(Animal())
run_twice(Dog())
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
run_twice(Tortoise())
静态语言 vs 动态语言
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“。
许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()
方法的对象。
获取对象信息
- 基本类型都可以用
type()
判断:
>>> type(123)
<class 'int'>
- 我们要判断class的类型,可以使用
isinstance()
函数。
先创建3种类型的对象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
然后,判断:
>>> isinstance(h, Husky)
True
>>> isinstance(h, Dog)
True
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True
- 如果要获得一个对象的所有属性和方法,可以使用
dir()
函数,它返回一个包含字符串的list
在Python中,如果你调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法,所以,下面的代码是等价的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
- 仅仅把属性和方法列出来是不够的,配合
getattr()
、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态:
#获取对象信息
class MyObject(object):
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x
obj = MyObject()
#测试该对象的属性:
print(hasattr(obj,'x')) # 有属性'x'吗?
print(hasattr(obj,'y'))# 有属性'y'吗?
print(setattr(obj,'y',19)) # 设置一个属性'y'
print(obj.y)# 获取属性'y'
#如果试图获取不存在的属性,会抛出AttributeError的错误:
print(getattr(obj, 'z')) # 获取属性'z'
getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
#获得对象的方法:
>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81
#从文件流fp中读取图像
def readImage(fp):
if hasattr(fp,'read'): #判断该fp对象是否存在read方法
return readData(fp) #如果存在,则该对象是一个流
return None #如果不存在,则无法读取
请注意,在Python这类动态语言中,根据鸭子类型,有read()
方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()
方法返回的是有效的图像数据,就不影响读取图像的功能。
实例属性和类属性
实例属性属于各个实例所有,互不干扰;
类属性属于类所有,所有实例共享一个属性;
不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。
#为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加:
class Student(object):
count = 0
def __init__(self, name):
self.name = name
Student.count += 1
print(Student.count)
bart = Student('Bart')
print(Student.count)
lisa = Student('Bart')
print(Student.count)
面向对象高级编程
错误,调试和测试
错误处理
高级语言通常都内置了一套try...except...finally...
的错误处理机制,Python也不例外。
try
用try
运行的代码,如果执行出错,则跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块
print('try...')
r = 10 / int('a') #int()函数可能会抛出ValueError
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e: #如果发生了不同类型的错误,应该由不同的except语句块处理。
print('except:', e)
else:
print('no error!') #当没有错误发生时,会自动执行else语句
finally:
print('finally...')
print('END')
Python所有的错误都是从BaseException
类派生的,常见的错误类型和继承关系看这里:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
使用try...except
捕获错误可以跨越多层调用,比如函数main()
调用bar()
,bar()
调用foo()
,结果foo()
出错了,这时,只要main()
捕获到了,就可以处理.
调用栈
# err.py:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()
执行,结果如下:
$ python3 err.py
Traceback (most recent call last): #错误的跟踪信息。
File "err.py", line 11, in <module> #调用main()出错
main()
File "err.py", line 9, in main #调用bar('0')出错
bar('0')
File "err.py", line 6, in bar #return foo(s) * 2这个语句出错
return foo(s) * 2
File "err.py", line 3, in foo #return 10 / int(s)这个语句出错
return 10 / int(s)
ZeroDivisionError: division by zero #int(s)本身并没有出错,但是int(s)返回0,在计算10/0时出错
出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。
记录错误
Python内置的logging
模块可以非常容易地记录错误信息:
# err_logging.py
import logging
...
def main():
try:
bar('0')
except Exception as e:
logging.exception(e)
...
程序打印完错误信息后会继续执行,并正常退出
抛出错误
首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise
语句抛出一个错误的实例:
# err_raise.py
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10 / n
foo('0')
如果可以选择Python已有的内置的错误类型(比如ValueError
,TypeError
),尽量使用Python内置的错误类型。
另一种错误处理的方式:
# err_reraise.py
def foo(s):
n = int(s)
if n==0:
raise ValueError('invalid value: %s' % s)
return 10 / n
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
bar()
在bar()
函数中捕获了错误,但是,打印一个ValueError!
后,又把错误通过raise
语句抛出
捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。
raise
语句如果不带参数,就会把当前错误原样抛出。此外,在except
中raise
一个Error,还可以把一种类型的错误转化成另一种类型:
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!')
只要是合理的转换逻辑就可以,但是,决不应该把一个IOError
转换成毫不相干的ValueError
。
调试
python中有一套调试程序的手段用来修复bug。
断言(assert)
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
#表达式n!=0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
return 10 / n
def main():
foo('0')
#运行结果:
#如果断言失败,assert语句本身就会抛出AssertionError:
Traceback (most recent call last):
...
AssertionError: n is zero!
启动Python解释器时可以用-O
参数(大写字母O)来关闭assert
,关闭后,你可以把所有的assert
语句当成pass
来看。
logging
logging
不会抛出错误,而且可以输出到文件:
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10/n)
#输出
INFO:root:n = 0
...
logging
允许指定记录信息的级别,有debug
,info
,warning
,error
等几个级别
pdb
Python的调试器可以让程序以单步方式运行,可以随时查看运行状态
python -m pdb err.py
1 查看代码
n 单步执行
p 变量名 查看变量
q 退出
pdb.set_trace()
在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点:
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)
#命令p查看变量,或者用命令c继续运行:
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File "err.py", line 7, in <module>
print(10 / n)
ZeroDivisionError: division by zero
IDE
单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
文档测试
IO编程
文件读写
读文件
f = open('/Users/michael/test.txt', 'r')
f.read()
f.close()
#或者
#使用with语句自动帮我们调用close()方法
with open('/path/to/file', 'r') as f:
print(f.read())
可以反复调用read(size)
方法,每次最多读取size个字节的内容
readline()
可以每次读取一行内容
调用readlines()
一次读取所有内容并按行返回list
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'删掉
二进制文件
读取图片、视频等等,用'rb'
模式打开
>>> f = open('/Users/michael/test.jpg', 'rb')
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节
字符编码
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')
encoding
参数: 遇到UTF-8编码, 如读取GBK编码的文件
errors
参数: 忽略非法编码的字符
写文件
传入标识符'w'
或者'wb'
表示写文本文件或写二进制文件
>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
忘记调用close()
的后果是数据可能只写了一部分到磁盘,剩下的会丢失.
with open('/Users/michael/test.txt', 'w') as f:
f.write('Hello, world!')
'w'
模式 覆盖写入
'a'
(append)模式 追加写入
StringIO和BytesIO
StringIO
在内存中读写str
- 写:先创建一个StringIO,然后像文件一样写入
getvalue()
方法用于获得写入后的str
from io import StringIO
f = StringIO()
f.write('hello')
f.write(' ')
f.write('world!')
print(f.getvalue())
- 读:用一个str初始化StringIO,然后像读文件一样读取
from io import StringI
f = StringIO('hello world!\ngoodbye')
while True:
s = f.readline()
if s == '':
break
print(s.strip()) #strip()用于移除字符串头尾指定字符,默认为空格或换行符
BytesIO
用于操作二进制数据,即在内存中读写bytes
#BytesIO
from io import BytesIO
f = BytesIO() #创建一个BytesIO,然后写入一些bytes:
f.write('中文'.encode('utf-8'))
print(f.getvalue())
#用一个bytes初始化BytesIO,然后,像读文件一样读取
g = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
g.read()
f=StringIO('ABCDEFGHIJK')
print(f.write('abcdef')) #输出 6 指针移动
print(f.read()) #输出 GHIJK 从移动后的位置开始读
print(f.getvalue()) #输出 abcdefGHIJK 保留了原来长度
小结: StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
操作文件和目录
Python内置的os
模块也可以直接调用操作系统提供的接口函数。
os.name # 操作系统类型
os.uname() #详细系统信息
os.environ #查看环境变量
os.environ.get('key')#获取某个环境变量的值
os.environ.get('x', 'default')
查看、创建和删除目录
os.path.abspath('.') #查看当前目录的绝对路径
os.path.join('/Users/michael', 'testdir') #要创建目录,首先把新目录的完整路径表示出来
os.mkdir('/Users/michael/testdir') #创建目录
os.rmdir('/Users/michael/testdir') #删除目录
os.rename('test.txt', 'test.py') #对文件重命名
把两个路径合成一个时,要通过os.path.join()
函数
要拆分路径时,通过os.path.split()
函数把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名
>>> os.path.split('/Users/michael/testdir/file.txt')
('/Users/michael/testdir', 'file.txt')
os.path.splitext()
可以直接获取文件扩展名:
>>> os.path.splitext('/path/to/file.txt')
('/path/to/file', '.txt')
os
模块的补充
shutil
模块
copyfile()
函数 可以完成文件复制
利用Python的特性来过滤文件
列出当前目录下的所有目录
>>> [x for x in os.listdir('.') if os.path.isdir(x)]
['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]
列出所有的.py
文件
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']
['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']
#split()函数
#string.split(str="", num=string.count(str))[n]
#str - - 分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等。
#num - - 分割次数。
#[n] - - 选取的第n个分片
练习
- 利用
os
模块编写一个能实现dir -l
输出的程序。
- 编写一个程序,能在当前目录以及当前目录的所有子目录下查找文件名包含指定字符串的文件,并打印出相对路径。
import os
dd = os.getcwd() #获取当前工作目录
def FindFile(fileName,dir):
li = os.listdir(dir)
for fi in li:
route = os.path.join(dir,fi)
if os.path.isfile(route) and fileName in fi:
print(os.path.relpath(route,dd)) #得到相对路径
if os.path.isdir(route):
FindFile(fileName,route)
dir = os.getcwd()
FindFile('123',dir)
序列化
我们把变量从内存中变成可存储或传输的过程称之为序列化.
反之,把变量内容从序列化的对象重新读到内存里称为反序列化,即unpickling。
Python中pickle
模块用于实现序列化
import pickle
d = dict(name = 'Bob', age = 20, score = 88) #创建对象
#print(pickle.dumps(d)) #将对象序列化成一个bytes
#print(pickle.loads(d)) #从bytes中反序列化出对象
f = open('dump.txt','wb') #创建文件
pickle.dump(d,f) #把对象序列化后写入一个file-like Object
f.close()
f = open('dump.txt','rb')
d = pickle.load(f) #从一个file-like Object中反序列化出对象
f.close()
print(d)
Pickle只能用于Python,因此,只能用Pickle保存那些不重要的数据。
JSON
把对象序列化为标准格式,比如XML,方便在不同的编程语言之间传递对象
如果序列化为JSON,就可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输
JSON和Python内置的数据类型对应如下:
JSON类型 | Python类型 |
{} | dict |
[] | list |
“string” | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python内置的json
模块提供了非常完善的Python对象到JSON格式的转换
import json
d = dict(name = 'Bob', age = 18, score = 90)
print(json.dumps(d)) #把Python对象变成一个JSON
#类似的,dump()方法可以直接把JSON写入一个file-like Object
json_str = '{"name": "Bob", "age": 18, "score": 90}'
print(json.loads(json_str)) #把JSON的字符串反序列化
#load()可以从file-like Object中读取字符串并反序列化
JSON进阶
Student
实例首先被student2dict()
函数转换成dict
,然后再被顺利序列化为JSON:
import json
class Student(object):
def __init__(self,name,age,score):
self.name = name
self.age = age
self.score = score
s = Student('Bob',20,88)
#转换函数
def sutdent2dict(std):
return {
'name' : std.name,
'age' : std.age,
'score' : std.score
}
#可选参数default
print(json.dumps(s,default=sutdent2dict))
dumps()
方法提供了大量可选参数让我们来定制JSON序列化。
https://docs.python.org/3/library/json.html#json.dumps
把任意class
的实例变为dict
:
print(json.dumps(s, default=lambda obj: obj.__dict__))
把JSON反序列化为一个Student
对象实例:
def dict2student(d):
return Student(d['name'], d['age'], d['score'])
json_str = '{"age": 20, "score": 88, "name": "Bob"}'
print(json.loads(json_str, object_hook=dict2student))
运行结果:
<__main__.Student object at 0x10cd3c190>
小结
Python语言特定的序列化模块是pickle
,但如果要把序列化搞得更通用、更符合Web标准,就可以使用json
模块。
json
模块的dumps()
和loads()
函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是,当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。
进程和线程
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。
多进程
fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
有了fork
调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务
Python的os
模块封装了常见的系统调用,其中就包括fork
,可以在Python程序中轻松创建子进程:
import os
print('Process (%s) start ...' % os.getpid())
pid = os.fork()
if pid == 0:
print('I am child process' (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
#运行结果:
Process (92428) start ...
I (92428) just created a child process (92429).
I am child process (92429) and my parent is 92428.
multiprocessing
multiprocessing
模块是跨平台版本的多进程模块,他提供了一个Process
类来代表一个进程对象
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
#创建一个Process实例,并传入一个执行函数和函数的参数
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join() #等待子进程结束后再继续往下运行,通常用于进程间的同步。
print('Child process end.')
Pool
进程池可以启动大量的子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
多线程
ThreadLocal
分布式进程
正则表达式
常用内建模块
- datetime
- collections
- base64
- struct
- hashlib
- hmac
- itertools
- contextlib
- urllib
- XML
- HTMLParser
访问数据库