开始语:
在python软件中,为了便于维护以及更好地实现模块化,好的程序都被分解成多个函数。
各个函数都有它的功能,我们可以通过自己定义的一系列函数来实现自己想要的功能。
(1)定义
使用def
语句定义函数。
格式:
def 函数名(参数列表):
"函数_文档字符串" # 函数说明,定义完成后,可通过.__doc__来获取
函数体
return 表达式 # 结束函数,不带表达式的return相当于返回None
函数体就是在调用函数时执行的一系列语句。
例子:
def add_abs(x,y = 0):
if x>=0:
return x+y
else:
return y-x
注:
- 如果定义的函数中带有默认参数(有默认值的参数,如
y = 0
),该参数及其后面所有参数都是可选的。 - 如果忘记给函数定义中的所有可选参数
(*args,**kwargs)
赋值,就会引发语法错误。 - 定义了函数之后,默认的参数值会传递给:以值的方式提供的对象。
如:
a = 10
def foo(x = a):
return x
a = 5
print(foo()) # 10
以下情况若使用可变对象作为默认值,默认参数将保留前面调用进行的修改。
def foo(x,items=[]):
items.append(x)
return items
print(foo(1)) # [1]
print(foo(2)) # [1, 2]
print(foo(3)) # [1, 2, 3]
为防止出现这种状况,可设置None
值,并附上相应的检查代码:
def foo(x,items=None):
if items is None:
items = []
items.append(x)
return items
print(foo(1)) # [1]
print(foo(2)) # [2]
print(foo(3)) # [3]
(2)调用
- 定义了一个函数之后,需要调用函数,即在函数名称后面加上参数元组,如
foo(1)
- 调用函数时,参数的数量及顺序必须与函数定义中相匹配,不然会引发
TypeError
。 - 位置参数必须位于默认参数等其他参数之前;
- 位置参数顺序必须与函数定义中相匹配;
- 默认参数的顺序可以不与函数定义中相一致,若不指明参数名称,就必须与位置参数那样与定义中相匹配;
- 使用关键字参数时,参数顺序不影响。
注:
关键词参数: 即在调用函数时,提供函数参数,可以用显示的为每个参数指定一个值,如foo(x =1,y = 0)
这种形式。
(3)参数
如果函数定义了多个参数,那么在调用函数的时候,传递的数据要和定义的参数一一对应。(同上)
参数分为可变参数、默认参数以及不定长参数。
可变参数(位置参数)
即上面出现在默认参数之前的不带默认值的参数,如foo(x,y=0)
中的参数x
。
默认参数
定义函数时,可以给参数设置默认值,这个参数就被称为默认参数;
如果默认参数没有传入值,则直接使用默认值,反之,则使用传入的新值;
默认参数可被忽略,其他参数必须要传入值;
不定长参数(任意数量)
若希望能够处理的参数个数比当初定义的参数个数多,此时可以在函数中使用不定长参数;
格式如下:
def 函数名([formal_args,]*args,**kwargs):
函数体
return 表达式
调用函数时,参数个数会优先匹配formal_args参数的个数,若个数相同,不定长参数返回空的元组或字典;若个数比formal_args多,分两种情况:
- 如果多余的传入的参数没有指定名称(如 2),args会以元组的形式存放这些多余的参数,即args是位置参数元组;
- 如果多余的传入的参数指定了名称(如a = 2),kwargs会以字典的形式存放这些多余的参数,即kwargs是关键字参数字典。
如果编写的函数接受大量可扩充的配置选项作为参数,但列出这些参数又显得过款时,使用双星号开头的参数就很有用。
(4)变量作用域
每次调用函数,函数内部都会创建一个局部命名空间,该局部环境包含函数参数名称以及在函数内定义赋值的变量名称。
在使用变量时,需要对函数中变量进行解析,解释器的解析原则是Local <- Enclosed <- Global <- Built-in。此为变量使用先后顺序。
下面对此进行解释:
首先搜索局部命名空间,即函数内部(参数及变量),若没有找到相应名称,接着找嵌套的闭包函数(闭包:将组成函数的语句和这些语句的执行环境打包在一起)中是否能查找到相应的名称,若还没找到,就尝试在定义该函数的模块中查找,即全局命名空间,如果也没匹配到,则在python内置命名空间中进行查找。若仍然未查找到,就引发NameError
异常。
下面见一个例子,用于理解声明变量作用域的关键字(先自己思考一下结果):
def scope_test():
def do_local():
spam = 'local spam'
def do_nonlocal():
nonlocal spam
spam = 'nonlocal spam'
def do_global():
global spam
spam = 'global spam'
spam = 'text spam'
do_local()
print('after local assignment:',spam)
do_nonlocal()
print('after nonlocal assignment:',spam)
do_global()
print('after global assignment:',spam)
scope_test()
print('In global scope:',spam)
结果为:
afterlocal assignment: text spam
after nonlocal assignment: nonlocal spam
after global assignment: nonlocal spam
In global scope: global spam
解释:
第一个结果很好理解,do_local()
函数里没有return函数,因此spam变量用的是嵌套的闭包函数中的变量spam = 'text spam'
(即Local中没有,即用Enclosed中的);
第二个结果,do_nonlocal()
函数里有nonlocal
关键字声明,即说明函数里面的spam = 'nonlocal spam'
是对闭包中的spam = 'text spam'
进行重新赋值,因而其值为nonlocal spam(print输出的总是处于同层次变量的值);
第三个结果,do_global()
函数里有global
关键字声明,由于全局命名空间中并没有spam变量,因此spam = 'global spam'
在全局命名空间中建立spam变量并对其进行赋值,不对闭包中spam变量产生影响,因此print函数输出变量spam的值仍然为nonlocal spam;
第四个结果,print函数位于全局命名空间中,因此输出的spam为全局变量,其值为global spam。
(5)装饰器
装饰器是一个函数,其主要用途是包装另一个函数或类。
首要目的是透明地修改或增强被包装对象的行为,也可以理解为增加被包装对象的功能。
举例:
假如有多个函数,都希望可以输出函数的执行日志这么一个需求,为减少代码重复,我们可以创建一个新的函数专门记录函数执行日志,谁需要记录执行日志,就将谁作为参数传递;并且对已经实现的函数,要遵守“封闭开放”的原则,不允许在函数内部进行修改,装饰器可以满足上述需求。
表示装饰器的语法是特殊符号@。
使用介绍:
@trace # 假设这个trace函数为一个记录执行日志的函数
def square(x): # 函数square需要执行日志,因此在其上方加上一个装饰器
return x**2
上述代码相当于:
def square(x):
return x**2
square = trace(square) # 函数对象本身传递给函数trace作为参数,并返回一个对象代替原来的square
实例:
def wrap(func):
print('正在装饰')
def inner():
print('正在验证权限')
func()
return inner
@wrap
def test():
print("test")
test()
执行过程:
(1)执行test()时,发现函数test()上面有装饰器@wrap,所以先执行@wrap;
此时,@wrap
相当于wrap(test)
;
(2)执行wrap(test),首先执行print
语句,输出“正在装饰”,接着执行定义函数inner
,最后将inner()
函数的引用作为返回值返回给wrap(test)
,然后根据 test = wrap(test)
, wrap(test)将其返回值赋给test
,因而此时,test指向inner函数;
(3)调用test()
,因为test指向inner函数的引用,test()函数相当于调用inner()
函数,输出“正在验证权限”,并输出“test”。
因此,既保证运行了装饰器函数,又不影响输出函数test(),很明显增强了test()函数的功能
当然,也可以有多个解释器:
@a
@b
@c
def d(x):
print('I'm the d.')
相当于:
d = a(b(c(d)))
装饰器也可以带有参数:
如果我们给装饰器添加参数,需要增加一层封装,先传递参数,然后再传递函数名。
@trace('length')
def square(x):
return x**2
相当于:
temp = trace('length') # 使用提供的参数调用装饰器
square = temp(square) # 调用装饰器返回的函数
(6)生成器与yield
函数使用关键字yield
定义生成器对象。
生成器是一个函数,它生成一个值的序列,可用于迭代。
见例子:
def self_range(n):
'''
This is a self-definition range function.
'''
print('decresing %d step by step'%n)
while n>=0:
print(n)
yield n
print(n)
n -= 1
print(n)
sr = self_range(10)
我们发现直接调用该函数,其中的代码是没有运行的,因为它返回的是一个生成器对象。
原因:
生成器对象只在调用__next__()时,生成器函数才会不断执行语句,直到遇到yield语句为止。
然后yield语句会在函数执行停止的地方生成一个结果,直到再次调用__next__()函数,才会继续执行yield之后的语句,直到遇到下一个yield语句停止。
这些话很重要,注意理解。
sr.__next__()
#decresing 10 step by step
#10 (由yield之前的print(n)输出)
#out[]:10(注意这个是__next__()的输出结果,即yield生成的结果)
sr.__next__()
#10 (由第一次yield语句之后的print(n)输出,此时n还是10)
#9
#9 (由第二次yield语句之前的print(n)输出)
#out[]:9(这个为此__next__()的输出结果,即yield生成的结果)
通常不会再生成器上直接调用__next__()
方法,而是在for语句以及一些序列操作中使用它.
for i in self_range(10):
print(i)
(7)协程与yield表达式
在函数内,yield可用作出现在赋值运算符右边的表达式,例如:
def receiver():
print("Ready to receive")
while True:
n = (yield)
print("Got %s"% n)
以这种方式使用yield语句的函数称为协程。**它的执行是为了响应发送给它的值。**它的行为也十分类似于生成器。
r = receiver()
r.__next__() # 对__next__()的初始调用是必不可少的,这样协程才能执行可通向第一个yield表达式的语句。
# 这时候,协程会挂起,等待相关生成器对象r的send()方法给它发送一个值。
# 传递给send()的值由协程中的(yield)表达式返回。
# 接收到值后,协程就会执行语句,直至遇到下一条yield语句。
r.send(1) # Got 1
r.send(2) # Got 2
r.send('Hello,world') # Got Hello,World
由于协程中需要首先使用__next__()
这件事情很容易被忽略,经常成为错误出现的根源。因此,建议使用一个能自动完成该步骤的装饰器来包装协程。
见下方:
def co_routine(func):
def start(*args,**kwargs):
g = func(*args,**kwargs)
g.__next__()
return g
return start
@co_routine
def receiver():
print("Ready to receive")
while True:
n = (yield)
print("Got %s"% n)
r = receiver()
r.send("Hello,World!")
结合上面的装饰器概念,我们再来大致理解一下执行程序工作顺序:
receiver()前加@co_routine,相当于receiver = co_routine(receiver)
;结合co_routine函数内容,相当于将receiver指向start函数;
r = receiver()
相当于先执行start()
函数,其里面将g = receiver()
,并且使用了__next__()
函数,然后并重新赋值给r;
最后给协程对象r送值,并返回。
协程的运行一般是无限期的,除非它被显示关闭或者自己退出。
使用close()
方法可以关闭输入值的流。
关闭后,若继续给协程发送值,就会引发StopIteration异常,同生成器那样,close()方法将引发GeneratorExit异常。
可以用throw(exceptiontype[,value[,tb]])方法在协程内部引发异常,tb指跟踪对象。
如果yield表达式中提供了值,协程可以使用yield语句同时接收和发出返回值。
def line_splitter(delimiter=None):
print("Ready to split...")
result = None
while True:
line = (yield result) #说明生成的是result的值
result = line.split(delimiter)
s = line_splitter(",")
s.__next__() # Ready to split...
s.send("A,B,C") # ['A','B','C']
这上面有个很细节的点,就是在yield
表达式前面加上了result = None
,原因就是为了避免我们在运行s.__next__()
时产生输出结果,因为此时result = None
。
注:
send()方法的返回值,就是传递给下一条yield语句的值。也就是说,send()方法的返回值来自下一个yield表达式,而不是接收send()传递的值的yield表达式
(8)列表推导式
函数的常用操作是将函数应用给一个列表的所有项,并使用结果创建一个新列表。
由于这种操作很常见,因此出现了叫做列表推导的运算符。
一般语法如下:
[expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN]
等价于:
s = []
for item1 in iterable1:
if condition1:
for item2 in iterable2:
if condition2:
...
for itemN in iterableN:
if conditonN:s.append(expression)
举例:
a = [1,-2,3,4,-6,9]
b = 'love'
s = [(i,j) for i in a for j in b if i>0]
print(s)
(9)生成器表达式
生成器表达式是一个对象,语法和列表推导相同,只是用圆括号代替方括号。
(expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN)
列表推导与生成器表达式之间的差异很微妙,但是十分重要。其主要表现在性能和内存使用上。
从上面我们可以知道,生成器只在我们调用它时才会执行,列表推导一旦创建,它就占用全部内存资源。
另外,生成器表达式不会创建序列形式的对象,因此无法进行索引等列表操作。但是可用list()函数转换。
(10)lambda表达式
使用lambda语句可以创建表达式形式的匿名函数:
lambda args : expression
args是以逗号分隔的参数列表。
而expression是用到这些参数的表达式。
使用lambda定义的代码必须是合法的表达式。
lambda语句中不能出现多条语句和其他非表达式语句,如for和while。
遵循与函数相同的作用域规则。
(11)递归
递归函数定义示例:
def factorial(n):
if n <=1:return 1
else:return n * factorial(n-1)
注意:
Python对递归函数调用的深度做了限制。
函数sys.getrecursionlimit()
返回当前最大的递归深度;
sys.setrecursionlimit()
可用于修改这个值。
超出递归深度时,就会引发RuntimeError异常。
打印一个嵌套列表中的所有项:
s = [[1,2,3,4],5,[6,[7,8],[9,10]]]
def flatten(lists):
for s lists:
if isinstance(s,list):
flatten(s)
else:
print(s)
flatten(s)
还可以用生成器的递归方法:
def gen_flatten(lists):
for s lists:
if isinstance(s,list):
for item in gen_flatten(s):
yield item
else:yield item
(12)文档字符串
通常,函数的第一条语句会使用文档字符串,用于描述函数的用途。
文档字符串保存在函数的__doc__属性中。
如果需要使用装饰器,要知道使用装饰器包装函数可能会破坏
与文档字符串相关的帮助功能。
例:
def wrap(func):
call(*args,**kwargs):
return func(*args,**kwargs)
return call
@wrap
def factorial(n):
'''
This Function is computig n fatorial.
'''
help(factorial)
这个问题的解决办法就是编写可以传递函数名称和文档字符串的装饰器函数。
def wrap(func):
call(*args,**kwargs):
return func(*args,**kwargs)
call.__doc__ = func.__doc__
call.__name__ = func.__name__
return call
因为这是一个常见问题,所以functools模块提供了函数wraps,用于自动复制这些属性。
from functools import wraps
def wrap(func):
@wraps(func)
call(*args,**kwargs):
return func(*args,**kwargs)
return call
@wraps(func)将属性从func传递给要定义的包装器函数。
(13)函数属性
可以给函数添加任意属性,如:
def foo():
print('Hello World!')
foo.person = '程序员'
foo.emotion = '惊吓'
函数属性保存在__dict__属性中,为一个字典。
print(foo.__dict__) # {'person': '程序员', 'emotion': '惊吓'}
(14)eval()、exec()和compile()函数
eval(source, globals=None, locals=None, /)
Evaluate the given source in the context(上下文) of globals and locals.
The source may be a string representing a Python expression(表达式字符串)
or a code object as returned by compile().(编译函数返回的代码对象)
The globals must(必须) be a dictionary(字典) and locals can(可以) be any
mapping(映射),
defaulting to the current globals and locals.
If only globals is given, locals defaults to it.
globals 和 locals 是可选的映射对象,分别用作代码执行的全局和局部命名空间。
exec(source, globals=None, locals=None, /)
Execute(执行) the given source in the context of globals and locals.
The source may be a string representing one or more Python statements(语句字符串)
or a code object as returned by compile().(编译函数返回的代码对象)
The globals must be a dictionary and locals can be any mapping,
defaulting to the current globals and locals.
If only globals is given, locals defaults to it.
见例子:
globals = {'x':7,'y':10,'birds':['Parrot','Swallow','Albatross']}
locals = {}
a = eval("3*x+4*y",globals,locals)
print(a) # 61
exec("for b in birds:print(b)",globals,locals)
# Parrot
# Swallow
# Albatross
给exec()或eval()函数传递字符串时,解析器首先会把这个字符串编译为字节码。因为这个过程十分耗资源,如果代码要反复执行多次,最好是预编译代码,然后在后续的调用中重用字节码。
compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
Compile source into a code object that can be executed by exec() or eval().
The source code may represent a Python module(模块), statement(语句) or expression(表达式).
The filename will be used for run-time error messages(运行时错误消息).
The mode must be 'exec' to compile a module, 'single' to compile a
single (interactive) statement, or 'eval' to compile an expression.
The flags argument, if present, controls which future statements influence
the compilation of the code.(控制未来语句影响代码的编译)
The dont_inherit argument, if true, stops the compilation inheriting
the effects of any future statements in effect in the code calling
compile; if absent or false these statements do influence the compilation,
in addition to any features explicitly specified.
见例子:
s = "for i in range(10):print(i)"
c = compile(s,'','exec') # 编译为代码对象
exec(c)
# 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
s2 = "3*x+4*y"
c2 = compile(s2,'','eval') # 编译为表达式
result = eval(c2,globals,locals)
print(result)
# 61