函数基础

函数

  • 定义

    • 函数是一段封装了特定功能的可重复使用的代码块。
    • 通过定义函数,你可以将代码模块化,使其更易于理解、调试和维护。
  • 优点

    • 为了解决代码的重复书写问题
    • 可以将功能的实现者和使用者分开,提高开发效率
    • 便于复用与维护、把大问题拆成小问题
  • 分类

    • 内置函数(built-in):随解释器(就是 python 命令启动的那个程序,它逐行读你的 .py 并当场执行)直接可用,不用 import,如 print、input、abs、len。
    • 库函数:住在标准库(装 Python 就自带的模块,如 math、os)或第三方库(要自己 pip install 的)里,要先 import 才能用,如 math.sqrt、os.getcwd、random.randint。
    • 自定义函数:自己封装的函数。
    • ⚠️注意:平时把 print、abs 叫「库函数」其实不准确:它们是内置函数,在 builtins 模块里,解释器启动时就加载好了。
  • 格式

    1
    2
    def 函数名(参数列表):  # 结尾必须是半角冒号 :
    ... # 占位函数体,写实际代码时替换掉
    • 参数可以没有,也可以有多个;文档里写成 [参数] 的方括号表示「可选」,是记号不是 Python 语法。
    • 中文输入法打出的全角冒号 : 是新手最常见的 SyntaxError 来源之一:实测 def f(): 直接报 invalid syntax。
    • ...(Ellipsis)是合法的占位语句,所以模板本身也能编译通过。
  • 函数名

    • 遵循标识符的命名规范(与变量名相同)
  • 函数调用

    • 函数定义以后不会执行,需要调用才执行。
    • 调用:函数名([参数])。
    • 定义要写在调用之前(同一个作用域内——作用域指一个名字在哪些位置能看见,函数里定义的名字在外面就取不到):Python 是自上而下执行的,先调用后定义会 NameError。
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 无参无返回值
    def print_hello():
    print('Hello')
    # print_hello()

    # 带参无返回值
    def print_n_hello(n):
    for i in range(n):
    print('Hello')
    # print_n_hello(5)

    # 带参有返回值
    def add(m, n):
    # 返回结果
    return m + n
    print(add(3, 5))
  • 函数参数

    • 形参:形式参数,函数定义处的参数
    • 实参:实际参数,函数调用处的参数
    • 必传参数(位置参数):定义时没有默认值的参数,调用时必须给够,少给一个就 TypeError。按位置传时顺序要和定义一致,但按关键字传就不受顺序限制(show(b=456, a=123) 合法)。
    • 默认参数:带默认值的参数,必须放在没有默认值的参数后面,否则 SyntaxError。
    • 可变长度参数:调用时传的参数比定义的多,多出来的会被打包进 *args 和 **kwargs——args 是元组(收多余的位置参数),kwargs 是字典(收多余的关键字参数)。两者同时出现时 *args 必须写在 **kwargs 前面。
    • 完整顺序:仅限位置参数 → 普通位置参数 → 默认参数 → *args → 仅限关键字参数 → **kwargs。Python 3.8+ 还能用 / 显式标出「仅限位置参数」、用 * 标出「仅限关键字参数」:
      1
      2
      3
      4
      5
      def pos_only(a, b, /):        # a、b 只能按位置传,pos_only(a=1, b=2) 会 TypeError
      return a, b

      def kw_only(a, *, b=2): # b 只能用名字传,kw_only(1, 3) 会 TypeError
      return a, b
    • 示例1
      1
      2
      3
      4
      5
      6
      7
      8
      9
      def show(a, b='默认值'):
      # a就是必传参数
      print(a)
      # b就是默认参数,就是有默认值的参数,必须放在最后
      print(b)

      # show(1, 2)
      # 关键字参数,传递参数的时候指定了名字,顺序就无所谓了
      show(b=456, a=123)
      输出:
      1
      2
      123
      456
      默认值在定义时求值一次,不是每次调用都重新算;所以默认值千万别用可变对象:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      def bad(x, acc=[]):        # 这个列表在所有调用之间共享
      acc.append(x)
      return acc

      bad(1) # [1]
      bad(2) # [1, 2] ← 惊了,不是 [2]

      def good(x, acc=None): # 惯用写法:默认 None,函数体内再造
      if acc is None:
      acc = []
      acc.append(x)
      return acc
    • 示例2
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      def var_len_args(a, b, name='默认名称', *args, **kwargs):
      print(a)
      print(b)
      print(name)
      # args是一个元组,用来存放所有的多出来的位置参数
      print(args)
      # kwargs 是字典,收所有多余的「关键字参数」
      print(kwargs)


      var_len_args(1, 2, 3, 4, 5, age=18)
      输出:
      1
      2
      3
      4
      5
      1
      2
      3
      (4, 5)
      {'age': 18}

      第三个位置参数 3 把 name 的默认值顶掉了,所以 name 是 3 而不是「默认名称」;剩下多出来的 4, 5 进 args,age=18 进 kwargs。

  • * 与 **:打包与解包

    同一个符号出现在定义处是打包、出现在调用处是解包,方向正好相反。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def show(a, b):
    print(a, b)

    nums = [1, 2]
    show(nums[0], nums[1]) # 略显啰嗦
    show(*nums) # 解包列表 / 元组:等价于 show(1, 2)

    def show2(a='aa', b='bb'):
    print(a, b)

    d = {'a': 'apple', 'b': 'banana'}
    show2(a=d['a'], b=d['b']) # 啰嗦
    show2(**d) # 解包字典:按「键 = 值」对上参数名

    另外几处常见用法:

    1
    2
    3
    first, *rest = [1, 2, 3]    # 解包赋值:rest 得到 [2, 3]
    print(*[1, 2, 3], sep='-') # 1-2-3
    merged = {**d1, **d2} # 字典合并(3.5+),同名键后者覆盖

    变量名别用 l、O、I 这类单字母:l 和数字 1 在很多字体里几乎一样,PEP 8 也建议避开。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    tu = (1, 2, 3, 4, 5)


    def qiuji(length, args):
    ji = 1
    for i in range(length):
    ji *= args[i]
    return ji


    def calc(func, *args):
    print(args)
    return func(len(args), args)


    print(calc(qiuji, *tu))

    输出:

    1
    120
    • 这里 calc(func, *args) 在定义处用 * 把多余位置参数打包成元组,
    • calc(qiuji, *tu) 在调用处用 * 把元组拆开传进去,一进一出正好对上。
    • 顺带一提,这个「求积」在现代 Python 里一行就有:math.prod(tu),或 functools.reduce(operator.mul, tu, 1)。
  • 返回值

    • return 会立即结束函数;一个函数里可以有多个 return,但每次调用只会执行到一个。
    • 没有 return、或 return 不带值,函数返回 None。
    • 「返回多个值」其实是返回一个元组,调用处做序列解包:
      1
      2
      3
      4
      def min_max(seq):
      return min(seq), max(seq) # 返回的是元组 (1, 5)

      lo, hi = min_max([1, 2, 3, 4, 5]) # 解包赋值
    • 返回值可能是 None 时,判断要用 is None,别用 if f():——0、''、[] 这些合法结果也会被当成假。
  • 文档注释(docstring)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def area(width, height):
    """计算矩形面积。

    Args:
    width: 宽
    height: 高
    """
    return width * height

    help(area) # 交互式里查文档
    print(area.__doc__) # 文档字符串就是函数的 __doc__ 属性
  • 作用域

    • 查找顺序是 LEGB:局部(Local) → 闭包外层函数(Enclosing) → 全局(Global) → 内置(Built-in)。
    • 函数里读全局变量不用声明,但赋值就默认创建局部变量,于是会出现「先读后写」的经典报错:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      count = 0

      def bump():
      print(count) # UnboundLocalError:因为下面有 count = ...,count 在整个函数里都是局部名
      count = 1

      def bump_ok():
      global count # 声明要改全局的那个
      count += 1
    • 改外层函数的变量用 nonlocal(写闭包计数器时常用),global 只能改模块级变量。
    • 尽量别用 global:函数依赖外部状态后不好测试和复用,把值当参数传进去、再返回出来更清楚。
  • 参数是怎么传进去的

    Python 既不是 C 那种「值传递」,也不是 C++ 那种「引用传递」,而是传对象引用(按赋值传递):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def append_inplace(lst):
    lst.append(9) # 改的是调用方那个对象本身 -> 看得见

    def rebind(lst):
    lst = lst + [9] # 只是让局部名字指向新对象 -> 调用方不受影响

    nums = [1]
    append_inplace(nums) # nums 变成 [1, 9]
    rebind(nums) # nums 还是 [1, 9]
    • 可变对象(列表、字典、集合)传进函数被原地修改,外面会看到;不可变对象(数字、字符串、元组)改不了。
    • 函数内对参数赋值不会传回去;要「返回新值」就 return。
  • 递归

    • 必须有终止条件,否则爆栈:RecursionError: maximum recursion depth exceeded。
    • 默认递归深度上限约 1000(sys.getrecursionlimit() 可查,sys.setrecursionlimit() 可调,但调大只是延后崩溃,栈溢出仍可能发生)。
    • 递归能写的循环都能写,Python 没有尾调用优化,深递归改成迭代更稳。
    1
    2
    def fact(n):
    return 1 if n <= 1 else n * fact(n - 1) # fact(5) = 120
  • 函数是对象,以及 lambda

    • 函数可以赋值给变量、当参数传、当返回值、放进容器——这是装饰器和高阶函数的基础:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      def shout():
      return 'hi'

      f = shout # 没有括号:把函数本身交给 f
      f() # 'hi'

      def call_twice(fn): # 函数当参数
      return fn() + fn()

      def make_mul(n): # 函数当返回值(闭包)
      def mul(x):
      return x * n
      return mul

      times3 = make_mul(3)
      times3(4) # 12
    • lambda 是只能写一个表达式的匿名函数,最常见的用处是给高阶函数当参数:

      1
      2
      sorted(['bb', 'a', 'ccc'], key=len)    # ['a', 'bb', 'ccc']
      list(map(lambda x: x * 2, [1, 2, 3])) # [2, 4, 6]
    • 注意事项:

      • 能用具名函数就别用 lambda 赋值给变量(f = lambda x: x 不如 def f(x): return x,报错时栈信息更友好)。
      • 本文示例里 calc(qiuji, *tu) 就是把函数当参数传的典型用法。
  • 练习

    1
    2
    1.前面所有的练习,能用函数的封装成函数
    2.封装函数:生成随机的字符串,参数:长度(默认为4),类型(默认为0)
本文结束 感谢您的阅读
正在加载今日诗词....