Python学习笔记(2) - 函数式编程入门

本文为python函数式编程原文的学习笔记,在原文上做了一些结构上的调整并加入了自己的一些理解。
原文链接:Python函数和函数式编程(两万字长文警告!一文彻底搞定函数,建议收藏!)

https://blog.csdn.net/qq_44721831/article/details/102883028

1. 重要概念说明

1-1 函数对象的创建和调用

定义语法

def 函数名([形参列表])

def是执行语句,Python解释执行def语句时会创建一个函数对象,并绑定到函数名变量。

  • 函数定义的第一行为函数签名(signature),指定函数名称以及函数每个形式参数变量名称。
  • 形式参数:在声明函数时可以声明的参数。
  • 实际参数:在调用函数时需要提供函数所需参数的值。

调用函数

函数名([实参列表])

调用函数之前程序必须先执行def语句,创建函数对象。内置函数对象会自动创建,import导入模块时会执行模块中的def语句。
函数的定义位置必须位于调用该函数的全局代码之前,故典型的Python程序结构顺序通常为:import > def > 全局代码

  • 实参列表必须与函数定义的形参列表一一对应。
  • 函数调用是表达式。如果函数有返回值,可以在表达式中直接使用;否则单独作为表达式语句使用。

1-2 函数的副作用

纯函数(pure function)

给定同样的实际参数,其返回值唯一,且不会产生其他的可观察到的副作用。

产生副作用的函数

在一般情况下,产生副作用的函数相当于其他程序设计语言中的过程。在这些函数中可以省略return语句:当Python执行完函数的最后一条语句后,将控制权返回给调用者。例如,print_star(n)的副作用是打印若干星号。
编写同时产生副作用和返回值的函数通常被认为是不良编程风格,但有一个例外,即读取函数。
例如:input()函数既返回一个值,同时又产生副作用(从标准输入中读取并消耗一个字符串)

1-3 参数的传递

在调用函数时,实际参数值按默认位置顺序依次传递给形式参数。

形式参数:在声明函数时可以声明的参数。
实际参数:在调用函数时需要提供函数所需参数的值。

形式参数变量和对象引用传递

声明函数时所声明的形参等同于函数体中局部变量,在函数体中的任何位置都可以使用。
局部变量和形参变量的区别主要在于局部变量在函数体中绑定到某个对象,而形参变量则绑定到函数调用代码传递对应的实参对象。

Python参数传递方法是传递对象引用,而不是传递对象的值

传递不可变对象的引用

在调用函数时,若传递的是不可变对象(例如int long str和bol对象)的引用,其结果实际上是创建了一个新的对象。

[例1] 错误的递增函数

i = 100
def inc(j,n):
    j+=n

inc(i,10)
print(i)

输出结果:

100

[例2] 正确的递增函数

i = 100
def inc(j,n):
    j += n
    return j
i = inc(i,10)
print(i)

输出结果:

110

从以上例子可以看出,“副作用函数”的副作用不会影响到不可变对象。尽管函数内的变量和原本定义的变量看起来相同,但实际上它们是不同的两个对象。稍微修改一下例1便可以看出其区别:

i = 100
def inc(j,n):
    j += n
    print(id(j))

inc(i,10)
print(id(i))

输出结果:

140719285412928
140719285412608

传递可变对象的引用

在调用函数时,如果传递的是可变对象的引用,例如list,则在函数体中可以直接修改对象的值。

import numpy
def exchange(a,i,j):
    temp = a[i]
    a[i] = a[j]
    a[j] = temp

a = numpy.linspace(start=5,stop=25,num=5)
print(a)
exchange(a,3,4)
print(a)

输出结果:

[ 5. 10. 15. 20. 25.]
[ 5. 10. 15. 25. 20.]

可选参数

在声明函数时,如果新网函数的一些参数是可选的,可以在声明函数时为这些参数指定默认值。在调用该函数时,如果没有传入对应的实参值,则使用声明时的默认参数值。

参数的声明顺序:
没有默认值的形参 --> 有默认值的形参
(因为函数在调用时默认按位置传递实参值)

位置参数和命名参数

位置参数:按位置传递的参数。
命名参数:也称为关键字参数。
注意:带星号的参数后面声明的参数强制为命名参数,且调用时必须使用命名参数赋值。如果这些参数没有默认值,则会引发错误。
如果不需要带星号的参数,只需要强制命名参数,则可以简单地使用一个星号,例如:

def total(initial=5, *, vegetables)

可变参数

  • 通过带星的参数(例如*param)向函数传递可变数量的实参。在调用函数时,从那一点后所有的参数被收集为一个tuple。
  • 通过带双星的参数(例如**param)向函数传递可变数量的实参。在调用函数时,从那一点后所有的参数被收集为一个字典。

注意:带星参数必须位于形参列表的最后位置。

[例1] 带*参数

## *param
def my_sum(a, b, *c):
    total = a + b
    for k in c:
        total = total + k
    return total
print(my_sum(1,2))
print(my_sum(1,2,3,4,5))

输出结果:

3
15

[例2] 带**参数

## **param
def my_sum2(a, b, *c, **d):
    total = a + b
    for k in c:
        total += k
    for key in d:
        total += d[key]
    return total
print(my_sum2(1,2))
print(my_sum2(1,2,3,4,5))
print(my_sum2(1,2,3,4,5, i=6, j=7))

输出结果:

3
15
28

强制命名参数

在带星号的参数后面声明参数会导致强制命名参数(Keyword- only),在调用时必须显式使用命名参数传递值,因为按位置传递的参数默认收集为一个元组,传递给前面带星号的可变参数。
如果不需要带星号的参数,只需要强制命名参数,则可以简单地使用一个星号。

参数类型检查

通常定义函数时,需要指定形式参数和返回值的类型;而在Python语言中,定义函数时不用限定其参数和返回值的类型。这种灵活性可以实现多态性,即允许函数适用于不同类型对象,例如my_average(a,b),既可以返回两个int对象的平均值,也可以返回两个float对象的平均值。

当使用不支持的类型参数调用函数时会产生错误。例如向my_average(a,b)传递str对象,将导致TypeError。 原则上可以增加代码检测这类型错误,但Python程序设计遵循一种惯例,即用户调用函数时必须理解并保证传入正确类型的参数值。

1-4 函数的返回值

return语句

在函数体中使用return语句可以实现从函数中返回一个值并跳出函数的功能。

多条return语句

return语句可以放置在函数中的任何位置,当执行到第一个return语句时返回到调用程序。

[例] 判断素数示例:先编写判断一个数是否为素数的函数,然后编写代码,判断并输出1~99中的素数。

def is_prime(n):
    if n < 2 : return False
    i = 2
    while i*i <=n:
        #一旦n能够被2~i中的任意数整除,n就不是素数,返回False
        if n % i == 0: return False
        i += 1
    return True

## 调用函数并输出0~99间的所有素数:
prime_num = []
for i in range(100):
    if is_prime(i) == True: prime_num.append(i)

print(prime_num)

输出结果:

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

使用tuple返回多个值

在函数体中使用return语句可以实现从函数返回一个值并跳出函数。如果需要返回多个值,则可以返回一个元组。

1-5 变量的作用域

变量的可访问范围被称为变量的作用域。变量按其作用域大致可以分为全局变量、局部变量和类成员变量。

全局变量

在一个源代码文件中,在函数和类定义之外声明的变量成为全局变量。
通过import语句导入模块,也可以通过全限定名称“模块名.变量名”访问,或者通过from-import语句导入模块中的变量来访问。

不同的模块都可以访问全局变量,这会导致全局变量的不可预知性。如果多个语句同时修改一个全局变量,则可能导致程序产生错误,而且难以发现和更正。

局部变量

在函数体中声明的变量(包括函数参数)成为局部变量,其有效范围(作用域)为函数体。
如果在一个函数中定义的局部变量(或形参变量)与全局变量重名,则局部变量(或形参变量)优先。

全局语句global

当函数局部变量名第一次出现,且在赋值语句之前,则该变量被解释为局部变量。也就是说,函数体中只能出现引用全局变量,而不能对其进行更改;如果在函数体中对全局变量进行赋值等操作,实则新建了一个局部变量。

[例1] 全局变量的错误引用和赋值

m = 100
n = 20
def f():
    print(m + 5)
    n += 10
f()

输出结果报错:

Traceback (most recent call last):
  File "d:\demo\Python\函数式编程\LearningMaterial.py", line 131, in <module>
    f()
  File "d:\demo\Python\函数式编程\LearningMaterial.py", line 130, in f
    n += 10
UnboundLocalError: local variable 'n' referenced before assignment

如果要为定义在函数体之外的全局变量赋值,可以使用global语句,表明变量是在外面定义的全局变量。
global语句可以指定多个全局变量,例如"global x,y,z"。一般应该尽量避免这样使用全局变量,因为会导致程序的可读性变差。

[例2] 使用global语句为全局变量赋值

pi = 3.1415926
print('global pi = ', pi)
def my_f():
    global pi #声明使用和操作全局变量
    pi = 3.14
    print('global pi = ', pi)

my_f()
print('global pi = ',pi)

输出结果:

global pi =  3.1415926
global pi =  3.14
global pi =  3.14

非局部语句nonlocal

在函数体中可以定义嵌套函数,在嵌套函数中,可以使用nonlocal语句为定义在上级函数体的局部变量赋值。

def outer_func():
    tax_rate = 0.17
    print('outer func tax rate = ', tax_rate)
    def inner_func():
        nonlocal tax_rate #声明使用和操作上一级函数体局部变量
        tax_rate = 0.05
        print('inner func tax rate = ', tax_rate)
    inner_func()
    print('outer func tax rate = ', tax_rate)

outer_func()

输出结果:

outer func tax rate =  0.17
inner func tax rate =  0.05
outer func tax rate =  0.05

类成员变量

类成员变量是在类中声明的变量,包括静态变量和实例变量,其作用域为类定义体内。
在外部,通过创建类的对象实例,然后通过"对象.实例变量"访问类的实例变量。

查看局部变量和全局变量

可以使用内置函数globals()和locals()查看并输出局部变量和全局变量列表。

1-6 递归函数

定义

递归函数即自调用函数,在函数体内部直接或间接地自己调用自己,即函数的嵌套调用是函数本身。递归函数常用来实现数值计算的方法。

[例1] 使用递归函数实现阶乘计算

def factorial(n):
    if n==1: return 1
    return n * factorial(n - 1)

## 输出1~9的阶乘
for i in range(1,10):
    print(str(i)+'! = ', factorial(i))

输出结果:

1! =  1
2! =  2
3! =  6
4! =  24
5! =  120
6! =  720
7! =  5040
8! =  40320
9! =  362880

递归函数的原理

递归提供了建立数学模型的一种直接的方法,与数学上的数学归纳法相对应。

每个递归函数必须包括以下两个主要部分:

  1. 终止条件:表示递归的结束条件,用于返回函数值
  2. 递归步骤:递归步骤把第n步的参数值的函数与第n-1步的参数值函数关联

另外,一序列的参数值必须逐渐收敛到结束条件。

[例1] 利用递归函数实现调和数

def harmonic(n):
    if n == 1: return 1.0
    return harmonic(n - 1) + 1.0/n

## 输出1~9的调和数
for i in range(1,10):
    print('H', i, '=', harmonic(i))

输出结果:

H 1 = 1.0
H 2 = 1.5
H 3 = 1.8333333333333333
H 4 = 2.083333333333333
H 5 = 2.283333333333333
H 6 = 2.4499999999999997
H 7 = 2.5928571428571425
H 8 = 2.7178571428571425
H 9 = 2.8289682539682537

注意事项

  1. 必须设置终止条件

缺少终止条件的递归函数将导致无限递归函数调用,其最终结果是内存耗尽,并抛出错误RuntimeError: maximum recursion depth exceeded(超过最大递归深度)。

  1. 必须保证收敛

递归调用所解决子问题的规模必须小于原始问题的规模。

  1. 必须保证内存和运算消耗控制在一定范围内

递归函数的应用

01 - 最大公约数(Greatest Common Divisor)

用于计算最大公约数问题的递归方法称为欧几里得算法,算法描述如下:
如果p>q,则p和q的最大公约数约等于q和(p%q)的最大公约数。

def gcd(p, q):
    if q == 0: return p
    return gcd(q, p % q)

#测试代码
p = random.randint(1, 100)
q = random.randint(1, 100)
print("p =", p, '\nq =', q, '\nGCD(p, q) =', gcd(p, q))

输出结果:

p = 75 
q = 57 
GCD(p, q) = 3
02 - 汉诺塔(Towers of Hanoi)

解题思路:假设柱子编号为a,b,c,定义函数hanoi(n, a, b, c)表示把n个圆盘从柱子a(可经由柱子b)移动到柱子c
终止条件:当n==1时,hanoi(n, a, b, c)为终止条件。即如果柱子上只有一个圆盘,则可以直接将其移动到柱子c上。
递归步骤:

  1. hanoi(n-1, a, c, b):把柱子a上的n个圆盘看成n-1个圆盘和一个底盘,先把n-1个圆盘经由c移动到b
  2. hanoi(1, a, b, c):把剩下的一个底盘直接移动到c
  3. hanoi(n-1, b, a, c):把b上的n-1个圆盘经由a移动到c
def hanoi(n, a, b, c):
    if n==1: print(a, '->', c) # 如果柱子上只有一个圆盘,则可以直接将其移动到柱子c上
    else:
        hanoi(n-1, a, c, b)
        hanoi(1, a, b, c)
        hanoi(n-1, b, a, c)

# 测试代码
hanoi(3, 'A', 'B', 'C')

输出结果:

A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

1-7 内置函数的使用

内置函数一览表

https://docs.python.org/zh-cn/3/library/functions.html

内置函数
abs()delatrr()hash()memoryview()set()
all()dict()help()min()setattr()
any()dir()hex()next()slice()
ascii()divmod()id()object()sorted()
bin()enumerate()input()oct()staticmethod()
bool()eval()int()open()str()
breakpoint()exec()isinstance()ord()sum()
bytearray()filter()issubclass()pow()super()
bytes()float()len()property()type()
chr()frozenset()list()range()vars()
classmethod()getattr()locals()repr()zip()
compile()globals()map()reversed()__import__()
complex()hasattr()max()round()

Python函数式编程基础

Python标准库functools提供了若干关于函数的函数,提供了Haskell和Standard ML中的函数式程序设计工具。

2-1 作为对象的函数

将函数赋值给变量

在Python语言中函数也是对象,故函数对象可以赋值给变量。

f = abs
print(type(f)) # <class 'builtin_function_or_method'>
print(f(-123))  # 123

将函数作为参数传递

函数对象也可以作为参数传递给函数,还可以作为函数的返回值。参数为函数对象的函数或返回函数对象的函数成为高阶函数。

def compute(f, s):
    return f(s)

compute(min, (1,5,3,2)) # 1

2-2 map()函数

基本语法:map(function, iterable)

  • function: 内置函数、自定义函数或者lambda匿名函数
  • iterable:表示一个或多个可迭代对象,可以是列表、字符串等
listDemo = [1, 2, 3, 4, 5]
new_list = map(lambda x: x * 2, listDemo)
print(new_list) # <map object at 0x000001A1CDBEF100>
print(list(new_list)) # [2, 4, 6, 8, 10]

map()函数可以传入多个可迭代对象作为参数:

listDemo = [1, 2, 3, 4, 5]
listDemo2 = [5, 6, 7, 8, 9]
new_list2 = map(lambda x,y: x ** y, listDemo, listDemo2)
print(list(new_list2)) # [1, 64, 2187, 65536, 1953125]

2-3 filter()函数

基本语法:filter(function, iterable)

filter()函数的功能是对iterable中的每个元素,都是用function函数判断,并返回True/False,最后将返回True的元素组成一个新的可遍历的集合。

# 返回一个列表中所有的偶数
listDemo = [1, 2, 3, 4, 5]
new_list3 = filter(lambda x: x % 2 == 0, listDemo)
print(list(new_list3)) # [2, 4]

2-4 reduce()函数

reduce(function, iterable)

  • function:必须是一个包含两个参数的函数

reduce()函数通常用来对一个集合做一些累积操作。reduce()函数在python3中已经被移除,放入了functools模块,因此在使用该函数之前,需要先导入functools模块。

# 计算某个列表元素的乘积
import functools
listDemo = [1, 2, 3, 4, 5]
product = functools.reduce(lambda x,y: x * y, listDemo)
print(product) # 120

小结

通常来说,对集合中的元素进行一些操作时,如果操作非常简单,比如相加、累积这种,那么应该优先考虑使用map()、filter()、reduce()实现。另外,在数据量非常多的情况下(比如机器学习的应用),一般更倾向于函数式的编程,因为效率更高。

当然,在数据量不高的情况下,使用for循环等方式也可以。

不过,如果要对集合中的元素做一些比较复杂的操作,考虑到代码的可读性,通常会使用for循环。

2-5 lambda匿名函数

lambda表达式(又称匿名函数)是现代编程语言争相引入的一种语法。如果说函数是命名的、方便复用的代码块,那么lambda表达式则是功能更灵活的代码块,它可以在程序中被传递和调用。

基本语法: lambda [parameter_list]: expression

lambda表达式的几个要点:

  1. lambda表达式必须使用lambda关键字定义
  2. lambda关键字之后、冒号左边的是参数列表(可以没有参数,也可以有多个参数;如果有多个参数则需要用逗号隔开)
  3. 冒号右边是lambda表达式的返回值

lambda表达式与函数的比较

函数比lambda表达式的适应性更强,lambda表达式只能创建简单的函数对象,但后者有以下两个优点:

  1. 对于单行函数,省去了定义函数的过程,代码更加简洁。
  2. 对于不需要多次复用的函数,使用lambda表达式可以在用完之后立即释放,提高了性能。
x = map(lambda x: x*x, range(8))
print(list(x))
# [0, 1, 4, 9, 16, 25, 36, 49]
y = map(lambda x: x*x if x % 2 ==0 else 0, range(8))
print(list(y))
# [0, 0, 4, 0, 16, 0, 36, 0]

lambda小结

lambda表达式是Python编程的核心机制之一。Python语言既支持面向过程编程,也支持面向对象编程。而lambda表达式是Python面向过程编程的语法基础,因此必须引起重视。

2-7 函数装饰器

I. 引入

Python内置的3种函数装饰器,分别是 @staticmethod @classmethod @property,其中staticmethod\classmethod\property都是Python内置函数。
另外,我们也可以开发自定义的函数装饰器。

当程序使用“@函数”(函数A)装饰另一个函数B时,实际上完成如下两步

  • 将被修饰的函数B作为参数传给函数A
  • 将函数B替换为第一步的返回值

所谓的装饰器,就是通过装饰器函数来修改原函数的一些功能,使得原函数不需要修改。

def funA(fn):
    print('A')
    fn() # 执行传入的fn参数
    return 'fkit'

# 下面装饰效果相当于:funA(funB),
# funB将会替换成funA()语句的返回值,即fkit

@funA
def funB():
    print('B')

print(funB)

输出结果:

A
B
fkit

II. 带参数的函数装饰器

def foo(fn):
    # 定义一个嵌套函数
    def bar(a):
        fn(a * (a - 1))
        print("*" * 15)
        return fn(a*(a - 1))
    return bar

@foo
def my_test(a):
    print("==my_test函数==", a)

print(my_test) # <function foo.<locals>.bar at 0x00000292E02A6280>

print(my_test(10))

输出结果:

==my_test函数== 90
***************
==my_test函数== 90
None

说明:上述函数用foo()修饰my_test(),首先要明确的是,装饰器函数foo把my_test()替换成了bar()函数。
在调用my_test()函数的时候:
① 首先执行bar()函数第一行的内容,即调用my_test(10*(10-1)),打印了“==my_test函数== 90”;
② 然后,执行bar()函数第二行,即打印了“***************”;
③ 然后继续执行第三行,返回my_test(10*(10-1)),于是函数继续执行my_test(10*(10-1)),即打印了“==my_test函数== 90”;
④ 最后,由于my_test()函数没有返回值,我们执行print()最后一行打印为None。

在此基础上,如果程序中还有另一个参数数量不同的函数也需要用同一个修饰器函数,可以采用以下的方式:

def foo(fn):
    # 定义一个嵌套函数
    def bar(*args, **kwargs):
        ...
        fn(*args, **kwargs)
        ...
    return bar

@foo
def my_test(a):
    print("==my_test==", a)

@foo
def new_test(a,b):
    print("==new_test==", a, ",", b)

III. 带自定义参数的函数装饰器

装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自己定义的参数。

def foo(num):
    def my_decorator(fn):
        def bar(*args, **kwargs):
            for i in range(num):
                fn(*args, **kwargs)
        return bar
    return my_decorator

@foo(3) # 经过装饰后执行3次
def my_test(a):
    print("my_test", a)

@foo(5) # 经过装饰后执行5次
def my_test_2(a, b):
    print("my_test 2", a, ",", b)

my_test(10)
my_test_2(5, 6)

输出结果:

my_test 10
my_test 10
my_test 10
my_test 2 5 , 6
my_test 2 5 , 6
my_test 2 5 , 6
my_test 2 5 , 6
my_test 2 5 , 6
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值