5 函数的设计和使用

函数定义

函数的本质就是一段有特定功能、可以重复使用的代码,这段代码已经被提前编写好了,并且为其起一个“好听”的名字(函数名)。在后续编写程序过程中,如果需要同样的功能,直接通过起好的名字就可以调用这段代码。

设计函数

设计函数时,应注意提高模块的内聚性,同时降低模块之间的隐式耦合。

在实际项目开发中,往往会把一些通用的函数封装到一个模块中,并把这个通用模块文件放到顶层文件夹中,这样更方便管理。

在编写函数时,应尽量减少副作用,尽量不要修改参数本身(例如实参),不要修改除返回值以外的其他内容。

不要在一个函数中执行太多的功能,尽量只让一个函数完成一个高度相关且大小合适的任务,一个函数的代码尽量能在一个屏幕内完整显示。

尽量减少不同函数之间的隐式耦合,减少全局变量的使用,使得函数之间仅通过调用和参数传递来显式体现其相互关系。

应充分利用Python函数式编程的特点,让自己定义的函数尽量符合纯函数式编程的要求,例如保证线程安全、可以并行运行等等。

函数定义语法

def 函数名([参数列表]):
    '''注释'''
    函数体
  • 函数形参不需要声明类型,也不需要指定函数返回值类型
  • 即使该函数不需要接收任何参数,也必须保留一对空的圆括号
  • 括号后面的冒号必不可少
  • 函数体相对于def关键字必须保持一定的空格缩进
  • Python允许嵌套定义函数

函数的说明文档

在定义函数时,开头部分的注释并不是必需的,但如果为函数的定义加上注释的话,可以为用户提供友好的提示。

函数的递归调用

函数的递归调用是函数调用的一种特殊情况,函数调用自己,自己再调用自己,自己再调用自己,...,当某个条件得到满足的时候就不再调用了,然后再一层一层地返回直到该函数第一次调用的位置。

设置递归深度

import sys
sys.setrecursionlimit(3000)

形参与实参

函数定义时括弧内为形参,一个函数可以没有形参,但是括弧必须要有,表示该函数不接受参数。函数调用时,将实参的引用传递给形参。在定义函数时,对参数个数并没有限制,如果有多个形参,需要使用逗号进行分隔。实参和形参的区别,就如同剧本选主角,剧本中的角色相当于形参,而演角色的演员就相当于实参。

修改值

对于绝大多数情况下,在函数内部直接修改形参的值不会影响实参,而是创建一个新变量。适用于实参类型为不可变类型(字符串、数字、元组)。

在有些情况下,可以通过特殊的方式在函数内部修改实参的值:如果传递给函数的实参是可变序列(列表,字典),并且在函数内部使用下标或可变序列自身的原地操作方法增加、删除元素或修改元素时,实参也得到相应的修改。

参数类型

在Python中,函数参数有很多种:可以为普通参数、默认值参数、关键参数、可变长度参数等等。Python在定义函数时不需要指定形参的类型,完全由调用者传递的实参类型以及Python解释器的理解和推断来决定。

位置参数

位置参数(positional arguments)是比较常用的形式,调用函数时实参和形参的顺序必须严格一致,并且实参和形参的数量必须相同。

语法

def 函数名(形参名, 形参名, ...):
	代码块

默认值参数

调用带有默认值参数的函数时,可以不对默认值参数进行赋值,也可以为其赋值,具有很大的灵活性。

语法

def 函数名(..., 形参名, 形参名=默认值):
	代码块

注意

  • 默认值参数必须出现在函数参数列表的最右端,任何一个默认值参数右边不能有非默认值参数。
  • 默认值参数只在函数定义时被解释一次。

关键字参数

关键字参数是指使用形式参数的名字来确定输入的参数值。

通过关键字参数,实参顺序可以和形参顺序不一致,但不影响传递结果,避免了用户需要牢记位置参数顺序的麻烦。

>>> def demo(a,b,c=5):
    print(a,b,c)

>>> demo(3,7)
3 7 5
>>> demo(a=7,b=3,c=6)
7 3 6
>>> demo(c=8,a=9,b=0)
9 0 8

可变长度参数

可变长度参数主要有两种形式:在参数名前加1个星号*或2个星号**。

*parameter用来接收多个位置实参并将其放在元组中。 **parameter接收多个关键参数并存放到字典中。

*parameter的用法

>>> def demo(*p):
    print(p)

>>> demo(1,2,3)
(1, 2, 3)
>>> demo(1,2)
(1, 2)
>>> demo(1,2,3,4,5,6,7)
(1, 2, 3, 4, 5, 6, 7)

**parameter的用法

>>> def demo(**p):
    for item in p.items():
        print(item)

>>> demo(x=1,y=2,z=3)
('x', 1)
('y', 2)
('z', 3)

几种不同类型的参数可以混合使用,但是不建议这样做。

参数传递的序列解包

传递参数时,可以通过在实参序列前加一个星号将其解包,然后传递给多个单变量形参。实际上对一个普通变量使用单星号前缀,能够将这个变量拆分成单个元素。单星号是无法读取到字典中的值的,永远只会读取到字典中的键。

如果函数实参是字典,可以在前面加两个星号进行解包,等价于关键参数。

注意

调用函数时对实参序列使用一个星号*进行解包后的实参将会被当做普通位置参数对待,并且会在关键参数和使用两个星号**进行序列解包的参数之前进行处理。

return语句

return语句用来从一个函数中返回一个值,同时结束函数。

对于以下情况,Python将认为该函数以return None(空值返回)结束,返回空值:

  • 函数没有return语句;
  • 函数有return语句但是没有执行到;
  • 函数有return也执行到了,但是没有返回任何值。

在调用函数或对象方法时,一定要注意有没有返回值。

如果程序需要有多个返回值,则既可将多个值包装成列表之后返回,也可直接返回多个值。如果 Python 函数直接返回多个值,Python 会自动将多个返回值封装成元组。

变量作用域

变量起作用的代码范围称为变量的作用域,不同作用域内变量名可以相同,互不影响。

在函数内部定义的普通变量只在函数内部起作用,出了函数就不能使用了,称为局部变量。当函数执行结束后,局部变量自动删除,不再可以使用。

局部变量的引用比全局变量速度快。

全局变量会增加函数之间的隐式耦合。

全局变量

全局变量可以通过关键字global来定义。这分为两种情况:

  • 如果一个变量在函数外没有定义,在函数内部也可以直接将一个变量定义为全局变量,该函数执行后,将增加一个新的全局变量。
  • 一个变量已在函数外定义,如果在函数内需要为这个变量赋值,并要将这个赋值结果反映到函数外,可以在函数内使用global将其声明为全局变量。

也可以这么理解:

  • 在函数内只引用某个变量的值而没有为其赋新值,如果这样的操作可以执行,那么该变量为(隐式的)全局变量;
  • 如果在函数内任意位置有为变量赋新值的操作,默认就是重新定义新的局部变量,该变量即被认为是(隐式的)局部变量,除非在函数内显式地用关键字global进行声明。

globals() 函数会以字典类型返回当前位置的全部全局变量。

如果局部变量与全局变量具有相同的名字,那么该局部变量会在自己的作用域内隐藏同名的全局变量。

局部(内部)函数

Python 支持在函数内部定义函数,此类函数又称为局部函数

lambda表达式

lambda表达式可以用来声明匿名函数(也可以定义具名函数),也就是没有函数名字的临时使用的小函数,尤其适合需要一个函数作为另一个函数参数的场合。

lambda表达式只可以包含一个表达式,该表达式可以任意复杂,其计算结果可以看作是函数的返回值。

语法:

name = lambda [list] : 表达式

[list] 作为可选参数,等同于定义函数是指定的参数列表。

该语法格式转换成普通函数的形式,如下所示:

def name(list):
	return 表达式
name(list)

高级话题

map()

内置函数map()可以将一个函数作用到一个或多个序列或迭代器对象上,返回可迭代的map对象。

recude()

标准库functools中的reduce()函数可以将一个接受2个参数的函数以迭代的方式从左到右依次作用到一个序列或迭代器对象的所有元素上。

filter()

内置函数filter将一个函数作用到一个序列上,返回该序列中使得该函数返回值为True的那些元素组成的filter对象。

包含yield语句的生成器函数

包含yield语句的函数可以用来创建生成器对象,这样的函数也称生成器函数

每次执行到yield语句会返回一个值然后暂停或挂起后面代码的执行,下次通过生成器对象的__next__()方法、内置函数next()、for循环遍历生成器对象元素或其他方式显式“索要”数据时恢复执行。

生成器对象具有惰性求值的特点,适合大数据处理。

>>> def f():
    a, b = 1, 1            #序列解包,同时为多个元素赋值
    while True:
        yield a            #暂停执行,需要时再产生一个新元素
        a, b = b, a+b      #序列解包,继续生成新元素

>>> a = f()                #创建生成器对象
>>> for i in range(10):    #斐波那契数列中前10个元素
    print(a.__next__(), end=' ')

1 1 2 3 5 8 13 21 34 55 
>>> for i in f():         #斐波那契数列中第一个大于100的元素
    if i > 100:
        print(i, end=' ')
        break

144
>>> a = f()               #创建生成器对象
>>> next(a)               #使用内置函数next()获取生成器对象中的元素
1
>>> next(a)               #每次索取新元素时,由yield语句生成
1
>>> a.__next__()          #也可以调用生成器对象的__next__()方法
2
>>> a.__next__()
3
>>> def f():
    yield from 'abcdefg'        #使用yield表达式创建生成器
	
>>> x = f()
>>> next(x)
'a'
>>> next(x)
'b'
>>> for item in x:              #输出x中的剩余元素
    print(item, end=' ')
	
c d e f g 

函数嵌套定义

在Python中,函数是可以嵌套定义的。

>>> def myMap(iterable, op, value):      #自定义函数
    if op not in '+-*/':
        return 'Error operator'
    def nested(item):                    #嵌套定义函数
        return eval(repr(item)+op+repr(value))
    return map(nested, iterable)         #使用在函数内部定义的函数

>>> list(myMap(range(5), '+', 5))        #调用外部函数
[5, 6, 7, 8, 9]
>>> list(myMap(range(5), '-', 5))
[-5, -4, -3, -2, -1]
>>> list(myMap(range(5), '*', 5))
[0, 5, 10, 15, 20]
>>> list(myMap(range(5), '/', 5))
[0.0, 0.2, 0.4, 0.6, 0.8]

可调用对象

可以使用嵌套函数定义可调用对象。

def linear(a, b):
    def result(x):
        return a * x + b
    return result

另外,任何包含__call__()方法的类的对象也是可调用的。

class linear:
    def __init__(self, a, b):
        self.a, self.b = a, b
    def __call__(self, x):
        return self.a * x + self.b

使用上面的两种方式中任何一个,都可以通过以下的方式来定义一个可调用对象:

taxes = linear(0.3, 2)

然后通过下面的方式来调用该对象:

taxes(5)

修饰器

修饰器(decorator,包装器)是函数嵌套定义的另一个重要应用。修饰器本质上也是一个函数,只不过这个函数接收其他函数作为参数并对其进行一定的改造之后返回新函数。(函数即对象)

当你希望在不修改函数本身的前提下扩展函数的功能时非常有用,在函数执行之前或者之后修改该函数的行为,而无需修改函数本身的代码。

Python面向对象程序设计中的静态方法、类方法、属性等也都是通过修饰器实现的,Python中还有很多这样的用法。

下面的代码演示了修饰器的定义与使用方法,定义其他函数调用之前或之后需要执行的通用代码,可作用于其他任何函数,提高代码复用度。

# 这是装饰器函数 
def print_args(func): 
      def inner_func(*args, **kwargs): 
            print(args) 
            print(kwargs) 
            return func(*args, **kwargs) # 调用原始函数并传递参数 
       return inner_func
@print_args #一种语法糖 ,也就是便捷写法,等价执行multiply=print_args(multiply)
   def multiply(num_a, num_b): 
    return num_a * num_b
print(multiply(3, 5))

把被修饰的函数作为参数传递给修饰函数。通常在装饰器中定义一个新函数并返回它。这个新函数将首先执行它需要执行的一些操作,然后调用原始函数,最后处理返回值。考虑这个简单的装饰函数,它打印原始函数接收的参数,然后调用它。

@符号后面的是修饰器本身,紧跟后面的则是将被修饰的函数(将隐含着赋值覆盖操作)。这种语法等价于使用@后面的修饰器先对get_text修饰,并且返回产生的新函数替代被修饰的函数名。后面直接用被修饰的函数名调用,但是却有了新的功能!

def before(func):                       #定义修饰器
    def wrapper(*args, **kwargs):
        print('Before function called.')
        return func(*args, **kwargs)
    return wrapper

def after(func):                        #定义修饰器
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print('After function called.')
        return result
    return wrapper

@before 
@after
def test():                             #同时使用两个修饰器改造函数
    print(3)
test()                                  #调用被修饰的函数

反编译Python字节码

可以使用Python扩展库uncompyle6或其他类似模块来完成这个功能。使用pip工具安装uncompyle6之后,使用类似于下面的代码对上面生成的.pyc文件进行反编译得到源代码:

uncompyle6.uncompyle_file('__pycache__\\Stack.cpython-35.opt-1.pyc', open('__pycache__\\Stack.py', 'w'))

另外,http://tool.lu/pyc/这个网站可以在线上传一个.pyc文件,然后可以得到Python源代码(个别地方可能不是非常准确,需要自己稍作调整),并且还提供了一定的代码美化功能,能够自动处理代码布局和排版规范。