Python面试题整理02——函数+参数/参数传递+装饰器/生成器
Python面试题整理02——函数+参数/参数传递+装饰器/生成器

Python面试题整理02——函数+参数/参数传递+装饰器/生成器

基础的定义、调用可以找一些基础的教学资料看一下,这儿就不再赘述。这儿重点整理常见的一些问题。

1. *args和**kwargs

这俩哥们代表可变参数,也就是这个函数不确定到底要多少或者什么固定的参数的时候,这俩哥们就可以拉出来占个位子,告诉函数你需要接收参数,接收多少不确定,但是你都得接收。*args接收任意数量的位置参数,将其打包成一个元组。**kwargs接收任意数量的关键字参数,将其打包成一个字典。

其实有点抽象,我们先理解下两个名词。位置参数:按照顺序传递的参数。关键字参数:通过参数名传递的参数。还觉得抽象的话看个代码。

def func(*args):
        print(args)
func(1, 2, 3)
# args == (1, 2, 3)

def func1(**kwargs):
        print(kwargs)
func1(a=1, b=2)
# kwargs == {'a': 1, 'b': 2}

2. 参数传递方式

先回忆下可变对象(字典、列表)和不可变对象(整数、字符串、元组)。

值和引用对象是两个东西,正如苹果这个概念和你眼前的那个苹果是两个东西一样。问:python的参数传递是值传递还是引用传递?答:都不是,而是赋值传递,有时也称为对象引用传递。

纯粹的值传递中,函数会得到外部实参值的完整副本,而在python中传递的不是值的副本,而是对象引用的副本。纯粹的引用传递中,函数内部形参就是外部实参的别名,对形参的任何重新赋值都会改变外部实参,而在python中,如果函数内部将形参重新赋值给一个全新的对象,并不会影响到外部的原始变量。

所以上面绕段子一样的解释核心就是面对一个现实的问题:一个对象的多个变量,能否通过其中一个变量来修改这个对象的内容。

2.1 参数不可变

当参数是不可变的,任何函数内部的修改实际操作都会创建一个新的对象,并将函数内部变量指向这个新对象。

理论复杂,代码展示:

def fun1(num, text):
    print(f"函数开始: num id={id(num)}, text id={id(text)}")
    num = num + 1  # 创建了一个新的整数对象
    text = text + " world" # 创建了一个新的字符串对象
    print(f"函数结束: num id={id(num)}, text id={id(text)}")
    print(f"函数内部: num={num}, text='{text}'")

a = 10  # 不可变
b = "hello"  # 不可变
print(f"调用之前: a id={id(a)}, b id={id(b)}")
fun1(a, b)
print(f"调用之后: a={a}, b='{b}'")
print(f"调用之后: a id={id(a)}, b id={id(b)}")

结果就是:

调用之前: a id=140727815112088, b id=2671008242048
函数开始: num id=140727815112088, text id=2671008242048
函数结束: num id=140727815112120, text id=2671008267696
函数内部: num=11, text='hello world'
调用之后: a=10, b='hello'
调用之后: a id=140727815112088, b id=2671008242048

可以看到,函数修改变量前,也就是“调用之前”和“函数开始”这两行,id都是一样的。但是一旦修改后,“函数结束”这一行的id就不一样了,而出了函数,原来的参数id还是没变。也就是不可变对象修改后,并不会影响函数外部,而是直接将函数内部的引用指向另一个对象。

2.2 参数可变

当参数可变类型时候,内部数据修改会反映到函数外部。直接看代码演示


def fun2(my_list, my_dict):
    print(f"函数开始: list id={id(my_list)}, dict id={id(my_dict)}")
    my_list.append(4)  # 直接在原始列表对象上进行修改
    my_dict['c'] = 3   # 直接在原始字典对象上进行修改
    print(f"函数结束: list id={id(my_list)}, dict id={id(my_dict)}")
    print(f"函数内部: list={my_list}, dict={my_dict}")

# --- 调用 ---
x = [1, 2, 3]
y = {'a': 1, 'b': 2}
print(f"调用之前: x id={id(x)}, y id={id(y)}")
fun2(x, y)
print(f"调用之后: x={x}, y={y}")
print(f"调用之后: x id={id(x)}, y id={id(y)}")

输出结果:

调用之前: x id=2158036635072, y id=2158037098176
函数开始: list id=2158036635072, dict id=2158037098176
函数结束: list id=2158036635072, dict id=2158037098176
函数内部: list=[1, 2, 3, 4], dict={'a': 1, 'b': 2, 'c': 3}
调用之后: x=[1, 2, 3, 4], y={'a': 1, 'b': 2, 'c': 3}
调用之后: x id=2158036635072, y id=2158037098176

可以看到参数的ID一直没有变化,函数内修改参数后,函数外的值也对应变化了,但是ID没变。也就是可变对象内部修改后,直接反应到了函数外部,但是ID不会有任何变化。

3. 装饰器

装饰器就是函数套函数,目标是不修改函数源代码的情况下,为其增加额外的功能。或者给一系列函数增加额外的功能,比如我们登录网站后每个操作都需要做一个权限认证,这个权限认证就可以写成装饰器,其他比如日志记录、性能测试、缓存等等。

如下代码所示,定义一个函数,它的参数是另一个函数。内部的wrapper可以理解为一个内部函数,用来替代原函数,*args,**kwargs如我们上述所讲,保证了参数传递不会破坏原函数调用方式。@是语法糖,写起来更好看一点。不用@的话,就是写成f = fun1(f)。

def fun1(func):
    def wrapper(*args,**kwargs):
        print("记录开始!")
        result = func(*args, **kwargs)
        data = result
        print(f"记录数据:{data}")
        print("记录结束!")
        return result
    return wrapper

@fun1
def f(a):
    result = a
    return result

f(3)

当然,也有复杂一点的装饰器,比如装饰器也需要参数的情况。这种情况下就在外面再套一层函数即可,如下代码所示

def log_with_level(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] - 开始")
            result = func(*args, **kwargs)
            print(f"[{level}] - 结束.")
            return result
        return wrapper
    return decorator


@log_with_level(level="DEBUG")
def add(x, y):
    print(f"--- 结果相加({x}, {y}) ---")
    return x + y

@log_with_level(level="WARNING")
def subtract(x, y):
    print(f"--- 结果相减({x}, {y}) ---")
    return x - y


# --- 调用 ---
add_result = add(10, 5)
print(f"{add_result}\n")

subtract_result = subtract(10, 5)
print(f"{subtract_result}")

4. 生成器

通过yield语句生成一个值,它不会一次性返回所有的值,而是每次请求一个值时才生成一个,惰性求值可以节省内存。

如果你是搞深度学习,这个思想应该很常见。深度学习数据集一般很大,不可能一下子全部载入,一般都是按需求按批次生成的。pytorch里面就是dataloader那个,就是类似的思想。同样的,在网页后端一块,数据库访问也是类似的惰性访问,ORM数据库查询就是用到才会去访问。

5. 匿名函数-lambda

一种没有名字、小型的、单行函数。感觉不常见似乎也没啥用,了解下应该就好。

# 传统函数定义
def add(x, y):
    return x + y

# 等价的 lambda 函数
add_lambda = lambda x, y: x + y

print(add(2, 3))       # 输出: 5
print(add_lambda(2, 3)) # 输出: 5

6. 递归

一个函数直接或者间接的调用自己。递归有两个条件,一是基线条件Base Case,也就是递归的出口,如果没有就会无限循环。二是递归步骤Recursive Step,通过一个修改过的参数,使得问题规模逐渐变小,接近基线条件。

感觉主要是一些算法会用,日常用的应该也不多,简单了解即可。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注