函数式编程:一等对象、作用域和高阶函数的综合指南

简介: 函数式编程:一等对象、作用域和高阶函数的综合指南

函数式编程

在Python中,函数是一等对象。这意味着函数可以像其他对象一样被操作和使用。在函数式编程中,我们可以将函数作为参数传递给其他函数,将函数赋值给变量,甚至将函数作为返回值返回。

一等对象的特点

一等对象一般都会具有以下特点:

  1. 对象是在运行时创建的。
  2. 能够赋值给变量或作为数据结构中的元素。
  3. 能够作为参数传递。
  4. 能够作为返回值返回。

作用域(scope)

作用域指的是变量生效的区域。

b = 20 # 全局变量
def fn():
    a = 10 # a定义在了函数内部,所以他的作用域就是函数内部,函数外部无法访问
    print('函数内部:','a =',a)
    print('函数内部:','b =',b)
# fn()    
# print('函数外部:','a =',a)
# print('函数外部:','b =',b)

在Python中一共有两种作用域:

全局作用域

  • 全局作用域在程序执行时创建,在程序执行结束时销毁。
  • 所有函数以外的区域都是全局作用域。
  • 在全局作用域中定义的变量,都属于全局变量,全局变量可以在程序的任意位置被访问。

函数作用域

  • 函数作用域在函数调用时创建,在调用结束时销毁。
  • 函数每调用一次就会产生一个新的函数作用域。
  • 在函数作用域中定义的变量,都是局部变量,它只能在函数内部被访问。

变量的查找:

  • 当我们使用变量时,会优先在当前作用域中寻找该变量,如果有则使用,如果没有则继续去上一级作用域中寻找,如果有则使用,如果依然没有则继续去上一级作用域中寻找,以此类推,直到找到全局作用域,依然没有找到,则会抛出异常 NameError: name 'a' is not defined
def fn2():
    def fn3():
        print('fn3中:','a =',a)
    fn3()
# fn2()    
a = 20
def fn3():
    # a = 10 # 在函数中为变量赋值时,默认都是为局部变量赋值
    # 如果希望在函数内部修改全局变量,则需要使用global关键字,来声明变量
    global a # 声明在函数内部的使用a是全局变量,此时再去修改a时,就是在修改全局的a
    a = 10 # 修改全局变量
    print('函数内部:','a =',a)
# fn3()
# print('函数外部:','a =',a)

命名空间(namespace)

命名空间指的是变量存储的位置,每一个变量都需要存储到指定的命名空间当中。每一个作用域都会有一个它对应的命名空间。全局命名空间,用来保存全局变量。函数命名空间用来保存函数中的变量。命名空间实际上就是一个字典,是一个专门用来存储变量的字典。

# locals()用来获取当前作用域的命名空间
# 如果在全局作用域中调用locals()则获取全局命名空间,如果在函数作用域中调用locals()则获取函数命名空间
# 返回的是一个字典
scope = locals() # 当前命名空间
print(type(scope))
# print(a)
# print(scope['a'])
# 向scope中添加一个key-value
scope['c'] = 1000 # 向字典中添加key-value就相当于在全局中创建了一个变量(一般不建议这么做)
# print(c)
def fn4():
    a = 10
    # scope = locals() # 在函数内部调用locals()会获取到函数的命名空间
    # scope['b'] = 20 # 可以通过scope来操作函数的命名空间,但是也是不建议这么做
    # globals() 函数可以用来在任意位置获取全局命名空间
    global_scope = globals()
    # print(global_scope['a'])
    global_scope['a'] = 30
    # print(scope)
fn4()

练习实操

  • 求阶乘
  • 递归函数
  • 幂运算函数
  • 测试代码

求阶乘

# 创建一个函数,可以用来求任意数的阶乘
def factorial(n):
    '''
        该函数用来求任意数的阶乘
        参数:
            n 要求阶乘的数字
    '''
    # 创建一个变量,来保存结果
    result = n
    for i in range(1,n):
        result *= i
    return result   

递归函数

# 创建一个函数,用来检查一个任意的字符串是否是回文字符串,如果是返回True,否则返回False
def hui_wen(s):
    '''
        该函数用来检查指定的字符串是否回文字符串,如果是返回True,否则返回False
        参数:
            s:就是要检查的字符串
    '''
    # 基线条件
    if len(s) < 2 :
        # 字符串的长度小于2,则字符串一定是回文
        return True
    elif s[0] != s[-1]:
        # 第一个字符和最后一个字符不相等,不是回文字符串
        return False    
    # 递归条件    
    return hui_wen(s[1:-1])

幂运算函数

# 创建一个函数 power 来为任意数字做幂运算 n ** i
def power(n , i):
    '''
        power()用来为任意的数字做幂运算
        参数:
            n 要做幂运算的数字
            i 做幂运算的次数
    '''
    # 基线条件
    if i == 1:
        # 求1次幂
        return n
    # 递归条件
    return n * power(n , i-1)

测试代码

# 求10的阶乘    
print(factorial(10))
# 检查字符串是否回文
print(hui_wen('abcdefgfedcba'))
# 对10进行5次幂运算
print(power(10, 5))

高阶函数

高阶函数是指满足以下条件之一的函数:

  1. 接收一个或多个函数作为参数。
  2. 将函数作为返回值返回。

高阶函数在函数式编程中非常常见。它们使得我们能够更灵活地处理函数,将函数作为数据进行操作和传递。

接收函数作为参数,或者将函数作为返回值的函数是高阶函数

当我们使用一个函数作为参数时,实际上是将指定的代码传递进了目标函数

# 创建一个列表
l = [1,2,3,4,5,6,7,8,9,10]
# 定义一个函数
#   可以将指定列表中的所有的偶数,保存到一个新的列表中返回
# 定义一个函数,用来检查一个任意的数字是否是偶数
def fn2(i) :
    if i % 2 == 0 :
        return True
    return False    
# 这个函数用来检查指定的数字是否大于5
def fn3(i):
    if i > 5 :
        return True    
    return False
def fn(func , lst) :
    '''
        fn()函数可以将指定列表中的所有偶数获取出来,并保存到一个新列表中返回
        参数:
            lst:要进行筛选的列表
    '''
    # 创建一个新列表
    new_list = []
    # 对列表进行筛选
    for n in lst :
        # 判断n的奇偶
        if func(n) :
            new_list.append(n)
    # 返回新列表
    return new_list

将函数作为返回值返回,也是一种高阶函数

在Python中,我们可以将函数作为返回值返回,这种函数被称为高阶函数。通过高阶函数,我们可以创建一些只有当前函数能访问的变量,这种函数称为闭包。

def fn():
    a = 10
    # 函数内部再定义一个函数
    def inner():
        print('我是fn2', a)
    # 将内部函数inner作为返回值返回   
    return inner
# r是一个函数,是调用fn()后返回的函数
# 这个函数实在fn()内部定义,并不是全局函数
# 所以这个函数总是能访问到fn()函数内的变量
r = fn()

在上面的代码中,我们定义了一个函数fn(),它内部定义了另一个函数inner(),然后将inner()函数作为返回值返回。当我们调用fn()函数后,会得到一个函数对象r,我们可以通过r来调用inner()函数,并且inner()函数可以访问到fn()函数内部的变量a

求多个数的平均值

def make_averager():
    nums = []
    def averager(n):
        nums.append(n)
        return sum(nums)/len(nums)
    return averager
averager = make_averager()
print(averager(10))
print(averager(20))
print(averager(30))
print(averager(40))

在上面的代码中,我们定义了一个函数make_averager(),它返回了一个内部函数averager()。通过调用make_averager()函数,我们可以得到一个计算平均值的闭包averager。每次调用averager()函数时,我们将一个数值添加到列表nums中,并返回当前所有数值的平均值。

我们可以多次调用averager()函数来计算不同数值序列的平均值,由于闭包的特性,它会记住之前的所有数值,从而得到正确的平均值。

filter()

filter()可以从序列中过滤出符合条件的元素,保存到一个新的序列中

参数:

  1. 函数,根据该函数来过滤序列(可迭代的结构)
  2. 需要过滤的序列(可迭代的结构)
    返回值:
    过滤后的新序列(可迭代的结构)
def fn4(i):
    return i % 3 == 0
r = filter(lambda i : i > 5 , l)

map()

map()函数可以对可迭代对象中的所有元素做指定的操作,然后将其添加到一个新的对象中返回

l = [1,2,3,4,5,6,7,8,9,10]
r = map(lambda i : i ** 2 , l)

sort()

该方法用来对列表中的元素进行排序

sort()方法默认是直接比较列表中的元素的大小

在sort()可以接收一个关键字参数 , key

key需要一个函数作为参数,当设置了函数作为参数

每次都会以列表中的一个元素作为参数来调用函数,并且使用函数的返回值来比较元素的大小

l = ['bb','aaaa','c','ddddddddd','fff']
# l.sort(key=len)
l = [2,5,'1',3,'6','4']
l.sort(key=int)

sorted()

这个函数和sort()的用法基本一致,但是sorted()可以对任意的序列进行排序

并且使用sorted()排序不会影响原来的对象,而是返回一个新对象

l = [2,5,'1',3,'6','4']
# l = "123765816742634781"
print('排序前:',l)
print(sorted(l,key=int))
print('排序后:',l)

reduce()

这个方法和map()、filter()不太一样

reduce()需要导入functools模块

from functools import reduce
l = [1,2,3,4,5]
r = reduce(lambda x,y : x + y , l)

练习

使用filter函数找到列表中所有的偶数,使用map函数将每个偶数变成字符串类型,并使用sorted函数进行排序。

l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_nums = filter(lambda n: n % 2 == 0, l)
str_nums = map(str, even_nums)
sorted_nums = sorted(str_nums)
print(sorted_nums)

输出:

['2', '4', '6', '8', '10']

装饰器

装饰器是一种特殊类型的函数,它可以用于修改或扩展其他函数的功能。装饰器通常使用Python的@语法来应用于目标函数。装饰器可以在不修改原函数代码的情况下,通过包裹原函数来添加额外的行为。

以下是一个示例装饰器的代码:

def decorator(func):
    def wrapper(*args, **kwargs):
        print("装饰器添加的额外功能")
        return func(*args, **kwargs)
    return wrapper
@decorator
def target_function():
    print("目标函数")
target_function()

输出:

装饰器添加的额外功能
目标函数

在上面的代码中,decorator是一个装饰器函数,它接收一个函数作为参数,并返回一个新的包装函数wrapper。包装函数在调用目标函数之前添加了额外的功能。通过将装饰器应用于target_function,我们可以在调用target_function时获得额外的功能。

创建几个函数

def add(a , b):
    '''
        求任意两个数的和
    '''
    r = a + b
    return r
def mul(a , b):
    '''
        求任意两个数的积
    '''
    r = a * b
    return r    

希望函数可以在计算前,打印开始计算,计算结束后打印计算完毕。我们可以直接通过修改函数中的代码来完成这个需求,但是会产生以下一些问题:

① 如果要修改的函数过多,修改起来会比较麻烦;

② 并且不方便后期的维护;

③ 并且这样做会违反开闭原则(OCP),即程序的设计要求开发对程序的扩展,要关闭对程序的修改。

r = add(123,456)
print(r)

我们希望在不修改原函数的情况下,来对函数进行扩展。

def fn():
    print('我是fn函数....')
# 只需要根据现有的函数,来创建一个新的函数
def fn2():
    print('函数开始执行~~~')
    fn()
    print('函数执行结束~~~')
fn2() 
def new_add(a,b):
    print('计算开始~~~')
    r = add(a,b)
    print('计算结束~~~')
    return r
r = new_add(111,222)    
print(r)

上边的方式,已经可以在不修改源代码的情况下对函数进行扩展了。但是,这种方式要求我们每扩展一个函数就要手动创建一个新的函数,实在是太麻烦了。为了解决这个问题,我们创建一个函数,让这个函数可以自动的帮助我们生成函数。

def begin_end(old):
    '''
        用来对其他函数进行扩展,使其他函数可以在执行前打印开始执行,执行后打印执行结束
        参数:
            old 要扩展的函数对象
    '''
    # 创建一个新函数
    def new_function(*args , **kwargs):
        print('开始执行~~~~')
        # 调用被扩展的函数
        result = old(*args , **kwargs)
        print('执行结束~~~~')
        # 返回函数的执行结果
        return result
    # 返回新函数        
    return new_function
f = begin_end(fn)
f2 = begin_end(add)
f3 = begin_end(mul)
r = f()
r = f2(123,456)
r = f3(123,456)
print(r)

begin_end()这种函数我们就称它为装饰器。通过装饰器,可以在不修改原来函数的情况下来对函数进行扩展。在开发中,我们都是通过装饰器来扩展函数的功能的。

在定义函数时,可以通过@装饰器,来使用指定的装饰器,来装饰当前的函数。可以同时为一个函数指定多个装饰器,这样函数将会按照从内向外的顺序被装饰。

def fn3(old):
    '''
        用来对其他函数进行扩展,使其他函数可以在执行前打印开始执行,执行后打印执行结束
        参数:
            old 要扩展的函数对象
    '''
    # 创建一个新函数
    def new_function(*args , **kwargs):
        print('fn3装饰~开始执行~~~~')
        # 调用被扩展的函数
        result = old(*args , **kwargs)
        print('fn3装饰~执行结束~~~~')
        # 返回函数的执行结果
        return result
    # 返回新函数        
    return new_function
@fn3
@begin_end
def say_hello():
    print('大家好~~~')
say_hello()

总结

函数式编程是一种编程范式,它将计算过程视为数学函数的组合。在函数式编程中,函数被视为一等对象,具有以下特点:

  1. 作为参数传递:函数可以作为参数传递给其他函数,以实现更灵活的功能。
  2. 作为返回值返回:函数可以作为另一个函数的返回值,以实现可定制的行为。
  3. 可以赋值给变量:函数可以被赋值给变量,以便进一步使用。

作用域是指在程序中定义变量的区域,它决定了变量的可见性和生命周期。常见的作用域包括全局作用域和函数作用域。

全局作用域是在整个程序中都可以访问的作用域,而函数作用域只在函数内部可见。

命名空间是一个用于存储变量和函数名称的容器。它提供了一种将名称与特定作用域中的对象关联起来的方式。

在实操练习中,使用函数式编程的概念来实现了以下功能:

  1. 求阶乘:通过递归函数实现了求阶乘的功能。
  2. 幂运算函数:通过高阶函数实现了对一个数进行幂运算的功能。
  3. 求多个数的平均值:通过高阶函数和reduce()函数实现了求多个数的平均值的功能。

装饰器是一种用于修改已有函数行为的函数。它可以在不修改原函数代码的情况下,给函数添加新的功能。在实操练习中,使用装饰器实现了对函数进行扩展的功能。

总结:函数式编程通过将函数视为一等对象,使得程序更具灵活性和可扩展性。它包括了一等对象、作用域、命名空间等概念,并可以通过高阶函数、递归函数、装饰器等实现各种功能。函数式编程是一种强大的编程范式,能够简化复杂问题的解决过程。

相关文章
|
7月前
什么是闭包,闭包的特性
什么是闭包,闭包的特性
|
3月前
|
前端开发 JavaScript 数据安全/隐私保护
第十一章 使用高阶函数和非高阶函数对组件进行优化
第十一章 使用高阶函数和非高阶函数对组件进行优化
|
4月前
|
人工智能 自然语言处理 前端开发
闭包是什么?闭包的用途是什么?
闭包是什么?闭包的用途是什么?
|
4月前
|
自然语言处理 JavaScript 前端开发
JavaScript开发基础问题:如何理解闭包及其作用?
JavaScript开发基础问题:如何理解闭包及其作用?
36 5
|
9月前
|
Go
高阶函数编程:探索Go语言中的函数一等公民
Go 函数一等公民。你是否听说过 `Go` 语言中的函数是一等公民?如果没有,那么恭喜你,本文将带你一起揭开这个神秘的面纱。如果你已经了解这个概念,你是否知道为什么 `Go` 语言中的函数被称为一等公民?不管你的答案是什么,通过阅读本文,你将对这个概念有更深入的了解。
60 0
高阶函数编程:探索Go语言中的函数一等公民
什么是闭包?闭包的用途是什么?闭包的缺点是什么?
变量的作用域有两种:全局变量和局部变量; 函数内部可以直接读取全局变量; 在函数外部无法读取函数内的局部变量。 能够读取其他函数内部变量的函数,就是闭包
88 0
|
Scala 开发者
偏函数的必要性 | 学习笔记
快速学习偏函数的必要性
41 0
|
开发者 Python
闭包的概念 | 学习笔记
快速学习闭包的概念
92 0
|
存储 Python
【Python函数式编程】——闭包
一个函数定义中引入了函数定义以外的变量,并且该函数可以在其定义以外被执行,这样的一个函数称为闭包。
137 0
【Python函数式编程】——闭包
|
自然语言处理 JavaScript 前端开发
【闭包概念】关于闭包概念不同解读——你可以自己理解。
写在前面: 闭包是被讲烂的内容,但是当我不了解的情况下,看过很多教程,听过很多道理,还是无法完全理解闭包这个东西。所以想要写一篇比较详细,前端小白也能够真正理解闭包概念的干货文章,本文参考很多闭包资料,希望能真正把闭包这个东西讲清楚,喜欢的朋友可以点个赞,或者点波关注。 首先需要了解一波js变量的作用域以及变量的生存周期。 前几天我发在掘金上面了,下面是一个内容概要,墙裂推荐所有小伙伴,还是点进去学习一波,作用域这个问题可以说是闭包最为重要的内容。 里面的内容包括: 1.变量的作用域(变量的有效范围)。2.变量作用域,js高级程序设计(红宝书)中的解释。3.红宝书中的作用域链栗子(还有一个例子
128 0
【闭包概念】关于闭包概念不同解读——你可以自己理解。