您现在的位置: 万盛学电脑网 >> 程序编程 >> 网络编程 >> 编程语言综合 >> 正文

Python函数式编程指南(二):从函数开始

作者:佚名    责任编辑:admin    更新时间:2022-06-22

   这篇文章主要介绍了Python函数式编程指南(二):从函数开始,本文讲解了定义一个函数、使用函数赋值、闭包、作为参数等内容,需要的朋友可以参考下

  2. 从函数开始

  2.1. 定义一个函数

  如下定义了一个求和函数:

  代码如下:

  def add(x, y):

  return x + y

  关于参数和返回值的语法细节可以参考其他文档,这里就略过了。

  使用lambda可以定义简单的单行匿名函数。lambda的语法是:

   代码如下:

  lambda args: expression

  参数(args)的语法与普通函数一样,同时表达式(expression)的值就是匿名函数调用的返回值;而lambda表达式返回这个匿名函数。如果我们给匿名函数取个名字,就像这样:

   代码如下:

  lambda_add = lambda x, y: x + y

  这与使用def定义的求和函数完全一样,可以使用lambda_add作为函数名进行调用。然而,提供lambda的目的是为了编写偶尔为之的、简单的、可预见不会被修改的匿名函数。这种风格虽然看起来很酷,但并不是一个好主意,特别是当某一天需要对它进行扩充,再也无法用一个表达式写完时。如果一开始就需要给函数命名,应该始终使用def关键字。

  2.2. 使用函数赋值

  事实上你已经见过了,上一节中我们将lambda表达式赋值给了add。同样,使用def定义的函数也可以赋值,相当于为函数取了一个别名,并且可以使用这个别名调用函数:

   代码如下:

  add_a_number_to_another_one_by_using_plus_operator = add

  print add_a_number_to_another_one_by_using_plus_operator(1, 2)

  既然函数可以被变量引用,那么将函数作为参数和返回值就是很寻常的做法了。

  2.3. 闭包

  闭包是一类特殊的函数。如果一个函数定义在另一个函数的作用域中,并且函数中引用了外部函数的局部变量,那么这个函数就是一个闭包。下面的代码定义了一个闭包:

   代码如下:

  def f():

  n = 1

  def inner():

  print n

  inner()

  n = 'x'

  inner()

  函数inner定义在f的作用域中,并且在inner中使用了f中的局部变量n,这就构成了一个闭包。闭包绑定了外部的变量,所以调用函数f的结果是打印1和'x'。这类似于普通的模块函数和模块中定义的全局变量的关系:修改外部变量能影响内部作用域中的值,而在内部作用域中定义同名变量则将遮蔽(隐藏)外部变量。

  如果需要在函数中修改全局变量,可以使用关键字global修饰变量名。Python 2.x中没有关键字为在闭包中修改外部变量提供支持,在3.x中,关键字nonlocal可以做到这一点:

  代码如下:

  #Python 3.x supports `nonlocal'

  def f():

  n = 1

  def inner():

  nonlocal n

  n = 'x'

  print(n)

  inner()

  print(n)

  调用这个函数的结果是打印1和'x',如果你有一个Python 3.x的解释器,可以试着运行一下。

  由于使用了函数体外定义的变量,看起来闭包似乎违反了函数式风格的规则即不依赖外部状态。但是由于闭包绑定的是外部函数的局部变量,而一旦离开外部函数作用域,这些局部变量将无法再从外部访问;另外闭包还有一个重要的特性,每次执行至闭包定义处时都会构造一个新的闭包,这个特性使得旧的闭包绑定的变量不会随第二次调用外部函数而更改。所以闭包实际上不会被外部状态影响,完全符合函数式风格的要求。(这里有一个特例,Python 3.x中,如果同一个作用域中定义了两个闭包,由于可以修改外部变量,他们可以相互影响。)

  虽然闭包只有在作为参数和返回值时才能发挥它的真正威力,但闭包的支持仍然大大提升了生产率。

  2.4. 作为参数

  如果你对OOP的模板方法模式很熟悉,相信你能很快速地学会将函数当作参数传递。两者大体是一致的,只是在这里,我们传递的是函数本身而不再是实现了某个接口的对象。

  我们先来给前面定义的求和函数add热热身:

   代码如下:

  print add('三角形的树', '北极')

  与加法运算符不同,你一定很惊讶于答案是'三角函数'。这是一个内置的彩蛋...bazinga!

  言归正传。我们的客户有一个从0到4的列表:

   代码如下:

  lst = range(5) #[0, 1, 2, 3, 4]

  虽然我们在上一小节里给了他一个加法器,但现在他仍然在为如何计算这个列表所有元素的和而苦恼。当然,对我们而言这个任务轻松极了:

  代码如下:

  amount = 0

  for num in lst:

  amount = add(amount, num)

  这是一段典型的指令式风格的代码,一点问题都没有,肯定可以得到正确的结果。现在,让我们试着用函数式的风格重构一下。

  首先可以预见的是求和这个动作是非常常见的,如果我们把这个动作抽象成一个单独的函数,以后需要对另一个列表求和时,就不必再写一遍这个套路了:

   代码如下:

  def sum_(lst):

  amount = 0

  for num in lst:

  amount = add(amount, num)

  return amount

  print sum_(lst)

  还能继续。sum_函数定义了这样一种流程:

  1. 使用初始值与列表的第一个元素相加;

  2. 使用上一次相加的结果与列表的下一个元素相加;

  3. 重复第二步,直到列表中没有更多元素;

  4. 将最后一次相加的结果返回。

  如果现在需要求乘积,我们可以写出类似的流程——只需要把相加换成相乘就可以了:

  代码如下:

  def multiply(lst):

  product = 1

  for num in lst:

  product = product * num

  return product

  除了初始值换成了1以及函数add换成了乘法运算符,其他的代码全部都是冗余的。我们为什么不把这个流程抽象出来,而将加法、乘法或者其他的函数作为参数传入呢?

   代码如下:

  def reduce_(function, lst, initial):

  result = initial

  for num in lst:

  result = function(result, num)

  return result

  print reduce_(add, lst, 0)

  现在,想要算出乘积,可以这样做:

   代码如下:

  print reduce_(lambda x, y: x * y, lst, 1)

  那么,如果想要利用reduce_找出列表中的最大值,应该怎么做呢?请自行思考:)

  虽然有模板方法这样的设计模式,但那样的复杂度往往使人们更情愿到处编写循环。将函数作为参数完全避开了模板方法的复杂度。

  Python有一个内建函数reduce,完整实现并扩展了reduce_的功能。本文稍后的部分包含了有用的内建函数的介绍。请注意我们的目的是没有循环,使用函数替代循环是函数式风格区别于指令式风格的最显而易见的特征。

  *像Python这样构建于类C语言之上的函数式语言,由于语言本身提供了编写循环代码的能力,内置函数虽然提供函数式编程的接口,但一般在内部还是使用循环实现的。同样的,如果发现内建函数无法满足你的循环需求,不妨也封装它,并提供一个接口。

  2.5. 作为返回值

  将函数返回通常需要与闭包一起使用(即返回一个闭包)才能发挥威力。我们先看一个函数的定义:

  代码如下:

  def map_(function, lst):

  result = []

  for item in lst:

  result.append(function(item))

  return result

  函数map_封装了最常见的一种迭代:对列表中的每个元素调用一个函数。map_需要一个函数参数,并将每次调用的结果保存在一个列表中返回。这是指令式的做法,当你知道了列表解析(list comprehension)后,会有更好的实现。

  这里我们先略过map_的蹩脚实现而只关注它的功能。对于上一节中的lst,你可能发现最后求乘积结果始终是0,因为lst中包含了0。为了让结果看起来足够大,我们来使用map_为lst中的每个元素加1:

  代码如下:

  lst = map_(lambda x: add(1, x), lst)

  print reduce_(lambda x, y: x * y, lst, 1)

  答案是120,这还远远不够大。再来:

  代码如下:

  lst = map_(lambda x: add(10, x), lst)

  print reduce_(lambda x, y: x * y, lst, 1)

  囧,事实上我真的没有想到答案会是360360,我发誓没有收周鸿祎任何好处。

  现在回头看看我们写的两个lambda表达式:相似度超过90%,绝对可以使用抄袭来形容。而问题不在于抄袭,在于多写了很多字符有木有?如果有一个函数,根据你指定的左操作数,能生成一个加法函数,用起来就像这样:

   代码如下:

  lst = map_(add_to(10), lst) #add_to(10)返回一个函数,这个函数接受一个参数并加上10后返回

  写起来应该会舒服不少。下面是函数add_to的实现:

   代码如下:

  def add_to(n):

  return lambda x: add(n, x)

  通过为已经存在的某个函数指定数个参数,生成一个新的函数,这个函数只需要传入剩余未指定的参数就能实现原函数的全部功能,这被称为偏函数。Python内置的functools模块提供了一个函数partial,可以为任意函数生成偏函数:

   代码如下: