1. 从面试官视角看Python基础题的价值
最近帮团队面试了几轮初级和中级Python开发,发现一个挺有意思的现象:很多候选人简历上项目写得天花乱坠,框架、中间件、高并发张口就来,但一碰到最基础的Python语言特性题,回答就开始含糊其辞,甚至直接卡壳。比如,问“Python里is和==有什么区别?”,能清晰说出“is比较的是对象标识符(id),==比较的是值”的,十个里大概只有三四个。再追问一句“那什么情况下a is b为True,但a == b可能为False?”,能举出小整数缓存或字符串驻留例子的就更少了。
这让我意识到,面试官手里那些所谓的“经典基础题”,其考察目的远不止于检验你是否背过答案。它们更像是一面镜子,能清晰地照出一个开发者对这门语言底层机制的理解深度、编码时的思考习惯,以及解决实际问题时的严谨性。Python以其语法简洁著称,但“简洁”不等于“简单”。恰恰是这些看似基础的语法糖和内置机制背后,隐藏着内存管理、对象模型、作用域规则等核心概念。能把基础题讲透的人,往往在写业务代码时更少出“妖蛾子”,debug时思路也更清晰。
因此,我整理了这份“Python基础篇面试题集锦”。它不仅仅是一个问题列表,更融合了我作为面试官的出题思路、作为过来人的踩坑经验,以及希望候选人能具备的底层认知。我会围绕每个问题,不仅给出“标准答案”,更会深入剖析“为什么这么问”、“常见的错误理解是什么”、“在实际编码中如何应用和避坑”。希望它能帮你真正吃透Python基础,在面试中展现出扎实的内功。
2. 变量、对象与内存:理解Python的“一切皆对象”
这是所有Python面试的起点,也是区分“会用Python”和“理解Python”的第一道分水岭。很多开发者写了很久代码,但对变量、对象、引用这三者的关系依然模糊。
2.1 可变对象与不可变对象:一切行为的根源
Python中所有数据都是对象,每个对象都有三个基本属性:标识(id,内存地址)、类型(type)和值(value)。根据对象的值能否被改变,分为可变对象(如list, dict, set)和不可变对象(如int, float, str, tuple)。
核心陷阱:赋值不是拷贝,而是贴标签。
a = [1, 2, 3] b = a # b只是a的一个新标签,指向同一个列表对象 b.append(4) print(a) # 输出:[1, 2, 3, 4]!a也被修改了这里的b = a并没有创建新的列表,只是让变量b也指向了变量a所指向的那个列表对象。所以通过b修改列表,a看到的自然也是被修改后的内容。这是初学者在函数传参时最容易踩的坑。
那么,如何真正复制一个列表呢?
- 浅拷贝:
b = a.copy()或b = a[:]或b = list(a)。这创建了一个新的列表对象,但新列表中的元素(如果它们是可变对象)依然和原列表共享。
a = [[1, 2], 3] b = a.copy() b[0].append(99) print(a) # 输出:[[1, 2, 99], 3] 内部列表被修改了!- 深拷贝:
import copy; b = copy.deepcopy(a)。这会递归地拷贝所有嵌套的可变对象,创建完全独立的副本。
面试官追问场景:“请问下面代码的输出是什么?并解释原因。”
def func(d): d['a'] = 10 d = {'b': 20} data = {} func(data) print(data)很多候选人会答{'b': 20}。正确答案是{'a': 10}。原因在于:函数调用时,实参data将其引用(指向空字典的标签)传递给了形参d。所以d['a'] = 10修改的是data和d共同指向的那个字典。随后d = {'b': 20}让形参d指向了一个全新的字典对象,但这丝毫不会影响实参data仍然指向原来的字典。这个例子完美诠释了“传对象引用”而非“传值”或“传引用”的概念。
2.2is与==:身份与相等的哲学
这是必考题,但很多人只背了结论,没理解其应用场景和背后的优化机制。
is:比较两个变量的内存地址是否相同,即id(a) == id(b)。判断的是“是不是同一个对象”。==:调用对象的__eq__()方法,比较两个对象的值是否相等。判断的是“值看起来是否一样”。
关键考点:小整数池与字符串驻留Python出于性能考虑,对一定范围内的小整数(通常是-5到256)和简单的字符串进行了缓存(驻留)。这意味着,这些对象在内存中只有一份。
a = 100 b = 100 print(a is b) # True,因为100在小整数池内,是同一个对象 c = 1000 d = 1000 print(c is d) # 在CPython中,通常为False(除非在同一个代码块中经编译器优化)。但 c == d 永远为True。 s1 = 'hello' s2 = 'hello' print(s1 is s2) # 通常为True,字符串驻留机制重要提示:永远不要用is来比较数值或字符串是否相等,应该始终使用==。is只用于比较单例对象,如None。
if x is None: # 正确 if x == None: # 功能相同,但风格不佳 if x is 100: # 错误!虽然可能碰巧对,但行为不确定。2.3 垃圾回收:引用计数与循环引用
Python通过引用计数为主,标记-清除和分代回收为辅的机制自动管理内存。
- 引用计数:每个对象都有一个计数器,记录有多少个引用指向它。当引用计数归零时,对象立即被销毁。这是最直接、快速的回收方式。
import sys a = [] print(sys.getrefcount(a)) # 输出可能是2(a变量 + getrefcount的形参) b = a print(sys.getrefcount(a)) # 输出变为3 del b print(sys.getrefcount(a)) # 输出变回2sys.getrefcount()本身调用也会增加一个临时引用,所以看到的数字会比预期多1。
- 循环引用问题:引用计数的致命弱点。当两个或多个对象相互引用,形成环,它们的引用计数永远不会归零。
class Node: def __init__(self): self.parent = None self.children = [] node1 = Node() node2 = Node() node1.children.append(node2) node2.parent = node1 # 此时,即使删除外部变量,node1和node2的引用计数仍为1(相互引用),无法被引用计数回收。 del node1, node2标记-清除:为了解决循环引用,Python的垃圾回收器会定期执行。它从一组“根对象”(如当前栈帧中的变量、全局变量等)出发,标记所有可达的对象。然后清扫堆内存,释放那些未被标记的对象(即不可达的循环引用组)。
分代回收:基于“年轻对象很快死,老对象存活久”的假设。新创建的对象在第0代,经历一次垃圾回收后存活下来的移到第1代,以此类推。回收器会更频繁地检查年轻代,而对老年代检查较少,以此平衡回收开销。
面试实战技巧:当被问到“Python的垃圾回收机制”时,不要只背“引用计数”。一定要提到它的局限性(循环引用)和补充机制(标记-清除、分代回收),并最好能举一个循环引用的例子。这能立刻体现你的深度。
3. 函数进阶:参数、作用域与装饰器
函数是Python的一等公民,对其机制的考察是面试的重中之重。
3.1 参数传递的“花式”玩法
Python的函数参数非常灵活,但也是混淆的重灾区。
位置参数、关键字参数、默认参数是最基础的。关键要理解默认参数只计算一次这个坑。
def append_to(element, target=[]): # 默认参数target=[]在函数定义时就被计算并创建 target.append(element) return target print(append_to(1)) # 输出:[1] print(append_to(2)) # 输出:[1, 2]! surprise!第二次调用时,target使用的是第一次调用后已经被修改过的那个默认列表。正确做法是使用None作为默认值:
def append_to(element, target=None): if target is None: target = [] target.append(element) return target*args和**kwargs是可变参数。
*args:接收任意数量的位置参数,在函数内部作为一个元组。**kwargs:接收任意数量的关键字参数,在函数内部作为一个字典。 它们可以同时使用,但顺序必须是(普通参数, *args, **kwargs)。
def logger(func): def wrapper(*args, **kwargs): # 这样可以接受func的任何签名 print(f"Calling {func.__name__}") return func(*args, **kwargs) # 在这里原样传回 return wrapper这里wrapper函数用*args, **kwargs接收所有参数,在调用原始函数func时又用*args, **kwargs“解包”传递出去,从而实现了对任意函数的透明包装。这是装饰器的经典写法。
仅限关键字参数:在*args之后,或一个单独的*之后出现的参数,必须通过关键字指定。
def connect(host, port, *, timeout=10, ssl=False): pass connect('localhost', 8080, timeout=5) # 正确 connect('localhost', 8080, 5) # 错误!timeout必须用关键字这个特性在需要强制提高代码可读性或避免参数顺序错误时非常有用。
3.2 作用域与闭包:变量查找的“LEGB”规则
当在函数内部访问一个变量时,Python会按照L(Local,局部)-> E(Enclosing,闭包)-> G(Global,全局)-> B(Built-in,内置)的顺序进行查找。
闭包是引用了外部函数变量的内部函数。它“记住”了它被定义时的环境。
def make_multiplier(factor): def multiplier(x): return x * factor # 这里引用了外部函数的变量factor return multiplier double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 输出:10 print(triple(5)) # 输出:15double和triple是两个不同的闭包,它们各自“记住”了不同的factor值(2和3)。闭包是实现装饰器、函数工厂和回调机制的基础。
global和nonlocal关键字:用于在内部作用域中修改外部变量。
global:声明变量来自全局作用域。nonlocal:声明变量来自最近的嵌套作用域(非全局)。
count = 0 def outer(): num = 10 def inner(): nonlocal num # 引用outer函数的num global count # 引用全局的count num += 1 count += 1 inner() print(num, count) # 输出:11 1如果不使用nonlocal,inner函数中的num += 1会被认为是创建一个新的局部变量num,从而导致UnboundLocalError。
3.3 装饰器:不修改源代码的“魔法”
装饰器本质上是一个接受函数作为参数并返回一个新函数的高阶函数。它提供了强大的AOP(面向切面编程)能力。
编写一个记录函数运行时间的装饰器:
import time import functools def timer(func): @functools.wraps(func) # 重要!保留原函数的元信息(如__name__) def wrapper(*args, **kwargs): start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() print(f"{func.__name__} took {end_time - start_time:.4f} seconds") return result return wrapper @timer def slow_function(): time.sleep(1) slow_function() # 输出:slow_function took 1.0012 seconds print(slow_function.__name__) # 输出:slow_function (如果没有@functools.wraps,会输出'wrapper')@functools.wraps(func)是一个非常重要的最佳实践,它将被装饰函数的__name__、__doc__等属性复制到包装函数中,使得装饰器对使用者更加透明。
带参数的装饰器:这需要再嵌套一层函数。
def repeat(num_times): """装饰器工厂,返回一个装饰器""" def decorator_repeat(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(num_times): result = func(*args, **kwargs) return result # 通常返回最后一次调用的结果 return wrapper return decorator_repeat @repeat(num_times=3) def greet(name): print(f"Hello {name}") greet("World") # 输出: # Hello World # Hello World # Hello World这里@repeat(num_times=3)实际上先调用了repeat(3),它返回decorator_repeat这个真正的装饰器,然后这个装饰器再应用到greet函数上。理解这个“三层嵌套”结构是掌握高级装饰器的关键。
类装饰器:如果一个类实现了__call__方法,它也可以作为装饰器使用。
class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.num_calls = 0 def __call__(self, *args, **kwargs): self.num_calls += 1 print(f"Call {self.num_calls} of {self.func.__name__}") return self.func(*args, **kwargs) @CountCalls def say_hello(): print("Hello!") say_hello() # 输出:Call 1 of say_hello \n Hello! say_hello() # 输出:Call 2 of say_hello \n Hello! print(say_hello.num_calls) # 输出:2类装饰器的好处是可以更方便地维护状态(如这里的调用次数num_calls)。
4. 面向对象编程:类与元类的深度
Python的OOP有其独特之处,深入理解其模型能让你写出更优雅、强大的代码。
4.1 实例属性、类属性与方法解析顺序
实例属性 vs 类属性:
- 实例属性定义在
__init__或实例方法中,以self.为前缀,每个实例独享。 - 类属性定义在类体中,所有实例共享。修改类属性会影响所有实例,这是一个常见的坑。
class Dog: tricks = [] # 类属性,所有Dog实例共享 def __init__(self, name): self.name = name def add_trick(self, trick): self.tricks.append(trick) # 这里修改的是类属性! d1 = Dog('Fido') d2 = Dog('Buddy') d1.add_trick('roll over') d2.add_trick('play dead') print(d1.tricks) # 输出:['roll over', 'play dead'] 两个狗的trick混在一起了!正确的做法是将tricks作为实例属性在__init__中初始化:self.tricks = []。
方法解析顺序(MRO)与super(): 在多重继承中,Python使用C3线性化算法来确定方法查找顺序,可以通过ClassName.__mro__查看。
class A: def say(self): print("A") class B(A): def say(self): print("B") super().say() # 调用MRO中的下一个类的say方法 class C(A): def say(self): print("C") super().say() class D(B, C): def say(self): print("D") super().say() print(D.__mro__) # 输出:(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) d = D() d.say() # 输出: # D # B # C # Asuper()并不是简单地调用父类,而是按照__mro__列表顺序调用“下一个”类。这使得协作式多重继承成为可能。在B.say中,super().say()调用的是C.say,而不是A.say,因为MRO顺序是D -> B -> C -> A。
4.2 魔法方法:让对象“活”起来
魔法方法(双下划线方法)是Python丰富对象行为的核心。
__new__和__init__:__new__是一个静态方法,负责创建对象(分配内存),并返回这个新对象。__init__是一个实例方法,负责初始化对象(设置初始状态)。__new__在__init__之前被调用。
class Singleton: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) # 调用object的__new__创建实例 return cls._instance # 永远返回同一个实例 def __init__(self, value): self.value = value # 注意:每次初始化都会覆盖value! a = Singleton(1) b = Singleton(2) print(a is b) # True print(a.value, b.value) # 输出:2 2 (因为第二次__init__覆盖了value)单例模式这个例子揭示了__new__和__init__的关系。__init__在每次实例化时都会被调用,即使返回的是同一个对象。因此,在单例模式中,要小心__init__重复初始化带来的副作用。
__str__和__repr__:__str__面向用户,追求可读性;__repr__面向开发者,追求明确性(理想情况下,eval(repr(obj)) == obj)。
class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point({self.x}, {self.y})" # 明确的、可复现的表示 def __str__(self): return f"({self.x}, {self.y})" # 简洁的、可读的表示 p = Point(1, 2) print(repr(p)) # 输出:Point(1, 2) print(str(p)) # 输出:(1, 2) print(p) # 默认调用 __str__,输出:(1, 2)__getitem__、__setitem__、__len__:让自定义类拥有序列或映射的行为。
class MyList: def __init__(self, data): self._data = list(data) def __getitem__(self, index): print(f"Getting item at {index}") return self._data[index] def __setitem__(self, index, value): print(f"Setting item at {index} to {value}") self._data[index] = value def __len__(self): return len(self._data) ml = MyList([1, 2, 3]) print(ml[1]) # 输出:Getting item at 1 \n 2 ml[1] = 99 # 输出:Setting item at 1 to 99 print(len(ml)) # 输出:3__call__:让实例可以像函数一样被调用。
class Adder: def __init__(self, n): self.n = n def __call__(self, x): return self.n + x add5 = Adder(5) print(add5(10)) # 输出:15 print(callable(add5)) # 输出:True这常用于创建有状态的函数或实现装饰器类。
4.3 元类:类的类
元类是Python中最“黑魔法”的概念之一。元类是类的类,它控制类的创建行为。type是所有类的默认元类。
使用type动态创建类:
# 传统方式 class MyClass: x = 10 # 动态创建方式 MyClass = type('MyClass', (), {'x': 10})type(name, bases, dict)三个参数:类名、继承的父类元组、类的属性字典。
自定义元类:通过继承type并重写__new__或__init__方法。
class Meta(type): def __new__(cls, name, bases, dct): # 在类创建前可以修改属性字典 dct['created_by_meta'] = True return super().__new__(cls, name, bases, dct) def __init__(self, name, bases, dct): super().__init__(name, bases, dct) # 类创建后可以做一些事情 self._registry = [] class MyClass(metaclass=Meta): pass print(MyClass.created_by_meta) # 输出:True print(MyClass._registry) # 输出:[]元类的典型应用场景包括:ORM框架(如Django的Model)、API验证、自动注册子类、强制编码规范等。例如,一个简单的注册元类:
class PluginRegistry(type): _plugins = [] def __new__(cls, name, bases, dct): new_class = super().__new__(cls, name, bases, dct) if name != 'BasePlugin': # 避免注册基类本身 cls._plugins.append(new_class) return new_class class BasePlugin(metaclass=PluginRegistry): pass class PluginA(BasePlugin): pass class PluginB(BasePlugin): pass print(PluginRegistry._plugins) # 输出:[<class '__main__.PluginA'>, <class '__main__.PluginB'>]所有继承BasePlugin的类都会被自动注册到PluginRegistry._plugins列表中,无需手动添加。这展示了元类如何介入类的创建过程,实现框架级别的控制。
5. 迭代器、生成器与协程:惰性求值的艺术
Python通过迭代器协议支持对容器元素的惰性访问,生成器则将其发挥到极致,而协程在此基础上实现了更复杂的控制流。
5.1 迭代器协议:__iter__与__next__
一个对象如果实现了__iter__()方法(返回一个迭代器对象)或__getitem__()方法(支持从0开始的整数索引),它就是可迭代的(Iterable)。for循环就是基于此。
迭代器(Iterator)则是一个实现了__iter__()(返回自身)和__next__()方法的对象。__next__()每次返回下一个元素,没有更多元素时抛出StopIteration异常。
class CountDown: def __init__(self, start): self.current = start def __iter__(self): return self # 迭代器必须返回自身 def __next__(self): if self.current <= 0: raise StopIteration num = self.current self.current -= 1 return num for num in CountDown(3): print(num) # 输出:3 2 1for循环的内部机制大致相当于:
iterator = iter(CountDown(3)) # 调用 __iter__ while True: try: num = next(iterator) # 调用 __next__ print(num) except StopIteration: break5.2 生成器:优雅的惰性序列
生成器是一种特殊的迭代器,它通过yield关键字定义,在每次迭代时“产出”一个值,并暂停执行,下次迭代时从暂停处继续。
生成器函数:
def fibonacci(limit): a, b = 0, 1 while a < limit: yield a # 产出当前值,并在此暂停 a, b = b, a + b gen = fibonacci(10) # 调用函数返回一个生成器对象,代码并未执行 print(next(gen)) # 输出:0, 执行到 yield a 暂停 print(next(gen)) # 输出:1, 从 a, b = b, a+b 开始,执行到 yield a 暂停 print(list(gen)) # 输出:[2, 3, 5, 8] 继续迭代直到结束生成器函数在调用时并不执行函数体,而是返回一个生成器对象。每次调用next(),函数执行到yield语句,返回其后的值并暂停。再次调用next(),从上次暂停处继续。
生成器表达式:更简洁的语法,类似于列表推导式,但用圆括号。
squares_gen = (x*x for x in range(10)) # 生成器表达式,惰性计算 squares_list = [x*x for x in range(10)] # 列表推导式,立即计算所有值 print(next(squares_gen)) # 输出:0 print(sum(squares_gen)) # 输出:285 (从1*1加到9*9,因为0已被取出)生成器表达式在处理大规模数据时能节省大量内存,因为它一次只产生一个值。
send()、throw()与close():生成器的高级通信与控制。
send(value):向生成器发送一个值,该值将成为当前yield表达式的结果,并返回生成器产出的下一个值。throw(type[, value[, traceback]]):在生成器暂停的yield处抛出一个异常。close():在生成器暂停处抛出GeneratorExit异常,终止生成器。
def accumulator(): total = 0 while True: value = yield total # yield total,并等待send进来的值赋给value if value is None: break total += value acc = accumulator() next(acc) # 启动生成器,执行到 yield total 暂停,返回0 print(acc.send(10)) # 发送10,value=10,total=10,执行到 yield total 暂停,返回10 print(acc.send(20)) # 发送20,total=30,返回30 acc.close() # 终止生成器这为生成器赋予了双向通信的能力,是理解协程的基础。
5.3 从生成器到协程:async/await的基石
协程(Coroutine)可以理解为更通用的、可多次进入和退出的子程序。在Python中,基于生成器的协程是asyncio库早期的基础。
生成器作为协程:
def simple_coroutine(): print("-> coroutine started") x = yield # 第一次send必须为None,或使用next()启动,产出None print(f"-> coroutine received: {x}") y = yield x + 10 print(f"-> coroutine received: {y}") coro = simple_coroutine() next(coro) # 或 coro.send(None), 输出:-> coroutine started, 执行到第一个yield暂停 result = coro.send(20) # 发送20给x,打印,执行到第二个yield暂停,产出 x+10=30 print(result) # 输出:30 coro.send(100) # 发送100给y,打印,协程结束,抛出StopIterationyield关键字在这里既用于产出值,也用于接收外部发送的值。这种模式使得生成器可以暂停、接收输入、继续执行,具备了协程的雏形。
yield from:用于简化在生成器中委托给另一个生成器的操作,也是早期实现异步IO的关键。
def chain(*iterables): for it in iterables: yield from it # 等价于 for item in it: yield item list(chain('AB', range(3))) # 输出:['A', 'B', 0, 1, 2]yield from subgen会建立调用方和子生成器subgen的直接通道,调用方可以直接send()值给subgen,subgen产出的值也直接返回给调用方。这为复杂的生成器嵌套和协程委托提供了清晰的语法。
现代Python中,原生的协程通过async def定义,使用await来挂起。但其核心思想——在等待IO时挂起并让出控制权——与生成器协程一脉相承。理解生成器的这些高级特性,对于深入理解asyncio的底层机制大有裨益。
6. 异常处理、上下文管理与描述符
健壮的程序离不开优雅的错误处理和资源管理,Python提供了强大的工具来实现这些。
6.1 异常处理:不仅仅是try...except
自定义异常:继承自Exception类。
class ValidationError(Exception): """自定义验证异常""" def __init__(self, message, field): super().__init__(message) self.field = field # 附加额外的上下文信息 def validate_age(age): if age < 0: raise ValidationError("Age cannot be negative", "age") if age > 150: raise ValidationError("Age seems unrealistic", "age") try: validate_age(-5) except ValidationError as e: print(f"Error in field '{e.field}': {e}")自定义异常可以携带更多关于错误上下文的信息,让上层处理逻辑更清晰。
else和finally子句:
else:当try块中没有异常抛出时执行。finally:无论是否发生异常,最终都会执行。常用于清理资源(如关闭文件、释放锁)。
def process_file(filename): f = None try: f = open(filename, 'r') data = f.read() except FileNotFoundError: print(f"File {filename} not found") return else: print(f"Successfully read {len(data)} bytes") return data finally: if f: f.close() # 确保文件总是被关闭 print("Cleanup done")else子句将正常逻辑与异常处理分离,使代码结构更清晰。finally是资源安全的最后保障。
异常链:使用raise ... from ...保留原始异常信息。
def inner(): raise ValueError("Inner error") def outer(): try: inner() except ValueError as e: raise RuntimeError("Outer error") from e try: outer() except RuntimeError as e: print(e) # 输出:Outer error print(e.__cause__) # 输出:ValueError: Inner error这在封装底层异常为更高级别的业务异常时非常有用,调试时能追溯到根本原因。
6.2 上下文管理器与with语句
上下文管理器用于在进入和退出某个代码块时执行特定的设置和清理操作,最典型的例子是文件操作with open(...) as f:。
实现上下文管理器:一个类需要实现__enter__和__exit__方法。
class ManagedFile: def __init__(self, filename, mode): self.filename = filename self.mode = mode self.file = None def __enter__(self): print(f"Opening file {self.filename}") self.file = open(self.filename, self.mode) return self.file # as 子句得到的对象 def __exit__(self, exc_type, exc_val, exc_tb): print(f"Closing file {self.filename}") if self.file: self.file.close() # 如果返回True,则with块中的异常会被抑制,否则异常会继续传播 return False with ManagedFile('test.txt', 'w') as f: f.write('Hello') # 无论是否发生异常,文件都会被关闭 # 输出: # Opening file test.txt # Closing file test.txt__exit__方法接收三个参数:异常类型、异常实例、回溯对象。如果没有异常发生,它们都是None。
使用contextlib.contextmanager装饰器:更简洁的方式创建上下文管理器。
from contextlib import contextmanager import time @contextmanager def timer(): start = time.perf_counter() try: yield # 在此处进入with块 finally: end = time.perf_counter() print(f"Elapsed time: {end - start:.4f} sec") with timer(): time.sleep(1) # 输出:Elapsed time: 1.0012 sec被@contextmanager装饰的函数必须是一个生成器,且只yield一次。yield之前的部分相当于__enter__,之后的部分(在finally中)相当于__exit__。
6.3 描述符:属性访问的幕后推手
描述符是实现了__get__、__set__或__delete__方法的类。它允许你自定义属性访问、赋值和删除的行为。这是实现@property、类方法、静态方法等高级特性的基础。
一个简单的验证描述符:
class PositiveNumber: def __init__(self, name): self.name = name # 属性名 def __get__(self, obj, objtype=None): if obj is None: return self # 通过类访问时,返回描述符实例本身 return obj.__dict__.get(self.name, 0) # 从实例的__dict__中获取值 def __set__(self, obj, value): if value <= 0: raise ValueError(f"{self.name} must be positive") obj.__dict__[self.name] = value # 将值存储在实例的__dict__中 def __delete__(self, obj): del obj.__dict__[self.name] class Order: quantity = PositiveNumber('quantity') # 描述符实例,作为类属性 price = PositiveNumber('price') def __init__(self, quantity, price): self.quantity = quantity # 触发 PositiveNumber.__set__ self.price = price def total(self): return self.quantity * self.price order = Order(5, 10) print(order.total()) # 输出:50 order.quantity = -1 # 触发 ValueError: quantity must be positive当访问order.quantity时,Python发现quantity是类Order的一个描述符属性,于是调用PositiveNumber.__get__(quantity, order, Order)。赋值操作同理。描述符将验证逻辑集中在了PositiveNumber类中,实现了关注点分离。
@property的本质:property本身就是一个描述符。
# 使用property装饰器 class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value <= 0: raise ValueError("Radius must be positive") self._radius = value @property def area(self): return 3.14159 * self._radius ** 2 # 等价的手动property实现 class CircleManual: def __init__(self, radius): self._radius = radius def get_radius(self): return self._radius def set_radius(self, value): if value <= 0: raise ValueError("Radius must be positive") self._radius = value radius = property(get_radius, set_radius) # property是一个描述符类 @property def area(self): return 3.14159 * self._radius ** 2property(fget=None, fset=None, fdel=None, doc=None)返回一个描述符对象。@property装饰器只是语法糖,它创建了一个property描述符并将其绑定到类属性上。理解描述符,你就理解了Python属性访问的整个魔法体系。