第5章:函数式编程

Posted 墨迹测试开发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第5章:函数式编程相关的知识,希望对你有一定的参考价值。


[TOC]

函数

简介

函数是应用程序最重要的方面。函数可以定义为可重用代码的有组织块,可以在需要时调用它。函数是由{}括起来的一组编程语句,函数体现了python程序的可重用性和模块性。


函数是应用程序最重要的方面。函数可以定义为可重用代码的有组织块,可以在需要时调用它。

Python允许我们将大型程序划分为称为函数的基本构建块。该函数包含由{}括起来的一组编程语句。可以多次调用函数以为python程序提供可重用性和模块性。

换句话说,我们可以说函数集合创建了一个程序。该函数也称为其他编程语言中的过程或子例程。

Python为我们提供了各种内置函数,如range()或print()。虽然,用户可以创建其功能,可以称为用户定义的功能。

python中函数的优点

函数有以下优点。

通过使用函数,我们可以避免在程序中一次又一次地重写相同的逻辑/代码。

我们可以在程序中和程序中的任何地方多次调用python函数。

当它被分成多个函数时,我们可以轻松地跟踪大型python程序。

可重用性是python函数的主要成就。

但是,函数调用总是在python程序中开销。

创建一个函数

在python中,我们可以使用def关键字来定义函数。下面给出了在python中定义函数的语法。

def my_function():

function-suite

return <expression>

函数块以冒号(:)开头,所有相同级别的块语句保持相同的缩进。

函数可以接受定义和函数调用中必须相同的任意数量的参数。

功能调用

在python中,必须在函数调用之前定义函数,否则python解释器会给出错误。一旦定义了函数,我们就可以从另一个函数或python提示符调用它。要调用该函数,请使用函数名称,后跟括号。

下面给出了一个打印消息“Hello Word”的简单函数。

def hello_world():
print("hello world")
hello_world()

输出:

hello world

功能中的参数

函数中的信息可以作为参数传递。参数在括号中指定。我们可以提供任意数量的参数,但我们必须用逗号分隔它们。

请考虑以下示例,其中包含一个接受字符串作为参数并打印它的函数。

例1

#defining the function
def func (name):
print("Hi ",name);
#calling the function
func("Ayush")

例2

#python function to calculate the sum of two variables
#defining the function
def sum (a,b):
return a+b;
#taking values from the user
a = int(input("Enter a: "))
b = int(input("Enter b: "))
#printing the sum of a and b
print("Sum = ",sum(a,b))

输出:

Enter a: 10
Enter b: 20
Sum = 30

在Python中通过引用调用

在python中,所有函数都通过引用调用,即,对函数内部引用所做的所有更改都将恢复为引用引用的原始值。

但是,在可变对象的情况下有一个例外,因为对像string这样的可变对象所做的更改不会恢复为原始字符串,而是创建一个新的字符串对象,因此会打印两个不同的对象。

示例1传递不可变对象(列表)

#defining the function
def change_list(list1):
list1.append(20);
list1.append(30);
print("list inside function = ",list1)
#defining the list
list1 = [10,30,40,50]
#calling the function
change_list(list1);
print("list outside function = ",list1);

输出:

list inside function =  [10, 30, 40, 50, 20, 30]
list outside function = [10, 30, 40, 50, 20, 30]

示例2传递可变对象(字符串)

#defining the function
def change_string (str):
str = str + " Hows you";
print("printing the string inside function :",str);
string1 = "Hi I am there"
#calling the function
change_string(string1)
print("printing the string outside function :",string1)

输出:

printing the string inside function : Hi I am there Hows you
printing the string outside function : Hi I am there

参数的类型

可能有几种类型的参数可以在函数调用时传递。

必需的参数

关键字参数

默认参数

可变长度参数

必需的参数

直到现在,我们已经了解了python中的函数调用。但是,我们可以在函数调用时提供参数。就所需的参数而言,这些是在函数调用时需要传递的参数,它们在函数调用和函数定义中的位置完全匹配。如果函数调用中未提供任何参数,或者参数的位置发生更改,则python解释器将显示错误。

请考虑以下示例。

例1

#the argument name is the required argument to the function func
def func(name):
message = "Hi "+name;
return message;
name = input("Enter the name?")
print(func(name))

输出:

Enter the name?John
Hi John

例2

#the function simple_interest accepts three arguments and returns the simple interest accordingly
def simple_interest(p,t,r):
return (p*t*r)/100
p = float(input("Enter the principle amount? "))
r = float(input("Enter the rate of interest? "))
t = float(input("Enter the time in years? "))
print("Simple Interest: ",simple_interest(p,r,t))

输出:

Enter the principle amount? 10000
Enter the rate of interest? 5
Enter the time in years? 2
Simple Interest: 1000.0

例3

#the function calculate returns the sum of two arguments a and b
def calculate(a,b):
return a+b
calculate(10) # this causes an error as we are missing a required arguments b.

输出:

TypeError: calculate() missing 1 required positional argument: 'b'

关键字参数

Python允许我们使用关键字参数调用该函数。这种函数调用将使我们能够以随机顺序传递参数。

参数的名称被视为关键字,并在函数调用和定义中进行匹配。如果找到相同的匹配项,则会在函数定义中复制参数的值。

请考虑以下示例。

例1

#function func is called with the name and message as the keyword arguments
def func(name,message):
print("printing the message with",name,"and ",message)
func(name = "John",message="hello") #name and message is copied with the values John and hello respectively

输出:

printing the message with John and  hello

示例2在调用时以不同顺序提供值

#The function simple_interest(p, t, r) is called with the keyword arguments the order of arguments doesn't matter in this case
def simple_interest(p,t,r):
return (p*t*r)/100
print("Simple Interest: ",simple_interest(t=10,r=10,p=1900))

输出:

Simple Interest:  1900.0

如果我们在函数调用时提供不同的参数名称,则会引发错误。

请考虑以下示例。

例3

#The function simple_interest(p, t, r) is called with the keyword arguments.
def simple_interest(p,t,r):
return (p*t*r)/100
print("Simple Interest: ",simple_interest(time=10,rate=10,principle=1900)) # doesn?t find the exact match of the name of the arguments (keywords)

输出:

TypeError: simple_interest() got an unexpected keyword argument 'time'

python允许我们在函数调用时提供所需参数和关键字参数的混合。但是,必须在关键字参数之后给出必需的参数,即,一旦在函数调用中遇到关键字参数,以下参数也必须是关键字参数。

请考虑以下示例。

例4

def func(name1,message,name2):
print("printing the message with",name1,",",message,",and",name2)
func("John",message="hello",name2="David") #the first argument is not the keyword argument

输出:

printing the message with John , hello ,and David

以下示例将导致错误,因为在函数调用中传递了关键字和必需参数的不正确混合。

例5

def func(name1,message,name2):
print("printing the message with",name1,",",message,",and",name2)
func("John",message="hello","David")

输出:

SyntaxError: positional argument follows keyword argument

默认参数

Python允许我们在函数定义中初始化参数。如果在函数调用时未提供任何参数的值,则可以使用定义中给定的值初始化该参数,即使函数调用中未指定参数也是如此。

例1

def printme(name,age=22):
print("My name is",name,"and age is",age)
printme(name = "john") #the variable age is not passed into the function however the default value of age is considered in the function

输出:

My name is john and age is 22

例2

def printme(name,age=22):
print("My name is",name,"and age is",age)
printme(name = "john") #the variable age is not passed into the function however the default value of age is considered in the function
printme(age = 10,name="David") #the value of age is overwritten here, 10 will be printed as age

输出:

My name is john and age is 22
My name is David and age is 10

可变长度参数

在大型项目中,有时我们可能不知道要提前通过的参数的数量。在这种情况下,Python为我们提供了灵活性,可以提供逗号分隔值,这些值在函数调用时被内部视为元组。

但是,在函数定义中,我们必须使用(star)将变量定义为 

请考虑以下示例。

def printme(*names):
print("type of passed argument is ",type(names))
print("printing the passed arguments...")
for name in names:
print(name)
printme("john","David","smith","nick")

输出:

type of passed argument is
printing the passed arguments...
john
David
smith
nick

变量范围

变量的范围取决于声明变量的位置。在程序的一个部分中声明的变量可能无法被其他部分访问。

在python中,变量是使用两种类型的范围定义的。

全局变量

局部变量

已知在任何函数外部定义的变量具有全局范围,而已知在函数内定义的变量具有局部范围。

请考虑以下示例。

例1

def print_message():
message = "hello !! I am going to print a message." # the variable message is local to the function itself
print(message)
print_message()
print(message) # this will cause an error since a local variable cannot be accessible here.

输出:

hello !! I am going to print a message.
File "/root/PycharmProjects/PythonTest/Test1.py", line 5, in
print(message)
NameError: name 'message' is not defined

例2

def calculate(*args):
sum=0
for arg in args:
sum = sum +arg
print("The sum is",sum)
sum=0
calculate(10,20,30) #60 will be printed as the sum
print("Value of sum outside the function:",sum) # 0 will be printed

输出:

The sum is 60
Value of sum outside the function: 0

lambda函数

Python允许我们不以标准方式声明函数,即不使用def关键字,而是使用lambda关键字声明匿名函数。Lambda函数可以接受任意数量的参数,但它们只能以表达式的形式返回一个值。


Python允许我们不以标准方式声明函数,即使用def关键字。而是使用lambda关键字声明匿名函数。但是,Lambda函数可以接受任意数量的参数,但它们只能以表达式的形式返回一个值。

匿名函数包含一小段代码。它模拟C和C ++的内联函数,但它不完全是内联函数。

下面给出了定义匿名函数的语法。

lambda arguments : expression

例1

x = lambda a:a+10 # a is an argument and a+10 is an expression which got evaluated and returned.
print("sum = ",x(20))

输出:

sum= 30

例2

Lambda函数的多个参数

x = lambda a,b:a+b # a and b are the arguments and a+b is the expression which gets evaluated and returned.
print("sum = ",x(20,10))

输出:

Sum = 30

为什么要使用lambda函数?

当我们在另一个函数中匿名使用lambda函数时,lambda函数的主要作用更好地描述。在python中,lambda函数可以用作高阶函数的参数作为参数。Lambda函数也用于我们需要的场景中考虑以下示例。

例1

#the function table(n) prints the table of n
def table(n):
return lambda a:a*n; # a will contain the iteration variable i and a multiple of n is returned at each function call
n = int(input("Enter the number?"))
b = table(n) #the entered number is passed into the function table. b will contain a lambda function which is called again and again with the iteration variable i
for i in range(1,11):
print(n,"X",i,"=",b(i)); #the lambda function b is called with the iteration variable i,

输出:

Enter the number??10
10 X 1 = 10
10 X 2 = 20
10 X 3 = 30
10 X 4 = 40
10 X 5 = 50
10 X 6 = 60
10 X 7 = 70
10 X 8 = 80
10 X 9 = 90
10 X 10 = 100

例2

使用lambda函数与过滤器

#program to filter out the list which contains odd numbers
List = {1,2,3,4,10,123,22}
Oddlist = list(filter(lambda x:(x%3 == 0),List)) # the list contains all the items of the list for which the lambda function evaluates to true
print(Oddlist)

输出:

[3,123]

例3

使用lambda函数与map

#program to triple each number of the list using map
List = {1,2,3,4,10,123,22}
new_list = list(map(lambda x:x*3,List)) # this will return the triple of each item of the list and add it to new_list
print(new_list)

输出:

[3,6,9,12,30,66,369]

递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。


在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n

所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

于是,fact(n)用递归的方式写出来就是:

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

上面就是一个递归函数。可以试试:

>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827
223758251185210916864000000000000000000000000L

如果我们计算fact(5),可以根据函数定义看到计算过程如下:

=> fact(5)
=> 5 * fact(4)
=> 5 * (4 * fact(3))
=> 5 * (4 * (3 * fact(2)))
=> 5 * (4 * (3 * (2 * fact(1))))
=> 5 * (4 * (3 * (2 * 1)))
=> 5 * (4 * (3 * 2))
=> 5 * (4 * 6)
=> 5 * 24
=> 120

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000):

>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

可以看到,return fact_iter(num - 1, num product)仅返回递归函数本身,num - 1和num product在函数调用前就会被计算,不影响函数调用。

fact(5)对应的fact_iter(5, 1)的调用如下:

=> fact_iter(5, 1)
=> fact_iter(4, 5)
=> fact_iter(3, 20)
=> fact_iter(2, 60)
=> fact_iter(1, 120)
=> 120

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。

装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。


由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。

>>> def now():
... print '2013-12-25'
...
>>> f = now
>>> f()
2013-12-25

函数对象有一个name属性,可以拿到函数的名字:

>>> now.__name__
'now'
>>> f.__name__
'now'

现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

def log(func):
def wrapper(*args, **kw):
print 'call %s():' % func.__name__
return func(*args, **kw)
return wrapper

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

@log
def now():
print '2013-12-25'

调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:

>>> now()
call now():
2013-12-25

把@log放到now()函数的定义处,相当于执行了语句:

now = log(now)

由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(args, *kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

def log(text):
def decorator(func):
def wrapper(*args, **kw):
print '%s %s():' % (text, func.__name__)
return func(*args, **kw)
return wrapper
return decorator

这个3层嵌套的decorator用法如下:

@log('execute')
def now():
print '2013-12-25'

执行结果如下:

>>> now()
execute now():
2013-12-25

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

>>> now = log('execute')(now)

我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有name等属性,但你去看经过decorator装饰之后的函数,它们的name已经从原来的'now'变成了'wrapper':

>>> now.__name__
'wrapper'

因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的name等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.name = func.name这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print 'call %s():' % func.__name__
return func(*args, **kw)
return wrapper

或者针对带参数的decorator:

import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print '%s %s():' % (text, func.__name__)
return func(*args, **kw)
return wrapper
return decorator

import functools是导入functools模块。模块的概念稍后讲解。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可。

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。

请编写一个decorator,能在函数调用的前后打印出'begin call'和'end call'的日志。

再思考一下能否写出一个@log的decorator,使它既支持:

@log
def f():
pass

支持:

@log('execute')
def f():
pass

偏函数

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。Python中的偏函数通过设定参数的默认值,可以降低函数调用的难度。


Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。

在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

>>> int('12345')
12345

但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:

>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

def int2(x, base=2):
return int(x, base)

这样,我们转换二进制就非常方便了:

>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:

>>> int2('1000000', base=10)
1000000

最后,创建偏函数时,实际上可以接收函数对象、args和*kw这3个参数,当传入:

int2 = functools.partial(int, base=2)

实际上固定了int()函数的关键字参数base,也就是:

int2('10010')

相当于:

kw = { base: 2 }
int('10010', **kw)

当传入:

max2 = functools.partial(max, 10)

实际上会把10作为*args的一部分自动加到左边,也就是:

max2(5, 6, 7)

相当于:

args = (10, 5, 6, 7)
max(*args)

结果为10。

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。



END

时光,在物转星移中渐行渐远,春花一梦,流水无痕,没有在最想做的时候去做的事情,都是人生的遗憾。人生需要深思熟虑,也需要一时的冲动。

第5章:函数式编程

以上是关于第5章:函数式编程的主要内容,如果未能解决你的问题,请参考以下文章

javaScript函数式编程

函数式编程思维第一章为什么

第8章 函数式编程(FP)

函数式编程

函数式编程

《On Java 8》中文版 第十三章 函数式编程