Joshua Chen Personal Blog

Back

📝 Note ✅ Ready · interview python 八股文

面试知识补充-Python八股

记录一下Python的一些基础知识

views | comments

1. Python 的基本数据类型#

Number 数字,String 字符串,Bool 布尔类型,List 列表,Tuple 元组,Set 集合,Dictionary 字典

其中,不可变数据:Number、String、Bool、Tuple 可变:List、Dictionary、Set

2. Python 的解释器和编译器的区别是什么#

解释器:逐行执行代码,每次执行的时候都需要解释,这意味着代码直接运行,无需先编译。Python 通常被认为是一种解释性语言,因为 Python 在执行时会被解释器逐行转为机器码,这使得开发过程更加灵活,但是牺牲了速度。

编译器:将源码整体编译成机器语言代码,然后执行,这个过程只发生一次,之后可以直接运行编译后的代码,通常运行速度比解释执行快,例如 C 和 Java。

这句话是直接抄的,翻译一下就是:

Java:先证明这段操作在类型规则上基本合理,再允许运行。
Python:先确认语法合法,运行时再看当前对象能不能完成这个操作。
text

3. 深拷贝和浅拷贝#

浅拷贝:创建一个新的对象,但是包含原始对象中包含的引用。 深拷贝:创建一个新的对象,并且递归复制原始对象中的所有项,包括复合对象以及其中包含的所有子对象。

由于深拷贝会复制对象中包含的所有子对象,如果对象结构非常大,这可能会导致性能问题。

换句话说就是:

浅拷贝会创建一个新的外层对象,但外层对象中的子对象引用仍指向原来的子对象。
因此修改共享子对象会同时影响两边;如果只是将原对象中的某个引用重新指向另一个对象,则不会影响复制对象。
深拷贝则会递归复制内部子对象,因此两边通常互相独立。
text

4. Python 如何管理内存#

Python 使用自动内存管理和垃圾回收机制来帮助程序员管理内存,主要机制包括:

  • 引用计数:Python 内部维护了一个引用计数器,用来确保对象被需要时保存在内存中,不再需要时释放。
  • 垃圾回收:对于循环引用的情况,引用计数器无能为力,因此 Python 的垃圾回收器会定期运行,检测并且清除循环引用的对象。

解释一下:函数结束时,局部引用会被移除;对象是否销毁,要看还有没有其他引用指向它。

例如局部变量:

def test():
    data = [1, 2, 3]
python

调用 test() 的时候,局部变量 data 引用列表对象,函数结束以后局部变量 data 被移除,因此列表的对象引用计数减 1,若计数为 0 就会释放。

def test():
    data = [1, 2, 3]
    return data

result = test()
python

这种情况,result 会保存 test 的结果,因此引用会从 test() -> data 变成 result -> data,所以这时候对象仍然存在。

循环引用#

a = []
b = []

a.append(b)
b.append(a)
python

这里删除外部变量 a 和 b 以后,内部仍然互相引用:

列表A ──→ 列表B
  ↑          │
  └──────────┘
text

因此引用计数可能会变成列表 A 和列表 B 都为 1,单靠计数无法释放。 这里不是无限递增,而是两个对象互相撑住对方的引用计数,导致无法归零,这时候需要垃圾回收器。

垃圾回收器#

垃圾回收器关心的是:这一组互相引用的对象,外部是否还能访问。 只要能从程序中仍在使用的引用走到它,就是可达对象,不能清理;如果没有任何引用就是不可达对象,可以清理。

什么时候回收?#

Python 用分代回收,根据对象分配和释放情况,在达到一定阈值以后触发检查。 新创建的对象为年轻代,如果存活较久则变为更老的一代。

循环垃圾回收主要关注可能形成引用环的容器对象,例如:

  • List
  • dict(字典)
  • set
  • 自定义类实例
  • 其他能引用对象的容器

而整数、字符串等不可变对象一般不会形成类似的对象引用环。

5. GIL (Global Interpreter Lock) 是什么#

GIL 是一个互斥锁,叫全局解释器锁,保证同一时刻只有一个线程可以执行 Python 字节码,这是因为 Python 解释器的内存管理并不是线程安全的。GIL 简化了 Python 的设计和实现,也限制了程序在多个处理器上的并行执行能力。

把一个普通的 Python 进程想象成一个工作室:

           唯一的工作证:GIL

        ┌───────────┼───────────┐
        │           │           │
      线程 A      线程 B      线程 C
text

线程可以有很多个,但是要进入”执行 Python 字节码的工作区”,通常必须先拿到唯一的工作证:

A 拿证执行 → 交还
B 拿证执行 → 交还
C 拿证执行 → 交还
text

遇到网络、文件等等待操作时,A 等待 I/O,先把工作证交出来,B 趁机执行 Python 代码。

也就是说:

GIL 是常规 CPython 为保护解释器和 Python 对象内部状态而设置的一把全局锁,它让同一进程中通常只有一个线程执行 Python 字节码,因此限制了 CPU 密集型多线程的并行能力,但不会让 I/O 多线程失去价值,也不能代替业务层面的线程锁。

6. Python 的列表和元组有什么区别#

  • 列表 (List):动态数组,可变类型,可以增加、删除或者修改列表中的元素。
  • 元组 (Tuple):不可变序列,一旦创建就不能修改,因为不可变性,元组可以作为字典的键,而列表不行。
  • 字典 (dict):一种按照「键 → 值」保存数据的数据结构,相当于通讯录,一个姓名对应一个电话号码。

在 Python 中写成:

contacts = {
    "张三": "13800",
    "李四": "13900",
}
python

这里张三是键(key),13800 是值(value),张三: 13800 是一组键值对。

字典用来干嘛?#

列表通过位置查找元素:

names = ["张三", "李四", "王五"]

print(names[0])  # 张三
python

这里要记住张三在第 0 个位置。

字典则通过名字或编号查找:

user = {
    "name": "张三",
    "age": 18,
    "school": "天津理工大学",
}

print(user["name"])  # 张三
print(user["age"])   # 18
python

也就是说:

列表:位置 → 数据
字典:键 → 数据
text

为什么列表不能作为字典的键?#

因为字典要求键必须是可哈希的,可以理解为:字典的键必须是稳定且不会变化的,Python 才能快速定位它对应的值。

不可变类型(字符串、整数和元组)都可以作为键,但是列表是可变的。如果列表作为字典的键,字典刚刚把它记录在一个位置,列表内容又发生了变化,Python 就找不到它了。

注意:元组只有在内部所有元素都可哈希时,才能作为字典的键。比如元组内部还有一个列表的时候,就没法作为字典的键。

7. Python 的装饰器是什么#

装饰器是一种特殊函数,用于在不修改原有函数代码的情况下增加新的功能,装饰器通过 @ 符号使用,放在一个函数定义之前。

在不修改原函数代码的情况下,给函数额外增加一层行为。

最常见的用途包括:记录日志、检查登录权限、统计运行时间、参数校验、缓存结果、捕获异常。

例如,原函数是:

def say_hello():
    print("你好")
python

现在希望它执行前后分别打印日志:

开始执行
你好
执行结束
text

最直接的方式是修改原函数,但如果有 100 个函数要修改则会非常麻烦。这里装饰器的思路是:

原函数

套上一层公共外壳

得到增强后的函数
text

写一个最基础的装饰器:

def log_decorator(func):
    def wrapper():
        print("开始执行")
        func()
        print("执行结束")
    return wrapper
python

使用方法是:

@log_decorator
def say_hello():
    print("你好")
python

直接调用 say_hello() 会依次输出:

开始执行
你好
执行结束
text

这里的 @log_decorator 就是在给 say_hello 套上装饰器。

装饰器本质上做了什么?#

装饰器写法是:

@log_decorator
def say_hello():
    print("你好")
python

而手动写法是:

def say_hello():
    print("你好")

say_hello = log_decorator(say_hello)
python

这里 say_hello = log_decorator(say_hello) 的过程是:

原来的 say_hello 函数

传给 log_decorator

log_decorator 返回 wrapper

变量 say_hello 改为指向 wrapper
text

所以装饰以后 say_hello() 实际上调用的是 wrapper(),然后 wrapper 内部又会调用原来的函数 func()

为什么函数可以被传进去?因为在 Python 中,函数也是对象,可以把函数赋值给变量,也可以把函数作为参数,也可以从另一个函数中返回函数。

例如:

def create_function():
    def inner():
        print("内部函数")
    return inner

func = create_function()
func()
python

这里 inner 是内部函数,调用 create_function() 会得到 inner 函数本身。

所以装饰器利用的就是这三个能力:函数可以被传入、函数可以被保存、函数可以被返回

如果原函数需要参数,装饰器也必须接收这些参数。

通用装饰器:*args 和 **kwargs#

如果装饰器不知道会接受多少参数,可以写:

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print("开始执行")
        result = func(*args, **kwargs)
        print("执行结束")
        return result
    return wrapper
python

这里 *args 收集位置参数,**kwargs 收集关键字参数。

一个完整的通用装饰器是:

def decorator(func):
    def wrapper(*args, **kwargs):
        # 原函数执行前的逻辑

        result = func(*args, **kwargs)

        # 原函数执行后的逻辑

        return result

    return wrapper
python

这里一定要 return,否则原函数虽然会返回数值,但是 wrapper 这个外层函数不会把这个值继续返回出去。

这里用的也是闭包的特性。

FastAPI 中的 @app.get 也是装饰器#

@app.get("/users")
def get_users():
    return [{"name": "Joshua"}]
python

这里的 @app.get("/users") 就是带参数的装饰器,它做的事情大致是:把 get_users 函数注册为 GET /users 的处理函数。当服务器收到 GET /users 时,FastAPI 就知道要调用 get_users()

因此装饰器不仅可以包裹函数,还可以在框架中完成注册、记录和配置。

FastAPI 是如何用装饰器的#

@app.get(...) 不是在调用接口函数

而是在程序启动时填写一张路由登记表:

"以后谁带着 GET /users/123 来,
就把请求翻译成 Python 参数,
再交给 get_user 函数。"
text

注册 → 记录 → 配置

8. 生成器#

一种不会一次性把所有结果都算出来,而是每次需要时才生产一个结果的对象。

核心关键词是 yield,做了什么?

返回一个结果,但先不要结束函数;把当前执行位置和局部变量保存下来,下次继续往下运行。

例如:

def demo():
    print("开始")
    yield 10

    print("继续")
    yield 20

    print("结束")
python

调用 g = demo(),现在还不会打印任何内容。

第一次print(next(g))

进入函数
打印"开始"
遇到 yield 10
暂停
返回 10
text

输出:

开始
10
text

第二次print(next(g)) 它不会重新从函数开头执行,而是从上次暂停的位置继续:

打印"继续"
遇到 yield 20
暂停
返回 20
text

第三次next(g) 继续执行:

打印"结束"
函数运行完毕
text

然后抛出 StopIteration

同时,生成器会保存运行状态:当前运行到哪一行、局部变量当前是多少、函数栈调用状态。因此它很像一个可以暂停和继续的函数。

为什么需要生成器?#

最大的优势是:

节省内存,并且可以处理很大的数据流。

假设要生成一千万个数字,普通列表的写法是:

numbers = [i for i in range(10_000_000)]
python

生成器则是:

numbers = (i for i in range(10_000_000))
python

它不会一次性保存所有数字,而是记录生成规则:需要一个数字 → 计算一个 → 用完之后再计算下一个。因此内存占用通常小很多。

生成器也就是所谓的懒加载#

所谓懒加载是:

不提前做,等真正需要的时候再做。

例如:

def read_data():
    print("读取第一个数据")
    yield 1

    print("读取第二个数据")
    yield 2
python

如果我只读取第一个:

g = read_data()
print(next(g))
python

第二部分根本不会执行。

很适合逐行读取大文件、分页读取数据库、网络数据流、大模型流式输出和数据处理管道等场景。

同时,生成器还有一个重要的特性:生成器通常只能消费一次。想要重新读,就要重新创建生成器。

比对列表和生成器:

列表:数据已经存好了,可以重复读取

生成器:数据流经过一次,用完就结束
text

比对 returnyield

def test():
    return 1
    return 2  # 永远不会执行
python

第一个 return 执行后函数立刻结束,第二个永远不会执行。返回结果并且结束函数

def test():
    yield 1
    yield 2
python

第一次获取 1,函数暂停,下一次继续,获取 2。产出结果并且暂停函数

举两个常见的 yield 例子#

1. 逐行读取大文件#

普通写法:

with open("large_file.txt", encoding="utf-8") as file:
    lines = file.readlines()
python

readlines() 会把所有行一次性放入列表,如果文件非常大,可能占用很多内存。

更常见的写法是:

with open("large_file.txt", encoding="utf-8") as file:
    for line in file:
        print(line)
python

文件对象本身支持迭代,会一行一行读取,而不是一次性加载整个文件。

2. 大模型流式输出#

假设大模型逐块返回文字:

def stream_answer():
    yield "Python"
    yield "生成器"
    yield "可以流式返回数据"
python

调用:

for chunk in stream_answer():
    print(chunk, end="")
python

输出会逐块出现:Python生成器可以流式返回数据

Web 服务中的流式响应、SSE 和大模型的 Token 流,都可以用类似的思想。

生成器和迭代器的关系#

生成器是一种特殊的迭代器。

生成器是包含 yield 的可暂停函数。它会保存上次执行状态,在需要时逐个生成数据,而不是一次性把所有结果存进内存。

9. 如何使用多线程#

threading 模块来创建和启动多线程,尽管 Python 的 GIL 限制了线程的并行执行,但是在执行 I/O 密集型任务时,多线程仍能提高程序整体的效率。

10. Python 中的异常如何工作的#

通过 tryexceptelsefinally 语块来处理异常:

  • try:放置可能引发异常的代码
  • except:处理异常
  • else:没有异常时执行
  • finally:无论是否发生异常都会执行

11. Python 中支持哪些类型的继承?#

Python 支持单继承多继承,单继承允许子类继承一个父类的属性和方法,而多继承允许子类同时继承多个父类。

12. 什么是鸭子类型#

鸭子类型是动态类型的一种风格,它关注对象的行为而不是对象的类型,如果一个对象实现了所需的方法和属性,则它可以视为特定的类型。

13. 如何在 Python 中管理包和模块?#

通过 import 语句导入模块和包,pip 是 Python 的包管理器,用于安装和管理第三方库。

14. Python 的 __init____call__ 方法有什么不同#

  • __init__:类的构造方法,用于初始化新创建的对象。
  • __call__:允许实例像函数那样被调用。

15. Python 的多态如何实现?#

多态允许不同的类实例使用相同的方法名称,但实现可以不同。在 Python 中,多态是隐式的,因为 Python 是动态类型语言,不需要显式声明接口或者基类。

16. 如何优化 Python 代码的性能#

  • 使用更高效的数据结构
  • 利用 Python 标准库中的内置函数
  • 避免在循环中使用全局变量
  • 使用列表推导式和生成器表达式
  • 对于 CPU 密集型任务,考虑使用 multiprocessing 模块来利用多核 CPU

🗂️ This is a 📝 note in the knowledge base.

Content may be incomplete or work-in-progress.

← Back

Comment seems to stuck. Try to refresh?✨