python的一等函数


在Python中,函数是一等对象,而一等对象的定义是满足下述条件的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传递给函数
  • 能作为函数的返回结果

我们通过下面的例子来证明python中的函数是对象:

>>> def factorial(n):
...     '''return n!'''
...     return 1 if n < 2 else n * factorial(n-1)
...
>>> factorial(41)
33452526613163807108170062053440751665152000000000
>>> factorial.__doc__
'return n!'
>>> type(factorial)
<class 'function'>

在第三行就可以看出,factorial是一个类的示例,另外再看看下面的例子可以看出,python中的函数满足上面所讲的一等对象定义的要求:

>>> fact=factorial
>>> fact
<function factorial at 0x10d6daea0>
>>> fact(5)
120
>>> map(factorial,range(11))
<map object at 0x10d5e9c88>
>>> list(map(factorial,range(11)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

下来再介绍下高阶函数,高阶函数就是接受函数作为参数,或者把函数作为结果返回的函数就是高阶函数。例如map函数,filterreduceapply(在python3中移除了)等。

>>> list(map(fact,range(6)))
[1, 1, 2, 6, 24, 120]
>>> list(map(factorial,filter(lambda n:n%2,range(6))))
[1, 6, 120]

为了使用高阶函数,有时候需要创建匿名函数会更加便利,在python中,使用lambda关键字来创建匿名函数,但是python中的lambda函数的定义体只能使用纯表达式,也就是不能赋值,也不能使用whiletrypython语句。如上面的例子。

可调用对象

除了用户定义的函数,调用运算符()还可以应用到其他对象上,如果要判断对象是否能调用,可以使用内置的callable()函数,下面列出了7中可调用对象:

  • 用户定义的函数,使用def语句或lambda表达式创建
  • 内置函数,比如len
  • 内置方法,如dict.get
  • 方法 在类的定义体中定义的函数
  • 类 调用类时候会运行__new__方法创建实例,然后运行__init__方法初始化,因为python没有new运算符,所以调用对象相当于调用函数
  • 类的实例 如果类定义了__call__方法,那么他的实例可以作为函数调用。
  • 生成器函数 使用yield关键字的函数或方法。

实现__call__方法的类是创建函数类对象的简便方法,此时必须在内部维护一个状态,让它在调用之间可用,创建保有内部状态的函数还有另一种方法--使用闭包。下面介绍下把函数视作对象处理的另一方面:运行时内省。

运行时内省

使用dir函数可以看到factorial具有下述属性:

>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

其中大多数属性是python对象共有的,和用户定义的常规类一样,函数使用__dict__属性存储赋予给它的用户属性。下面介绍下函数专有而用户定义的一般对象没有的属性:

>>> class C: pass  
>>> obj=C()
>>> def func(): pass
>>> sorted(set(dir(func))-set(dir(obj)))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

上面所得到的差集就是类的实例没有而函数有的属性列表。下面的表格是介绍这些属性的类型和其说明:

名称 类型 说明
__annotations__ dict 参数和返回值的注解
__call__ method_wrapper 实现()运算符
__closure__ tuple 函数闭包
__code__ code 编译成字节码的函数元数组和函数定义体
__defaults__ tuple 形式参数的默认值
__get__ method_wrapper 实现只读描述符协议
__globals__ dict 函数所在模块的全局变量
__kwdefaults__ dict 仅限关键字形式参数的默认值
__name__ str 函数名称
__qualname__ str 函数的限定名称

在python3中,提供了仅限关键字参数,调用函数是也可以使用*和**展开可迭代对象,映射到单个参数,下面来介绍这些特性:

>>> def tag(name, *content, cls=None, **attrs):
...     '''生成一个或多个html标签'''
...     if cls is not None:
...         attrs['class'] = cls
...     if attrs:
...         attr_str = ''.join(' %s="%s"' % (attr, value) for attr, value in sorted(attrs.items()))
...     else:
...         attr_str = ''
...     if content:
...         return '\n'.join('<%s%s>%s</%s>' % (name, attr_str, c, name) for c in content)
...     else:
...         return '<%s%s />' % (name, attr_str)
...     
>>> tag('br')
'<br />'
>>> tag('p','hello',id=33)
'<p id="33">hello</p>'
>>> tag(content='testing',name="img")
'<img content="testing" />'
>>> my_tag={'name':'img','title':'Sunset','src':'sunset.jpg','cls':'framed'}
>>> tag(**my_tag)
'<img class="framed" src="sunset.jpg" title="Sunset" />'
>>> tag('p','hello','world',cls='sidebar')
'<p class="sidebar">hello</p>\n<p class="sidebar">world</p>'

仅限关键字参数是python3新增的特性,cls参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。定义函数时如果想指定仅限关键字参数,要把它们放在前面有的参数后面。如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个,如下所示:

>>> def f(a,*,b)
      return(a,b)
>>>f(1,b=2)
(1,2)

要注意,仅限关键字番薯不一定要有默认值,可以和上面的b那样,强制必须传入实参。

支持函数式编程的包

python中的operator模块为多个算数运算符提供了对应的函数,比如:

>>> from functools import reduce
>>> from operator import mul
>>> def fact(n):
...     return reduce(mul,range(1,n+1))
...
>>> fact(4)
24
>>> data=[('xiaoming','19'),('xiaozhang','18')]
>>> sorted(data,key=itemgetter(1))
[('xiaozhang', '18'), ('xiaoming', '19')]
>>> class F():
...     def __init__(self):
...         self.name='zhangsan'
...         self.age=29
...         self.info={'city':'shanghai'}
...         
>>> f=F()
>>> from operator import attrgetter
>>> attrgetter('name')(f)
'zhangsan'
>>> attrgetter('info.keys')(f)
<built-in method keys of dict object at 0x10d5f6510>

itemgetterattrgetter会自行构建函数,itemgetter使用的是[]运算符,因此不仅支持序列,还支持映射和任何实现了__getitem__方法的类,而attrgetter的作用是根据名称提取对象属性,也就是.运算符。