本文介绍Python中的函数语法及常用方法。

Python中的函数

为什么要有函数?

之前写的叫脚本

就是从上到下执行的一坨代码。

把代码不加区分的写到一起,执行的时候就是从上到下依次执行。

这样会有什么问题呢?

是不是冗余?你看一眼你的代码好多都是重复的内容。

写着写着突然要修改某个地方,然后这段代码可能很多地方都用到了。怎么办?

一个一个找出来再一个一个去修改。

初识函数

函数就相当于工具。

使用函数一般分为两个阶段:

准备工具—>函数定义阶段

使用工具—>函数调用阶段。

内置函数

程序运行要解释器,所以内置函数是内置在解释器里面的,不用你去定义,他都提前定义好了,拿过来就用。

内置的函数不够用,怎么办?自定义函数!

函数的定义

为什么要定义呢?

要定义怎么函数要实现的功能,事先准备好这个函数,

不定义 怎么使用?

使用函数一定要:先定义后使用。

Python中函数定义的格式

函数的调用

函数名加()。

函数的返回值

为什么要有return?

有结果需要返回的

无参函数通常不用返回值

因为它的功能通常都是用来执行一些语句,不需要返回值

有参函数通常是做一些计算需要返回值。

**注意:**函数如果没有显示指定返回值,默认返回 None。

return的用法 :

函数执行的过程中只要执行一个return,函数就结束了。

所以多个返回值只能写在一起,不能通过多个return返回。

函数的参数介绍

Python是弱类型的语言

不会强制约束你传给函数的参数是什么类型。

Python3有新特性–类型注解

def my_min(a:int, b:int)->int:
    print(a if a <= b else b)

print(my_min.__annotations__)

形参和实参

形参和实参的概念

def foo(a):  # 在函数定义阶段,括号内定义的参数-->形式参数(形参)-->相当于变量名
    print(a)
    
foo(1)  # 在函数调用阶段,括号内定义的参数--> 实际参数(实参)-->相当于变量值

可以把形参看成是变量名,把实参看成是变量值。有参函数的调用过程就相当于在函数内部把实参的值赋值给了形参的那个变量。这个变量只在函数内部生效。

在这个例子里,就相当于在foo内部做了a = 1的赋值操作。

调用时生效,调用结束就失效。

a = 10000
def foo(a):
    print(a)
    
foo(1)
print(a)  # 10000

函数调用的时候直接传具体的数可以,那直接传变量可以么?

当然可以了,看下面的例子。

x = 100  # 声明一个变量x
foo(x)  # 把x传递给函数foo

传递的变量名与形参可以不相同。

def bar(x):
    x = 3
x = 1
bar(x)
print(x)  # 3函数内部的x和函数外部的x不是一个x。

但是上面只适用于Python中的不可变类型,对可变类型则会影响到外部变量的值:

# 首先我们稍微修改下我们的bar函数
def bar(x):
    x.append(4)

x = [1, 2, 3]
bar(x)
print(x)  # [1, 2, 3, 4]

总结一下:

Python中的不可变对象我们传参的时候是传的值的拷贝,在函数内部修改的是值的拷贝,不会影响函数外部。

对于可变对象我们传参的时候是传的值的引用,在函数内部修改的是值本身,会影响到函数外部。

位置参数和默认参数

站在实参的角度来说,有三种:

首先我们定义一个函数:

def foo(a, b):
    print(a)
    print(b)
  1. 按位置传值

    foo(1, 2)
    foo(2, 1)
    

  2. 按关键字传值

    foo(a=1, b=2)
    foo(b=2, a=1)
    

  3. 混着用

    foo(1, b=2)
    

    注意:

    1. 按位置传值必须写在按关键字传值的前面。

      foo(b=2, 1)  # 这么写是错误的
      
    2. 不能对一个参数重复传值。

      foo(1, a=1, b=2)  # 这么写也是错误的
      

站在形参的角度来分析:

def foo(a, b):
    print(a)
    print(b)
  1. 位置参数:必须传值的参数。

    foo(1, 2)  # 只要函数定义了,调用的时候就必须传值。
    foo(1)  # 少一个不行
    foo(1, 2, 3)  # 多一个也不行
    
  2. 默认参数

    def foo(a, b=1):  # b是默认参数,函数定义的时候提供一个默认值
        print(a)
        print(b)
    
    foo(1)  # 此时不传b是可以的,不传就默认使用函数定义时的那个默认值
    foo(1, 2)  # 传b也是可以的,传了就用传递进来的那个值
    foo(b=2, a=1)  # 按关键字参数传值也是可以的。
    

那什么时候用位置参数,什么时候用默认参数呢?

拿open()函数举例,把变动比较小的参数定义成默认参数。

默认参数必须注意的问题

  1. 默认参数必修写在位置参数的后面

    def foo(b=1, a):  # 这么写就报错了...
        print(a)
        print(b)
    

    补充:

    1. 函数定义过程中只检测语法是否有错误。

      def foo(b=1):  # 函数定义的阶段是不会报错的
          print(a)
          print(b)
      
          # 但是当你调用上面的函数的时候,就会报错
          foo()  # NameError: name 'a' is not defined
      

    2. 默认参数的值在函数定义阶段就确定了。下面的例子中:调用阶段在外面修改原来的x时不会影响到函数内部的默认参数值。

      x = "100"
      def foo(a=x):
          print(a)
      
      x = 1
      foo()  # 打印的是100
      
    3. 默认参数陷阱

      def foo(name, name_list=[]):
          name_list.append(name)
          print(name_list)
      
      foo("Alex")
      foo("Rain")
      

      输出:

      ["Alex"]
      ["Alex", "Rain"]
      

      也就是说,foo函数并不会如我们想的那样每次调用都执行name_list=[],而是在def定义的时候初始化一次,就如同每个函数都共享了一个name_list。

可变参数

*args

def foo(a, *arg):
    print(a)
    print(args)

从实参角度来说,按位置传值,多余的那些值会统一交给*来处理。*会把那些参数塞到args的元组里。

foo(1, 2, 3, 4, 5, 6, 7, 8)

输出:

1
(2, 3, 4, 5, 6, 7, 8)

注意:

位置参数、默认参数和*args同时出现时要注意的事项:

def foo(a, b=1, *args):
    print(a)
    print(b)
    print(args)

foo(1, 2, 3, 4, 5)  # a=1, b=2, c=(3, 4, 5),并没有达到预期的要求。
foo(1, b=2, 3, 4, 5)  # 这么报错

应该这么定义:

def foo(a, *args, b=1):
    print(a)
    print(b)
    print(args)
    
foo(1, 2, 3, 4, 5, y=100)

也就是说*args要放到位置参数后面。

*args就相当于很多个位置参数。 *args = *(1, 2, 3, 4, …)

def foo(a, b, c):
    print(a)
    print(b)
    print(c)

foo(*(1, 2, 3))  # 加个*相当于把(1, 2, 3)打散

输出:

1
2
3

**kwargs

def foo(a, **kwargs):
    print(a)
    print(kwargs)
    
foo(1, b=2, c=3)

输出:

1
{"b": 2, "c": 3}
foo(1, a=1,b=2,c=3)  # 报错,因为相当于给参数a传了俩值。

** 的作用是按关键字传值多余的那些值塞到kwargs的字典里。

注意事项和上面的*args类似,**kwargs应该放在最后。

def foo(a, *args, **kwargs):
    print(a)
    print(args)
    print(kwargs)

递归

什么是递归?

从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?「从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?『从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……』」

函数的嵌套调用

函数内部是可以调用其他函数的,这种调用就叫函数的嵌套调用。

而递归就是函数在其内部直接或间接调用自己。

Python中使用递归的注意事项

  1. 必须有明确的退出条件

  2. 每次进入更深一层递归时,问题规模比上次递归都应有所减少

  3. 递归效率不高,递归层数过多会导致栈溢出

    在计算机中,函数调用是通过栈(stack)这种数据结构实现的。每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,就会导致栈溢出。)

查看Python中递归限制:

>>> import sys
>>> sys.getrecursionlimit()
1000

常见应用:

二分法查找

data = [1, 3, 6, 7, 9, 12, 14, 16, 17, 18, 20, 21, 22, 23, 30, 32, 33, 35]
 
 
def binary_search(dataset,find_num):
    print(dataset)
 
    if len(dataset) >1:
        mid = int(len(dataset)/2)
        if dataset[mid] == find_num:  #find it
            print("找到数字",dataset[mid])
        elif dataset[mid] > find_num :# 找的数在mid左面
            print("\033[31;1m找的数在mid[%s]左面\033[0m" % dataset[mid])
            return binary_search(dataset[0:mid], find_num)
        else:# 找的数在mid右面
            print("\033[32;1m找的数在mid[%s]右面\033[0m" % dataset[mid])
            return binary_search(dataset[mid+1:],find_num)
    else:
        if dataset[0] == find_num:  #find it
            print("找到数字啦",dataset[0])
        else:
            print("没的分了,要找的数字[%s]不在列表里" % find_num)
 
 
binary_search(data,66)

扫码关注微信公众号