Fork me on GitHub

Effective Functions

stonef

关于Python函数的基础

Python的函数是第一类对象

Python所定义的函数属于第一类对象,那什么是第一类对象呢?

“第一类对象”这一名称最早由克里斯托弗·斯特雷奇在1960年代发明。第一类对象(first-class object)与面向对象编程(oriented-object programming)中的概念并没有什么严密的关系,它可以指程序中的任何实体。一般第一类对象所特有的特性为:

  1. 可以将其存入变量或其他数据结构
  2. 可以将其作为参数传递给别的函数
  3. 可以将其作为函数的返回值
  4. 可以在执行期间创造,而无需完全在设计期全部写出
  5. 即使没有被系结至某一变量或名称,也可以存在

绝大多数语言中,数值与基础型别都是第一类对象,然而不同语言中对函数的区别很大,例如C语言C++中的函数不是第一类对象,因为在这些语言中函数不能在执行期创造,而必须在设计时全部写好。

掌握这一概念有助于lambdadecorator以及函数式编程的理解和使用。为了一一说明Python中所定义的函数所具有的第一类对象特性,接下来所使用的例子都将以yell这个函数为基础进行拓展。

1
2
3
4
5
6
7
def yell(text):
"""
返回输入字符串的大写格式。
"""
return text.upper() + '!'
>>> yell('hello')
'HELLO!'

函数即是对象

在Python中,一切皆对象。字符串,列表,模块等都是对象,函数也不例外。

yell既然作为python中的对象,就可以将其赋值给另一个变量。

1
2
3
4
5
>>> bark = yell
>>> bark('woof')
'WOOF'
>>> id(bark) == id(yell)
True

这里赋值语句进行的操作是为函数创建另一个引用名称,两个引用名称指向的是内存中的同一块地址。函数对象本身和函数名(或者说函数的引用)可以看做一个事物内部两个独立的部分,我们可以用语句删除其中一个yell,而bark仍然指向之前定义的函数,bark可以正常调用。

1
2
3
4
5
6
>>> del yell
>>> yell('hello')
NameError: "name 'yell' is not defined"

>>> bark('hey')
'HEY!'

函数可以存入其他数据结构中

1
2
3
4
5
>>> funcs = [bark, str.lower, str.capitalize]
>>> funcs
[<function __main__.yell(text)>,
<method 'lower' of 'str' objects>,
<method 'capitalize' of 'str' objects>]

访问存储在列表中的函数对象与访问列表元素并无二致。甚至可以直接通过索引访问到函数。

1
2
3
4
5
6
7
8
>>> for f in funcs:
print(f, f('hey there'))
<function yell at 0x000001CEDA214AE8> HEY THERE!
<method 'lower' of 'str' objects> hey there
<method 'capitalize' of 'str' objects> Hey there

>>> funcs[0]('heyha')
'HEYHO!'

函数对象本身作为参数传递给别的函数

1
2
3
4
5
6
def greet(func):
greeting = func('Hi, I am a Python program')
print(greeting)

>>> greet(bark)
'HI, I AM A PYTHON PROGRAM'

我们定义了greet函数,greet以其他函数的引用作为参数,并在greet内部调用其他函数,并打印函数调用后的结果。

1
2
>>> list(map(bark, ['hello', 'hey', 'hi']))
['HELLO', 'HEY', 'HI']

以其他函数为参数的函数又称为高阶函数,Python内建函数中最典型的高阶函数就是mapmap接收一个函数引用和一个可迭代的参数列表,返回函数在每个参数上的掉调用结果。

嵌套函数

顾名思义,嵌套函数即在函数内部再次定义了函数,而通常情况下,外部函数会返回内部函数的引用,或者返回调用内部函数后的结果。

1
2
3
4
5
6
7
def speak(text):
def whisper(t):
return t.lower() + '...'
return whisper(text)

>>> speak('Hello World')
'hello world...'

聪明如你,容易看出内部函数的生存期仅限于当我们调用外部函数时。

1
2
3
4
5
>>> whisper('Yo')
NameError: "name 'whisper is not defined"

>>> speak.whisper
AttributeError: " 'function' object has no attribute 'whisper' "

内部函数无法直接访问,除非我们在外部函数块中返回内部函数的引用。

1
2
3
4
5
6
7
8
9
def get_speak_func(volume):
def whisper(text):
return text.lower() + '...'
def yell(text):
return text.upper() + '!'
if volume > 0.5:
return yell
else:
return whisper

函数get_speak_func并未调用任何内部函数,它仅仅根据参数volumn大小返回指定的某个内部函数,即返回了某种行为或者动作,而非内容。既然返回了某个函数,就可以直接调用这个函数,或者将其赋值给变量。

1
2
3
4
5
6
7
8
9
>>> get_speak_func(0.3)
<function __main__.get_speak_func.<locals>.whisper>

>>> get_speak_func(0.7)
<function __main__.get_speak_func.<locals>.yell>

>>> speak_func = get_speak_func(0.7)
>>> speak_func('Hello')
'HELLO!'

稍作总结,如果函数的定义是为了实现某种功能,那么别的函数可以通过参数传递的方法获得这种功能,也可以将其定义在自己内部返回这项功能。

函数可以记录局部状态

所谓记录局部状态,就是处于外部函数作用域中的变量,可以被内部函数捕捉到。将之前的get_speak_func函数稍作修改:

1
2
3
4
5
6
7
8
9
10
11
12
def get_speak_func(text,volume):
def whisper():
return text.lower() + '...'
def yell():
return text.upper() + '!'
if volume > 0.5:
return yell
else:
return whisper

>>> get_speak_func('hello world', 0.7)()
'HELLO WORLD!'

注意到内部函数whisperyell并没有接受任何参数,但是却可以使用外部函数的参数text。这种内部函数可以使用外部函数参数的行为称作语法闭包(注意:这里内部只能使用外部函数的参数变量,而不能使用外部函数块内定义的变量)。语法闭包有什么用处呢?之前提到过,嵌套函数可以用来返回某种行为或功能,而语法闭包则可以让程序对这种行为或功能进行多种配置。在下面这个例子中,利用make_adder可以制造多个加法函数。

1
2
3
4
5
6
7
8
9
10
11
12
def make_adder(n):
def add(x):
return x+n
return add

>>> plus_3 = make_adder(3)
>>> plus_3(4)
7

>>> plus_5 = make_adder(5)
>>> pluse_5(4)
9

OPP中的对象也能像函数那样使用

所有的函数都是对象,但是反之不一定成立。OPP中的对象并非函数,但是通过定义__call__(self,),却可以使其像函数那样可调用(callable)。将对象实例作为函数调用,会自动调用函数__call__。我们可以通过函数callable检查某个对象是否可以调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Adder():
def __init__(self,n):
self.n = n
def __call__(self,x):
return self.n+x

>>> plus_3 = Adder(3)
>>> plus_3(4)
7

>>> callable(pluse_3)
True
>>>> callable(yell)
True
>>> callable('hello')
False

总结

  • 一切皆对象,函数也不例外。我们可以将其赋值给某个变量,将其存储在某种数据结构中,将其传递给其他函数,或从其他函数返回。
  • 利用第一类对象函数,我们可以抽象出,并传递某种行为或功能。
  • 函数也可以嵌套,内部函数可以记录外部函数参数的状态,此种用法成为闭包。
  • 对象也可以被调用,即可以像函数那样被调用。

Lambdas: 匿名函数

用关键字lambda可以声明小型匿名函数,其用法和用def声明的函数用法类似。但是,用lambda声明的函数可以不用和变量名绑定即可立即调用。

1
2
3
4
5
6
>>> add = lambda x, y: x+y
>>> add(5, 3)
8

>>> (lambda x, y: x+y)(5, 3)
8

参考资料

Python Tricks 3-1: Python’s Funstions Are First-Class