从浅拷贝到装饰器,一文搞定Python高级语法核心要点

在Python面试中,高级语法往往是区分初级和中级开发者的分水岭。很多人在写Python代码时,只停留在基础语法层面,但对于浅拷贝与深拷贝的区别、迭代器与生成器的原理、闭包与装饰器的应用场景等问题,往往说不出个所以然。

今天,我将用最通俗易懂的方式,带你彻底搞懂Python高级语法的7个核心知识点。无论你是准备面试,还是想提升自己的Python功力,这篇文章都值得你花时间细细品味。

01 浅拷贝与深拷贝:一个让无数人翻车的知识点

先看一个灵魂拷问

当你写代码时,下面这几种操作有什么区别?

python

list2 = list1          # 直接赋值
list2 = list1.copy()   # 浅拷贝
list2 = copy.deepcopy(list1)  # 深拷贝

如果你答不上来,那这篇文章就是为你准备的。

本质区别是什么?

直接赋值:只是对象的引用,相当于给原对象起了一个别名。你改我也改,我们本是同根生。

浅拷贝:拷贝父对象,但不会拷贝对象内部的子对象。只有第一层是独立的,内层子对象还是共享的。

深拷贝:完全拷贝父对象及其所有子对象。所有层级都是独立的,真正做到“你走你的阳关道,我过我的独木桥”。

用代码说话

来看一个典型案例:

python

import copy

list1 = [1, 2, 3, [100, 200, 300]]
list2 = copy.copy(list1)  # 浅拷贝

print(id(list1[3]))  # 列表元素地址
print(id(list2[3]))  # 同样的地址,说明子对象是共享的

list1[3].append(400)  # 修改内层列表
print(list2[3])  # [100, 200, 300, 400]  list2也被改了

重点来了:当你修改整型元素时:

python

list1[0] = 100  # 整型是不可变类型
print(list2[0])  # 1  list2没变

为什么?因为整型是不可变类型,修改时会创建新的对象,不会影响原引用。

但当你修改列表元素时:

python

list1[3].append(400)  # 列表是可变类型
print(list2[3])  # [100, 200, 300, 400]  list2跟着变了

这就是浅拷贝的陷阱:第一层独立,内层共享

深拷贝才是真正的“分身”

python

list1 = [1, 2, 3, [100, 200, 300]]
list3 = copy.deepcopy(list1)

print(id(list1[3]))  # 地址A
print(id(list3[3]))  # 地址B,完全不同的地址

list1[3].append(400)
print(list3[3])  # [100, 200, 300]  list3纹丝不动

特殊情况要留意

  • 数字、字符串等原子类型:无法拷贝,拷贝后还是同一个对象

  • 纯原子类型的元组:深拷贝后地址不变

  • 包含可变对象的元组:深拷贝后地址会变

python

tuple1 = (1, 2, 3, [])  # 包含可变对象
tuple2 = copy.deepcopy(tuple1)
print(id(tuple1), id(tuple2))  # 地址不同

02 迭代器:for循环背后的秘密

可迭代对象 vs 迭代器

很多人在面试中被问到这两个概念的区别时,会一脸茫然。其实很简单:

可迭代对象:能用for循环遍历的对象,比如list、tuple、dict、str等。

迭代器:不仅能用for循环,还能用next()函数逐个取值的对象。

判断方法:

python

from collections.abc import Iterable, Iterator

print(isinstance([], Iterable))   # True  列表是可迭代对象
print(isinstance([], Iterator))   # False 但不是迭代器

print(isinstance((x for x in range(10)), Iterator))  # True 生成器是迭代器

迭代器的工作原理

for循环在幕后做了什么?来揭秘:

python

list1 = [1, 2, 3]
it = iter(list1)  # 获取迭代器

while True:
    try:
        print(next(it))  # 逐个获取元素
    except StopIteration:  # 取完了就结束
        break

这就是迭代器协议:实现__iter__()__next__()方法。

自己动手实现迭代器

看一个反向迭代器的实现:

python

class Reverse:
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index -= 1
        return self.data[self.index]

rev = Reverse([2, 3, 5, 7, 11])
for num in rev:
    print(num)  # 11, 7, 5, 3, 2

03 生成器:内存友好的迭代器

为什么需要生成器?

想象一个场景:你需要生成1亿个数字。如果用列表,会占用大量内存。如果用生成器,每次只产生一个值,内存占用极小。

python

# 列表推导式 - 一次性生成所有值
list_comp = [x for x in range(100000000)]  # 内存爆炸

# 生成器表达式 - 按需生成
gen_exp = (x for x in range(100000000))  # 内存友好

yield的神奇之处

yield是生成器的核心,它让函数可以“暂停”并“记住”状态:

python

def fibonacci():
    a, b = 0, 1
    while True:
        yield b
        a, b = b, a + b

f = fibonacci()
print(next(f))  # 1
print(next(f))  # 1
print(next(f))  # 2
print(next(f))  # 3

每次调用next(),函数从上一次yield的地方继续执行,直到再次遇到yield。

获取return值

生成器函数可以有return,但需要通过捕获StopIteration异常来获取:

python

def fibo(n):
    a, b, counter = 0, 1, 0
    while counter < n:
        yield b
        a, b, counter = b, a + b, counter + 1
    return "done"

f = fibo(5)
try:
    while True:
        print(next(f))
except StopIteration as result:
    print("返回值:", result)  # 返回值: done

send()方法:双向通信

生成器不仅能产生值,还能接收值:

python

def task_scheduler():
    task_id = 0
    int_val = 0
    char_val = 'A'
    
    while True:
        match task_id:
            case 0:
                task_id = yield int_val
                int_val += 1
            case 1:
                task_id = yield char_val
                char_val = chr(ord(char_val) + 1)

scheduler = task_scheduler()
print(next(scheduler))        # 0
print(scheduler.send(1))      # A
print(scheduler.send(0))      # 1
print(scheduler.send(1))      # B

注意:第一次调用必须用send(None)来启动生成器。

04 命名空间与作用域:变量去哪儿了

三种命名空间

Python有三种命名空间:

  1. 内置命名空间:Python解释器启动时创建,包含print、len等内置函数

  2. 全局命名空间:模块被导入时创建,模块级别的变量和函数

  3. 局部命名空间:函数被调用时创建,函数内的变量

作用域的四层查找规则

Python查找变量时遵循LEGB规则:

  • L - Local:最内层,当前函数/方法的局部变量

  • E - Enclosing:外层闭包函数的作用域

  • G - Global:当前模块的全局变量

  • B - Built-in:内置命名空间

python

x = 10  # 全局变量

def outer():
    x = 20  # 外层变量
    
    def inner():
        x = 30  # 局部变量
        print(x)  # 30
    inner()

outer()

global和nonlocal的使用

默认情况下,在函数内给变量赋值会创建局部变量:

python

count = 0

def increment():
    count += 1  # 报错!引用前未定义
    # 因为这里count被认为是局部变量

def increment_correct():
    global count  # 声明使用全局变量
    count += 1

nonlocal用于嵌套函数中修改外层变量:

python

def outer():
    x = 10
    
    def inner():
        nonlocal x  # 声明使用外层变量
        x += 1  # 修改外层变量
        return x
    
    return inner

05 闭包:让函数记住它的环境

什么是闭包?

闭包是一个函数,它“记住”了它被创建时的环境,即使外部函数已经执行完毕。

构建闭包的三要素:

  1. 外部函数内定义一个内部函数

  2. 内部函数引用外部函数的变量

  3. 外部函数返回内部函数

python

def linear(a, b):
    # a和b在外部函数中
    def inner(x):
        return a * x + b  # 内部函数引用外部变量
    return inner  # 返回内部函数

# 创建闭包
y = linear(2, 3)
print(y(5))  # 13
# linear函数已经执行完毕,但y还记得a=2, b=3

闭包有什么用?

  1. 避免使用全局变量

  2. 实现数据隐藏

  3. 延迟计算

查看闭包中的值

python

def linear(a, b):
    def inner(x):
        return a * x + b
    return inner

y = linear(1, 2)
print(y.__closure__)  # 单元格对象元组
print(y.__closure__[0].cell_contents)  # 1
print(y.__closure__[1].cell_contents)  # 2

06 装饰器:优雅地扩展函数功能

装饰器的本质

装饰器本质上是一个函数,它接收一个函数作为参数,返回一个增强后的函数。

python

def decorator(func):
    def wrapper(*args, **kwargs):
        print("执行前...")
        result = func(*args, **kwargs)
        print("执行后...")
        return result
    return wrapper

@decorator
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Python")  # 执行前... Hello, Python! 执行后...

实现一个实用装饰器

来写一个计算函数执行时间的装饰器:

python

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__}执行时间: {end - start:.4f}秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "完成"

slow_function()  # slow_function执行时间: 1.0002秒

带参数的装饰器

有时我们需要给装饰器传参:

python

from math import sqrt

def times(n):
    def get_absolute(f):
        def inner(x):
            x = abs(x)
            for _ in range(n):
                x = f(x)
            return x
        return inner
    return get_absolute

@times(2)  # 开两次根号
def sqrt_func(x):
    return sqrt(x)

print(sqrt_func(-16))  # 2.0

类装饰器

用类实现装饰器,更面向对象:

python

from math import sqrt

class AbsoluteDecorator:
    def __init__(self, func):
        self.func = func
    
    def __call__(self, x):
        x = abs(x)
        return self.func(x)

@AbsoluteDecorator
def sqrt_func(x):
    return sqrt(x)

print(sqrt_func(-4))  # 2.0

07 多层装饰器的执行顺序

当多个装饰器叠加时,执行顺序是从下往上:

python

def decorator_a(func):
    def wrapper(x):
        print("装饰器A执行")
        return func(x)
    return wrapper

def decorator_b(func):
    def wrapper(x):
        print("装饰器B执行")
        return func(x)
    return wrapper

@decorator_a  # 后执行
@decorator_b  # 先执行
def func(x):
    return x

func(1)
# 输出:
# 装饰器B执行
# 装饰器A执行

总结

回顾一下我们今天学到的Python高级语法核心要点:

  1. 浅拷贝与深拷贝:理解引用、浅拷贝和深拷贝的区别,掌握可变对象和不可变对象在拷贝时的行为差异

  2. 迭代器:认识可迭代对象和迭代器的区别,理解for循环背后的迭代器协议

  3. 生成器:掌握yield的用法,学会使用生成器表达式和生成器函数来优化内存使用

  4. 命名空间与作用域:理解LEGB查找规则,掌握global和nonlocal的正确用法

  5. 闭包:理解闭包的形成条件和应用场景,学会利用闭包实现数据隐藏和延迟计算

  6. 装饰器:掌握装饰器的本质,能够编写带参数的多层装饰器,理解类装饰器的实现方式

这些知识点不仅是面试中的高频考点,更是写出优雅Python代码的必备技能。它们体现了Python设计哲学中的“简洁而不简单”,让你能用更少的代码实现更强大的功能。

记住:掌握这些高级特性,不是为了炫技,而是为了写出更清晰、更高效、更易维护的代码。当你真正理解并运用它们时,你的Python水平会上一个新台阶。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐