Skip to content

函数

约 2026 个字 161 行代码 预计阅读时间 9 分钟

函数

定义函数

Python 使用 def 关键字定义函数,函数本质上是对象。

def f():
    pass

print(type(f))  # 输出:<class 'function'>

匿名函数(Lambda 表达式)

匿名函数使用 lambda 定义,仅适用于简单的一行函数体,主要是因为python靠缩进来判断代码块,如果函数体复杂,那么缩进会很麻烦,所以python不支持多行lambda:

g = lambda x, y, z: x + y + z
print(type(g))  # 输出:<class 'function'>

函数参数

位置参数 & 关键字参数

  • 位置参数按顺序传入;
  • 关键字参数使用 key=value 形式;
  • 关键字参数必须位于位置参数之后
  • 默认参数在定义时即被绑定,不是调用时再计算,默认参数在定义时必须放最后面:
# 默认参数可以看作是函数自己的成员
def f(a, l=[]):
    l.append(a)
    return l

f(10)
print(f(11))  # 输出:[10, 11]
print( f.__defaults__) # 输出:([10, 11],)

可变长参数

  • *args:接收任意数量的位置参数,作为元组;
  • **kwargs:接收任意数量的关键字参数,作为字典;
  • */** 也可用于实参,用于序列或字典拆包:
def func(*args, **kwargs):
    print(args, kwargs)

func(1, 2, x=3, y=4)
# 输出:(1, 2) {'x': 3, 'y': 4}

可变与不可变

  • 不可变对象(如 int、str)作为参数不会被函数内部修改,因为是拷贝了一份;
  • 可变对象(如 list、dict)可能会在函数中被修改,因为传递的是引用(指针)。

返回值

  • 没有 return 语句时默认返回 None
  • 可以返回多个值(打包成元组);
  • 函数可作为返回值(高阶函数/装饰器):
def outer():
    def inner():
        return "Hello"
    return inner

命名空间与作用域

  • 内置命名空间:包括标准函数和常量;
  • 全局命名空间:模块级别的变量;
  • 局部命名空间:函数内部变量;
  • 查找顺序为:局部 → 全局 → 内置
  • 若需在函数内部修改全局变量,使用 global 关键字声明:
x = 1

def modify():
    global x
    x = 2

print(x)
# 结果为 2

这是是错误的,因为x会被当做一个局部变量,然后对一个未定义的变量 +1 会报错

x = 1
def f():
    x += 1

f()

常用内置函数(部分)

  • sorted(iterable, key=None, reverse=False):排序,可迭代对象都行
  • map(func, iterable):映射函数,对可迭代对象的每个元素都施加func,返回一个迭代器
  • zip(*iterables):打包多个可迭代对象为元组,如果长短不一以短的为准,返回一个迭代器
  • filter(func, iterable):过滤函数,对可迭代对象的每个元素都施加func,返回funcTrue的元素,返回一个迭代器
  • eval(expression):执行一个字符串形式的 Python 表达式,并返回表达式的值
  • exec(code):接受一个包含 Python 语句的字符串或代码对象,并执行这些语句
  • all(iterable):全部为真返回 True
  • any(iterable):任一为真返回 True

模块与包

模块

每个 .py 文件都是一个模块。

导入方式:

import 模块名
import 模块名 as 别名
from 模块名 import 函数或变量

包(Package)

包是带有 __init__.py 文件的文件夹(可为空),用于组织多个模块。

跨目录导入模块:

import sys
sys.path.append("your/path/here")  # 添加路径到搜索路径中

迭代器与生成器

迭代器

迭代器是一个表示数据流的对象,这个对象每次只返回一个元素,Python迭代器必须支持__next__方法,这个方法不接受参数,并总是返回数据流中的下一个元素。如果数据流中没有元素,__next__会抛出StopInteration异常。迭代器未必是有限的。

迭代器未必是可迭代对象,可迭代对象是可以用for循环生成值序列的任何对象,可以用内置函数iter()尝试把任意对象转化成可迭代对象,如果对象不支持iter语法,用for语句循环时会抛出TypeErroriter的参数的格式如下:iter(object,[sentinel]),第一个参数是支持__iter__的对象,第二个参数是触发StopInteration的条件。

生成器

普通函数返回一个值,而生成器返回一个产生数据流的迭代器。生成器在退出一个函数的时候不扔掉局部变量而且可以在退出函数的地方重新恢复运行,生成器可以看作一个可恢复的函数。

yield语句仅在定义生成器函数的时候使用,并且仅被用于生成器函数的函数体内部。yield有两种形式:

  • yield 表达式
  • yield from 生成器

生成器的一个很好的例子是

def flat(items):
    for x in items:
        if isinstance(x, list):
            yield from flat(x)
        else:
            yield x

items = [1, 4, [3, 7, [5, 6], 8], 9]
flatlist = []
for t in flat(items):
    flatlst.append(t)
print(flatlst)

生成器在处理大数据量时很有作用,生成器返回一个像列表一样的对象,但和普通列表不同,它一次只生成一个对象,而不是生成全部对象再返回,这被称作惰性计算。

函数是一等对象

通常把“一等对象”定义为:

  • 在运行时创建
  • 能赋值给变量或者数据结构的元素
  • 能作为参数传递给函数
  • 能作为函数的返回结果

简单理解为函数名其实是一个变量,可以对它进行赋值、返回等操作,函数对象有一个__name__属性,是函数的名称。

用户定义的可调用类型

既然Python中的函数可以当作对象,那么为什么对象不能当作函数调用?为此,只需要实现实例方法__call__

import random

class BingoCage:
    def __init__(self,items):
        self._items = list(items)
        random.shuffle(self._items)# 打乱顺序
    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')
    def __call__(self):
        return self.pick()

BingoCage本身可以当作函数调用,相当于是BingoCage.pick()的快捷方式

装饰器

基础知识

装饰器是一种可调用对象,其参数是另外一个函数(被装饰的函数)。装饰器可能会对被装饰的函数做一些处理,然后返回函数,或者把函数替换成另一个函数或可调用对象,作用是可以动态地给函数添加功能。实际上,装饰器就是一个返回函数的高阶函数

装饰器有一个关键特性,它们在被装饰的函数定义之后立即执行,这通常是在导入时。

作用域规则

Python的规则和C/C++有一点点区别,比如

#include<iostream>

using namespace std;

int b = 6;
void f(int a){
    cout << a << ' ';
    cout << b << endl;
    int b = 9;
}

int main(){
    f(3);
}

这段代码显然是正确的,f里面的int b定义的局部变量虽然会覆盖全局的b,但是全局的b先被输出了,所以结果是3 6。但是换成python,结果就不一样了

b = 6
def f(a):
    print(a,end=' ')
    print(b)
    b = 9
f(3)

这段代码会直接报错,错误是UnboundLocalError: cannot access local variable 'b' where it is not associated with a value,这说明Python判定b是局部变量,会尝试从局部获取值,但是一开始又没有赋值,所以会直接报错。这并不是bug,而是一种设计选择:Python不要求声明变量,但是会假定在函数主体中赋值的变量是局部变量

此外,Python其实针对上述问题提供了解决方案,即使用global指定b为全局变量,注意b运行后的值是9

b = 6
def f(a):
    global b
    print(a,end=' ')
    print(b)
    b = 9
f(3)

另外Python的局部作用域也是特殊的,if-else,for,while中定义的“临时”变量其实会一直存在,比如

x = 1
if(x>0):
    y=1
else:
    y=2
# 输出 1
print(y)

闭包

闭包其实就是延伸了作用域的函数,包括函数主体中的非全局变量和局部变量。

# 动态返回平均值
def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager
# 调用
avg = make_averager()
avg(10)
avg(11)
avg(12)

seriesmake_averager函数的局部变量,但是我们其实只调用了一次make_averager,当调用avg的时候make_averager已经返回,局部作用域已经消失,series就变成了一个自由变量(即未在局部作用域中绑定),但是它是仍然存在的。

也许你会觉得上面那段代码写的并不好,为什么要用一个列表存?直接记录总和以及总数量不行吗?还真不行

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count

    return averager
avg = make_averager()
avg(10)

事实上这段代码会报错UnboundLocalError: cannot access local variable 'count' where it is not associated with a value,原因是在average中对count赋值了,这会导致count被认为是局部变量,就不再是自由变量,也就不在闭包中了。为了解决这个问题Python提供了nonlocal关键字,它的作用是把变量标记为自由变量,如果为nonlocal声明的变量赋予新值,那么闭包中绑定的值也会随之更新。

还有就是返回闭包时不能包含循环变量,因为返回函数并没有被立即执行

def count():
    f = []
    for i in range(3):
        def f():
             return i*i
        f.append(f)
    return f
# 答案都是2,因为这三个函数都是同一个闭包,i最后为2
f1, f2, f3 = count()

如果一定要这么干该怎么办,可以再套一层函数

def count():
    f = []
    def g(j):
        return j*j
    for i in range(3):
        f.append(g(i))
    return f
# 答案依次是 0,1,2 因为g被立即调用了,所以绑定到的是当时的循环变量值
f1, f2, f3 = count()

实现装饰器

# 可以打印出函数调用的耗时,接受的参数和返回的值
import time
import functools

def clock(func):
    # 保留被装饰函数的元信息(如名称、文档字符串等)
    @functools.wraps(func)
    # *args,**kwargs表示任意参数
    def clocked(*args,**kwargs):
        t0 = time.perf_counter()
        result = func(*args,**kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k,v in kwargs.items())
        arg_str = ','.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked