Python函数学习笔记

1.函数

1.1调用函数

当代码存在大量重复的时候,就需要调用函数。

1.2定义函数

1.2.1语法

def 函数名(参数列表):
    函数体

函数返回值用 return
例:

def my_abs(x):
    if(x >=0):
        return x
    else:
        return -x
print(my_abs(99))

1.2.2交互环境
在python交互环境中定义函数,会出现...,按回车两次重新回到>>>提示符下:

>>> def my_abs(x):
...     if x >= 0:
...             return x
...     else:
...             return -x
...
>>>

1.2.3文件保存函数

使用from 文件名(不包含.py扩展名) import 函数名语法调用
例:

>>> from abstest import my_abs
>>> my_abs(-9)
9

1.2.4空函数

  • 语法:
def 函数名():
      pass

pass 用来作为占位符,还没想好函数的代码,可以先放一个pass,让代码运行起来。
1.2.5参数检查

  • 调用函数,参数个数不对,Python解释器会自动检查出来,并抛出TypeError:
>>> my_abs(1,2)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: my_abs() takes 1 positional argument but 2 were given

  • 参数类型不对,Python解释器无法检测。试试my_abs和内置函数abs的差别:
>>> my_abs("A")
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in my_abs
TypeError: '>=' not supported between instances of 'str' and 'int'
>>> abs("A")
Traceback (most recent call last):
  File "", line 1, in 
TypeError: bad operand type for abs(): 'str'
>>>

my_abs没有参数检查,会导致if语句出错,所以要对``my_abs```做参数类型的检查,只允许整数和浮点数类型的参数。

  • 数据类型检查 isinstance()
def my_abs(x):
    if not isinstance(x, (int, float)):
        raise TypeError('bad operand type')
    if(x >=0):
        return x
    else:
        return -x
my_abs("A")

添加参数检查之后,传入错误的参数类型,函数就可以抛出错误:

>>> my_abs("A")
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 3, in my_abs
TypeError: bad operand type

1.2.6返回多个值

  • 多个返回值用逗号隔开
    例:
import math
def move(x, y, step, angle=0):
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny
  • 获取返回值
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0

但其实这只是一种假象,Python函数返回的仍然是单一值:

>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

pathon函数返回的值是一个tuple类型,在语法上tuple可以省略()
结:
①定义函数时,需要知道函数名和参数的个数;
②如果有必要,可以先对参数的数据类型做检查;
③可以使用return返回函数结果;
④函数执行完毕也没有return语句时,自动return None;
⑤函数可以同时返回多个值,但其实就是一个tuple。
练习

image.png

# -*- coding: utf-8 -*-
import math
def quadratic(a,b,c):
    s = math.sqrt(b**2-4*a*c)
    x = (-b + s)/(2*a)
    y = (-b - s)/(2*a)
    if not isinstance(a, (int,float)):
        raise TypeError("bbd operand type")
    else:
        return x,y

print('quadratic(2, 3, 1) =', quadratic(2, 3, 1)) #(-0.5, -1.0)
print('quadratic(1, 3, -4) =', quadratic(1, 3, -4)) #(1.0, -4.0)

1.3函数的参数

1.3.1位置参数

  • 调用的函数时,传入的值会按照位置顺序依次赋值给每一个参数。
    例:求n次方的一个函数
def power(x,n):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s
print(power(2,3))

1.3.2默认参数

  • 如果调用参数时,少传了参数就会报错,告诉你缺少了位置参数。这时候就需要用到默认参数。
    n给一个默认参数
def power(x,n = 2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s
print(power(2))

设置了默认参数,如果没传就是用默认参数,如果传递了就是用传递的参数。

注:
①必选参数在前,默认参数在后,否则会报错;
思考:为什么默认参数不能放在必选参数前面?
因为调用的函数时,传入的值会按照位置顺序依次赋值给每一个参数。
②当函数有多个参数时,变化小的参数可以放在后面,作为默认参数。

  • 使用默认参数的好处?
    最大的好处是能降低调用函数的难度;
    大大简化了函数的调用,调用时不用写那么多了参数了

  • 不按顺序提供部分默认参数,传递参数时,把参数名写上。例如:

def enroll(name,gender,age=6,city="Chengdu"):
    print("name:",name)
    print("gender:",gender)
    print("age:",age)
    print("city:",city)
enroll("xiaoming","F",city = "Shanghai")
  • 默认参数的坑:使用可变对象作为默认参数
def add_end(L=[]):
    L.append('END')
    return L

正常调用时,结果没什么问题

>>> add_end([1,2,3])
[1, 2, 3, 'END']
>>> add_end([4,5,6])
[4, 5, 6, 'END']
>>>

使用默认参数调用时,第一次调用是对的,第二次开始,结果就不对了

>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>>

注:默认参数必须指向不变对象

  • 使用不可变对象None来实现,就不会有问题了
def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L
  • 为什么要设计strNone这样的不变对象呢?
    因为对象创建了,对象内部数据就不能修改,这样就减少了修改数据导致的错误。
    由于对象不变,多个地方同时读取对象时不需要加锁。
    再写程序时,可以设计一个人不变对象,尽量设计一个不变对象。

1.3.3可变参数
可变参数就是传入的参数个数是可变的,可以是任意个,也可以是0个。

  • 要定义函数,首先要确定输入的参数。
    由于参数个数不确定,首先想到的是把值作为一个list或tuple传进来。
    但是调用的时候,需要先组装一个list或tuple,所以需要可变参数来简化,在参数前面加一个*号。
def calc(*numbers):
    sum = 0
    for n in numbers:
        sum += n*n
    return sum
print(calc(1,2,3))
  • 怎么传递已经存在的一个list或tuple呢?
    调用时在变量前加*号,把list或tuple的元素变成可变参数传进去。
nums = [1,2,3]
print(calc(*nums))

1.3.3关键字参数
关键字参数就是允许传入0个或任意个含参数名的参数。

  • 函数调用时,自动组装成一个dict。在参数前面加**
def person(name,age,**kw):
    print("name:",name,"age:",age,"other",kw)
person("xiaoming",12,city="Chengdu",job="engineer")
#result:
#name: xiaoming age: 12 other {'city': 'Chengdu', 'job': 'engineer'}
  • 关键字函数有什么用?
    可以用来扩展函数的功能。比如填写一些资料,有些是必填项,有些是选填项,如果用户愿意填写选填项的话,就能收集到更多的信息了。

  • 和可变参数相似,可以先组装一个dict,然后把dict转换成关键字参数传进去,简写在组装的变量前加**

extra = {"city":"Chengdu","job":"engineer"}
person("xiaoming",12,city=extra["city"],job=extra["job"])
person("xiaoming",12,**extra) #是上面那句的简写
#result:
#name: xiaoming age: 12 other {'city': 'Chengdu', 'job': 'engineer'}

注:参数接受到变量的值是一份拷贝,函数内部对参数进行改变,外部的变量不会受影响。
1.3.4命名关键字参数
命名关键字参数的作用就是限制关键字参数的名字,规定只接收哪些名字可以作为关键字参数。

  • 定义函数时,使用特殊分隔符*号,*号后面的参数被视为命名关键字参数。
def person(name,age,*,city,job):
    print(name,age,city,job)
person("xiaoming",12,city="Chengdu",job="engineer")
#result:
#xiaoming 12 Chengdu engineer
  • 如果传入了其他的关键字参数,就会报错
person("xiaoming",12,city="Chengdu",job="engineer",hobby="sing a song")
#error:
#TypeError: person() got an unexpected keyword argument 'hobby'
  • 如果定义了可变参数,可以不用加**,直接在后面跟着写命名关键字即可
def person(name,age,*args,city,job):
    print(name,age,args,city,job)
person("xiaoming",12,2,3,4,city="Chengdu",job="engineer")
#result:
#xiaoming 12 (2, 3, 4) Chengdu engineer
  • 使用命名关键字参数,调用时必须传入参数名,否则会报错。
def person(name,age,**kw):
    print("name:",name,"age:",age,"other:",kw)
person("xiaoming",12,"Chengdu","engineer")
#error:
#TypeError: person() takes 2 positional arguments but 4 were given

如果缺少参数名,Python会把参数作为位置参数,传递的参数个数不对,所以就报错了。

  • 可以有缺省值(默认值),用来简化调用,调用时可以不用传入参数。
def person(name,age,*,city="Chengdu",job):
    print(name,age,city,job)
person("xiaoming",12,job="engineer")
#result:
#xiaoming 12 Chengdu engineer

没有可变参数,必须加上*号分隔符,不然会被当作是位置参数。
1.3.5参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

  • 定义若干种参数
def f1(a,b,c = 0,*args,d,**kw):
    print('a =',a,'b =',b,'c =',c,'args =',args,"d =",d,'kw =',kw)
f1(1,2,d = 4) #命名关键字参数是必须写上的
f1(1,2,3,"arg1","arg2",d = 4,kw1="kw1",kw2="kw2")
#result:
#a = 1 b = 2 c = 0 args = () d = 4 kw = {}
#a = 1 b = 2 c = 3 args = ('arg1', 'arg2') d = 4 kw = {'kw1': 'kw1', 'kw2': 'kw2'}
  • 通过tuple和dict也可以调用上述函数
args = (1,2,3,"arg1")
kw = {"d":4,"kw1":"kw1"}
f1(*args,**kw)
#result
#a = 1 b = 2 c = 3 args = ('arg1',) d = 4 kw = {'kw1': 'kw1'}

****注意:虽然可以组合5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。***
练习
以下函数允许计算两个数的乘积,请稍加改造,变成可接收一个或多个数并计算乘积:

# -*- coding: utf-8 -*-
def product(x, y):
    return x * y
# 测试
print('product(5) =', product(5))
print('product(5, 6) =', product(5, 6))
print('product(5, 6, 7) =', product(5, 6, 7))
print('product(5, 6, 7, 9) =', product(5, 6, 7, 9))
if product(5) != 5:
    print('测试失败!')
elif product(5, 6) != 30:
    print('测试失败!')
elif product(5, 6, 7) != 210:
    print('测试失败!')
elif product(5, 6, 7, 9) != 1890:
    print('测试失败!')
else:
    try:
        product()
        print('测试失败!')
    except TypeError:
        print('测试成功!')

解答:

def product(x,*args):
    for arg in args:
        x *= arg
    return x
#product() 会报错

结:

  • 默认参数一定要用不可变参数,否则程序执行时会有逻辑错误!
  • 注意定义可变参数和关键字参数的语法:
    *args(可变参数),args接受的是一个tuple;
    **kw(关键字参数),kw接受的是一个dict。
  • 调用函数时如何传入可变参数和关键字的语法:
    可变参数
    ①直接传入:func(1,2,3)
    ②先组装list或tuple,再通过*args传入:func(*(1,2,3))
    关键字参数
    ①直接传入:func(a=1,b=2)
    ②先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})
  • *args**kw是Python的习惯写法,也可以用其他的参数名,但最好使用习惯写法。
  • 命名的关键字参数是为了限制传入的参数名,同时可以提供默认值。
  • 定义命名的关键字参数在没有可变参数的时候,需要写上分隔符*,否则否则定义的将是位置参数。

1.4递归函数

在函数内部调用函数自身的函数就是递归函数。
例如求1-n的阶乘:

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

所有的递归都可以写成循环的方式,但是循环的逻辑不如递归清晰。
防止栈溢出
在计算机中,调用函数是通过栈(stack)这种数据结构,每当进入一个函数调用,栈就会加一层栈帧,返回就会减少一层栈帧。由于栈大小有限,所以递归的次数过多,会导致栈溢出。
上面的例子传入1000的话,就会出现溢出。

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

解决递归调用栈溢出,通过尾递归优化,尾递归和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。

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

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

fact(n)改成尾递归,,只调用了递归函数本身,num -1和num*product在函数调用前就会被计算,不影响函数调用。但是即使把fact(n)函数改成尾递归方式,也会导致栈溢出。

结:python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。
练习
汉诺塔的移动可以用递归函数非常简单地实现。

请编写move(n, a, b, c)函数,它接收参数n,表示3个柱子A、B、C中第1个柱子A的盘子数量,然后打印出把所有盘子从A借助B移动到C的方法,例如:

# -*- coding:utf-8 -*-
def move(n,a,b,c):
      if n == 1:
          print(a,"--->",c)

解答:

def move(n,a,b,c):
    if n == 1:
        print(a, '-->', c)
    else:
        move(n-1,a,c,b)
        move(1,a,b,c)
        move(n-1,b,a,c)
move(3,'A','B','C')

解题参考:https://www.youtube.com/watch?v=1QgJEcnsqsQ

你可能感兴趣的:(Python函数学习笔记)