测试开发之Python核心笔记(10):Python函数

10.1 定义函数

  • 使用def关键字

  • 函数可以有返回值也可以没有返回值

# content of test_sample.py
def inc(x):  # 有返回值return x + 1def test_answer():  # 没有返回值assert inc(3) == 5

10.2 调用函数

  • add_one=inc(3)
  • 函数被调用时,这个函数此前必须定义过

10.3 函数的参数

10.3.1 位置参数

def divmod(x, y): # known case of builtins.divmod""" Return the tuple (x//y, x%y).  Invariant: div*y + mod == x. """return (0, 0)

这是一个Python内置的获得商和余数的函数。这个函数有两个参数:x和y,这两个参数都是位置参数,调用这个函数时,要按照顺序传入的两个实参值,实参值会依次赋给参数x和y。

10.3.2 默认值参数

def open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True): pass

这是Python内置的打开文件的函数,调用open打开文件时,可以只传递file文件路径,其他的参数可以不传递实参值,因为其他的参数都具有默认值。mode是打开文件的模式,默认值是r,表示以读的方式打开文件,默认值参数必须指向不可变对象。

  • 不要这样:
def add_end(li=[]):pass
  • 要这样:
def add_end(li=None):pass

10.3.3 变长参数

可以给函数一次性传递多个参数,或者传递一个列表或元组。定义函数时在参数名前加一个星号*。

def calc(*numbers):sum = 0for n in numbers:sum = sum + n * nreturn sum

函数calc的功能是求任意多个参数的平方和。函数定义时,在参数名加一个星号*。这样的函数,在调用时,可以传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。实际例子如下:

calc(1, 2,3,4)
calc(1, 2)
calc()
nums = [1, 2, 3]
calc(*nums)  # 还可以传递列表或者元组等进去

max是python内置的求最大值的函数。它的定义如下:

def max(*args, key=None): # known special case of max"""max(iterable, *[, default=obj, key=func]) -> valuemax(arg1, arg2, *args, *[, key=func]) -> valueWith a single iterable argument, return its biggest item. Thedefault keyword-only argument specifies an object to return ifthe provided iterable is empty.With two or more arguments, return the largest argument."""pass

第一个参数是一个变长参数,上面举了两个例子,第一个参数可以是可迭代对象,也可以是多个参数。返回值是多个参数中的最大值,或者可迭代对象中的最大值。比如:

 print(max(1, 2, 3, 4, -9, -5))  # 多个参数print(max([1, 2, 3, 4, -9, -5])) # 可迭代对象,这里是一个列表

max函数还有一个默认值参数key,接受一个函数名,这时max函数返回的值是,将args参数分别用到key函数后的结果中的最大值。比如,找出一个数字列表中绝对值最大的元素:

max([1, 2, 3, 4, -9, -5], key=abs)

10.3.4 关键字参数

下面这个sorted函数是Python内置的排序函数,它的第一参数是一个变长参数,第二个参数是个关键字参数,关键字参数是以两个星号开头。

def sorted(*args, **kwargs): # real signature unknown"""Return a new list containing all items from the iterable in ascending order.A custom key function can be supplied to customize the sort order, and thereverse flag can be set to request the result in descending order."""pass

关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个字典。

sorted函数默认是升序排序的,可以传入一个关键字参数reverse进行降序排序。方式是:

print(sorted([1, 2, 3, 4, -9, -5], reverse=True))

再来自定义一个:

def person(name, age, **kwargs):print(name,age,kwargs)

关键字参数可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。传递关键字参数有两种方式:

  • 传入字典

    extra = {'city': 'Beijing', 'job': 'Engineer'}
    person('Jack', 24, **extra)   # 字典参数名签名放两个**
    
  • 直接传入key=value

    person('Jack', 24, salary=10000, department='finace') 
    

10.3.5 参数组合

定义函数时,参数顺序必须是:位置参数、默认参数、变长参数、关键字参数。定义顺序不能乱。

def f1(a, b, c=0, *args, **kw):  pass 

10.4 看懂Python源码中函数的定义

有些朋友平时反映,看不懂官方文档中介绍函数的说明,比如查看min的源码:

def min(*args, key=None): # known special case of min"""min(iterable, *[, default=obj, key=func]) -> valuemin(arg1, arg2, *args, *[, key=func]) -> valueWith a single iterable argument, return its smallest item. Thedefault keyword-only argument specifies an object to return ifthe provided iterable is empty.With two or more arguments, return the smallest argument."""pass

min 函数的源码文档中,举了两个例子,其中的形参有 * 符号,又有 []?他们都表示啥意思呢?

函数形参列表中符号 * 表示,后面的形参只能为关键字参数,不能为位置参数,[]里面的参数是可选的,不是必填的。

也就是说,min函数要这么用,例如传递一个key参数和default参数时,必须写明参数名::

>>> a = [1,2,3,4,2,2,3,4] 
>>> min(a,key=lambda x: a.count(x), default=1)  # 求出个数最少的元素
1

因为default参数是可选的,因此还可以不传default参数:

>>> list=[1,2,5,9,4,6,3]
>>> min(list,key=lambda x:-x)  # 取反后最小的值
9

再来看一个min函数的用法,求一段文本中出现次数最少的字母。

import stringdef maxcharactor(content):content = content.lower()return min(string.ascii_lowercase, key=content.count)if __name__ == '__main__':print(maxcharactor("/Users/chunming.liu/learn/learn_python_by_coding/learn_string/"))

平时大家更多看到的是这么使用 min 函数,没有key参数:

min([1,2,3,4,2,2,3])

这是因为在min函数说明中,key参数是放在[] 中的,说明它是可选的。

自定义一个函数 func,参数 b 位于 * 后面,只能为关键字参数:

def func(a,*,b):passfunc(a,b=1)f(a,1) # 这种调用是错误的
TypeError: f() takes 1 positional argument but 2 were given

再看一个内置函数 sum:

sum(iterable, start=0, /)Return the sum of a 'start' value (default: 0) plus an iterable of numbersWhen the iterable is empty, return the start value.This function is intended specifically for use with numeric values and mayreject non-numeric types.

看到形参列表中有一个 /,它表示 / 前的参数只能是位置参数,不能是关键字参数。

因此,以下调用是合法的:

a = [1,3,2,1,4,2]sum(a,2) # start=2 表示求和的初始值为 2
15

以下调用是非法的,iterable 参数不能被赋值为关键字实参:

sum(iterable=a,start=2)TypeError: sum() takes no keyword arguments

以后再查看Python中函数用法时,应该可以看懂了吧。

10.5 偏函数functools.partial

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。

先来看下Python官方的描述:

# content of _functools.py
class partial(object):"""partial(func, *args, **keywords) - new function with partial applicationof the given arguments and keywords."""pass

翻译过来就是,functools.partial可以给一个函数设置位置参数和关键字参数的默认值,并且返回一个新的可调用对象。之后调用这个新的可调用对象时,就自动带上了functools.partial设置的默认参数值,从而可以传递更少的参数。举个例子:

from functools import partialdef add(a, b, *arg, **kwargs):return a + b, arg, kwargsadd10 = partial(add, 10, kw1=1)   # 设置add函数的位置参数a的默认值为10,关键词参数kwargs中的参数kw1的默认值是1,并返回新的可调用对象add10
print(add10(11, 12))  # 调用新的函数对象add10,只需要在传递add函数的变量b的值就可以了

用Pycharm调试上面的代码,在add函数的return语句处添加断点,可以看到add10这个对象中args属性里面有一个10,kwwargs里面有一个"kw"=1,这就是通过partial偏函数设置进去的。

当调用add10时,只需要提供变量b=11和arg的值12,add10就能够将其与偏函数设置的值一起传递给原函数add了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FaBMDW6O-1596962204726)(pics/partial.png)]

10.6 函数参数传递的机制

这一节有点难。看不懂没关系。继续往后学,回头再来看。

10.6.1 值传递与引用传递

  • 值传递

    • 拷贝参数的值,然后传递给函数里的新变量。这样,原变量和新变量之间互相独立,互不影响。
  • 引用传递

    • 把参数的引用传给新的变量,这样,原变量和新变量就会指向同一块内存地址。如果改变了其中任何一个变量的值,那么另外一个变量也会相应地随之改变。

10.6.2 Python 参数传递是赋值传递

这里首先引用 Python 官方文档中的一段说明:

“Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per Se.”

准确地说,Python 的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。不要与引用传递弄混了,引用传递的是一个地址。

  • 不可变对象作为参数

    def my_func1(b):b = 2  #③函数执行时,变量b指向对象2a = 1  # ①实参变量a指向对象1
    my_func1(a)  #②因为是赋值传递,变量 b 也指向了变量a指向的对象 1 
    a  # ④函数执行完成后,变量a依然指向对象1
    1
    

    代码中注释里面的序号是程序的执行顺序。函数被调用时(②),因为是赋值传递,所以函数形参变量b指向了 实参变量 a 所指向的对象1。但当代码执行到 b = 2 时(③),系统会重新创建一个值为 2 的新对象,并让 b 指向它;而 a 仍然指向 1 这个对象。所以,a 的值不变,仍然为 1。

    稍作改变,让函数返回新变量,赋给 a,再来看看。

    def my_func2(b):b = 2  #③函数执行时,变量b指向对象2return b  # ④关键,函数返回一个指向对象2的变量a = 1 # ①实参变量a指向对象1
    a = my_func2(a)  # ②将my_func返回值(指向对象2)赋值给了a,所以a 也指向了对象2
    a
    2
    
  • 可变对象作为参数

    当可变对象当作参数传入函数里的时候,改变可变对象的值,就会影响所有指向它的变量。

    def my_func3(l2):l2.append(4)  # ③在原对象[1,2,3]上修改,对象变为 [1,2,3,4]l1 = [1, 2, 3] # ① l1是个可变对象,指向了对象[1,2,3]
    my_func3(l1) #②因为是赋值传递,变量 l2 也指向了变量l1指向的对象 [1,2,3]
    l1  # ④函数执行完成后,l1和l2指向的对象发生了改变
    [1, 2, 3, 4]  
    

    如果多次调用my_func3,那么每次l1得到的结果都不一样,l1不断在列表后面添加元素4。

    def my_func3(l2):l2.append(4)  # ③在原对象[1,2,3]上修改,对象变为 [1,2,3,4]
    if __name__ == '__main__':l1 = [1, 2, 3] # ① l1是个可变对象,指向了对象[1,2,3]my_func3(l1) #②因为是赋值传递,变量 l2 也指向了变量l1指向的对象 [1,2,3,4]print(l1)my_func3(l1)  # ②因为是赋值传递,变量 l2 也指向了变量l1指向的对象 [1,2,3,4,4]print(l1)my_func3(l1)  # ②因为是赋值传递,变量 l2 也指向了变量l1指向的对象 [1,2,3,4,4,4]print(l1)
    

    不改变原来可变对象的值,而是生成新对象,结果与上面不同。看下下面的:

    def my_func4(l2):l2 = l2 + [4]  # 创建了一个“末尾加入元素 4“的新对象,原对象没变l1 = [1, 2, 3]
    my_func4(l1)
    l1
    [1, 2, 3]  # l1对象没变
    

    实际工程中,通常函数内部重新创建一个新的列表(可变对象),对这个新列表(可变对象)进行操作,而不是操作输入的列表,并增加语句return 语句。再来看一个类似的例子:

    def multiply_2_pure(l):new_list = []  # 创建一个新列表for item in l:new_list.append(item * 2)return new_list  # 返回新列表
    

10.7 函数变量作用域

10.7.1 局部变量

定义在函数内部,它的作用域是在函数内部,函数调用结束之后,函数里面保存的信息就被销毁了。比如下面的这个代码示例,连续调用函数4次,每次输出的值都是4,即3+1,这说明每次调用fun函数之后,函数内部定义的局部变量num就被销毁了,再次被调用时将被重新初始化为1,那如果要保存函数的局部变量,怎么办呢?后面介绍的“闭包”就可以完成这个任务。

def fun(step):num = 1num += stepprint(num)i = 1
while i < 5:fun(3)i += 1

10.7.2 全局变量

定义在整个文件层次上,如下MIN_VALUE和MAX_VALUE这两个变量就是全局变量,通常大写。

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

不能在函数内部随意改变全局变量的值,一定要改变的话,要在全局变量前面加个global。global的作用就是在“函数局部作用域”内声明表示一个全局变量,从而可以在函数内部修改全局变量的值(否则只能访问不能修改)。

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):global MIN_VALUE  # 表示函数内部的这个MIN_VALUE是来自全局变量MIN_VALUEMIN_VALUE += 1print(MIN_VALUE) 
validation_check(5)  # 输出2

10.7.3 局部变量覆盖全局变量

如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量。

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):MIN_VALUE = 3print(MIN_VALUE) # 输出3
validation_check(5)

10.8 函数的嵌套

函数可以嵌套定义。

  • 形式1,在函数内部直接调用定义的内部函数
def outer(name):num = 100def inner(weight, height, age):weight += 1height += 1age += 1print(name, weight, height, age)inner(100, 200, 300)  # 直接调用“内部函数”outer('盒子')
  • 形式2,在函数内部return定义的内部函数
def outer(name):num = 100def inner(weight, height, age):weight += 1height += 1age += 1print(name, weight, height, age)return inner  # 返回函数对象alias = outer('盒子')  # 调用外部函数,得到inner对象
alias(100, 200, 300)  # 调用inner函数对象,传入三个参数
  • 闭包,外部函数执行完后,外部函数的参数仍然会被内部函数记住。
def nth_power(exponent):  # 外部函数def exponent_of(base):return base ** exponent  # 内部函数使用了外部函数的参数return exponent_of  # 外部函数返回内部函数exponent_ofsquare = nth_power(2)  # exponent_of赋值给变量square,square记住了外部函数的exponent参数值2
cube = nth_power(3)  # exponent_of赋值给变量cube,记住了外部函数的exponent参数值3
print(square(9))  # 计算9的平方
print(cube(9))  # 计算9的立方

nth_power(2)被调用时,exponent被赋值为2,并且保存到exponent_of函数对象中。当square(9)被调用时,其实就是调用exponent_of函数,这个函数内部base是9,exponent是2,因此square(9)的返回值是81。

通过Pycharm的Debug功能来看一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUyC8oSi-1596962204729)(./pics/闭包.png)]

从这里可以看出,代码执行到print(square(9))这一行时,nth_power(2)已经执行结束了,但是它的局部变量exponent的信息却被保存了下来,从调试窗口可以看到它的值是2,从而当进入内部函数exponent_of时,就能够计算9**2了,这就是“闭包”的最大的作用——保存局部信息不被销毁。

函数特点总结:

  • 函数也是对象
  • 函数对象可以赋值给变量
  • 函数对象可以作为参数传递给另外的函数
  • 函数对象可以作为另外一个函数的返回值
  • 函数可以嵌套定义


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部