函数的分类:内建函数,max(),reversed()等
库函数:math.ceil()等
函数的作用:封装,复用,简洁,美观易懂
函数定义,调用:定义需要在调用之前,也就是说调用时,已经被定义过了
否则,抛NameError异常
函数是可调用对象,callable() True,False
函数的参数:
参数调用时,传入的参数要和定义的个数想匹配(可变参数例外)
位置参数
- def f(x,y,x) 调用使用f(1,3,5)
- 按照参数定义顺序传入实参
关键字参数
- def f(x,y,z)调用使用f(x=1,y=3,z=5)
- 使用形参的名字来传入实参,传参的顺序可以和定义的顺序不同
传参
- 位置参数必须在关键字参数之前传入,位置参数是按位置对应的
def add(x, y):
return x+y
print(add(5, 7)) ------> 12
print(add(x=6, y=9)) ------> 15
print(add(y=2, 8)) ------> SyntaxError: positional argument follows keyword argument
参数默认值
定义时,在形参后跟上一个值
def add(x=4, y=5):
return x+y
def add(x=4, y=5):
return x+y
print(add(5, 7))
print(add(5, y=77))
print(add(5))
print(add(x=5))
print(add(y=8))
print(add())
print(add(y=5, x=7))
错误:
print(add(x=5, 7))
print(add(y=5, 7))
定义时的错误:
def add1(x=4, y): # 应该为 def add1(y,x=4):
作用:可以未传入足够的实参的时候,对没有给定的参数赋值为默认值,有时不需要每次都要输入所有参数,可简化函数调用
举例:
定义一个函数login,参数名为host,port,username,password
def login(host='127.0.0.1', port='8080', username='panqi', password='hehe'):
print('{}:{}@{}/{}'.format(host, port, username, password))
login()
login('192.168.1.111', '80', 'tom', 'haha')
login('localhost', port='80', password='haha')
login(password='heihei', port='80', host='www')
可变参数 ---- 一个形参可以匹配任意个参数
有多个数,需要累加求和
def add(nums):
sumnum = 0
for x in nums:
sumnum += x
return sumnum
print(add([1, 3, 5]))
print(add((2, 4, 6)))
# 传入一个可迭代对象,迭代元素求和
位置参数的可变参数
def add(*nums):
sumnum = 0
for x in nums:
sumnum += x
return sumnum
print(add(1, 3, 5))
# 在形参前使用*表示该形参是可变参数,可以接收多个实参
# 收集多个实参为一个tuple
关键字参数的可变参数
def showconfig(**kwargs):
for k, v in kwargs.items():
print('{}={}'.format(k, v))
showconfig(host='127.0.0.1', port='8080', username='panqi', password='hehe')
# 形参前使用**符号,表示可以接收多个关键字参数
# 收集的实参名称和值组成一个字典
out:
port=8080
username=panqi
password=hehe
host=127.0.0.1
可变参数混合使用
def showconfig(username, password, **kwargs):
print(username, password, kwargs)
showconfig(host='127.0.0.1', port='8080', username='panqi', password='hehe')
out:
panqi hehe {'host': '127.0.0.1', 'port': '8080'}
-----------------------------------------------------------------------------
def showconfig(username, *args, **kwargs):
print(username, args, kwargs)
showconfig('panqi', '8080', '192.168.1.177', password='hehe')
out:
panqi ('8080', '192.168.1.177') {'password': 'hehe'}
-----------------------------------------------------------------------------
def showconfig(username, password, *args, **kwargs):
print(username, password, args, kwargs)
out:
panqi hehe ('8080',) {'host': '192.168.1.177'}
可变参数总结:
- 有位置可变参数和关键字可变参数
- 位置可变参数在形参前使用一个*号
- 关键字可变参数在形参前使用两个*号(**)
- 位置可变参数和关键字可变参数都可收集若干个实参,位置可变参数收集形成一个tuple,关键字可变参数收集形成一个dict
- 混合使用参数时,可变参数要放到参数列表的最后,普通参数要放到参数列表前面,位置可变参数要在关键字可变参数之前
举例:
def fn(x, y, *args, **kwargs):
print(x)
print(y)
print(args)
print(kwargs)
fn(3, 5, 7, 9, 10, a=1, b='python')
print('------------------------------')
fn(3, 5)
print('------------------------------')
fn(3, 5, 7)
print('------------------------------')
fn(3, 5, a=1, b='python')
# fn(7, 9, y=5, x=3, a=1, b='python') # 错误 7,9分别赋值给x,y,又y=5,x=3,重复
out:
3
5
(7, 9, 10)
{'a': 1, 'b': 'python'}
------------------------------
3
5
()
{}
------------------------------
3
5
(7,)
{}
------------------------------
3
5
()
{'a': 1, 'b': 'python'}
keyword-only参数(python3加入)
如果在一个*号参数后,或者一个位置可变参数,出现的普通参数,不再是普通参数,而是keyword-only参数
def fn(*args, x, y, **kwargs):
print(x)
print(y)
print(args)
print(kwargs)
fn(7, 9, y=5, x=3, a=1, b='python')
# args可以看做已截获了所有的位置参数,y,x不使用关键字参数就不可能拿到实参
out:
3
5
(7, 9)
{'b': 'python', 'a': 1}
而def(**kwargs, x):
这种写法没有必要,kwagrs会截获所有关键字参数,就算写了x=9,x也永远得不到这个值,所以语法错误
keyword-only参数另一种形式
def fn(*, x, y):
print(x, y)
fn(x=5, y=6)
# *号之后,普通形参都变成了必须给出keyword-only参数
可变参数和参数默认值
def fn(*args, x=5):
print(args)
print(x)
fn() # 等价于fn(x=5)
print('--------------------')
fn(5)
print('--------------------')
fn(x=6)
print('--------------------')
fn(1, 2, 3, x=10)
out:
()
5
--------------------
(5,)
5
--------------------
()
6
--------------------
(1, 2, 3)
10
def fn(y, *args, x=5): # x是keyword-only参数
print('x={}, y={}'.format(x, y))
print(args)
# fn() # y没有传参
fn(5) # 5传给y 可变位置参数没有,x为默认值
# fn(x=6) # y没有传参
fn(1, 2, 3, x=10) # 1传给y 2,3为可变位置参数,x=10
# fn(y=17,2,3,x=10) # 位置参数跟在关键字参数后面
# fn(1,2,y=3,x=10) #1已经位置参数给了y,后面又关键字传参y=3,重复了
out:
x=5, y=5
()
x=10, y=1
(2, 3)
def fn(x=5, **kwargs):
print('x={}'.format(x))
print(kwargs)
fn()
print('--------------')
fn(5)
print('--------------')
fn(x=6)
print('--------------')
fn(y=3, x=10)
print('--------------')
fn(3, y=10)
out:
x=5
{}
--------------
x=5
{}
--------------
x=6
{}
--------------
x=10
{'y': 3}
--------------
x=3
{'y': 10}
参数规则
参数列表参数一般顺序是,普通参数---缺省参数---可变位置参数---keyword-only参(可带缺省省)---可变关键字参数
def fn(x, y, z=3, *arg(*), m=4, n, **kwargs)
def fn(x, y, z=3, *args, m=4, n, **kwargs):
print(x, y, z)
print(args)
print(m)
print(n)
print(kwargs)
fn(2, 4, 6, 8, 10, n=16, k='hehe')
out:
2 4 6
(8, 10)
4
16
{'k': 'hehe'}
def connect(host='localhost', port='3306', user='admin', password='admin', **kwargs):
print(host, port)
print(user, password)
print(kwargs)
connect(db='cmdb')
print('--------------------------')
connect(host='192.168.1.123', db='cmdb')
print('--------------------------')
connect(host='192.168.1.123', db='cmdb', password='mysql')
out:
localhost 3306
admin admin
{'db': 'cmdb'}
--------------------------
192.168.1.123 3306
admin admin
{'db': 'cmdb'}
--------------------------
192.168.1.123 3306
admin mysql
{'db': 'cmdb'}
参数解构
def add(x, y):
return x+y
print(add(4, 5))
t = (4, 5)
print(add(t[0], t[1]))
print(add(*t))
print(add(*(4, 5)))
print(add(*[4, 5]))
print(add(*{4, 5}))
print(add(*range(4, 6)))
# 全部一个结果,为9
- 给函数提供实参的时候,可以集合类型前使用*或者**,把集合类型的结构解开,并提取所有元素作为函数的实参
- 非字典类型使用*解构成位置参数
- 字典类型使用**解构成关键字参数
- 提取出来的元素数目要和参数的要求匹配,也要和参数的类型匹配
def add(x, y):
return x+y
print(add(*(4, 5)))
print('-------------')
print(add(*[4, 5]))
print('-------------')
print(add(*{4, 6}))
print('-------------')
d = {'x': 5, 'y': 6}
print(add(**d))
print('-------------')
print(add(**{'x': 5, 'y': 6}))
# add(**{'a': 5, 'b': 6}) 关键字名字不对
out:
9
-------------
9
-------------
10
-------------
11
-------------
11
参数解构和可变参数
- 给函数提供实参的时候,可以在集合类型前使用*或者**,把集合类型的结构解开,提取出所有元素作为函数的实参
def add(*iterable):
print(iterable)
result = 0
for x in iterable:
result += x
return result
print(add(1, 2, 3))
print(add(*[1, 2, 3]))
print(add(*range(10)))
out:
(1, 2, 3)
6
(1, 2, 3)
6
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
45
函数的返回值总结:
- 函数使用return语句返回“返回值”
- 所有函数都有返回值,如果没有return语句,会隐式调用return None
- return语句并不一定是函数的语句块的最后一条语句
- 一个函数可以存在多个return语句,但只有一条可以被执行,如果没有,会隐式调用return None
- 如果有必要,可以显示调用return None,简写为return
- 函数如果执行了return语句,函数就会返回,不再执行下去了
- 作用:结束函数调用,返回值
def fn():
return [1, 2, 3] # 返回一个值(列表),此值有三个元素
def fn1():
return 1, 2, 3 # 返回一个值(元组)
def fn2():
return 1, # 返回一个值(元组)
def fn3():
return 1 # 返回一个单值,1
x, y, z = fn() # 使用解构提取更为方便
作用域
一个标识符的可见范围,就是作用域,一般常说的是变量的作用域
如下:
x = 5
def fn():
print(x)
fn()
#没问题,正常输出5
------------------------------------
x = 5
def fn():
x += 1 ---> x = x + 1 赋值即定义,相当于重新定义了一个本地变量x,
print(x) 而此时,本地并没有x,报UnboundLocalError未绑定本地变量
且x也覆盖不了外部的x,不管是局部还是全局的
fn()
特别重要
全局作用域:
- 在整个程序运行环境中都可见
局部作用域:
- 在函数、类等内部可见
- 局部变量使用范围不能超过其所在的局部作用域
def fn():
x = 1 # 局部作用域,在fn内
def fn1():
print(x) # x不可见
def outer1():
o = 65
def inner(): # 先定义不执行,等第8条,调用才执行
print('inner{}'.format(o)) # 此时,本地没有变量:o 向上一层找到o = 65 第二个输出
print(chr(o)) # A 第三个输出
print('outer{}'.format(o)) # outer65 第一个输出
inner() # 调用 执行inner函数
outer1()
out:
outer65
inner65
A
def outer2():
o = 65
def inner(): # 先定义不执行,等第9条,调用才执行
o = 97
print('inner{}'.format(o)) # 此时,本地有变量:o = 97 第二个输出 inner97
print(chr(o)) # chr(97) 第三个输出 a
print('outer{}'.format(o)) # 本地作用域内,有变量o = 65 第一个输出 outer65
inner() # 调用 执行inner函数
outer2()
out:
outer65
inner97
a
以上例子可以看出
- 外层变量作用域在内层作用域内可见
- 内层作用域inner中,如果定义了o=97,相当于当前作用域中重新定义了一个新的变量o,但这个o并没有覆盖外层作用域outer中的o
全局变量global
x = 5
def fn():
global x
x += 1
return x
print(fn())
out:6
前面提到x += 1,会报错,这里使用global,将fn内的x变量声明为使用外部的全局作用域中定义的x
全局作用域中必须有x的定义
def fn():
global x
x = 10
x += 1
return x
print(fn())
print(x)
out:
11
11
但是,x = 10 赋值即定义,x在内部作用域为一个外部作用域的变量赋值,这里的x的作用域还是全局的
global总结:
- 外部作用域变量会内部作用域可见,但为了函数封装,尽量要函数传参
闭包
自由变量:未在本地作用域中定义的变量,例如定义在内层函数外的外层函数的作用域中的变量
闭包是一个概念,出现在嵌套函数中
指的是内层函数引用到了外层函数的自由变量,就形成了闭包
def fn():
c = [0]
def fn1():
c[0] += 1 # 修改的是列表的元素 这里的c引用的是自由变量,也就是fn里的c
return c[0] # python2中实现闭包的方式,python3还可以使用nonlocal关键字
return fn1
foo = fn()
c = 111
print(foo(), foo())
print(foo())
out:
1 2
3
nonlocal关键字
使用后,将变量标记为不在本地作用域定义,而在上级的某一级局部作用域中定义,但不能是全局作用域中定义
def fn():
count = 0
def fn1():
nonlocal count
count += 1
return count
fn1()
return fn1
foo = fn()
print(foo())
print(foo())
print(foo())
out:
2
3
4
count是外层函数的局部变量,被内部函数引用
内部函数使用nonlocal关键字声明count变量在上级作用域而非本地作用域中定义
上面代码且可形成闭包
默认值的作用域
def foo(xyz=1):
print(xyz)
foo() -------> 1
foo() --------> 1
print(xyz) ----------> NameError: name 'xyz' is not defined
def foo(xyz=[]):
xyz.append(1)
print(xyz)
foo() -----> [1]
foo() ------> [1, 1]
print(xyz) ------> NameError: name 'xyz' is not defined 当前作用域没有xyz变量
第二次foo() 输出为[1, 1]
因为函数也是对象,python把函数的默认值放了在属性中,这个属性就伴随着这个函数对象的整个生命周期
查看foo.__defaults__属性
def foo(xyz=[], u='abc', z=123):
xyz.append(1)
return xyz
print(foo(), id(foo))
print(foo.__defaults__)
print(foo(), id(foo))
print(foo.__defaults__)
out:
[1] 2824230231712
([1], 'abc', 123)
[1, 1] 2824230231712
([1, 1], 'abc', 123)
函数地址没有变,就是说函数这个对象一直没有变是,调用它,它的属性__default__中使用元组保存默认值
xyz默认值是引用类型,引用类型的元素变动,并不是元组的变化
非引用类型例子:
def foo(w, u='abc', z=123):
u = 'xyz'
z = 789
print(w, u, z)
print(foo.__defaults__)
foo('magedu')
print(foo.__defaults__)
out:
('abc', 123)
magedu xyz 789
('abc', 123)
- 属性__default__中使用元组保存所有位置参数默认值,它不会因为在函数体内使用了它而发生变化
- 元组不可变
def foo(w, u='abc', *, z=123, zz=[456]): # z、zz 是keyword-only参数
u = 'xyz'
z = 789
zz.append(1)
print(w, u, z, zz)
print(foo.__defaults__)
foo('magedu')
print(foo.__kwdefaults__)
out:
('abc',) # 单元组
magedu xyz 789 [456, 1]
{'z': 123, 'zz': [456, 1]}
- 属性__kwdefaults__中使用字典保存所有keyword-only参数的默认值
使用可变类型作为默认值,就可能修改这个默认值
有时候这个特性是好的,有的时候这种特性是不好的,有副作用
def foo(xyz=[], u='abc', z=123):
xyz = xyz[:] # 等同 a = xyz[:] 所以前面的这个xyz不是形参中的xyz
xyz.append(1) # 这里xyz.append 也类似a.append 与形参中的xyz无关,不影响其值改变
print(xyz)
foo() # [1]
print(foo.__defaults__) # ([], 'abc', 123)
foo() # [1]
print(foo.__defaults__) # ([], 'abc', 123)
foo([10]) # [10, 1]
print(foo.__defaults__) # ([], 'abc', 123)
foo([10, 5]) # [10, 5, 1]
print(foo.__defaults__) # ([], 'abc', 123)
# 后面为输出结果
# xyz都是传入参数或者默认参数的副本,如果想修改原参数,无能为力
def foo(xyz=None, u='abc', z=123):
if xyz is None:
xyz = []
xyz.append(1)
print(xyz)
foo() # xyz为None,进if语句,xyz = [], xyz.append(1) xyz为[1]
print(foo.__defaults__) # (None, 'abc', 123)
foo() # xyz为None,进if语句,xyz = [], xyz.append(1) xyz为[1]
print(foo.__defaults__) # (None, 'abc', 123)
foo([10]) # 不使用默认值,为[10],append(1), xyz为[10,1]
print(foo.__defaults__) # (None, 'abc', 123)
foo([10, 5]) # 不使用默认值,为[10, 5],append(1), xyz为[10,5,1]
print(foo.__defaults__) # (None, 'abc', 123)
out:
[1]
(None, 'abc', 123)
[1]
(None, 'abc', 123)
[10, 1]
(None, 'abc', 123)
[10, 5, 1]
(None, 'abc', 123)
使用不可变类型默认值,如果使用缺省值None就创建一个列表,如果传入一个列表,就修改这个列表
第一种方法:
- 使用影子拷贝创建一个新的对象,永远不能改变传入的参数
第二种方法:
- 通过值的判断就可灵活的选择创建或者修改传入对象
- 这种方式灵活,应用广泛
- 很多函数的定义,都可以看到使用None这个不可变的值作为默认参数,可以说是一种惯用法
函数的销毁
def foo(xyz=[], u='abc', z=123):
xyz.append(1)
z = 111
return xyz, z
a = foo
print(id(a)) # 2240486465184
print(foo(), id(foo), foo.__defaults__) # ([1], 111) 2240486465184 ([1], 'abc', 123)
def foo(xyz=[], u='abc', z=123): # 重新定义过foo
xyz.append(1)
return xyz
print(foo(), id(foo), foo.__defaults__) # [1] 2240486465320 ([1], 'abc', 123)
del foo # 相当于对象的引用计数减1
print(a(), id(a), a.__defaults__)
# 但a还保留着foo函数对象所指向的地址 ([1, 1], 111) 2240486465184 ([1, 1], 'abc', 123)
# 注意这个foo是第一个的foo,与下面这个重新赋值定义的foo不一样,看从id就能看出来
# print(foo(), id(foo), foo.__defaults__) 这条会报下面的错误
# 而重新赋值定义的foo,又被del,引用计数减1,所以会报NameError: name 'foo' is not defined
全局函数的销毁
- 重新定义同名函数
- del语句删除函数对象,引用计数减1
- 程序结束时
def foo(xyz=[], u='abc', z=123):
xyz.append(1)
def inner(a=10):
pass
print(id(inner))
def inner(a=100):
print(xyz, a)
print(id(inner))
return inner
print(id(foo))
bar = foo() # 会首先输出第6行,接着第10行,可以看出两个inner已经不是同一个,第一个被覆盖了
# 而且它会得到foo函数的返回值,inner函数对象(第二个)
print(id(bar), foo.__defaults__, bar.__defaults__)
# 所以id(bar)应该和第10行一样,而从bar.__defaults__也能看出,为第二个inner,a的默认值
# del bar # 只相当于inner的引用计数减1(有可能别地还有个kao = foo(),那就又引用到了),等到其引用计数为0,就得gc了,
# 但bar这个标识符就不能再调用什么东西了,所以最后两句就会报错
print(id(foo)) # 这是全局函数,得程序结束时,或重定义,或del时,才能删除,这样不为函数调用结束就消亡
print(foo.__defaults__)
print(id(bar))
print(bar.__defaults__)
out:
2258046377632
2258046377768
2258047541320
2258047541320 ([1], 'abc', 123) (100,)
2258046377632
([1], 'abc', 123)
2258047541320
(100,)
局部函数的销毁
- 重新在上级作用域定义同名函数
- del语句删除函数对象
- 上级作用域销毁时
这里的上级作用域销毁:
# del bar 如果不销毁bar
del foo # 销毁foo,虽然inner的引用计数减1,但还是能输出下面两行
print(id(bar))
print(bar.__defaults__)
# 因为bar还在引用inner
函数的执行流程
def foo1(b, b1=3):
print("foo1 called", b, b1)
def foo2(c):
foo3(c)
print("foo2 called", c)
def foo3(d):
print("foo3 called", d)
def main():
print("main called")
foo1(100, 101)
foo2(200)
print("main ending")
main()
out:
main called
foo1 called 100 101
foo3 called 200
foo2 called 200
main ending
1、全局帧中生成foo1,foo2,foo3,main函数对象
2、main函数调用
3、main中查找内建函数print压栈,将常量字符串压栈,调用函数,弹出栈顶
4、main中全局查找函数foo1压栈,将常100,101压栈,调用函数foo1,创建栈帧,
print函数压栈,字符串和变量b,b1压栈,调用函数,弹出栈顶,返回值
5、main中全局查找foo2函数压栈,将常量200压栈,调用foo2,创建栈帧,
foo3函数压栈,变量c引用压栈,调用foo3,创建栈帧,foo3完成print函数调用后返回,
foo2恢复调用,执行print后,返回值。
6、main中foo2函数调用结束后,弹出栈顶,main继续执行下一条print函数调用,弹出栈顶,main函数返回
递归
- 递归一定要有边界条件
- 当边界条件不满足时,递归前进
- 当边界条件满足时,递归返回
# Loop
num = 8
per = 0
cur = 1
for i in range(num):
per, cur = cur, per+cur
print(per, end=' ')
print()
def fb(n):
"""婓波那契数列:F(n)=F(n-1)+F(n-2)"""
return 1 if n < 2 else fb(n-1) + fb(n-2)
# 1 1 2 3 5 8 13 21
for i in range(8):
print(fb(i), end=' ')
out:
1 1 2 3 5 8 13 21
1 1 2 3 5 8 13 21
超过递归深度限制,抛RecursionError:maxinum recursion depth exceeded
查看sys.getrecursionlimit()
效率比较
import datetime
start = datetime.datetime.now()
pre = 0
cur = 1
num = 35
for i in range(num-1):
pre, cur = cur, pre + cur
print(pre, end=' ')
delta = (datetime.datetime.now() - start).total_seconds()
print(delta)
print()
num = 35
start = datetime.datetime.now()
def fib(n):
return 1 if n < 2 else fib(n-1) + fib(n-2)
for i in range(num):
print(fib(i), end=' ')
delta = (datetime.datetime.now() - start).total_seconds()
print(delta)
out:
1 1 2 3 5 8 13 21 34 55 89 ..... 2178309 3524578 5702887 0.0
1 1 2 3 5 8 13 21 34 55 89 ..... 2178309 3524578 5702887 9227465 7.147847
循环时间忽略,而递归要7.14秒
婓波拉契数列的改进:
import datetime
start = datetime.datetime.now()
def fib(n, pre=0, cur=1): # recursion
pre, cur = cur, pre + cur
print(pre, end=' ')
if n == 2:
delta = (datetime.datetime.now() - start).total_seconds()
print(delta)
return
fib(n-1, pre, cur)
fib(35)
out:
1 1 2 3 5 8 13 21 34 55 89 ..... 2178309 3524578 5702887 0.0
间接递归:
def foo1():
foo2()
def foo2():
foo1()
foo1()
就是通过别的函数调用了函数自身
如果构成循环递归调用是非常危险的,但是往往这种情况在代码复杂的情况下,还是可能会发生调用
只能让代码规范来避免
总结:
- 递归是一种很自然的表达,符合逻辑思维
- 递归相对运行效率低,每一次调用函数都要开辟栈帧
- 递归有深度限制,太深,函数反复压栈,栈内存很快就会溢出
- 如果是有限次数的递归,可以使用递归调用,或者循环代替
- 绝大多数递归,都可以使用循环实现
- 能不用则不用递归
练习: