Python函数式编程

目录

高效的函数

函数是 Python 的头等对象

Python 程序中的所有数据都是由对象或对象之间的关系来表示的。①字符串、列表和模块等都是对象。Python 中的函数也不例外,同样是对象。

由于 yell 函数是 Python 中的一个对象,因此像任何其他对象一样,也可以将其分配给另一 个变量:

def yell(text):
    return text.upper()+"!"
bark = yell # 这一行没有调用函数,而是获取 yell 引用的函数对象,再创建一个指向该对象的名称 bark。
# 现在调用 bark 就可以执行相同的底层函数对象
bark('hello') # 'HELLO!'

此时,删除 yell 的引用,不会影响 bark 的引用:

del yell
yell('hello') # NameError: name 'yell' is not defined
bark('hello') # 'HELLO!'

Python在创建函数时为每个函数附加了一个用于调试的字符串标识符,使用函数的 name 属性可以访问该标识符:

bark.__name__ # 'yell'

虽然函数的__name__仍然是 yell,但已经无法用这个名称在代码中访问函数对象。名称标识符仅仅用来辅助调试,指向函数的变量和函数本身实际上是彼此独立的。

函数可以传递给其他函数

由于函数是对象,因此可以将其作为参数传递给其他函数。将函数对象作为参数传递给其他函数的功能非常强大,可以用来将程序中的行为抽象出来并传递出去。

def greet(func):
    greeting = func("Hi, I am a Python program")
    print(greeting)
greet(bark) # 'HI, I AM A PYTHON PROGRAM!'

向 greet 函数传递 bark 函数,greeting = bark(“Hi, I am a Python program”) = “HI, I AM A PYTHON PROGRAM!”,然后打印出来。

能接受其他函数作为参数的函数被称为高阶函数。高阶函数是函数式编程风格中必不可少的一部分。

Python 中具有代表性的高阶函数是内置的 map 函数。map 接受一个函数对象和一个可迭代对象,然后在可迭代对象中的每个元素上调用该函数来生成结果。map 返回一个迭代器,可以使用 list 函数将其转换为列表。

list(map(bark,['hello','hey','hi'])) # ['HELLO!', 'HEY!', 'HI!']

map 遍历整个列表并将 bark 函数应用于每个元素,然后将结果放入一个列表中。

函数可以嵌套

Python 允许在函数中定义函数,这通常被称为嵌套函数或内部函数。

def speak(text):
    def whisper(t):
        return t.lower()+"..."
    return whisper(text)
speak('Hello, World') # 'hello, world...'

每次调用 speak 时,都会定义一个新的内部函数 whisper 并立即调用。’Hello, World’被传递给text参数,然后whisper函数被调用,返回whisper(‘Hello, World’) = ‘hello, world…‘,最后speak函数返回这个值。

内部函数可以访问外部函数的局部变量,但外部函数不能访问内部函数的局部变量。也就是说whisper函数可以访问speak函数的text参数,但speak函数不能访问whisper函数的t参数。

whisper('Hello, World') # NameError: name 'whisper' is not defined

whisper 函数的作用域仅限于 speak 函数,因此在 speak 函数外部无法访问它。

那怎么才能从 speak 外部访问嵌套的 whisper 函数呢?由于函数是对象,因此可以将内部函数返回给父函数的调用者.

例如,下面这个函数定义了两个内部函数。顶层函数根据传递进来的参数向调用者返回对应的内部函数:

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) # <function get_speak_func.<locals>.yell at 0x7f9b8c0b0d08>
get_speak_func('Hello, world',0.3) # <function get_speak_func.<locals>.whisper at 0x7f9b8c0b0c80>

也就是说,get_speak_func 实际上不调用任何内部函数,只是根据 volume 参数选择适当的内部函数,然后返回这个函数对象。

如何调用这个函数对象呢?可以像调用普通函数一样调用它:

speak_func = get_speak_func('Hello, World', 0.7)
speak_func('Hello') # 'HELLO!'

这意味着函数不仅可以通过参数接受行为,还可以返回行为。这里的 speak_func 函数就是一个行为。

函数可捕捉局部状态

内部函数不仅可以从父函数返回,还可以捕获并携带父函数的某些状态。

下面对前面的 get_speak_func 示例做些小改动来逐步说明这一点。新版在内部就会使用 volume 和 text 参数,因此返回的函数是可以直接调用的:

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()

仔细看看内部函数 whisper 和 yell,注意其中并没有 text 参数。但不知何故,内部函数仍然可以访问在父函数中定义的 text 参数。它们似乎捕捉并“记住”了这个参数的值。 拥有这种行为的函数被称为词法闭包(lexical closure),简称闭包。闭包在程序流不在闭包范围内的情况下,也能记住封闭作用域(enclosing scope)中的值.

词法闭包的一个重要特性是,它们可以捕捉并携带封闭作用域中的状态。这意味着函数不仅可以返回行为,还可以预先配置这些行为。

def make_adder(n): 
    def add(x):
        return x + n
    return add # 返回函数对象
plus_3 = make_adder(3) # plus_3 是一个add函数
plus_3(4) # 7
plus_5 = make_adder(5) # plus_5 是一个add函数,但是n的值不同
plus_5(4) # 9

在这个例子中,make_adder 作为工厂函数来创建和配置各种 adder 函数。注意,这些 adder函数仍然可以访问 make_adder 函数中位于封闭作用域中的参数n。

对象也可以作为函数使用

虽然 Python 中的所有函数都是对象,但反之不成立。有些对象不是函数,但依然可以调用,因此在许多情况下可以将其当作函数来对待。

如果一个对象是可调用的,意味着可以使用圆括号函数调用语法,甚至可以传入调用参数。 这些都由__call__双下划线方法完成。

class Adder:
    def __init__(self, n):
        self.n = n
    def __call__(self, x):
        return self.n + x # self.n 是Adder对象的属性,x是调用时传入的参数
plus_3 = Adder(3) # plus_3 是一个Adder对象,调用__init__方法
# 在幕后,像函数那样“调用”一个对象实例实际上是在尝试执行该对象的__call__方法
plus_3(4) # 7,调用__call__方法,返回self.n + x

在这个例子中,Adder 类的实例可以像函数一样被调用。这是因为 Adder 类定义了__call__方法,这个方法的实现就是一个函数。当然,并不是所有的对象都可以调用,因此 Python 内置了 callable 函数,用于检查一个对象是否可以调用。

lambda 是单表达式函数

你可能想知道 lambda 有什么独特之处:如果只是比用 def 声明函数稍微方便一点,那有什么大不了的?

来看下面的例子,同时脑海里要记着函数表达式这个概念:

(lamba x,y:x+y)(5,3)# 8,lambda表达式的结果是一个函数对象,意味着可以使用圆括号函数调用语法,圆括号之后的参数是传入函数的参数

从概念上讲,lambda 表达式 lambda x,y:x + y 与用 def 声明函数相同,但从语法上来说表达式位于 lambda 内部。两者的关键区别在于,lambda 不必先将函数对象与名称绑定,只需在 lambda 中创建一个想要执行的表达式,然后像普通函数那样立即调用进行计算。

lambda 和普通函数定义之间还有另一个语法差异。lambda 函数只能含有一个表达式,这意味着 lambda 函数不能使用语句或注解(annotation),甚至不能使用返回语句。 那么应该如何从 lambda 返回值呢?执行 lambda 函数时会计算其中的表达式,然后自动返回表达式的结果,所以其中总是有一个隐式的返回表达式。因此有些人把 lambda 称为单表达式函数

lambda 的用途

从技术上讲,每当需要提供一个函数对象时,就可以使用 lambda 表达式。而且,因为 lambda 是匿名的,所以不需要先分配一个名字.

比如说,可以用lamba表达式来定义简短的key函数:

tuples = [(1,'d'),(2,'b'),(3,'c'),(4,'a')]
sorted(tuples,key = lamba x: x[1]) # [(4,'a'),(2,'b'),(3,'c'),(1,'d')]

上面的例子按照每个元组中的第 2 个值对元组列表进行排序。在这种情况下,用 lambda 函数能快速修改排序顺序。

下面是另外一个例子,使用lambda函数快速改变排序顺序:

sorted(range(-5,6),lamba x: x*x) # [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]

lamba函数的另外应用场景是作为回调函数传递给其他函数。比如,下面的代码使用 lambda 函数来定义一个回调函数,然后将其传递给内置的 filter 函数:

list(filter(lambda x: x % 2, range(10))) # [1, 3, 5, 7, 9]
# filter函数的第一个参数是一个函数对象,第二个参数是一个可迭代对象

lambda 还有一个有趣之处:与普通的嵌套函数一样,lambda 也可以像词法闭包那样工作。


def make_repeater(n):
    return lambda s: s * n
repeat_5 = make_repeater(5)
repeat_5(3) # 15
repeat_5('hello') # 'hellohellohellohellohello'
repeat_3 = make_repeater(3)
repeat_3('hello') # 'hellohellohello'
repeat_3(3) # 9

lambda 的局限性

若工作代码用到了 lambda,虽然看起来很“酷”,但实际上对自己和同事都是一种负担。

将 lambda 和 map()或 filter()结合起来构建复杂的表达式也很难让人理解,此时用列表解析式或生成器表达式通常会清晰不少.

# 用lambda和filter构建复杂的表达式
list(filter(lambda x: x % 2==0, range(16))) # [0, 2, 4, 6, 8, 10, 12, 14]
# 用列表解析式
[x for x in range(16) if x % 2 == 0] # [0, 2, 4, 6, 8, 10, 12, 14]

装饰器

Python 的装饰器可以用来临时扩展和修改可调用对象(函数、方法和类)的行为,同时又不会永久修改可调用对象本身。

装饰器的一大用途是将通用的功能应用到现有的类或函数的行为上,这些功能包括:

  • 日志记录
  • 访问控制和授权
  • 衡量函数,如执行时间
  • 限制请求速率(rate-limiting )
  • 缓存,等等

    装饰器是什么?

装饰器是用来“装饰”或“包装”另一个函数的,在被包装函数运行之前和之后执行一些代码。

那么简单装饰器的实现会是什么样子的呢?用基本术语来说,装饰器是可调用的,将可调用 对象作为输入并返回另一个可调用对象。

下面这个函数就具有这种特性,因此可以认为它是最简单的装饰器:

def null_decorator(func):
    return func

下面用这个装饰器函数装饰(或包装)另一个函数:

def greet():
    return 'Hello!'
greet = null_decorator(greet)
greet() # 'Hello!'

这个例子中定义了一个 greet 函数,然后立即运行 null_decorator 函数来装饰它。这个例子看起来没什么用,因为 null_decorator 是刻意设计的空装饰器。

Python中有一个语法糖,可以让装饰器更加简洁,就是使用 @ 符号。下面的代码和上面的代码是等价的:

@null_decorator
def greet():
    return 'Hello!'
greet() # 'Hello!'

在函数定义之前放置一个@null_decorator,相当于先定义函数然后运行这个装饰器。

注意,使用@语法会在定义时就立即修饰该函数。这样,若想访问未装饰的原函数则需要折 腾一番。因此如果想保留调用未装饰函数的能力,那么还是要手动装饰需要处理的函数。

装饰器可以修改行为

在熟悉装饰器语法之后,下面来编写一个有实际作用的装饰器来修改被装饰函数的行为。

这个装饰器将被装饰函数返回的结果转换成大写字母:

def uppercase(func):
    def wrapper():
        original_result = func() 
        modified_result = original_result.upper() # 修改行为, 转换成大写
        return modified_result # 返回修改后的结果
    return wrapper

这个装饰器的实现中,定义了一个内部函数 wrapper,它将被装饰函数的返回值转换成大写字母。然后,这个内部函数被返回,作为装饰器函数。

这个 uppercase 装饰器不像之前那样直接返回输入函数,而是在其中定义一个新函数(闭 包)。在调用原函数时,新函数会包装原函数来修改其行为。

包装闭包(新函数)可以访问未经装饰的输入函数(原函数),并且可在调用输入函数之前 和之后自由执行额外的代码。

来看看加了uppercase装饰器的greet函数,会对原来的greet函数产生什么影响:

@uppercase
def greet():
    return 'Hello!'
greet() # 'HELLO!'

与 null_decorator 不同,uppercase 装饰器在装饰函数时会返回一个不同的函数对象:

>>> greet 
<function greet at 0x10e9f0950> 
>>> null_decorator(greet) 
<function greet at 0x10e9f0950> 
>>> uppercase(greet) 
<function uppercase.<locals>.wrapper at 0x76da02f28> 

uppercase 定义并返回了另一个函数(闭包),这个函数在后续调用时会运行原输入函数并修改其结果.

利用这种特性可以将可重用的代码块(如日志记录和其他功能)应用于现有的函数和类。因 此装饰器是 Python 中非常强大的功能,在标准库和第三方包中经常用到.

函数式编程

函数式编程通过在函数中定义表达式和对表达式求值完成计算。它尽量避免由于状态变化和使用可变对象引入复杂性,让程序变得简洁明了。

编程范式并没有统一的划分标准。其中两个范式:函数式编程和命令式编程。

在命令式语言(比如 Python)中,计算的状态是通过不同命名空间中变量的值反映的。变量的值决定计算的当前状态,一条语句通过增加或改变(甚至是删除)变量来改变当前状态。“命令式”语言的每一条语句都是一个通过某种方式改变状态的命令。

有两种方法可以返回一系列值,而不是生成器表达式。

  • 编写显式 for 循环:for x in some_iter: yield x
  • 使用 yield from 语句:yield from some_iter

来源

《Python函数式编程第2版》没看懂

《深入理解Python特性》28-40页

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦