4. 一等公民

这里的一等公民特指 Python 中的函数.

一等公民

一等公民是指编程语言中满足如下特点的对象:

  • 可以在运行时创建,

  • 能够被赋值给变量,

  • 可以作为集合对象的元素,

  • 还能够作为函数的参数以及返回值.

为什么我在这里要强调 Python 中的函数是一等公民呢? 因为在很多其他诸如 C, C++, Java 等语言中, 函数和整数/字符串/字典/列表等对象地位并不是平等的. 这是 Python 一个非常重要的特点.

Python 中的函数之所以是一等公民, 是因为 Python 中的一切都是对象, 即 everything in python is an object. 换句话说: Python 中函数是一等公民这一现象是 Python 一切皆对象的必然结果.

Listing 4.1 examples/object/first_class_public/function_is_object.py
def fib(n):
    '''
    return nth fibonacci sequence number
    '''
    if n == 1 or n == 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(f'fib(10) = {fib(10)}')
print(f'docstring of fib is {repr(fib.__doc__.strip())}')
print(f'class of fib is {fib.__class__.__name__}')
$ python3 examples/object/first_class_public/function_is_object.py
fib(10) = 89
docstring of fib is 'return nth fibonacci sequence number'
class of fib is function

既然函数是一等公民, 那么他可以给起个别名, 或者是带入到函数的入参中. 如下面的代码, 函数 fib 可以赋给 a, 然后调用 a 就等价于调用 fib. fib 也可以作为 map 的入参, 这使得在 Python 当中, 很容易实现函数式编程.

Listing 4.2 examples/object/first_class_public/name_a_function.py
def fib(n):
    '''
    return nth fibonacci sequence number
    '''
    if n == 1 or n == 2:
        return n
    return fib(n - 1) + fib(n - 2)

a = fib

def subfix(number):
    return {1: 'st', 2: 'nd', 3: 'rd'}.get(number % 10, 'th')

print(f'a(10) = {a(10)}')

for index, number in enumerate(map(fib, range(1, 10)), start=1):
    print(f'the {index}{subfix(index)} fibonacci sequence number is {number}')
$ python3 examples/object/first_class_public/name_a_function.py
a(10) = 89
the 1st fibonacci sequence number is 1
the 2nd fibonacci sequence number is 2
the 3rd fibonacci sequence number is 3
the 4th fibonacci sequence number is 5
the 5th fibonacci sequence number is 8
the 6th fibonacci sequence number is 13
the 7th fibonacci sequence number is 21
the 8th fibonacci sequence number is 34
the 9th fibonacci sequence number is 55

Hint

装饰器本身就是一个参数和返回值都是函数的函数.

除了 map 之外, Python 中还有很多函数的参数都是函数, 比如:

  • sorted 函数的 key 参数.

  • filter 函数的第一个参数.

Caution

事实上, mapfilter 并不是函数, 而是类. 比较严谨的说法是 filter 初始化函数 __init__ 的第一个参数是一个函数.

在 Python 中, 还提供了匿名函数, 你可以像调用对象一样来调用匿名函数.

Listing 4.3 examples/object/first_class_public/lambda_expressions.py
add = lambda x, y: x + y
print(f'1 + 2 = {add(1, 2)}')

names = sorted(['qiqi', 'haha', 'popo'], key=lambda x: x[-1])
print(names)
$ python3 examples/object/first_class_public/lambda_expressions.py
1 + 2 = 3
['haha', 'qiqi', 'popo']

在 Python 中, 这被称为 Lambda 表达式, 考虑到函数也是对象, 本质上, Lambda 表达式是 Python 提供的一个语法糖. Listing 4.3 展开即Listing 4.4. 根据他们的运行结果也可以看出二者是一样的.

Listing 4.4 examples/object/first_class_public/lambda_expressions_modified.py
def _(x, y):
    return x + y
add = _
print(f'1 + 2 = {add(1, 2)}')

def _(x):
    return x[-1]
names = sorted(['qiqi', 'haha', 'popo'], key=_)
print(names)
$ python3 examples/object/first_class_public/lambda_expressions_modified.py
1 + 2 = 3
['haha', 'qiqi', 'popo']

由于 Python 的 Lambda 表达式表达能力孱弱, 定义体本身只能使用纯表达式, 因此, 在 Lambda 表达式中:

  • 不能有赋值语句.

  • 不能有流程控制关键词, 比如 while, try 等关键词.

当某个函数满足:

  • 作为参数传递给其他参数,

  • 可以写成纯表达式,

  • 其他地方不会再使用这个函数,

  • 你实在是不知道给这个函数如何命名,

这四个条件时, 才建议使用匿名函数.

在 Python 中, 函数是一等公民, 是一个真正的对象, 同时, 对象也可以表现的像函数一样, 只需要实现对象的 __call__ 方法即可, 比方说, 可以像Listing 4.5 一样实现一个 add 函数.

Listing 4.5 examples/object/first_class_public/callable_object.py``````
class Add():
    def __call__(self, x, y):
        return x + y

add = Add()
print(f'add(1, 2) = {add(1, 2)}')
print(f'function add is callable? {callable(add)}')
print(f'class Add is callable? {callable(Add)}')
$ python3 examples/object/first_class_public/callable_object.py
add(1, 2) = 3
function add is callable? True
class Add is callable? True

实际上, 普通函数也有 __call__ 方法, 在 99% 的情况下, 调用 __call__ 方法和直接调用函数是一样的.

Listing 4.6 examples/object/first_class_public/define_a_function.py
def add(x, y):
    return x + y

sub = lambda x, y: x - y

print(f'add.__call__ is {add.__call__}')
print(f'add(1, 2) = {add(1, 2)}')
print(f'add.__call__(1, 2) = {add.__call__(1, 2)}')
print()
print(f'sub.__call__ is {sub.__call__}')
print(f'sub(1, 2) = {sub(1, 2)}')
print(f'sub.__call__(1, 2) = {sub.__call__(1, 2)}')
$ python3 examples/object/first_class_public/define_a_function.py
add.__call__ is <method-wrapper '__call__' of function object at 0x7faf74049670>
add(1, 2) = 3
add.__call__(1, 2) = 3

sub.__call__ is <method-wrapper '__call__' of function object at 0x7faf72ef9160>
sub(1, 2) = -1
sub.__call__(1, 2) = -1

那 1% 的情况是什么呢?

Listing 4.7 examples/object/first_class_public/modify_call_method.py``````
def add(x, y):
    return x + y

sub = lambda x, y: x - y

add.__call__ = sub.__call__

print(f'add.__call__ is {add.__call__}')
print(f'sub.__call__ is {sub.__call__}')
print(f'add(1, 2) = {add(1, 2)}')
print(f'add.__call__(1, 2) = {add.__call__(1, 2)}')
$ python3 examples/object/first_class_public/modify_call_method.py
add.__call__ is <method-wrapper '__call__' of function object at 0x7fc4bc8e31f0>
sub.__call__ is <method-wrapper '__call__' of function object at 0x7fc4bc8e31f0>
add(1, 2) = 3
add.__call__(1, 2) = -1

我们可以看到, 在Listing 4.7 中, 函数 add__call__ 方法已经被替换成了 sub__call__ 方法了. 此时直接调用 add 方法和调用 add 函数的 __call__ 方法得到的结果是不同的.

Hint

在没有充分了解 Python 运作机制的情况下, 请不要像Listing 4.7 中一样通过 MonkeyPatch 的方式修改 Python 的对象, 以防止不可预期的情况发生.