python之自定义函数

269 阅读10分钟

函数基础

到底什么是函数,如何在python程序中定义函数。 说白了,函数就是为了实现某一功能的代码段,只要写好以后,就可以重复利用。看一下下面的例子:

def my_func(message):
    print('Got a message: {}'.format(message))

# 调用函数 my_func()
my_func('Hello World')
# 输出
Got a message: Hello World

其中: 。def是函数的声明;

。my_func是函数的名称;

。括号里面的message则是函数的参数;

。而print那行则是函数的主体部分,可以执行相应的语句;

。在函数最后,可以返回调用结果return,也可以不返回。

总结一下,大概是下面的这种形式:

def name(param1, param2, ..., paramN):
    statements
    return/yield value # optional

和其他需要编译的语言不一样的是,def是可执行语句,这意味着直到被调用前都是不存在的。当程序调用函数时,def语句才会创建一个新的函数对象,并赋予其名字。

先看几个例子:

def my_sum(a, b):
    return a + b

result = my_sum(3, 5)
print(result)

# 输出
8

这里我们定义了my_sum()这个函数,它有两个参数a和b,作用是相加;随后,调用my_sum()函数,分别是把3和5赋予a和b;最后,返回其相加的值,赋予变量result,并输出得到8.

def  find_largest_elemet(l):
       if not  isinstance(l,list):
               print('input  is not  type of  list')
               return
       if len(l) == 0:
           print('empty  input')
           return
       largest_element = l[0]
       for  item in l:
              if  item  > largest_element:
                   largest_element = item
       print('largest element is: {}'.format(largest_element))

find_largest_element([8, 1, -3, 2, 0])

#输出
largest element is: 8.

这个例子中,定义了函数find_largest_element, 作用是遍历输入的列表,找出最大的值并打印。因此,当我们调用它,并传递列表[8,1, -3,2, 0]作为参数时,程序就会输出largest element is: 8

需要注意,主程序调用函数时,必须保证这个函数此前已经定义过,不然就会报错,比如:

my_func('hello world')
def my_func(message):
    print('Got a message: {}'.format(message))
    
# 输出
NameError: name 'my_func' is not defined

但是,如果我们在函数内部调用其他函数,函数间那个声明在前,那个在后就无所谓,因为def是可执行语句,函数在调用之前都不存在,我们只需保证调用时,所需的函数都已经声明定义:

def my_func(message):
    my_sub_func(message) # 调用my_sub_func()在其声明之前不影响程序执行
    
def my_sub_func(message):
    print('Got a message: {}'.format(message))

my_func('hello world')

# 输出
Got a message: hello world

另外,python函数的参数可以设定默认值,比如下面这样的写法:

def func(param = 0):
    ...

这样,在调用函数func()时,如果参数param没有传入,则参数默认为0;而如果传入了参数param,其就会覆盖默认值。

python和其他语言相比的一大特点是,python是dynamically typed的,可以接受任何数据类型(整型,浮点,字符串等等)。对函数参数来说,这一点同样适用。比如还是刚刚的my_sum函数,我们也可以把列表作为参数来传递,表示将两个列表相连接:

print(my_sum([1,2], [3,4]))

#输出
[1, 2, 3, 4]

同样,也可以把字符串作为参数传递,表示字符串的合并拼接:

print(my_sum('hello ', 'world'))

# 输出
hello world

当然,如果两个参数的数据类型不同,比如一个列表,一个是字符串,两者无法相加,那就会报错:

print(my_sum([1, 2], 'hello'))
TypeError: can only concatenate list (not "str") to list

可以看到,python不用考虑输入的数据类型,而是将其交给具体的代码去判断执行,同样的一个函数(比如上面的相加函数my_sum()),可以同时应用在整型,列表,字符串等等的操作中。在编程语言中,把这种行为称为多态。这也是python和其他语言,比如Java,c等很大的一个不同点。当然,python这种方便的特性,在实际使用中也会带来诸多问题。因此,必要时得在开头加上数据的类型检查。

python函数的另一大特性,是python支持函数的嵌套。所谓的函数嵌套,就是指函数里面又有函数,比如:

def f1():
    print('hello')
    def f2():
        print('world')
    f2()
f1()

# 输出
hello
world

这里函数f1()的内部,又定义了函数f2().在调用函数f1()时,会先打印字符串'hello',然后f1()内部再调用f2(),打印字符串'world'。你也许会问,为什么需要函数嵌套?

其实,函数的嵌套,主要有下面两个方面的作用。

第一,函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户,密码等),不想暴露在外,那你就可以使用函数的嵌套,将其封装在内部函数中,只通过外部函数来访问。比如:

def connect_DB():
    def get_DB_configuration():
        ...
        return host, username, password
    conn = connector.connect(get_DB_configuration())
    return conn

这里的函数 get_DB_configuration便是内部函数,它无法在connect_DB()函数以外被单独调用。也就是说,下面这样的外部直接调用是错误的:

get_DB_configuration()

# 输出
NameError: name 'get_DB_configuration' is not defined

我们只能通过调用外部函数connect_DB()来访问它,这样一来,程序的安全性便有了很大的提高。

第二,合理的使用函数嵌套,能够提高程序的运行效率。我们来看下面这个例子:

def factorial(input):
    # validation check
    if not isinstance(input, int):
        raise Exception('input must be an integer.')
    if input < 0:
        raise Exception('input must be greater or equal to 0' )
    ...

    def inner_factorial(input):
        if input <= 1:
            return 1
        return input * inner_factorial(input-1)
    return inner_factorial(input)

print(factorial(5))

这里,使用递归的方式计算一个数的阶乘。因为在计算之前,需要检查输入是否合法,所以写成了函数嵌套的形式,这样一来,输入是否合法就只用检查一次。而如果不使用函数嵌套,那么每调用一次递归便会检查一次,这是没有必要的,也会降低程序的运行效率。实际工作中,如果遇到相似的情况,输入检查不是很快,还会耗费一定的资源,那么运用函数的嵌套就十分必要了。

函数作用变量域

python函数中变量的作用域和其他语言类似。如果变量是在函数内部定义的,就称为局部变量,只在函数内部有效,一旦函数执行完毕,局部变量就会被回收,无法访问,比如下面的例子:

def read_text_from_file(file_path):
    with open(file_path) as file:
        ...

在函数内部定义了file这个变量,这个变量只在read_text_from_file这个函数里有效,在函数外部则无法访问。

相对应的,全局变量则是定义在整个文件层次上的,比如下面这段代码:

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    if value < MIN_VALUE or value > MAX_VALUE:
        raise Exception('validation check fails')

这里的MIN_VALUE和MAX_VALUE就是全局变量,可以在文件内的任何地方被访问,当然在函数内部也是可以的。不过,我们不能在函数内部随意改变全局变量的值。比如,下面的写法就是错误的:

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

如果运行这段代码,程序变会报错:
UnboundLocalError: local variable 'MIN_VALUE' referenced before assignment

这是因为python解释器会默认函数内部的变量为局部变量,但是又发现局部变量MIN_VALUE并没有声明,因此就无法执行相关操作。所以,如果我们一定要在函数内部改变全局变量的值,就必须加上global这个声明:

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    global MIN_VALUE
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

这里的global关键字,并不表示重新创建了一个全局变量MIN_VALUE,而是告诉python解释器,函数内部的变量MIN_VALUE,就是之前定义的全局变量,并不是新的全局变量,也不是全局变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。

另外,如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量,比如下面这种:

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    MIN_VALUE = 3
    ...

在函数validation_check()内部,我们定义了和全局变量同名的局部变量MIN_VALUE,那么MIN_VALUE在函数内部的值,就应该是3而不是1了。

类似的,对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上nonlocal这个关键字:

def outer():
      x = "local"
      def inner():
             nonlocal x  #nonlocal关键字表示这里的x就是外部函数outer定义的变量x
      x = 'nonlocal'
      print("inner:" ,x)
      inner()
      print("outer:", x)
outer()

#输出
inner: nonlocal
outer: nonlocal

如果不加上nonlocal这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。

def outer():
        x = "local"
        def inner():
              x = 'nonlocal' #这里的x是inner这个函数的局部变量
              print("inner:", x)
        inner()
        print("outer:", x)
outer()

#输出
inner:nonlocal
outer:local

闭包

闭包其实和上面的嵌套函数类似,不同的是,这里外部函数返回的是一个函数,而不是一个具体的值。返回的函数通常赋予一个变量,这个变量可以在后面被继续执行调用。

举例说明一下。比如,我们想计算一个数的n次幂,用闭包可以写成下面的代码:

def  nth_power(exponent):
        def exponent_of(base):
                 return base  **  exponent
        return  exponent_of  #返回值是exponent_of函数

square = nth_power(2)
cube = nth_power(3)

square
# 输出
<function __main__.nth_power.<locals>.exponent(base)>

cube
# 输出
<function __main__.nth_power.<locals>.exponent(base)>

print(square(2))  # 计算2的平方
print(cube(2)) # 计算2的立方
# 输出
4 # 2^2
8 # 2^3

这里外部函数nth_power()返回值,是函数exponent_of(),而不是一个具体的数值。需要注意的是,在执行完square = nth_power(2)和cube = nth_power(3)后,外部函数nth_power()的参数exponent,仍然会被内部函数exponent_of()记住。这样,之后我们调用square(2)或者cube(2)时,程序就能顺利地输出结果,而不会报错说参数exponent没有定义了。

其实也可以说为什么需要闭包,上面的程序也可以写成下面的形式:

def nth_power_rewrite(base, exponent):
    return base ** exponent

不过,使用闭包的一个原因,是让程序变得更简洁易读。设想一下,比如需要计算很多个数的平方,那么使用闭包将会带来极大的效率。

# 不使用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...

# 使用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...

其实,和上面的函数嵌套有点类似,函数开头需要做一些额外的工作,而你又需要多次调用这个函数时,将那么额外工作的代码放在外部函数,就可以减少多次调用导致的不必要开销,提高程序的运行效率。更多时候闭包常常和装饰器一起使用。

闭包必须使用嵌套函数。闭包看似仅仅返回了一个嵌套函数,但是需要注意的是,它其实连同嵌套函数的外部环境变量也一同保存返回回来了(例子中的exponent变量),这个环境变量是在调用其外部函数时设定的,这样一来,对于一些参数性,不常改变的设定变量,我们可以通过这个形式来设定,这样返回的闭包函数仅需要关注那些核心输入变量,节省了效率,这样做也大大减少了全局变量的使用,增加代码可读性的同时,也会让代码变得更加安全。