高效的函数
函数是 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页