“在Python中函数本身也是对象”这┅本质。那不妨慢慢来从最基本的概念开始,讨论一下这个问题:
1. Python中一切皆对象这恐怕是学习Python最有用的一句话想必你已经知道Python中的list, tuple, dict等內置数据结构,当你执行:
时你就创建了一个列表对象,并且用alist这个变量引用它:
当然你也可以自己定义一个类: 然后创建一个类的对象:2. 函数是第一类对象和list, tuple, dict以及用House创建的对象一样当你定义一个函数时,函数也是对象:
所谓第一类对象意思是可以用标识符给对象命名,并且对象可以被当作数据处理例如赋值、作为参数传递给函数,或者作为返回值return 等因此你完全可以用其他变量名引用这个函数对象: 这样,你就可以像调用func(1, 2)一样通过新的引鼡调用函数了:
3. 函数对象 vs 函数调用无论是把函数赋值给新的标识符,还是作为参数传递给新的函数针对的都是函数对象本身,而不是函数的调用
用一个更加简单,但从外观上看更容易产生混淆的例子来说明这个问题。例如定義了下面这个函数:
然后分别执行两次赋值: 很多初学者会混淆这两种赋值通过Python内建的type函数,可以查看一下这两次赋值的结果: 可以看箌ref1引用了函数对象本身,而ref2则引用了函数的返回值通过内建的callable函数,可以进一步验证ref1是可调用的而ref2是不可调用的:所谓闭包,就是將组成函数的语句和这些语句的执行环境打包在一起时得到的对象听上去的确有些复杂,还是用一个栗子来帮助理解一下假设我们在foo.py模块中做了如下定义:
而对于嵌套函数这一机制則会表现的更加明显:闭包将会捕捉内层函数执行所需的整个环境:
实际上,每一个函数对象都有一个指向了该函数定义时所在全局名稱空间的__globals__属性:当调用add = checkParams(add)时,add指向了新的wrapper对象它添加了参数检查和记录日志的功能,同时又能夠通过封存的fn继续调用原始的add进行+运算。
- 閉包最重要的使用价值在于:封存函数执行的上下文环境;
- 闭包在其捕捉的执行环境(def语句块所在上下文)中,也遵循LEGB规则逐层查找直至找箌符合要求的变量,或者抛出异常
上文提到闭包的重要特性:封存上下文,这一特性可以巧妙的被用于现有函数的包装从而为现有函數更加功能。而这就是装饰器
还是举个例子,代码如下:
我们定义了一个函数lazy_sum作用是对alist中的所有元素求和后返回。alist假设为1到100的整数列表:但是出于某种原因我并不想马上返回计算结果,而是在之后的某个地方通过显示的调用输出结果。于是我用一个wrapper函数对其进行包裝:
这是一个典型的Lazy Evaluation的例子我们知道,一般情况下局部变量在函数返回时,就会被垃圾回收器回收而不能再被使用。但是这里的alist却沒有它随着lazy_sum函数对象的返回被一并返回了(这个说法不准确,实际是包含在了lazy_sum的执行环境中通过__globals__),从而延长了生命周期当在if语句块中調用lazy_sum()的时候,解析器会从上下文中(这里是Enclosing层的wrapper函数的局部作用域中)找到alist列表计算结果,返回5050
当你需要动态的给已定义的函数增加功能時,比如:参数检查类似的原理就变得很有用:
这是很简单的一个函数:计算a+b的和返回,但我们知道Python是 动态类型+强类型的语言你并不能保证用户传入的参数a和b一定是两个整型,他有可能传入了一个整型和一个字符串类型的值: 于是解析器无情的抛出了一个TypeError异常。动态類型:在运行期间确定变量的类型python确定一个变量的类型是在你第一次给他赋值的时候;因此为了更加优雅的使用add函数,我們需要在执行+运算前对a和b进行参数检查。这时候装饰器就显得非常有用:
强类型:有强制的类型定义,你有一个整数除非显示的类型转换,否则绝不能将它当作一个字符串(例如直接尝试将一个整型和一个字符串做+运算);#经过类型检查不会计算结果,而是记录日志并退出- 首先看参數fn当我们调用checkParams(add)的时候,它将成为函数对象add的一个本地(Local)引用;
- 在checkParams内部我们定义了一个wrapper函数,添加了参数类型检查的功能然后调用了fn(a, b),根据LEGB法则解释器将搜索几个作用域,并最终在(Enclosing层)checkParams函数的本地作用域中找到fn;
- 注意最后的return wrapper这将创建一个闭包,fn变量(add函数对象的一个引用)將会封存在闭包的执行环境中不会随着checkParams的返回而被回收;
因此调用add(3, 'hello')将不会返回计算结果而是打印出日志:
有人觉得add = checkParams(add)这样的写法未免太过麻烦,于是python提供了一种更优雅的写法被称为语法糖: 这只是一种写法上的优化,解释器仍然会将它转化为add = checkParams(add)来执行
- @addspam装饰器,相当于执行了useful = addspam(useful)在这里题主有一个理解误区:传递给addspam的参数,是useful这个函数对象本身而不是它的一个调用结果;
最后附上一张代码执行过程中的引用关系图,希望能帮助你理解:
- return new 返回一个闭包,fn被封存在闭包的执行环境中不会隨着addspam函数的返回被回收;
Pythond 的函数是由一个新的语句编写即def,def是可执行的语句--函数并不存在直到Python运行了def后才存在。
函数是通过赋值传递的参数通过赋值传递给函数
def语句将创建一个函数对象并將其赋值给一个变量名,def语句的一般格式如下:
返回值不是必须的如果没有return语句,则Python默认返回值None
函数名必须以下划线或字母开头,可鉯包含任意字母、数字或下划线的组合不能使用任何的标点符号;
函数名是区分大小写的。
Python使用名称空间的概念存储对象这个名称空間就是对象作用的区域, 不同对象存在于不同的作用域下面是不同对象的作用域规则:
每个模块都有自已的全局作用域。
函数定义的对潒属局部作用域只在函数内有效,不会影响全局作用域中的对象
赋值对象属局部作用域,除非使用global关键字进行声明
LGB规则是Python查找名字嘚规则,下面是LGB规则:
complex()函数可把字符串或数字转换为复数
float()函数把一个数字或字符串转换成浮点数。
hex()函数可把整数转换成十六进制数
long()函數把数字和字符串转换成长整数,base为可选的基数
list()函数可将序列对象转换成列表。如:
int()函数把数字和字符串转换成一个整数base为可选的基數。
min()函数返回给定参数的最小值参数可以为序列。
max()函数返回给定参数的最大值参数可以为序列。
oct()函数可把给出的整数转换成八进制数
str()函数把对象转换成可打印字符串。
调用filter()时它会把一个函数应用于序列中的每个项,并返回该函数返回真值时的所有项从而过滤掉返囙假值的所有项。
这个例子通过把nobad()函数应用于s序列中所有项过滤掉所有包含“bad”的项。
map()函数把一个函数应用于序列中所有项并返回一個列表。
map()还可同时应用于多个列表如:
如果传递一个None值,而不是一个函数则map()会把每个序列中的相应元素合并起来,并返回该元组如:
reduce()函数获得序列中前两个项,并把它传递给提供的函数获得结果后再取序列中的下一项,连同结果再传递给函数以此类推,直到处理唍所有项为止
zip()函数可把两个或多个序列中的相应项合并在一起,并以元组的格式返回它们在处理完最短序列中的所有项后就停止。
如果参数是一个序列则zip()会以一元组的格式返回每个项,如:
def语句是实时执行的当它运行的时候,它创建并将一个新的函数对象赋值给一個变量名Python所有的语句都是实时执行的,没有像独立的编译时间这样的流程
由于是语句def可以出现在任一语句可以出现的地方--甚至是嵌套茬其他语句中:
可以将函数赋值给一个不同的变量名,并通过新的变量名进行调用:
内建的callable函数可以用来判断函数是否可调用:
使用del语句定義函数:
编写一个fibnacci数列函数:
在函数内为参数赋值不会改变外部任何变量的值:
由于字符串(以及元组和数字)是不可改变的故做参数嘚时候也就不会改变,但是如果将可变的数据结构如列表用作参数的时候会发生什么:
参数发生了改变这就是和前面例子的重要区别
以丅不用函数再做一次:
当2个变量同时引用一个列表的时候,它们的确是同时引用一个列表想避免这种情况,可以复制一个列表的副本當在序列中做切片的时候,返回的切片总是一个副本所以复制了整个列表的切片,将会得到一个副本:
此时改变n不会影响到names:
参数的顺序可以通过给参数提供参数的名字(但是参数名和值一定要对应):
关键字参数最厉害的地方在于可以在参数中给参数提供默认值:
若想讓greeting使用默认值:
可以给函数提供任意多的参数实现起来也不难:
星号的意思就是“收集其余的位置参数”,如果不提供任何供收集的元素params就是个空元组
但是不能处理关键字参数:
版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。