学过 C 再看 Python 函数最大的感受就是一个字——松不用写返回类型、参数随便传、返回值一次能给好几个。本文直接从 C 视角出发把 Python 函数里和 C 一样的地方一笔带过重点展开和 C 不一样的地方。一、函数是什么和 C 一样函数就是一段可以被重复使用的代码片段。区别在于动机C 里函数是程序组织的基石main 入口、模块化、复用Python 里同样如此。真正值得关注的是语法和设计哲学上的差异后面逐点展开。二、定义与调用先看一段对比# Python def calcSum(beg, end): total 0 for i in range(beg, end 1): total i return total result calcSum(1, 100)// C int calcSum(int beg, int end) { int total 0; for (int i beg; i end; i) total i; return total; } int result calcSum(1, 100);对比点CPython定义关键字无直接写返回类型def返回类型声明必须int、void等不需要形参类型声明必须不需要函数体边界{ }缩进无返回值时voidreturn;省略 return返回None两个共同点快速确认先定义再使用。定义只是登记函数体不会执行必须调用才执行。调用几次执行几次。三、参数差异最集中的区域3.1 形参不需要类型声明Python 是动态类型语言同一个形参可以接收不同类型的实参def test(a): print(a) test(10) # 整数 test(hello) # 字符串 test(True) # 布尔这在 C 中只能用模板或std::variant之类的手段勉强实现。Python 直接放开代价是没有编译期类型检查类型错误要等运行时才暴露。3.2 形参、实参的对应关系形参是占位符实参是具体值。调用时逐个对应个数必须匹配def test(a, b, c): print(a, b, c) test(10) # 报错缺少实参3.3 默认参数Python 和 C 都有默认参数且规则一致带默认值的参数必须放在不带默认值的参数后面。def add(x, y, debugFalse): if debug: print(f调试信息: x{x}, y{y}) return x y print(add(10, 20)) # 不传第三个参数debugFalse print(add(10, 20, True)) # 显式传 True def add(a,b1): return ab print(add(1)) #2int add(int x, int y, bool debug false) { if (debug) std::cout debug: x , y \n; return x y; }3.4 关键字参数C 没有默认情况下实参按位置依次传给形参。Python 额外允许按形参名传参顺序随意def test(x, y): print(x, y) test(x10, y20) test(y100, x200) # 顺序反过来也行C 要实现类似效果只能定义多个重载函数或用结构体把参数打包。Python 的关键字参数让参数很多时调用意图清晰这件事变得零成本。3.5 传参的本质对象引用传递C 的默认传参是值拷贝想改原值得用引用/指针。Python 的传参规则要换一个角度看传进去的是对象引用但对象分可变与不可变。传入不可变对象int、str、bool、tuple函数内部重新赋值不影响外部变量。传入可变对象list、dict 等函数内部修改它外部会跟着变。def modify(x, lst): x 100 # 只改局部变量 x外部不变 lst.append(4) # 修改了同一个 list 对象外部跟着变 num 1 a [1, 2, 3] modify(num, a) print(num) # 1 print(a) # [1, 2, 3, 4]这与 C 的直觉差异很大也是写 Python 最常见的坑之一。要不要在函数里改外部数据取决于你传的是不是可变对象而不是形参有没有写。四、返回值4.1return立即结束函数和 C 一致执行到return函数立刻回到调用处后面代码不执行。def isOdd(num): if num % 2 0: return False return True # 只有 num 为奇数才会执行到这里4.2 多返回值C 里最羡慕的特性Python 可以用逗号一次返回多个值def getPoint(): x 10 y 20 return x, y a, b getPoint() # 同时拿到两个值 多元赋值 print(a, b)不想接收的返回值用_忽略_, b getPoint() # 只关心第二个值C 要返回多个值通常用std::pair、std::tuple、结构体或者引用形参——每种都有一段样板代码。Python 是语言级支持底层实现是返回一个元组然后自动拆包。// C 的对应写法 std::pairint, int getPoint() { return {10, 20}; } auto [a, b] getPoint();4.3 没有return时返回NonePython 函数永远有返回值。没写return默认返回None相当于 C 里没有return的void函数。区别是None是一个真实的值可以赋值、可以比较只是表示什么都没有。五、变量作用域5.1 局部变量与全局变量函数内部定义的变量是局部变量只在函数内生效函数外部的变量是全局变量。同名变量在不同作用域中互不干扰x 20 def test(): x 10 print(f函数内部 x {x}) # 10 test() print(f函数外部 x {x}) # 20函数内部读变量时局部找不到会自动去全局作用域找。5.2global关键字C 没有对应的东西想在函数内部修改全局变量必须先用global声明x 20 def test(): global x x 10 test() print(x) # 10全局变量被修改了如果不写global函数里的x 10会被当成新建一个局部变量和全局x毫无关系。这是 Python 的经典陷阱读全局不用声明改全局必须声明。C 没有这个机制——全局变量可以直接在函数里改。Python 这样做是为了强制明确避免无意识地污染全局状态。5.3if/for/while不产生新作用域与 C 最大差异之一C 里if、for的{ }内部是一个新的块作用域块内变量出块即失效for (int i 0; i 10; i) { } // 此处 i 已不可见Python 里只有函数和类才产生作用域。if/for/while中定义的变量外面照常能用for i in range(1, 10): pass print(i) # 9循环结束后 i 依然存在对写惯 C 的人来说这一点要特别警惕Python 的循环变量会泄漏到函数作用域后面的代码可能不小心读到循环残留值。六、函数的执行与调用栈调用函数时程序跳到函数体执行执行完或遇到return回到调用位置继续。函数内部可以调用其他函数称为嵌套调用层数不限。把函数返回值直接作为另一个函数的参数称为链式调用x 20 def test(): x 10 print(f函数内部 x {x}) # 10 test() print(f函数外部 x {x}) # 20Python 用函数调用栈记录每一层调用每层对应一个栈帧函数内部的局部变量就存在各自的栈帧里。这个模型和 C 完全一致PyCharm 调试器可以直观看到。七、递归递归是嵌套调用自己。代码层面和 C 几乎一样注意事项也相同def factor(n): if n 1: return 1 return n * factor(n - 1) print(factor(5)) # 120两个硬性要求必须有结束条件否则无限递归。每次递归的实参要逼近结束条件。递归的取舍和 C 相同优点代码量极少符合数学归纳法的直觉。缺点难理解、容易栈溢出、性能通常不如循环。一个 Python 特有的点Python 对递归深度有硬性限制默认约 1000 层sys.getrecursionlimit()可查超过直接抛RecursionError。C 没有这个显式限制但栈空间同样有限本质风险一致。所以实际开发中递归要慎重。八、一点扩展函数是一等公民这是 Python 函数和 C 函数最大的哲学差异PDF 里没细讲但对理解 Python 非常重要在 Python 中函数本身也是一个对象可以赋值给变量、放进列表、作为参数传递def add(x, y): return x y def mul(x, y): return x * y # 把函数当值用 op add print(op(1, 2)) # 3 # 函数作为参数传入 def calc(op, a, b): return op(a, b) print(calc(mul, 3, 4)) # 12C 里要这样做需要函数指针或std::function。Python 把这个能力做成语言默认行为这也是 Python 中高阶函数、装饰器等高级玩法的基石后续写代码时会大量遇到。九、小结主题CPython类型声明必须静态类型不需要动态类型默认参数有规则相同有规则相同关键字参数无有多返回值tuple / 结构体语言级支持修改全局变量直接改需global声明块级作用域{}产生新作用域if/for/while 不产生传参语义值/引用对象引用看可变性递归限制栈空间限制额外有深度硬限制函数地位对象可作值传递一等公民天然可作值传递核心就三件事与 PDF 课程总结一致函数的定义、调用、参数传递。对 C 开发者来说真正需要重新建立的直觉只有三条不用写类型但要接受运行时才报错。传参是对象引用可变不可变决定了能不能改外部数据。作用域只有函数级global改全局、if/for 不隔离变量。