Python面试(八股)
1. 可变对象和不可变对象
(1). 不可变对象( Immutable Objects
)
不可变对象指的是那些一旦创建后其内容就不能被修改的对象。如果尝试修改不可变对象的内容,将会创建一个新的对象而不是修改原来的对象。常见的不可变类型包括:
- 数字类型:
int, float, complex
- 字符串:
str
- 元组:
tuple
- 冻结集合:
frozenset
可以理解为对象的内容一旦修改(改变),对象的内存地址改变
s = "Hello"
print(id(s)) # 输出原始字符串的内存地址
s += ", World!"
print(id(s)) # 输出新字符串的内存地址,与之前不同
(2). 可变对象
可变对象是指在其创建之后还可以对其内容进行修改的对象。这意味着你可以在不改变对象身份的情况下更改它的内容。常见的可变类型包括:
列表: list
字典: dict
集合: set
**自定义类实例:**除非特别设计为不可变
lst = [1, 2, 3]
print(id(lst)) # 输出列表的内存地址
lst.append(4)
print(id(lst)) # 内存地址保持不变,说明是原地修改
2. @staticmethod和@classmethod
(1). @staticmethod
定义:使用@staticmethod
装饰的方法不接受隐式的第一个参数(如self
或cls
)。这意味着这些方法既不能访问实例属性也不能访问类属性。
用途:通常用于那些与类有关但不需要访问类或实例内部数据的功能。 这样的方法更像是普通的函数,只是由于组织上的原因被放在了类中。
特点:
- 不能访问实例属性: 由于静态方法没有self参数,所以无法访问任何与特定对象实例相关的属性。
- 不能访问类属性: 同样地,因为没有cls参数,静态方法也无法直接访问类级别的属性。
class Example:
class_var = "I am a class variable" # 类变量
def __init__(self, value):
self.instance_var = value # 实例变量
@staticmethod
def static_method():
# 下面这两行会导致错误,因为静态方法无法访问实例或类属性
# print(self.instance_var) # AttributeError: 'staticmethod' object has no attribute 'instance_var'
# print(class_var) # NameError: name 'class_var' is not defined
print("This is a static method.")
def instance_method(self):
print(f"Instance variable: {self.instance_var}")
print(f"Class variable: {Example.class_var}")
# 创建实例
ex = Example("I am an instance variable")
# 调用静态方法
Example.static_method() # 输出: This is a static method.
# 调用实例方法
ex.instance_method()
# 输出:
# Instance variable: I am an instance variable
# Class variable: I am a class variable
在这个例子中,my_static_method
是一个静态方法,它可以直接通过类名调用,不需要创建类的实例。
(2). @classmethod
定义: 使用@classmethod
装饰的方法接收一个隐含的第一个参数,这个参数通常是cls,代表类本身。因此,类方法可以访问和修改类级别的属性,也可以调用其他类方法。
用途: 常用于需要操作类级别数据的方法,或者当你需要从该方法返回类的不同子类时很有用。
示例 1: 访问和修改类级别属性
假设我们有一个Person
类,其中包含一个类级别的属性count
,用于记录创建了多少个Person
对象。我们可以使用类方法来更新这个计数器。
class Person:
count = 0 # 类变量,用于跟踪创建了多少个Person对象
def __init__(self, name):
self.name = name
Person.count += 1 # 每当创建一个新的实例时增加计数
@classmethod
def get_count(cls):
return cls.count # 使用cls访问类变量
# 创建一些Person实例
p1 = Person("Alice")
p2 = Person("Bob")
# 使用类方法获取当前的计数
print(Person.get_count()) # 输出应该是2,因为创建了两个实例
在这个例子中,get_count
是一个类方法,它可以通过cls
访问类级别的属性count
,而无需实例化Person
类。
示例 2: 提供替代构造函数
有时候,你可能希望提供多种方式来创建类的实例。你可以利用类方法作为“工厂方法”,为不同的需求提供不同的构造逻辑。
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # 返回一个新实例
# 使用标准构造函数
date1 = Date(2023, 4, 1)
# 使用类方法提供的替代构造函数
date2 = Date.from_string("2023-04-01")
print(date1.year, date1.month, date1.day) # 输出:2023 4 1
print(date2.year, date2.month, date2.day) # 输出:2023 4 1
这里,from_string
类方法允许用户从字符串格式的数据创建Date
对象,这增加了灵活性。
示例 3: 调用其他类方法
类方法还可以调用其他的类方法或静态方法,这在需要链式操作或者复用已有逻辑的情况下非常有用。
class MathOperations:
@classmethod
def add(cls, a, b):
return a + b
@classmethod
def multiply(cls, a, b):
return a * b
@classmethod
def combined_operation(cls, a, b):
sum_result = cls.add(a, b)
product_result = cls.multiply(a, b)
return sum_result, product_result
result = MathOperations.combined_operation(5, 3)
print(result) # 输出:(8, 15),分别是加法和乘法的结果
在上面的例子中,combined_operation
类方法内部调用了另外两个类方法add
和multiply
来完成一系列计算。
3. 迭代器和生成器
(1) 迭代器(Iterator
)
迭代器是一个可以记住遍历位置的对象,它从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。 在Python
中,要创建一个迭代器对象,需要实现两个方法:__iter__()
和 __next__()
(1.1) 内置迭代器:
序列迭代器: 列表、元组、字符串等序列类型都有默认的迭代器。
my_list = [1, 2, 3]
iterator = iter(my_list)
print(next(iterator)) # 输出: 1
字典视图迭代器: 如 .keys()
, .values()
, .items()
返回的都是迭代器对象。
python
深色版本
my_dict = {'a': 1, 'b': 2}
keys_iterator = iter(my_dict.keys())
print(next(keys_iterator)) # 输出: 'a'
文件迭代器: 打开的文件对象也是迭代器,可用于逐行读取文件内容。
with open('example.txt', 'r') as file:
for line in file:
print(line.strip())
(1.2) 自定义迭代器
- iter():返回迭代器对象本身。
- next():返回容器中的下一个值。如果没有更多的元素可供返回,则抛出 StopIteration 异常。 迭代器的一个重要特性是可以节省内存,因为它不需要一次性加载整个数据集到内存中,而是按需生成数据。
class MyIterator:
def __init__(self, max_value):
self.max_value = max_value
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current < self.max_value:
value = self.current
self.current += 1
return value
else:
raise StopIteration
# 使用自定义迭代器
my_iter = MyIterator(3)
for i in my_iter:
print(i) # 输出: 0, 1, 2
(2) 生成器(Generator
)
(2.1) 生成器函数
生成器是一种特殊的迭代器,它是通过函数来创建的,但是与普通函数不同的是,生成器使用了 yield
关键字而不是 return
。每当生成器函数执行到 yield
语句时,它会暂停并保存当前的所有状态,然后返回 yield 的值给调用者。当后续再次调用生成器时,它会从上次离开的地方继续执行 。这种机制使得生成器非常适合处理大数据集或惰性计算(lazy evaluation
),因为它不需要一次性加载所有数据到内存中。
yield 与 return 的区别:
return
:一 旦执行了 return
语句,函数就会结束,并且所有的局部变量都会被销毁。
yield
: 每当执行到 yield
语句时,函数会暂停并返回一个值给调用者,但是函数的状态会被保存下来,下次调用时可以从上次暂停的地方继续执行。
def generator(n):
for i in range(n):
print("before yield")
yield i
print("after yield")
gen = generator(3)
print(next(gen)) # 第一次调用next
print("---")
# 使用for循环遍历剩余的元素 自动调用__next__
# 第二、第三次都是在这个下面调用并打印
for i in gen:
print(i)
完整输出结果:
before yield
0
---
after yield
before yield
1
after yield
before yield
2
after yield
(2.2) 生成器表达式
生成器表达式提供了一种简洁的方式来创建生成器,类似于列表推导式的语法,但使用圆括号 ()
而不是方括号 []
。与列表推导式不同的是,生成器表达式不会一次性生成所有元素并存储在内存中,而是按需生成每个元素。
gen_exp = (x*x for x in range(5))
print(next(gen_exp)) # 输出: 0
print(next(gen_exp)) # 输出: 1
print(next(gen_exp)) # 输出: 4
# 继续打印剩余的平方数...
4. 装饰器
Python
装饰器(Decorator
)是一种用于修改函数或方法行为的高级特性。它本质上是一个返回函数的函数,通常用于在不改变原函数定义的情况下,为函数添加新的功能。装饰器广泛应用于日志记录、访问控制、性能测量等场景。
4.1 基本概念
装饰器的基本语法是使用 @decorator_name
语法语法糖(Syntactic Sugar
)将装饰器应用到一个函数或方法上。例如:
@my_decorator
def my_function():
print("执行函数")
这相当于下面的代码:
def my_function():
print("执行函数")
my_function = my_decorator(my_function)
简单示例
以下是一个简单的装饰器示例,该装饰器会在调用函数前后打印消息:
def simple_decorator(func):
def wrapper():
print("函数之前的操作")
func()
print("函数之后的操作")
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
输出结果将是:
函数之前的操作
Hello!
函数之后的操作
4.2 带参数的装饰器
如果需要装饰的函数带有参数,可以通过在 wrapper
函数中使用 *args
和 **kwargs
来处理任意数量的位置参数和关键字参数:
def decorator_with_arguments(func):
def wrapper(*args, **kwargs):
print("函数之前的操作")
result = func(*args, **kwargs)
print("函数之后的操作")
return result
return wrapper
@decorator_with_arguments
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
输出结果将是:
函数之前的操作
Hello, Alice!
函数之后的操作
4.3 带参数的装饰器工厂
有时你可能希望装饰器本身也接受参数。这时可以创建一个装饰器工厂,即一个返回装饰器的函数:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(3)
def say_hello():
print("Hello!")
say_hello()
这段代码会让 say_hello
函数执行三次。
4.4 类装饰器
除了函数装饰器外,还可以使用类作为装饰器。为此,你需要实现 __call__()
方法,使得类实例可调用:
class ClassDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("函数之前的操作")
result = self.func(*args, **kwargs)
print("函数之后的操作")
return result
@ClassDecorator
def say_goodbye():
print("Goodbye!")
say_goodbye()
5. 深拷贝和浅拷贝
5.1 浅拷贝(Shallow Copy
)
浅拷贝创建一个新的对象,但不递归地复制嵌套的对象。换句话说,原对象和新对象共享嵌套对象的引用。
特点
- 创建一个新对象。
- 新对象包含对原始对象中元素的引用,而不是这些元素的副本。
- 如果原始对象中的元素是可变对象(如列表、字典),则新旧对象共享这些可变对象。
import copy
original_list = [[1, 2], [3, 4]]
shallow_copied_list = copy.copy(original_list)
# 修改浅拷贝中的一个子列表
shallow_copied_list[0][0] = 'X'
print("Original List:", original_list) # 输出: Original List: [['X', 2], [3, 4]]
print("Shallow Copied List:", shallow_copied_list) # 输出: Shallow Copied List: [['X', 2], [3, 4]]
可以看到,修改浅拷贝中的子列表也影响了原始列表,因为它们共享相同的子列表对象。
5.2 深拷贝(Deep Copy
)
深拷贝不仅创建一个新的对象,还会递归地复制所有嵌套的对象。这意味着新对象和原始对象完全独立,没有任何共享的引用。
特点
- 创建一个新对象。
- 新对象包含原始对象中所有元素的副本,包括嵌套对象的所有层级。
- 原始对象和新对象之间没有共享的引用。
import copy
original_list = [[1, 2], [3, 4]]
deep_copied_list = copy.deepcopy(original_list)
# 修改深拷贝中的一个子列表
deep_copied_list[0][0] = 'X'
print("Original List:", original_list) # 输出: Original List: [[1, 2], [3, 4]]
print("Deep Copied List:", deep_copied_list) # 输出: Deep Copied List: [['X', 2], [3, 4]]
6 lambda
函数
lambda
函数的基本语法如下:
lambda 参数1, 参数2, ... : 表达式
- 参数:可以有多个参数,用逗号分隔。
- 表达式: 是一个单一的表达式,而不是一个代码块。
lambda
函数会返回该表达式的值。 - 快速理解
lambda
函数的一个有效方法是明确其输入(参数)和输出(表达式的结果)。你可以将 lambda 函数视为一个简单的函数定义,并且只关注它的输入和输出。
示例
6.1 无参数的 lambda
函数:
f = lambda: "Hello, World!"
print(f()) # 输出: Hello, World!
6.2 带参数的 lambda
函数:
add = lambda x, y: x + y
print(add(5, 3)) # 输出: 8
6.3 带有默认参数的 lambda
函数:
power = lambda x, n=2: x ** n
print(power(2)) # 输出: 4 (2^2)
print(power(2, 3)) # 输出: 8 (2^3)
6.4 使用条件表达式的 lambda
函数:
max_value = lambda a, b: a if a > b else b
print(max_value(10, 20)) # 输出: 20
使用场景
lambda
函数最常用于需要将一个小函数作为参数传递给其他函数的场合,比如高阶函数(如 map(), filter(), sorted()
等)。
1. map()
函数
map()
函数可以对可迭代对象中的每个元素应用一个函数,并返回一个新的迭代器。
numbers = [1, 2, 3, 4]
squared_numbers = map(lambda x: x ** 2, numbers)
print(list(squared_numbers)) # 输出: [1, 4, 9, 16]
2. filter()
函数
filter()
函数可以根据指定条件过滤可迭代对象中的元素。
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # 输出: [2, 4, 6]
3. sorted()
函数
sorted()
函数可以根据指定的关键字对可迭代对象进行排序。
students = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 20},
{"name": "Charlie", "age": 22}
]
sorted_students = sorted(students, key=lambda student: student["age"])
print(sorted_students)
# 输出: [{'name': 'Bob', 'age': 20}, {'name': 'Charlie', 'age': 22}, {'name': 'Alice', 'age': 25}]
4. 在列表推导式中使用 lambda
虽然列表推导式本身已经很简洁了,但在某些情况下结合 lambda
函数可以进一步简化代码。
numbers = [1, 2, 3, 4]
doubled = [(lambda x: x * 2)(n) for n in numbers]
print(doubled) # 输出: [2, 4, 6, 8]
7. Python垃圾回收机制
Python
的垃圾回收机制(Garbage Collection, GC
)主要用于自动管理内存,释放不再使用的对象所占用的内存资源。理解 Python
的垃圾回收机制有助于编写更高效的代码,并避免内存泄漏等问题。
7.1 引用计数(Reference Counting
)
这是 Python 最基本的垃圾回收机制。每个对象都有一个引用计数器,记录当前有多少个引用指向该对象。当引用计数变为零时,说明没有其他对象在使用它,可以安全地释放其占用的内存。
工作原理
- 当一个对象被创建并赋值给一个变量时,该对象的引用计数加一。
- 当有新的变量引用同一个对象时,引用计数再次加一。
- 当某个变量不再引用该对象时,引用计数减一。
- 当引用计数降为零时,
Python
自动调用该对象的__del__
方法(如果定义了),然后释放其占用的内存。
7.2 循环引用检测(Cycle Detection
)
虽然引用计数机制简单高效,但它无法处理循环引用的情况。例如,两个对象相互引用形成一个闭环,即使这些对象已经没有任何外部引用,它们的引用计数也不会降为零,从而导致内存泄漏。
为了解决这个问题,Python
使用了一个基于 标记-清除(Mark-and-Sweep
) 和 分代收集(Generational Garbage Collection
) 的算法来检测和清理循环引用。
工作原理
标记-清除算法分为两个主要阶段:标记阶段 和 清除阶段。
标记阶段:
- 从根对象(如全局变量、活动栈帧等)开始遍历所有可达对象,并将这些对象标记为活跃对象。
- 根对象是指那些始终可以被访问的对象,例如当前正在使用的变量、函数调用栈中的局部变量等。
清除阶段:
- 扫描整个堆内存,找到未被标记的对象并将其删除。
- 清除阶段会释放这些对象占用的内存,并将其返回给可用内存池。
假设我们有以下对象图:
A -> B
B -> C
C -> D
D -> E
E -> A (循环引用)
在这个例子中,A -> B -> C -> D -> E -> A
形成了一个循环引用。如果没有其他外部引用指向这些对象,那么即使引用计数不会降为零,这些对象也应该被回收。
标记阶段:
- 从根对象开始遍历,假设只有
A
被根对象引用。 - 遍历
A
,发现它引用了B
,标记B
。 - 遍历
B
,发现它引用了C
,标记C
。 继续遍历 C
和D
,标记D
和E
。- 最终,所有对象都被标记为活跃对象。
清除阶段:
- 扫描整个堆内存,发现没有未被标记的对象,因此不需要清除任何对象。
如果根对象不再引用 A
,则在标记阶段无法到达 A
及其循环引用链上的对象,这些对象会被标记为不可达并在清除阶段被删除。
分代回收(Generational Garbage Collection
)
分代回收是一种优化策略,基于一个观察:大多数对象在创建后很快就会被销毁,而那些存活较长时间的对象不太可能被销毁。因此,Python
将对象分为三个世代(Generation
),分别是第 0 代、第 1 代和第 2 代。
工作原理
-
(1) 对象分配与提升:
- 新创建的对象属于第
0
代。 - 当第
0
代对象经过一次垃圾回收后仍然存活,它会被提升到第1
代。 - 类似地,第
1
代对象经过垃圾回收后仍然存活,则会被提升到第2
代。
- 新创建的对象属于第
-
(2) 垃圾回收频率:
- 第
0
代对象的垃圾回收频率最高,因为它们最有可能被快速销毁。 - 第
1
代和第2
代对象的垃圾回收频率逐渐降低,因为它们更有可能长期存活。
- 第
-
(3) 阈值设置:
- 每个世代都有一个阈值,表示在该世代对象数量达到一定值时触发垃圾回收。
- 默认的阈值可以通过
gc.get_threshold()
获取,并且可以通过gc.set_threshold(gen0, gen1, gen2)
进行设置。
8 多线程与多进程
在 Python
中,多线程(Multithreading
)和多进程(Multiprocessing
)是两种常见的并行编程方法,用于提高程序的性能和响应速度。尽管它们都旨在实现并发执行,但它们的工作原理和适用场景有所不同。
CPU
密集型任务: CPU
密集型任务是指那些主要依赖于 CPU
计算能力的任务。这类任务通常需要大量的计算资源,包括复杂的数学运算、数据处理、图像处理、视频编码等。
I/O
密集型任务: I/O
密集型任务是指那些主要依赖于外部输入输出操作的任务。这类任务通常涉及大量的文件读写、网络请求、数据库查询等。
8.1 多进程
基本概念
多进程是指在一个程序中同时运行多个进程。每个进程都有独立的内存空间和解释器实例, 因此它们可以真正并行执行任务,不受 GIL 的限制。
工作原理
8.1.1进程间通信(IPC
):
- 进程之间不能直接共享内存,必须通过特定的机制进行通信,如
Queue
、Pipe
或Manager
对象。 multiprocessing.Queue
提供了一个线程和进程安全的队列,用于在不同进程之间传递数据。multiprocessing.Pipe
提供了双向通信通道,适用于父子进程之间的通信。
8.1.2 创建和管理进程:
- 使用
multiprocessing.Process
类来创建和管理进程。 - 进程可以通过调用
start()
方法开始执行,并通过join()
方法等待进程完成。
示例代码
python
深色版本
import multiprocessing
import time
def print_numbers(queue):
for i in range(5):
queue.put(f"数字: {i}")
time.sleep(1)
def print_letters(queue):
for letter in 'ABCDE':
queue.put(f"字母: {letter}")
time.sleep(1)
if __name__ == "__main__":
# 创建一个队列用于进程间通信
queue = multiprocessing.Queue()
# 创建进程
p1 = multiprocessing.Process(target=print_numbers, args=(queue,))
p2 = multiprocessing.Process(target=print_letters, args=(queue,))
# 启动进程
p1.start()
p2.start()
# 从队列中读取数据并打印
while True:
if not queue.empty():
print(queue.get())
if not p1.is_alive() and not p2.is_alive():
break
# 等待两个进程完成
p1.join()
p2.join()
8.2 多线程(Multithreading
)
基本概念
多线程是指在一个进程中同时运行多个线程。每个线程都是一个独立的执行路径,可以并发执行不同的任务。Python
的 threading
模块提供了对多线程的支持。
工作原理
(1)GIL(Global Interpreter Lock)
:
CPython
解释器使用GIL
来确保同一时刻只有一个线程在执行Python
字节码(Python
源代码被编译成字节码,这是一种低级的中间表示形式,由Python
虚拟机解释执行)。这意味着即使有多个线程,它们也不能真正并行执行CPU
密集型任务。- 但是,对于
I/O
密集型任务(如文件读写、网络请求等),多线程仍然可以提高效率,因为这些任务在等待I/O
操作时会释放GIL
,允许其他线程继续执行。
(2)创建和管理线程:
- 使用
threading.Thread
类来创建和管理线程。 - 线程可以通过调用
start()
方法开始执行,并通过join()
方法等待线程完成。
知识扩展
GIL 的作用
- 简化内存管理: GIL 简化了 Python 内存管理的设计,使得解释器不需要处理复杂的线程同步问题。
- 保护内置数据结构: 许多 Python 内置的数据结构和库并不是线程安全的,GIL 提供了一种简单的保护机制,防止多个线程同时修改这些数据结构。
为什么说 Python 的多线程是“假的”
- 无法实现真正的并行计算:由于 GIL 的存在,多线程不能真正并行执行 CPU 密集型任务。即使在多核 CPU 上,也只能有一个线程在执行Python 字节码,这与我们通常理解的多线程并行计算相悖。
- 增加了上下文切换的开销:在某些情况下,特别是当线程频繁切换时,上下文切换的开销可能会导致性能下降,甚至比单线程执行还要慢。
- 误导性:初学者可能会误以为使用多线程可以显著提升程序的性能,尤其是在 CPU 密集型任务中,但实际上效果并不明显,甚至可能适得其反。
示例代码
import threading
import time
def print_numbers():
for i in range(5):
print(f"数字: {i}")
time.sleep(1)
def print_letters():
for letter in 'ABCDE':
print(f"字母: {letter}")
time.sleep(1)
# 创建线程
t1 = threading.Thread(target=print_numbers)
t2 = threading.Thread(target=print_letters)
# 启动线程
t1.start()
t2.start()
# 等待两个线程完成
t1.join()
t2.join()