关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数 。至于到底传入了哪些,就需要在函数内部通过ss检查。
仍以person()
函数为例,我们检查是否有传入的city
和job
参数:
def person(name, age, **ss): if 'city' in ss: # 有city参数 pass if 'job' in ss: # 有job参数 pass print('name:', name, 'age:', age, 'other:', ss) person('Joek', 24, city='shanghai', addr='yang', number=123456) ''' name: Joek age: 24 other: {'city': 'shanghai', 'addr': 'yang', 'number': 123456} '''
可以看出,调用者可以传入不受限制的关键字参数,不仅仅只是我们想要的city和job参数
命名关键字参数
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
def person(name, age, *, city, job): print(name, age, city, job) # person('Joek', 24, city='shanghai', job='it', number=123456) ''' TypeError: person() got an unexpected keyword argument 'number' ''' person('Joek', 24, city='shanghai', job='it') ''' Joek 24 shanghai it '''
和关键字参数**ss
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
def person(name, age, *args, city, job): print(name, age, args, city, job)
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
def person(name, age, *arg, city, job): print(name, age, city, job) person('Joek', 24, 'shanghai', 'it') ''' TypeError: person() takes 2 positional arguments but 4 were given '''
由于调用时缺少参数名city
和job
,Python解释器把这4个参数均视为位置参数,但person()
函数仅接受2个位置参数。
在*后面的参数都是命名关键字参数,传值的时候必须按照关键字参数进行传值,*args后面的参数也是命名关键字参数,命名关键字参数可以有缺省值(city是默认参数可以不传值),从而简化调用:
def person(name, age, *, city='shanghai', job): print(name, age, city, job) person('Joek', 24, job='it')
由于命名关键字参数city
具有默认值,调用时,可不传入city
参数
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
def person(name, age, city, job): # 缺少 *,city和job被视为位置参数 pass
函数对象
函数是第一类对象: 指的是函数的内存地址可以像一个变量值一样去使用,函数作为对象可以赋值给一个变量、可以作为元素添加到集合对象中、可作为参数值传递给其它函数,还可以当做函数的返回值,这些特性就是第一类对象所特有的
函数身为一个对象,拥有对象模型的三个通用属性:id、类型、和值
函数值可以被引用
def person(name): print(name) #id()函数用于获取对象的内存地址,python里面一切皆对象,哈哈 都有id print(id(person),type(person),person) #2026874247368 <class 'function'> <function person at 0x000001D7EB1E98C8> per=person per('joek')
赋值给另外一个变量时,函数并不会被调用,仅仅是在函数对象上绑定一个新的名字而已
同理,你还可以把该函数赋值给更多的变量,唯一变化的是该函数对象的引用计数不断地增加,本质上这些变量最终指向的都是同一个函数对象
变量值可以当作参数传给另外一个函数
def person(name): print(name) def show(func): name='joek' return func(name) show(person) # joek
变量值可以当作函数的返回值
def person(name): print(name) def put(func): return person put(person)('joek') # joek
函数接受一个或多个函数作为输入或者函数输出(返回)的值是函数时,我们称这样的函数为高阶函数,比如上面的 show 和put 都属于高阶函数。
Python内置函数中,典型的高阶函数是 map 函数,map 接受一个函数和一个迭代对象作为参数,调用 map 时,依次迭代把迭代对象的元素作为参数调用该函数。
def person(name): print(name) lens = map(person, ["a","b","c","d"]) print(list(lens)) ''' a b c d [None, None, None, None] 我没有在person中return任何值,就返回了四个None的列表 '''
map 函数的作用相当于:
print([person(i) for i in ["the","zen","of","python"]])
只不过 map 的运行效率更快一点。
变量值可以当作容器类型的元素
容器对象(list、dict、set等)中可以存放任何对象,包括整数、字符串,函数也可以作存放到容器对象中
funcs = [person, str, len] for f in funcs: print(f("joek"))
person是我们自定义的函数,str 和 len 是两个内置函数。for 循环逐个地迭代出列表中的每个元素时,函数对象赋值给了f变量,调用 f(“joek”) 与 调用 person(“joek”) 本质是一样的效果,每次 f 都重新指向一个新的函数对象。当然,你也可以使用列表的索引定位到元素来调用函数。
funcs[0]("joek") # 等效于 person("joek")
补充:python函数中,参数是传值,还是传引用
在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码。
代码段1:
def foo(arg): arg = 2 print(arg) a = 1 foo(a) # 输出:2 print(a) # 输出:1
看了代码段1的同学可能会说参数是值传递。
代码段2:
def bar(args): args.append(1) b = [] print(b)# 输出:[] print(id(b)) # 输出:4324106952 bar(b) print(b) # 输出:[1] print(id(b)) # 输出:4324106952
看了代码段2,这时可能又有人会说,参数是传引用,那么问题来了,参数传递到底是传值还是传引用或者两者都不是?为了把这个问题弄清楚,先了解 Python 中变量与对象之间的关系。
变量与对象
Python 中一切皆为对象,数字是对象,列表是对象,函数也是对象,任何东西都是对象。而变量是对象的一个引用(又称为名字或者标签),对象的操作都是通过引用来完成的。例如,[]是一个空列表对象,变量 a 是该对象的一个引用
a = []
a.append(1)
在 Python 中,「变量」更准确叫法是「名字」,赋值操作 = 就是把一个名字绑定到一个对象上。就像给对象添加一个标签。
a = 1
整数 1 赋值给变量 a 就相当于是在整数1上绑定了一个 a 标签。
a = 2
整数 2 赋值给变量 a,相当于把原来整数 1 身上的 a 标签撕掉,贴到整数 2 身上。
b=a
把变量 a 赋值给另外一个变量 b,相当于在对象 2 上贴了 a,b 两个标签,通过这两个变量都可以对对象 2 进行操作。
变量本身没有类型信息,类型信息存储在对象中,这和C/C++中的变量有非常大的出入(C中的变量是一段内存区域)
参数的传递本质
Python 函数中,参数的传递本质上是一种赋值操作,而赋值操作是一种名字到对象的绑定过程,清楚了赋值和参数传递的本质之后,现在再来分析前面两段代码。
def foo(arg): arg = 2 print(arg) a = 1 foo(a) # 输出:2 print(a) # 输出:1
在例子中,变量 a 绑定了 1,调用函数 foo(a) 时,相当于给参数 arg 赋值 arg=1,这时两个变量都绑定了 1。在函数里面 arg 重新赋值为 2 之后,相当于把 1 上的 arg 标签撕掉,贴到 2 身上,而 1 上的另外一个标签 a 一直存在。因此 print(a) 还是 1。
再来看一下代码
def bar(args): args.append(1) b = [] print(b)# 输出:[] print(id(b)) # 输出:4324106952 bar(b) print(b) # 输出:[1] print(id(b)) # 输出:4324106952
执行 append 方法前 b 和 arg 都指向(绑定)同一个对象,执行 append 方法时,并没有重新赋值操作,也就没有新的绑定过程,append 方法只是对列表对象插入一个元素,对象还是那个对象,只是对象里面的内容变了。因为 b 和 arg 都是绑定在同一个对象上,执行 b.append 或者 arg.append 方法本质上都是对同一个对象进行操作,因此 b 的内容在调用函数后发生了变化(但id没有变,还是原来那个对象)
最后,回到问题本身,究竟是是传值还是传引用呢?说传值或者传引用都不准确。非要安一个确切的叫法的话,叫传对象(call by object)。
python不允许程序员选择采用传值还是传引用。Python参数传递采用的肯定是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值--相当于通过“传引用”来传递对象。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象--相当于通过“传值'来传递对象。
如果作为面试官,非要考察候选人对 Python 函数参数传递掌握与否,与其讨论字面上的意思,还不如来点实际代码。
默认参数的陷阱
def bad_append(new_item, a_list=[]): a_list.append(new_item) return a_list
这段代码是初学者最容易犯的错误,用可变(mutable)对象作为参数的默认值。函数定义好之后,默认参数 a_list 就会指向(绑定)到一个空列表对象,每次调用函数时,都是对同一个对象进行 append 操作。因此这样写就会有潜在的bug,同样的调用方式返回了不一样的结果。
>>> print bad_append('one') ['one'] >>> print bad_append('one') ['one', 'one']
而正确的方式是,把参数默认值指定为None
def good_append(new_item, a_list=None): if a_list is None: a_list = [] a_list.append(new_item) return a_list
函数的嵌套
函数的嵌套调用:在一个函数内部又调用其他函数
def max2(x,y): if x > y: return x else: return y def max4(a,b,c,d): res1=max2(a,b) res2=max2(res1,c) res3=max2(res2,d) return res3 print(max4(1,2,3,4))
函数的嵌套定义: 在函数内又定义了其他函数
def func(): def foo(): print('from foo') print(foo) foo() x=1 print(x) func()
实现了 __call__ 的类也可以作为函数
对于一个自定义的类,如果实现了 __call__ 方法,那么该类的实例对象的行为就是一个函数,是一个可以被调用(callable)的对象。例如:
class Add: def __init__(self, n): self.n = n def __call__(self, x): return self.n + x # add = Add(1) # print(add(4)) # 5
执行 add(4) 相当于调用 Add._call__(add, 4),self 就是实例对象 add,self.n 等于 1,所以返回值为 1+4
确定对象是否为可调用对象可以用内置函数callable来判断。
print(callable(1)) # False print(callable(int)) #True
名称空间与作用域
名称空间相关
名称空间Namespaces:指的就是存放名字与值内存地址绑定关系的地方(内存空间)
名称空间分为四种类型
内置名称空间: 存放的是python解释器自带的名字 产生:python解释器的启动则产生 销毁:python解释器关闭则销毁 全局名称空间: 在顶级定义的名字 产生:执行python程序时产生 销毁:python程序执行完毕后则销毁 局部名称空间: 在函数内定义的名字 产生: 在函数调用时临时产生 销毁: 在函数调用完毕后则销毁 还有一种特殊的容器 直接外围空间(上层函数wrapper)的本地作用域,查找变量(如果有多层嵌套,则由内而外逐层查找,直至最外层的函数) 产生 wrapper在创建的时候,就会随之创建一个特殊的容器,用于保存上层作用域中变量的引用。可以这么说,wrapper 函数创建的闭包捕获了外部的 data 变量的引用。按理说,wrapper这个变量在 test 函数运行结束后,也是会被销毁的。事实也的确如此,如果没有后面的 setTimeout 的话。正因为 setTimeout 一直拿着 wrapper 这个函数的引用,而wrapper 形成的闭包又捕获了 data 变量的引用,因此 data 数据会一直存在,并不会在 test 函数结束之后立马销毁。setTimeout 在 10s 之后会运行 wrapper 指向的函数,然后会释放掉函数引用,也就是说 10s 后没有变量再引用 wrapper 指向的函数了,那么 wrapper 形成的闭包也可以得到销毁,捕获的上层变量也一并得到了释放。所以例子中,wrapper形成的闭包是在 10s 后被销毁的。 销毁 只要没人再保存这个函数的引用了,这个函数和函数所形成的闭包也就会被一并销毁
名称空间的产生的先后顺序: 内置->全局->局部;
查找名字的顺序:从当前位置往外一层一层查找,在任何一层先找到了符合要求的变量,则不再向更外层查找。如果直到Builtin层仍然没有找到符合要求的变量,则抛出NameError异常。这就是变量名解析的:LEGB法则,之前在变量的博客里说明过
作用域:指的是作用范围
全局作用域:包含内置与全局名称空间的名字 特点:全局存活,全局有效 局部作用域:包含局部名称空间的名字 特点:临时存活,局部有效 全局作用域:包含的是内置名称空间与全局名称空间的名字, 特点 在任何位置都能够访问的到 该范围内的名字会伴随程序整个生命周期 局部作用域:包含的是局部名称空间的名字 特点: 只能在函数内使用 调用函数时生效,调用结束失效
作用域关系是在函数定义阶段就已经固定死了,与调用位置无关
def f1(): print(xxx) xxx=111 def f2(): xxx=222 f1() f2() xxx=111 yyy=333 def f1(): xxx=222 print(xxx) #xxx=222 yyy=222 print(yyy) f1()
global与nonlocal关键字
global关键字
如果确实想要在函数内部改变全局变量的值并且让全局变量永久发生改变,则需要global关键字
count = 5 def myfun(): global count # global关键字的出现,告诉pyhton 用户要改变全局变量了 count = 10 return count print(count) print(myfun()) ''' 5 10 '''
nonlocal关键字
1.外部必须有这个变量 2.在内部函数声明nonlocal变量之前不能再出现同名变量 3.内部修改这个变量如果想在外部有这个变量的第一层函数中生效
def f1(): a = 5 def f2(): nonlocal a #不写nonlocal会报错 if 8 < 5: a = 7 return a return f2() print(f1())
闭包函数
因为闭包作用如下def outter(): x=1 def inner(): print('from inner',x) return inner f=outter() def foo(): # print(f) x=111111111111111111111111111111111111 f() foo()
为函数体传值的两种方式
方式一:直接以参数的形式传入
def foo(name): print('hello %s' %name) foo('egon') foo('egon') foo('egon')
方式二:闭包函数
def outter(name): # name='egon' def foo(): print('hello %s' %name) return foo f=outter('egon') # print(f) f() f() f() f1=outter('alex') f1() f1() f1()
彻底理解闭包
def make_mul(n): def mul(x): return x*n return mul test1=make_mul(3) test2=make_mul(5) print(test1) print(test2) print(test2(test1(2))) ''' <function make_mul.<locals>.mul at 0x000001B87A3299D8> <function make_mul.<locals>.mul at 0x000001B87A329A60> 30 ''' print(test1.__closure__[0].cell_contents) print(test2.__closure__[0].cell_contents) ''' 3 5 '''
从上面这个例子里,可以看到局部变量n会保存起来,并且在这里并没有使用全局变量,就可以达到这个目标,并且数据也可以隐藏起来。
在python里可以通过__closure__来查看闭包时保存的环境变量的值
闭包的应用
闭包的重要特性:封存上下文,这一特性可以巧妙的被用于现有函数的包装,从而为现有函数更加功能,而这就是装饰器,后面会更详细的讨论
# alist = [1, 2, 3, ..., 100] --> 1+2+3+...+100 = 5050 def lazy_sum(): return reduce(lambda x, y: x + y, alist) 我们定义了一个函数lazy_sum,作用是对alist中的所有元素求和后返回 alist假设为1到100的整数列表 alist = range(1, 101) 但是出于某种原因,我并不想马上返回计算结果,而是在之后的某个地方,通过显示的调用输出结果。于是我用一个wrapper函数对其进行包装: def wrapper(): alist = range(1, 101) def lazy_sum(): return reduce(lambda x, y: x + y, alist) return lazy_sum lazy_sum = wrapper() # wrapper() 返回的是lazy_sum函数对象 if __name__ == "__main__": lazy_sum() # 5050
这是一个典型的Lazy Evaluation的例子,我们知道,一般情况下,局部变量在函数返回时,就会被垃圾回收器回收,而不能再被使用。但是这里的alist却没有,它随着lazy_sum函数对象的返回被一并返回了(这个说法不准确,实际是包含在了lazy_sum的执行环境中,通过__globals__),从而延长了生命周期。
当在if语句块中调用lazy_sum()的时候,解析器会从上下文中(这里是Enclosing层的wrapper函数的局部作用域中)找到alist列表,计算结果,返回5050
参数检查
def add(a, b): return a+b
这是很简单的一个函数:计算a+b的和返回,但我们知道Python是 动态类型+强类型 的语言,你并不能保证用户传入的参数a和b一定是两个整型,他有可能传入了一个整型和一个字符串类型的值:
于是,解析器无情的抛出了一个TypeError异常。
动态类型:在运行期间确定变量的类型,python确定一个变量的类型是在你第一次给他赋值的时候
强类型:有强制的类型定义,你有一个整数,除非显示的类型转换,否则绝不能将它当作一个字符串(例如直接尝试将一个整型和一个字符串做+运算)
因此,为了更加优雅的使用add函数,我们需要在执行+运算前,对a和b进行参数检查 这时候装饰器就显得非常有用
import logging logging.basicConfig(level=logging.INFO) def add(a, b): return a + b def checkParams(fn): def wrapper(a, b): if isinstance(a, (int, float)) and isinstance(b, (int, float)): # 检查参数a和b是否都为整型或浮点型 return fn(a, b) # 是则调用fn(a, b)返回计算结果 # 否则通过logging记录错误信息,并友好退出 logging.warning("variable 'a' and 'b' cannot be added") return return wrapper # fn引用add,被封存在闭包的执行环境中返回 if __name__ == "__main__": # 将add函数对象传入,fn指向add # 等号左侧的add,指向checkParams的返回值wrapper add = checkParams(add) add(3, 'hello') # 经过类型检查,不会计算结果,而是记录日志并退出
注意checkParams函数
首先看参数fn,当我们调用checkParams(add)的时候,它将成为函数对象add的一个本地(Local)引用;
在checkParams内部,我们定义了一个wrapper函数,添加了参数类型检查的功能,然后调用了fn(a, b),根据LEGB法则,解释器将搜索几个作用域,并最终在(Enclosing层)checkParams函数的本地作用域中找到fn;
注意最后的return wrapper,这将创建一个闭包,fn变量(add函数对象的一个引用)将会封存在闭包的执行环境中,不会随着checkParams的返回而被回收;
当调用add = checkParams(add)时,add指向了新的wrapper对象,它添加了参数检查和记录日志的功能,同时又能够通过封存的fn,继续调用原始的add进行+运算。
因此调用add(3, 'hello')将不会返回计算结果,而是打印出日志
chiyu@chiyu-PC:~$ python func.py WARNING:root:variable 'a' and 'b' cannot be added
下面这种只是一种写法上的优化,解释器仍然会将它转化为add = checkParams(add)来执行
@checkParams def add(a, b): return a + b
闭包补充
还有一个容易产生错误的事例也经常被人在介绍python闭包时提起,我一直都没觉得这个错误和闭包有什么太大的关系,但是它倒是的确是在python函数式编程是容易犯的一个错误,我在这里也不妨介绍一下。先看下面这段代码
for i in range(3): print(i)
在程序里面经常会出现这类的循环语句,Python的问题就在于,当循环结束以后,循环体中的临时变量i不会销毁,而是继续存在于执行环境中。还有一个python的现象是,python的函数只有在执行时,才会去找函数体里的变量的值
flist = [] for i in range(3): def foo(x): print(x + i) flist.append(foo) for f in flist: f(2) ''' 4 4 4 '''
可能有些人认为这段代码的执行结果应该是2,3,4.但是实际的结果是4,4,4。这是因为当把函数加入flist列表里时,python还没有给i赋值,只有当执行时,再去找i的值是什么,这时在第一个for循环结束以后,i的值是2,所以以上代码的执行结果是4,4,4.
解决方法也很简单,改写一下函数的定义就可以了
flist = [] for i in range(3): def foo(x,y=i): print(x + y) flist.append(foo) #加了一个y=i,相当于向列表中塞入了三个默认值不同的函数的内存地址,完美解决问题 for f in flist: f(2)