首页 > 代码库 > wk_04

wk_04

函数

函数是对程序逻辑进行结构化或过程化的一直编程方法。能将整块代码巧妙的隔离成易于管理的小块,把重复代码放到函数中而不是进行大量的拷贝--这样既能节省空间,也有助于保持一致性,因为你只需要改变单个的拷贝而无须去寻找再修改大量复制代码的拷贝。

创建函数

  • def语句

函数是用def语句来创建的,语法如下:

def function_name(params):
    pass

In [1]: def add(x,y):
   ...:     print(‘{} + {} = {}‘.format(x,y,x+y))

标题行由def关键字,函数的名字,以及参数的集合组成。def子句的剩余部分可以包括文档子串和必须的函数体。

  • 声明和定义

在某些编程语言中,函数声明和函数定义区分开的。一个函数声明包括提供对函数名、参数的名字,但不必给出函数的任何代码,具体的代码通常属于函数定义的范畴。
在声明和定义有区别的语言中,往往是因为函数的定义可能和其声明放在不同的文件中。Python将这两者视为一体,函数的子句有声明的标题行以及随后的定义体组成的。

  • 返回值

函数会向调用者返回一个值,而实际编程中大部分偏函数更接近过程,不显示地返回任何东西。在Python中对应的返回对象类型是none。

In [5]: def less(x,y):
   ...:     return x-y
   
In [6]: less(2,3)
Out[6]: -1

In [7]: a=less(5,4)

In [8]: a
Out[8]: 1

In [10]: def fn(x,y):
    print (x+y) 

In [11]: fn(2,3)
5

In [12]: b=fn(3,4)
7

In [13]: print(b)
None

In [14]: type(b)
Out[14]: NoneType

上面的fn()函数的行为就像一个过程,没有返回值,其实是不能说没有返回值,确切说fn()函数的返回值是None。

In [18]: def foo():
    return[‘xyz‘,1000000,-11]

In [19]: foo()
Out[19]: [‘xyz‘, 1000000, -11]

In [20]: c=foo()

In [22]: type(c)
Out[22]: list

In [23]: def bar():
   ....:     return ‘abc‘,[23.3,23],‘Guido‘

In [25]: bar()
Out[25]: (‘abc‘, [23.3, 23], ‘Guido‘)

In [26]: d=bar()

In [27]: type(d)
Out[27]: tuple

foo()函数返回一个列表,bar()函数返回一个元组。由于元组语法上不需要一定带上括号,所以让人真的以为可以返回多个对象。为增加代码的可读性可以给元组加上括号。
下面是关于函数返回数目的总结:

返回对象的数目 Python实际返回的对象
0 None
1 object
>1 tuple

注意:一个函数可以有任意多个return语句,但是始终只会执行一个return语句,执行return语句,会返回到调用方的作用域,函数的作用域就被销毁了。

In [29]: lst
Out[29]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [30]: def append(lst,x):
   ....:     return
   ....:     lst.append(x)
   ....: 

In [31]: append(lst,3)

In [32]: lst
Out[32]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [32]: lst
Out[32]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [33]: def p(lst):
   ....:     for x in lst:
   ....:         if x == 2:
   ....:             return
   ....:     else:
   ....:         print(‘Hi‘) 

In [34]: p([1,2,3,4,5,5])

In [35]: p([1,3,4,5,5])
Hi

In [36]: def loop(lst1,lst2):
   ....:     for x in lst1:
   ....:         for y in lst2:
   ....:             print(x,y)
   ....:             if y==2:
   ....:                 return
   
In [41]: loop(range(10),range(8))
0 0
0 1
0 2

函数传参

位置参数

位置参数必须以在被调用函数中定义的准确顺序来传递。另外,没有任何默认参数的话,传入函数(调用)参数的准确数目必须和声明的数字一致。

In [1]: def less(x,y):
   ...:     return(x-y)

In [2]: less(2,3)
Out[2]: -1

In [3]: less(3,2)
Out[3]: 1

默认参数

如果在函数调用时没有为参数提供值则使用预先定义的默认值这就是默认参数。默认参数定义在函数声明时在标题行中给出,语法:参数名等号默认值。定义之后说明如果没有值传递给那个位置参数,那么这个参数将取默认值。
Python中用声明变默认参数语法是:所有的位置参数必须出现在任何一个默认参数之前。

In [4]: def net_conn(user,host,port=12345,passwd=‘qwert‘):
   ...:     pass

In [5]: net_conn(‘root‘,‘192.168.2.3‘)

In [7]: net_conn(‘root‘,‘192.168.2.3‘,3456)

In [8]: net_conn(‘root‘,‘192.168.2.3‘,‘adadad‘)

In [9]: net_conn(‘root‘,‘192.168.2.3‘,30000,‘adadada‘)

上面的例子进行传递参数有正确也有错误的,第一个进行传参是正确的,后面没有填写的port和passwd会使用默认参数。第二个也是正确的,这种会更改默认的12345端口。第三个是错误的,这种本身是想更改密码,由于参数是按顺序传递的则会把密码的参数传递给port中。第四个是正确的,这种会将端口和密码都会更改。同时在上面的例子中由于user和host是没有默认值的所以必须要填写参数。
上面的例子中我们发现有一个不足之处就是如果想更改的默认参数在后面则还要将这个参数之前的所有参数全部填写一遍,不管是位置参数还是默认参数。而若不想填写默认参数则可以在传参中使用关键字参数,具体关键字参数如何使用后面在会讲解。下面讲下如果默认参数是可变的参数现象。

In [18]: def append(x,lst=[]):
   ....:     lst.append(x)
   ....:     return lst

In [21]: append(1)
Out[21]: [1]

In [22]: append(2)
Out[22]: [1, 2]

In [24]: append(3,[3,4,5])
Out[24]: [3, 4, 5, 3]

In [27]: append(8)
Out[27]: [1, 2, 8]

上面例子中我们若将默认参数设置成列表,若在传参中使用了默认的参数则会发现原先传递的位置参数会一直保存,而我们不使用默认的参数则输出的才是我们想要的。具体原因是函数的作用域的问题,若不想出现上面的错误则函数可以做以下修改:

In [28]: def append1(x,lst=None):
   ....:     if lst is None:
   ....:         lst=[]
   ....:     lst.append(x)
   ....:     return(lst)

In [32]: append1(2)
Out[32]: [2]

In [33]: append1(3)
Out[33]: [3]

In [34]: append1(1,[2,3,4,5])
Out[34]: [2, 3, 4, 5, 1]

通常来说,当默认参数是可变的时候,需要特别注意作用域的问题。

关键字参数

关键字参数仅仅针对函数的调用。他是让调用者通过函数调用中的参数名字来区分参数。这样操作就允许参数缺失或者不按顺序,因为解释器能通过给出的关键字来匹配参数的值。

In [39]: def net_conn(user,host=‘192.168.0.23‘,port=45678,passwd=‘qwert‘):
   ....:     pass

In [40]: net_conn(‘root‘)

In [41]: net_conn(‘root‘,port=3456)

In [42]: net_conn(‘root‘,passwd=‘qazwsx‘,host=‘192.168.0.24‘)

通过关键字参数和默认参数的结合使用就可以很灵活的进行函数传参,同时要注意给位置参数传参必须按顺序传参,并且一一对应,而关键字参数则不需要按顺序。同时在定义是位置参数在最前面而默认参数在最后。

可变位置参数(元组)

当函数被调用的时候,所有的形参(必须的和默认的)都将值赋给了在函数声明中想对应的局部变量。剩下的非关键字参数按顺序插入到一个元组中便于访问。
可变位置参数是一个元组,其必须放在位置和默认参数之后,其语法如下:

def function_name([formal_args,] *vargs_tuple):
    ‘function_documentation_string‘
    function_body_suite

*号操作符之后的形参将做为元组传递给函数。元组保存了所有传递给函数的“额外”参数(匹配了所有位置和关键字参数后剩余的)。如果没有给出额外的参数,元组为空。

In [1]: def fn(arg1,arg2=‘a‘,*arg3):
   ...:     print(‘arg1= ‘,arg1)
   ...:     print(‘arg2= ‘,arg2)
   ...:     for x in arg3:
   ...:         print(‘arg3= ‘,x)
   
In [3]: fn(‘a‘)
arg1=  a
arg2=  a

In [4]: fn(‘a‘,‘b‘)
arg1=  a
arg2=  b

In [10]: fn(‘a‘,‘b‘,‘c‘,‘d‘,‘e‘)
arg1=  a
arg2=  b
arg3=  c
arg3=  d
arg3=  e

可变关键字参数

可变关键字参数使用**定义,在函数体内,可变关键字参数是一个字典。可变关键字参数的key都是字符串,并且符合标示符定义规范。

In [17]: def fn1(arg1,arg2=‘a‘,**arg3):
   ....:     print(arg1)
   ....:     print(arg2)
   ....:     print(arg3)
   
In [19]: fn1(1,2,arg3=4,arg4=5)
1
2
{‘arg3‘: 4, ‘arg4‘: 5}

In [20]: fn1(1,arg3=4,arg4=5)
1
a
{‘arg3‘: 4, ‘arg4‘: 5}
  • 可变位置参数只能以位置参数的形式调用

  • 可变关键字参数只能以关键字参数的形式调用

同时两者是可以结合在一块使用的,下面就两者结合使用举例:

In [27]: def fn2(x,y=4,*arg1,**arg2):
                    print(x)
                    print(y)
                    print(arg1)
                    print(arg2)

In [28]: fn2(1,2,3,4,5,a=1,b=3)
1
2
(3, 4, 5)
{‘b‘: 3, ‘a‘: 1}

In [29]: fn2(1,a=1,b=3)
1
4
()
{‘b‘: 3, ‘a‘: 1}

在函数定义时可以有位置参数、默认参数、可变位置参数和可变关键字参数,这四个可以混合使用,但是为了避免出错和提高代码的可读性一般会遵循以下要求:

  • 可变参数后置

  • 可变参数不和默认参数一起出现

  • 可变位置参数必须在可变关键字参数之前

如果我们必须要将可变参数和默认参数一起出现使用则可以使用下面的方法:

def fn(**kwargs):
    a = kwargs.get(‘a‘, 1)

参数解构

在函数的调用时除了可以用关键字参数还可以使用参数的解构,而参数解构发生在函数调用时。传参的顺序:位置参数,线性结构解构,关键字参数,字典解构。解构的时候, 线性结构的结构是位置参数,字典解构是关键字参数。

*可以把线性结构解包成位置参数

In [38]: def fn4(a,b,c):
   ....:     print(a,b,c)
   
In [39]: lst1
Out[39]: [1, 2, 3]

In [40]: fn4(*lst1)
1 2 3

上面的例子中如果参数传入超过定义参数的个数是会报TypeError的错误的,我们可以让其结合可变位置参数来使用:

In [43]: def fn5(a,b,c,*arg):
   ....:     print(a,b,c)

In [44]: lst2=[1,2,3,4,5,6]

In [45]: fn5(*lst2)
1 2 3

**可以把字典解构成关键字参数

In [49]: def fn(a, b, c, *args, **kwargs):
   ....:     print(a,b,c) 

In [50]: d
Out[50]: {‘a‘: 1, ‘b‘: 2, ‘c‘: 3, ‘d‘: 4}

In [51]: fn(**d)
1 2 3

尽量的少的同时使用两种解构,除非你真的知道在做什么。

参数槽

*号之后的参数,必须以关键字参数的形式传递,称之为参数槽。参数槽使用应注意以下问题:

  • 参数槽通常和默认参数搭配使用

  • *之后也就是命名参数必须要有,可以有默认值

  • 非命名参数有默认值时,命名参数可以没有默认值

  • 默认参数应该在每段参数的最后

  • 使用参数槽时, 不能使用可变位置参数,可变关键字参数,必须放在命名参数之后

In [4]: def fn2(a,b,*,c,d):
   ...:     print(a,b,c,d)
   
In [6]: fn2(1,b=2,c=23,d=3)
1 2 23 3

在参数槽中*号前面称为非命名参数,而*号后面称为命名参数。如果没有定义命名参数则会报SyntaxError的错误。

In [9]: def fn4(a,b=1,*,c,d=6):
   ...:     print(a,b,c,d)
   
In [12]: fn4(2,b=2,c=2)
2 2 2 6

In [13]: fn4(2,2,c=2)
2 2 2 6

In [14]: def fn5(a,b=2,*,c,d=4,e):
   ....:     print(a,b,c,d,e)

In [17]: fn5(2,2,c=4,d=4,e=3)
2 2 4 4 3

在非命名参数中若定义的有默认值,则可以在命名参数中既可以定义默认值也可以不用定义默认值,其中命名参数的顺序没有要求。为了代码的可读性还是把带有默认值的放在最后。

In [23]: def fn6(a,b=3,*,c,d=3,**args):
   ....:     print(a,b,c,d,args)

In [24]: fn6(2,c=3,d=4,f=4,g=6)
2 3 3 4 {‘g‘: 6, ‘f‘: 4}

使用参数槽时, 不能使用可变位置参数,但是可以使用可变关键字参数,其必须放在命名参数之后。

类型示意

类型示意是Python3.5出现的,它并没有做类型检查,仅仅只是一个示意而已。使用类型示意有下面几个好处:

  • 更清晰的自文档

  • 帮助IDE做检查

  • 可以通过这种机制,做类型检查

In [1]: def add(x:int , y:int) -> int:
   ...:     return(x+y)
   
In [2]: add(2,3)
Out[2]: 5

In [4]: add(2.3,3)
Out[4]: 5.3

In [7]: help(add)
Help on function add in module __main__:
add(x:int, y:int)

递归

如果函数包含了对其自身的调用,该函数就是递归。递归广泛应用于语言识别和使用递归函数的数学应用中。例如:斐波那契数列和求阶乘等。下面就上面两种使用举例:
斐波那契数列:

In [12]: def fib(n):
                if n==0:
                    return 1
                if n==1:
                    return 1
                return fib(n-2)+fib(n-1)

In [18]: fib(10)
Out[18]: 89

阶乘:

In [19]: def fn(n):
   ....:     if n==0:
   ....:         return 1
   ....:     if n==1:
   ....:         return 1
   ....:     return n*fn(n-1)

In [20]: fn(10)
Out[20]: 3628800

由于递归总是涉及到压栈和出栈,所以递归函数在Python中非常慢,并且有深度限制,所以尽量避免使用递归。在Python中递归的限制可以通过 sys.getrecursionlimit() 得到深度限制,通过sys.setrecursionlimit调整递归深度限制。
注意:循环都可以转化为递归,但不是所有递归都可以转化为循环。

生成器函数

生成器函数是一个带yield语句的函数。一个函数或者子程序只能返回一次,但一个生成器能暂停执行并返回一个中间的结果,这就是yield的功能,返回一个值给调用者并暂停执行。当生成器被next()方法调用的时候,它会准确的从离开地方继续。
请注意yield和return的区别:

  • yield 弹出值,暂停函数

  • return返回值, 并且结束函数

  • 当yield和return同时存在时, return的返回值会被忽略,但是return依然可以中断生成器

In [50]: def gen(x):
   ....:     for i in range(10):
   ....:         if i ==3:
   ....:             return i
   ....:         yield i
   
In [51]: g = gen(10)

In [52]: for i in g:
   ....:     print(i) 
0
1
2

注意:可以使用生成器来替换递归,同时所有的递归,都可以用生成器替换。

In [54]: def fn1():
                ret =1
                index=1
                while True:
                    yield ret
                    index += 1
                    ret *= index

In [55]: g1=fn1()

In [56]: next(g1)
Out[56]: 1

In [57]: next(g1)
Out[57]: 2

In [58]: next(g1)
Out[58]: 6

In [59]: next(g1)
Out[59]: 24

In [60]: next(g1)
Out[60]: 120
def g(n):
    def factorial():
        ret = 1
        idx = 1
        while True:
            yield ret
            idx += 1
            ret *= idx
    gen = factorial()
    for _ in range(n-1):
        next(gen)
    return next(gen)

In [63]: fn2(5)
Out[63]: 120

In [64]: fn2(6)
Out[64]: 720

生成器函数的列表传参
在Python2 中使用:

In [1]: lst = [1,2,3,4,5,7,9]

In [2]: def gen(lst):
   ...:     for x in lst:
   ...:         yield x        

In [3]: g=gen(lst)

In [4]: for i in g:
   ...:     print(i) 
1
2
3
4
5
7
9

在Python3 中使用:

In [5]: def gen1(lst):
   ...:     yield from lst

In [6]: g1=gen1(lst)

In [7]: for j in g1:
   ...:     print(j)
1
2
3
4
5
7
9

wk_04