【Python开发面试题及答案】核心考点+原理解析+实战场景

文章目录

  • Python开发面试题及答案:核心考点+原理解析+实战场景
    • 摘要
    • [一、Python 基础语法(初级/必考点)](#一、Python 基础语法(初级/必考点))
      • [1. 简述 Python 的核心特性?](#1. 简述 Python 的核心特性?)
      • [2. Python 中可变对象和不可变对象的区别?请举例说明。](#2. Python 中可变对象和不可变对象的区别?请举例说明。)
      • [3. 解释 Python 的 GIL(全局解释器锁)及其影响?](#3. 解释 Python 的 GIL(全局解释器锁)及其影响?)
      • [4. Python 中装饰器的原理和用途?请实现一个简单的装饰器。](#4. Python 中装饰器的原理和用途?请实现一个简单的装饰器。)
      • [5. 列表(list)和元组(tuple)的区别?什么时候用元组?](#5. 列表(list)和元组(tuple)的区别?什么时候用元组?)
      • [6. Python 中 `init` 和 `new` 的区别?](#6. Python 中 __init____new__ 的区别?)
      • [7. 简述 Python 的垃圾回收机制(GC)?](#7. 简述 Python 的垃圾回收机制(GC)?)
    • 二、数据结构与算法(中高级/核心考点)
      • [8. Python 中字典(dict)的实现原理?为什么查询速度快?](#8. Python 中字典(dict)的实现原理?为什么查询速度快?)
      • [9. 实现一个 LRU 缓存(最近最少使用),要求 get 和 put 操作时间复杂度 O(1)?](#9. 实现一个 LRU 缓存(最近最少使用),要求 get 和 put 操作时间复杂度 O(1)?)
      • [10. 列表推导式和生成器表达式的区别?如何选择?](#10. 列表推导式和生成器表达式的区别?如何选择?)
      • [11. 简述 Python 中的排序算法(`sorted` 函数)原理?](#11. 简述 Python 中的排序算法(sorted 函数)原理?)
    • 三、面向对象与设计模式(中高级/加分项)
      • [12. Python 中的继承机制?如何实现多继承?多继承的菱形问题如何解决?](#12. Python 中的继承机制?如何实现多继承?多继承的菱形问题如何解决?)
      • [13. 什么是单例模式?Python 中如何实现单例模式?](#13. 什么是单例模式?Python 中如何实现单例模式?)
        • [方式 1:使用 `new` 方法(最常用)](#方式 1:使用 __new__ 方法(最常用))
        • [方式 2:使用装饰器](#方式 2:使用装饰器)
        • [方式 3:使用模块(Python 特有,最简单)](#方式 3:使用模块(Python 特有,最简单))
      • [14. 什么是鸭子类型(Duck Typing)?Python 中如何体现?](#14. 什么是鸭子类型(Duck Typing)?Python 中如何体现?)
    • 四、并发编程(中高级/实战考点)
      • [15. Python 中多线程、多进程、异步 IO 的区别?如何选择?](#15. Python 中多线程、多进程、异步 IO 的区别?如何选择?)
        • [(1)多线程(IO 密集型)](#(1)多线程(IO 密集型))
        • [(2)多进程(CPU 密集型)](#(2)多进程(CPU 密集型))
        • [(3)异步 IO(高并发 IO 密集型)](#(3)异步 IO(高并发 IO 密集型))
      • [16. 什么是协程?Python 中如何实现协程?](#16. 什么是协程?Python 中如何实现协程?)
        • [方式 1:Python 3.5+ `async/await` 语法(推荐)](#方式 1:Python 3.5+ async/await 语法(推荐))
        • [方式 2:早期 `yield from` 语法(不推荐,仅了解)](#方式 2:早期 yield from 语法(不推荐,仅了解))
    • 五、生态工具与实战场景(中高级/落地能力)
      • [17. 简述 FastAPI 的核心优势?与 Flask/Django 的区别?](#17. 简述 FastAPI 的核心优势?与 Flask/Django 的区别?)
      • [18. SQLAlchemy ORM 的核心概念?如何实现增删改查?](#18. SQLAlchemy ORM 的核心概念?如何实现增删改查?)
      • [19. 如何处理 Python 项目中的配置管理?](#19. 如何处理 Python 项目中的配置管理?)
        • [方案 1:使用 Pydantic Settings(推荐,适合中大型项目)](#方案 1:使用 Pydantic Settings(推荐,适合中大型项目))
        • [方案 2:使用 `python-dotenv`(适合小型项目)](#方案 2:使用 python-dotenv(适合小型项目))
    • 六、性能优化与问题排查(高级/架构能力)
      • [20. Python 代码性能优化的常见方法?](#20. Python 代码性能优化的常见方法?)
        • [1. 代码层面优化](#1. 代码层面优化)
        • [2. 内存优化](#2. 内存优化)
        • [3. 并发优化](#3. 并发优化)
        • [4. 第三方库优化](#4. 第三方库优化)
        • [5. 工具辅助优化](#5. 工具辅助优化)
      • [21. 如何排查 Python 项目中的内存泄漏?](#21. 如何排查 Python 项目中的内存泄漏?)
        • [1. 确认内存泄漏](#1. 确认内存泄漏)
        • [2. 定位泄漏对象](#2. 定位泄漏对象)
        • [3. 分析泄漏原因](#3. 分析泄漏原因)
        • [4. 修复与验证](#4. 修复与验证)

Python开发面试题及答案:核心考点+原理解析+实战场景

摘要

若对您有帮助的话,请点赞收藏加关注哦,您的关注是我持续创作的动力!有问题请私信或联系邮箱:funian.gm@gmail.com

Python 作为当前最热门的编程语言之一,凭借简洁语法、丰富生态和跨场景适配能力,广泛应用于后端开发、数据分析、人工智能、爬虫、自动化测试等领域。企业面试 Python 开发岗位时,核心考察「基础语法深度理解、数据结构与算法应用、面向对象设计、并发编程、生态工具使用、项目工程化」六大模块。

一、Python 基础语法(初级/必考点)

1. 简述 Python 的核心特性?

答案解析

  • 动态类型语言:无需声明变量类型,解释器运行时自动推断(如 a = 1 为 int,a = "hello" 转为 str);
  • 解释型语言:无需编译,通过 Python 解释器逐行执行(跨平台性强,但执行速度略慢于编译型语言);
  • 简洁易读:语法接近自然语言,代码量少(如实现相同功能,Python 代码量约为 Java 的 1/3);
  • 面向对象+函数式编程:支持类与对象(封装/继承/多态),同时支持函数作为一等公民(可赋值、传参、返回);
  • 丰富生态:内置标准库(如 os/sys/json)+ 第三方库(如 requests/Django/TensorFlow)覆盖全场景;
  • 跨平台:支持 Windows/macOS/Linux,代码无需修改即可在不同系统运行。

考点分析:考察对 Python 本质的理解,避免仅罗列"语法简洁"等表面特性,需结合底层设计(动态类型、解释型)说明优势。

2. Python 中可变对象和不可变对象的区别?请举例说明。

答案解析

  • 核心区别:是否允许在原内存地址上修改内容(即"原地修改");

  • 不可变对象:创建后无法修改内容,修改时会生成新对象(新内存地址);

    • 示例:int(整数)、str(字符串)、tuple(元组)、frozenset(冻结集合);

    • 代码示例:

      python 复制代码
      a = "hello"
      print(id(a))  # 输出:140708456725808(原地址)
      a += " world"
      print(id(a))  # 输出:140708456726064(新地址)
  • 可变对象:创建后可修改内容,修改时不改变内存地址(原地修改);

    • 示例:list(列表)、dict(字典)、set(集合);

    • 代码示例:

      python 复制代码
      b = [1, 2, 3]
      print(id(b))  # 输出:140708456682368(原地址)
      b.append(4)
      print(id(b))  # 输出:140708456682368(地址不变)

高频坑点:函数参数传递时,不可变对象按"值传递"(修改不会影响原对象),可变对象按"引用传递"(修改会影响原对象):

python 复制代码
def func(x, y):
    x += 1  # 不可变对象,生成新值,不影响原对象
    y.append(4)  # 可变对象,原地修改,影响原对象

a = 1
b = [1,2,3]
func(a, b)
print(a)  # 输出:1(无变化)
print(b)  # 输出:[1,2,3,4](被修改)

考点分析:考察对 Python 内存模型的理解,是后续函数参数传递、字典键选择(必须为不可变对象)的基础。

3. 解释 Python 的 GIL(全局解释器锁)及其影响?

答案解析

  • 定义:GIL 是 CPython 解释器(Python 官方默认解释器)的核心机制,本质是一把互斥锁,确保同一时刻只有一个线程在 CPU 上执行 Python 字节码;
  • 核心影响:
    1. 多核 CPU 环境下,Python 多线程无法实现"真正并行"(仅能实现并发,即线程切换执行),CPU 密集型任务性能提升有限;
    2. IO 密集型任务(如网络请求、文件读写)不受影响(线程等待 IO 时会释放 GIL,其他线程可执行);
  • 解决方案:
    • CPU 密集型任务:使用多进程(multiprocessing 模块,每个进程有独立 GIL)或 C 扩展(如 Cython)、PyPy 解释器(无 GIL);
    • IO 密集型任务:使用多线程(threading 模块)或异步 IO(asyncio 模块)。

代码示例:CPU 密集型任务用多进程优化:

python 复制代码
from multiprocessing import Pool
import time

# CPU 密集型函数(计算斐波那契数列)
def fib(n):
    return n if n <= 1 else fib(n-1) + fib(n-2)

if __name__ == "__main__":
    # 多进程(4个进程)
    start = time.time()
    with Pool(4) as pool:
        results = pool.map(fib, [30]*4)  # 并行计算4个 fib(30)
    print(f"多进程耗时:{time.time() - start:.2f}s")  # 约2秒

    # 单线程
    start = time.time()
    results = [fib(30) for _ in range(4)]
    print(f"单线程耗时:{time.time() - start:.2f}s")  # 约7秒

考点分析:考察 Python 并发编程的底层限制,是区分初级和中级开发者的关键考点,需结合实际场景说明解决方案。

4. Python 中装饰器的原理和用途?请实现一个简单的装饰器。

答案解析

  • 原理:装饰器是"包装函数"的函数,基于闭包和函数式编程思想,在不修改原函数代码的前提下,为函数添加额外功能(如日志、计时、权限校验);
  • 核心特性:
    1. 不修改原函数的定义和调用方式;
    2. 支持嵌套装饰器、带参数装饰器;
  • 常见用途:日志记录、性能计时、权限验证、缓存、异常捕获。

代码示例 1:无参数装饰器(计时功能)

python 复制代码
import time

# 定义装饰器(接收原函数作为参数)
def timer_decorator(func):
    # 闭包函数(接收原函数的参数)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)  # 执行原函数
        end_time = time.time()
        print(f"函数 {func.__name__} 执行耗时:{end_time - start_time:.2f}s")
        return result  # 返回原函数结果
    return wrapper

# 使用装饰器(@语法糖)
@timer_decorator
def add(a, b):
    time.sleep(1)
    return a + b

# 调用原函数(实际调用的是 wrapper)
print(add(2, 3))  # 输出:函数 add 执行耗时:1.00s → 5

代码示例 2:带参数装饰器(日志级别)

python 复制代码
def log_decorator(level="info"):
    # 外层函数接收装饰器参数
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level.upper()}] 调用函数 {func.__name__},参数:{args}, {kwargs}")
            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# 带参数使用装饰器
@log_decorator(level="debug")
def multiply(a, b):
    return a * b

multiply(3, 4)  # 输出:[DEBUG] 调用函数 multiply,参数:(3, 4), {} → 12

考点分析:考察闭包、函数式编程思想,是 Python 高级语法的基础,实际项目中应用广泛(如 Flask/Django 的路由装饰器)。

5. 列表(list)和元组(tuple)的区别?什么时候用元组?

答案解析

特性 列表(list) 元组(tuple)
可变性 可变(支持增删改) 不可变(创建后固定)
语法 [] 定义 () 定义
内存占用 较大(需预留扩容空间) 较小(固定大小,无需扩容)
支持的方法 多(append/sort/pop 等) 少(仅 count/index 等)
适用场景 动态数据(需频繁修改) 静态数据(无需修改,如配置项、坐标)
作为字典键 不可以(可变对象) 可以(不可变对象)

使用场景

  • 元组:存储无需修改的数据(如 RGB = (255, 255, 255))、函数多返回值(return a, b 本质是元组)、字典的键({("a", 1): "value"});
  • 列表:存储需要动态调整的数据(如用户列表、任务队列)。

高频坑点:元组中若包含可变对象(如列表),该对象可修改:

python 复制代码
t = (1, 2, [3, 4])
t[2].append(5)  # 允许修改元组内的可变对象
print(t)  # 输出:(1, 2, [3, 4, 5])

考点分析:考察对内置数据结构的选型能力,需结合可变性、内存、场景综合判断。

6. Python 中 __init____new__ 的区别?

答案解析

  • 核心区别:二者都是类的构造相关方法,但执行时机和作用不同;

    方法 执行时机 作用 返回值
    __new__ 类实例化时优先执行(创建对象) 负责创建类的实例(分配内存) 必须返回创建的实例对象
    __init__ __new__ 执行后执行(初始化对象) 负责初始化实例的属性(赋值) 无返回值(默认 None

代码示例

python 复制代码
class MyClass:
    def __new__(cls, *args, **kwargs):
        print("__new__:创建实例")
        instance = super().__new__(cls)  # 调用父类的 __new__ 分配内存
        return instance  # 必须返回实例

    def __init__(self, name):
        print("__init__:初始化实例")
        self.name = name  # 初始化属性

obj = MyClass("Test")
# 输出:
# __new__:创建实例
# __init__:初始化实例
print(obj.name)  # 输出:Test

特殊用途__new__ 可用于单例模式(确保类只有一个实例):

python 复制代码
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
print(a is b)  # 输出:True(同一实例)

考点分析:考察对 Python 类实例化底层流程的理解,属于中级考点,常结合单例模式提问。

7. 简述 Python 的垃圾回收机制(GC)?

答案解析

Python 的垃圾回收机制主要基于「引用计数」,辅以「标记-清除」和「分代回收」机制,自动释放无用对象的内存:

  1. 引用计数(核心)
    • 每个对象维护一个计数器,记录当前引用该对象的变量个数;
    • 引用增加时计数器+1(如 a = obj),引用减少时计数器-1(如 a = None、变量超出作用域);
    • 计数器为 0 时,对象被标记为垃圾,触发内存回收。
  2. 标记-清除(解决循环引用)
    • 引用计数无法处理循环引用(如 a = [1]b = [2]a.append(b)b.append(a),此时 ab 计数器均为 1,但无外部引用);
    • 定期扫描所有对象,标记可达对象(有外部引用),未被标记的对象视为垃圾并清除。
  3. 分代回收(优化性能)
    • 基于"大多数对象存活时间短"的规律,将对象分为 3 代(0 代:新创建对象,1 代:存活一次 GC 的对象,2 代:存活多次 GC 的对象);
    • 代龄越高,GC 扫描频率越低(0 代每创建一定数量对象扫描,2 代扫描频率最低),减少 GC 对性能的影响。

手动触发 GC

python 复制代码
import gc
gc.collect()  # 手动触发垃圾回收

考点分析:考察对 Python 内存管理的底层理解,避免内存泄漏(如循环引用未解除)。

二、数据结构与算法(中高级/核心考点)

8. Python 中字典(dict)的实现原理?为什么查询速度快?

答案解析

  • 实现原理:Python 字典基于「哈希表(Hash Table)」实现,核心是"键(key)→ 哈希值 → 索引 → 值(value)"的映射流程;
  • 查询流程:
    1. 对字典的键(必须为不可变对象)计算哈希值(hash(key));
    2. 通过哈希值与哈希表大小取模,得到数组索引;
    3. 若索引位置无元素,直接返回 KeyError;若有元素,比较键是否相等(解决哈希冲突),相等则返回对应值。
  • 查询速度快的原因:
    • 哈希表的查询时间复杂度为 O(1)(平均情况),无需遍历整个字典,直接通过哈希值定位索引;
    • 对比列表(查询时间复杂度 O(n)),字典查询效率呈指数级提升。

哈希冲突解决:Python 采用「开放寻址法」(早期用链表法),当两个键的哈希值映射到同一索引时,通过探测相邻位置找到空闲槽位存储元素。

高频坑点

  • 字典的键必须是不可变对象(int/str/tuple 等),因为可变对象(list/dict)的哈希值会随内容变化,导致无法定位;
  • Python 3.7+ 字典保证插入顺序,3.6- 版本不保证(底层优化了哈希表结构)。

考点分析:考察对核心数据结构底层实现的理解,是中高级面试的高频题,需结合哈希表原理说明查询效率。

9. 实现一个 LRU 缓存(最近最少使用),要求 get 和 put 操作时间复杂度 O(1)?

答案解析

  • LRU 缓存特性:限定缓存容量,当缓存满时,淘汰最近最少使用的元素;
  • 实现思路:结合「哈希表(dict)+ 双向链表」,哈希表保证 O(1) 查询,双向链表保证 O(1) 插入/删除;
    • 哈希表:键为缓存键,值为双向链表节点(存储键和值);
    • 双向链表:头部存储最近使用的元素,尾部存储最少使用的元素;
  • 核心操作:
    1. get(key):若键存在,将节点移到链表头部,返回值;否则返回 -1;
    2. put(key, value):若键存在,更新节点值并移到头部;若不存在,创建新节点插入头部;若缓存满,删除链表尾部节点(最少使用),并删除哈希表对应项。

代码实现

python 复制代码
class ListNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity  # 缓存容量
        self.cache = dict()  # 哈希表:key → ListNode
        # 虚拟头节点和尾节点(简化边界处理)
        self.head = ListNode()
        self.tail = ListNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    # 辅助函数:将节点移到链表头部(最近使用)
    def _move_to_head(self, node):
        # 移除节点
        node.prev.next = node.next
        node.next.prev = node.prev
        # 插入头部
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    # 辅助函数:删除链表尾部节点(最少使用)
    def _remove_tail(self):
        node = self.tail.prev
        node.prev.next = self.tail
        self.tail.prev = node.prev
        return node  # 返回删除的节点(用于删除哈希表项)

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._move_to_head(node)  # 标记为最近使用
        return node.value

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            # 键存在:更新值并移到头部
            node = self.cache[key]
            node.value = value
            self._move_to_head(node)
        else:
            # 键不存在:创建新节点
            new_node = ListNode(key, value)
            self.cache[key] = new_node
            self._move_to_head(new_node)
            # 缓存满:删除尾部节点
            if len(self.cache) > self.capacity:
                removed_node = self._remove_tail()
                del self.cache[removed_node.key]

# 测试
lru = LRUCache(2)
lru.put(1, 1)
lru.put(2, 2)
print(lru.get(1))  # 输出:1(移到头部)
lru.put(3, 3)      # 缓存满,淘汰 2
print(lru.get(2))  # 输出:-1
lru.put(4, 4)      # 缓存满,淘汰 1
print(lru.get(1))  # 输出:-1
print(lru.get(3))  # 输出:3
print(lru.get(4))  # 输出:4

考点分析:考察数据结构组合应用能力,LRU 缓存是面试高频算法题,需掌握哈希表+双向链表的设计思路,确保操作时间复杂度 O(1)。

10. 列表推导式和生成器表达式的区别?如何选择?

答案解析

  • 语法区别:列表推导式用 [],生成器表达式用 ()

  • 核心区别:是否一次性生成所有数据(内存占用差异);

    特性 列表推导式([x for x in iter] 生成器表达式((x for x in iter)
    数据生成方式 一次性生成所有数据,存入内存 惰性生成(迭代时才生成下一个数据)
    内存占用 大(数据量大时易内存溢出) 小(仅存储生成器对象,不存数据)
    返回类型 list 实例 generator 实例
    访问方式 支持索引、切片(如 lst[0] 仅支持迭代(for 循环、next()
    重复访问 可多次访问(数据已存入内存) 仅可迭代一次(迭代后耗尽)

代码示例

python 复制代码
# 列表推导式:一次性生成 100 万个整数(占用大量内存)
lst = [x for x in range(10**6)]
print(type(lst))  # <class 'list'>
print(len(lst))   # 1000000

# 生成器表达式:仅创建生成器对象(内存占用极小)
gen = (x for x in range(10**6))
print(type(gen))  # <class 'generator'>
print(next(gen))  # 0(首次迭代生成第一个数据)
print(next(gen))  # 1(第二次迭代生成第二个数据)

选择原则

  • 数据量小(如 1 万条以内):用列表推导式(访问方便,语法简洁);
  • 数据量大(如 10 万条以上)或无限迭代器:用生成器表达式(节省内存,避免溢出);
  • 需重复访问数据:用列表推导式(生成器迭代一次后耗尽);
  • 仅需迭代一次(如遍历后直接处理):用生成器表达式。

高频坑点:生成器表达式迭代一次后耗尽,再次迭代无数据:

python 复制代码
gen = (x for x in range(3))
print(list(gen))  # [0,1,2](迭代耗尽)
print(list(gen))  # [](无数据)

考点分析:考察对内存优化的理解,实际项目中数据量较大时,生成器表达式是关键优化手段。

11. 简述 Python 中的排序算法(sorted 函数)原理?

答案解析

  • Python 3.0+ 中 sorted 函数和列表的 sort 方法,底层使用「Timsort 排序算法」(结合归并排序和插入排序的混合算法);
  • 核心特性:
    1. 稳定性:相等元素的相对顺序保持不变(如 sorted([(2,1), (1,2), (2,3)], key=lambda x: x[0]) 输出 [(1,2), (2,1), (2,3)]);
    2. 时间复杂度:平均/最坏时间复杂度 O(n log n),空间复杂度 O(n);
    3. 适应性:对已部分有序的数据优化(插入排序处理小规模或有序数据,归并排序处理大规模数据)。

关键参数

  • key:指定排序依据(如 sorted(lst, key=lambda x: x[1]) 按元组第二个元素排序);
  • reverse:是否降序(默认 False,升序)。

代码示例

python 复制代码
# 按字典值排序
d = {"a": 3, "b": 1, "c": 2}
sorted_items = sorted(d.items(), key=lambda x: x[1])
print(sorted_items)  # 输出:[('b', 1), ('c', 2), ('a', 3)]

# 稳定排序示例
lst = [(2, "x"), (1, "y"), (2, "z")]
sorted_lst = sorted(lst, key=lambda x: x[0])
print(sorted_lst)  # 输出:[(1, 'y'), (2, 'x'), (2, 'z')]((2,"x") 和 (2,"z") 顺序不变)

考点分析 :考察对内置排序函数底层的理解,避免仅知道用法而不清楚原理,实际项目中需结合 key 参数灵活排序。

三、面向对象与设计模式(中高级/加分项)

12. Python 中的继承机制?如何实现多继承?多继承的菱形问题如何解决?

答案解析

  • 继承核心:子类继承父类的属性和方法,实现代码复用,支持"方法重写"(子类覆盖父类方法);
  • 单继承语法:class 子类(父类): ...
  • 多继承语法:class 子类(父类1, 父类2, ...): ...(子类同时继承多个父类的属性和方法)。

多继承的菱形问题

  • 场景:子类继承的多个父类,最终继承自同一个顶层父类(如 A → B → DA → C → DA 继承 BC),此时调用 A 的方法可能存在歧义(到底继承 B 还是 C 的方法);
  • 解决方式:Python 3 采用「C3 线性化算法」(MRO:方法解析顺序),按以下规则确定方法调用顺序:
    1. 子类优先于父类;
    2. 同一层级父类按继承顺序排列;
    3. 保持父类的 MRO 顺序。

代码示例

python 复制代码
class D:
    def show(self):
        print("D")

class B(D):
    def show(self):
        print("B")

class C(D):
    def show(self):
        print("C")

class A(B, C):  # 多继承:B 在前,C 在后
    pass

a = A()
a.show()  # 输出:B(按 MRO 顺序:A → B → C → D)
print(A.mro())  # 查看 MRO 顺序:[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>]

方法重写与super()

python 复制代码
class Animal:
    def run(self):
        print("Animal runs")

class Dog(Animal):
    def run(self):
        super().run()  # 调用父类方法
        print("Dog runs fast")

dog = Dog()
dog.run()
# 输出:
# Animal runs
# Dog runs fast

考点分析 :考察面向对象的核心特性,多继承的菱形问题是面试高频考点,需理解 MRO 机制和 super() 的作用。

13. 什么是单例模式?Python 中如何实现单例模式?

答案解析

  • 单例模式:保证一个类只有一个实例,且提供全局唯一访问入口(如配置类、数据库连接池);
  • 常用实现方式(按推荐度排序):
方式 1:使用 __new__ 方法(最常用)
python 复制代码
class Singleton:
    _instance = None  # 存储唯一实例
    _initialized = False  # 避免重复初始化

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if not Singleton._initialized:
            print("初始化单例实例")
            self.config = {"host": "localhost", "port": 3306}
            Singleton._initialized = True

# 测试
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # 输出:True(同一实例)
print(s1.config)  # 输出:{'host': 'localhost', 'port': 3306}
方式 2:使用装饰器
python 复制代码
def singleton_decorator(cls):
    instances = {}  # 缓存实例(支持多个类的单例)

    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton_decorator
class Config:
    def __init__(self):
        print("Config 初始化")
        self.host = "localhost"

# 测试
c1 = Config()
c2 = Config()
print(c1 is c2)  # 输出:True
方式 3:使用模块(Python 特有,最简单)

Python 模块是天然的单例模式(模块导入时仅执行一次),将单例逻辑写在模块中,导入后直接使用:

python 复制代码
# singleton_module.py
print("模块初始化")
config = {"host": "localhost", "port": 3306}

def get_config():
    return config

# 测试(其他文件导入)
import singleton_module
print(singleton_module.get_config())  # 输出:{'host': 'localhost', 'port': 3306}
import singleton_module  # 再次导入,模块不重复执行
print(singleton_module.get_config() is singleton_module.get_config())  # 输出:True

适用场景:配置管理、数据库连接池、日志对象、全局计数器等需要唯一实例的场景。

考点分析:考察设计模式的实际应用,单例模式是 Python 面试中最常考的设计模式,需掌握至少 2 种实现方式,并理解各自优缺点。

14. 什么是鸭子类型(Duck Typing)?Python 中如何体现?

答案解析

  • 定义:鸭子类型是动态类型语言的核心特性,核心思想是"关注对象的行为(方法/属性),而非对象的类型";
    • 通俗解释:"如果一个东西走起来像鸭子、叫起来像鸭子,那么它就是鸭子";
  • Python 中的体现:
    • 无需显式继承接口,只要对象实现了所需的方法/属性,即可被视为"符合接口要求";
    • 例如:Python 的 for 循环可迭代任何"可迭代对象"(实现 __iter____getitem__ 方法),无论其是否继承 Iterable 类。

代码示例

python 复制代码
# 鸭子类型示例:无需继承,只需实现相同方法
class Duck:
    def quack(self):
        print("Duck quacks")

class Goose:
    def quack(self):
        print("Goose quacks like a duck")

class Dog:
    def bark(self):
        print("Dog barks")

# 函数接收"会叫的鸭子"(只要实现 quack 方法)
def make_quack(animal):
    animal.quack()  # 不关心 animal 的类型,只关心是否有 quack 方法

duck = Duck()
goose = Goose()
dog = Dog()

make_quack(duck)   # 输出:Duck quacks
make_quack(goose)  # 输出:Goose quacks like a duck
make_quack(dog)    # 报错:AttributeError: 'Dog' object has no attribute 'quack'

对比静态类型语言 :Java 等静态类型语言需显式实现接口(如 class Duck implements Quackable),而 Python 通过鸭子类型实现"隐式接口",更灵活。

考点分析:考察对 Python 动态类型特性的深度理解,是区分初级和中级开发者的关键考点。

四、并发编程(中高级/实战考点)

15. Python 中多线程、多进程、异步 IO 的区别?如何选择?

答案解析

三者均为 Python 并发编程的实现方式,核心区别在于"是否真正并行""CPU 利用率""适用场景":

特性 多线程(threading 多进程(multiprocessing 异步 IO(asyncio
底层实现 操作系统线程,共享进程内存 操作系统进程,独立内存空间 单线程+事件循环,协程切换
GIL 影响 受影响(无法真正并行) 不受影响(每个进程独立 GIL) 不受影响(单线程无 GIL 竞争)
内存占用 低(共享内存) 高(独立内存) 极低(单线程+协程)
切换开销 中(线程上下文切换) 高(进程上下文切换) 极低(协程用户态切换)
适用场景 IO 密集型(网络请求、文件读写) CPU 密集型(计算、加密) IO 密集型(高并发网络请求)
代码复杂度 中(需处理进程间通信) 中(需掌握异步语法)

选择原则

  1. CPU 密集型任务(如大数据计算、加密解密):用多进程(multiprocessing)或 PyPy 解释器;
  2. IO 密集型任务(如接口调用、数据库查询):
    • 低并发(100 以内):用多线程(threading),代码简单;
    • 高并发(1000+):用异步 IO(asyncio),内存占用低、切换快;
  3. 混合任务(既有 CPU 密集又有 IO 密集):用多进程+异步 IO(进程处理 CPU 任务,进程内用异步处理 IO 任务)。

代码示例对比

(1)多线程(IO 密集型)
python 复制代码
import threading
import time
import requests

def fetch_url(url):
    response = requests.get(url)
    print(f"URL: {url}, Status: {response.status_code}")

urls = [
    "https://www.baidu.com",
    "https://www.google.com",
    "https://www.github.com"
]

# 多线程执行
start = time.time()
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"多线程耗时:{time.time() - start:.2f}s")  # 约1-2秒(并行等待 IO)
(2)多进程(CPU 密集型)
python 复制代码
from multiprocessing import Pool
import time

def calculate(n):
    # CPU 密集型计算:计算 n 的阶乘
    result = 1
    for i in range(1, n+1):
        result *= i
    return result

if __name__ == "__main__":
    nums = [100000]*4
    start = time.time()
    # 多进程执行
    with Pool(4) as pool:
        results = pool.map(calculate, nums)
    print(f"多进程耗时:{time.time() - start:.2f}s")  # 约3秒
(3)异步 IO(高并发 IO 密集型)
python 复制代码
import asyncio
import aiohttp
import time

async def fetch_url_async(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            print(f"URL: {url}, Status: {response.status}")

urls = [
    "https://www.baidu.com",
    "https://www.google.com",
    "https://www.github.com"
]

start = time.time()
# 异步执行
asyncio.run(asyncio.gather(*[fetch_url_async(url) for url in urls]))
print(f"异步 IO 耗时:{time.time() - start:.2f}s")  # 约1秒(协程切换,无线程开销)

考点分析:考察并发编程的实际应用能力,是后端开发岗位的核心考点,需结合场景选择合适的并发方案。

16. 什么是协程?Python 中如何实现协程?

答案解析

  • 协程(Coroutine):又称"微线程",是用户态的轻量级线程,由程序(而非操作系统)控制调度,切换开销极低(无需操作系统介入);
  • 核心特性:
    1. 单线程内执行,通过"事件循环"调度,避免线程上下文切换开销;
    2. 协作式调度:协程主动放弃 CPU 时(如遇到 IO 等待),切换到其他协程执行;
    3. 无 GIL 竞争:单线程内无多线程的 GIL 锁问题,高并发时性能更优。

Python 实现协程的方式

方式 1:Python 3.5+ async/await 语法(推荐)
python 复制代码
import asyncio

# 定义协程函数(async def 声明)
async def coro1():
    print("Coro1 start")
    await asyncio.sleep(1)  # 模拟 IO 等待,主动放弃 CPU
    print("Coro1 end")

async def coro2():
    print("Coro2 start")
    await asyncio.sleep(2)  # 模拟 IO 等待
    print("Coro2 end")

# 事件循环调度协程
async def main():
    # 并发执行多个协程
    task1 = asyncio.create_task(coro1())
    task2 = asyncio.create_task(coro2())
    await task1
    await task2

# 运行事件循环
asyncio.run(main())
# 输出:
# Coro1 start
# Coro2 start
# Coro1 end(1秒后)
# Coro2 end(2秒后,总耗时2秒,而非3秒)
方式 2:早期 yield from 语法(不推荐,仅了解)
python 复制代码
import asyncio

@asyncio.coroutine
def coro():
    yield from asyncio.sleep(1)
    print("Coro done")

asyncio.run(coro())

协程 vs 线程

  • 线程:操作系统调度,切换开销大(内核态切换),多线程受 GIL 限制;
  • 协程:程序自身调度,切换开销小(用户态切换),单线程内高并发,仅适用于 IO 密集型任务。

考点分析 :考察对 Python 高并发编程的理解,async/await 是 Python 3.5+ 核心语法,在 FastAPI、Starlette 等框架中广泛应用,是中高级面试高频考点。

五、生态工具与实战场景(中高级/落地能力)

17. 简述 FastAPI 的核心优势?与 Flask/Django 的区别?

答案解析

  • FastAPI 核心优势:
    1. 高性能:基于 Starlette(异步框架)和 Pydantic(数据校验),性能接近 Node.js 和 Go;
    2. 自动生成 API 文档:支持 Swagger UI(/docs)和 ReDoc(/redoc),无需手动编写文档;
    3. 强类型支持:基于 Pydantic 实现请求参数校验和类型提示,开发效率高、错误少;
    4. 异步支持:原生支持 async/await,适配高并发 IO 场景;
    5. 轻量级+灵活:类似 Flask 的微框架设计,可按需集成数据库、缓存等组件。

与 Flask/Django 的区别

特性 FastAPI Flask Django
定位 高性能异步 API 框架 轻量级同步微框架 全栈式同步框架(内置 ORM/Admin)
性能 极高(异步+Pydantic) 中等(同步) 中等(同步,ORM 优化)
API 文档 自动生成(Swagger/ReDoc) 需第三方插件(如 Flask-RESTPlus) 需第三方插件
数据校验 内置 Pydantic 校验 需手动或第三方库(如 marshmallow) 表单校验,API 需额外处理
异步支持 原生支持 3.0+ 支持(有限) 4.0+ 支持(有限)
适用场景 高并发 API、微服务 小型应用、原型开发 大型应用、后台管理系统

代码示例(FastAPI 快速上手)

python 复制代码
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="FastAPI Demo")

# 数据模型(Pydantic)
class User(BaseModel):
    name: str
    age: int
    email: str

# 路由定义
@app.get("/")
def read_root():
    return {"message": "Hello FastAPI"}

@app.post("/users", response_model=User)
def create_user(user: User):
    # 自动校验请求体,不符合 User 模型会返回结构化错误
    return user

# 启动服务:uvicorn main:app --reload
# 访问文档:http://127.0.0.1:8000/docs

考点分析:考察对 Python Web 框架的选型能力,FastAPI 是近年来最热门的 API 框架,在后端开发面试中频率逐渐升高,需掌握其核心优势和适用场景。

18. SQLAlchemy ORM 的核心概念?如何实现增删改查?

答案解析

  • SQLAlchemy 是 Python 最流行的 ORM(对象关系映射)框架,核心是将 Python 类映射到数据库表,通过操作对象实现数据库操作(无需编写原生 SQL);
  • 核心概念:
    1. Engine:数据库连接引擎(管理连接池,如 create_engine("mysql+pymysql://user:pass@host/db"));
    2. Session:数据库会话(类似游标,用于执行增删改查操作);
    3. Base:模型基类(所有数据模型都继承自 declarative_base());
    4. Model:数据模型类(映射到数据库表,属性映射到字段)。

代码示例(增删改查)

  1. 安装依赖:pip install sqlalchemy pymysql
  2. 实现代码:
python 复制代码
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 1. 创建数据库连接引擎
DATABASE_URL = "mysql+pymysql://root:123456@localhost:3306/test_db"
engine = create_engine(DATABASE_URL, echo=True)  # echo=True 打印 SQL 语句

# 2. 创建模型基类
Base = declarative_base()

# 3. 定义数据模型(映射到数据库表)
class User(Base):
    __tablename__ = "users"  # 数据库表名

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50), index=True)
    email = Column(String(100), unique=True, index=True)

# 4. 创建数据库表(若不存在)
Base.metadata.create_all(bind=engine)

# 5. 创建会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 增删改查操作
def crud_demo():
    # 获取会话
    db = SessionLocal()

    # (1)新增(Create)
    new_user = User(name="Alice", email="alice@example.com")
    db.add(new_user)
    db.commit()  # 提交事务
    db.refresh(new_user)  # 刷新对象,获取数据库生成的 id
    print(f"新增用户:{new_user.id} - {new_user.name}")

    # (2)查询(Read)
    # 单条查询
    user = db.query(User).filter(User.name == "Alice").first()
    print(f"查询用户:{user.id} - {user.email}")
    # 多条查询
    users = db.query(User).filter(User.name.like("%A%")).all()
    print(f"查询所有符合条件的用户:{[u.name for u in users]}")

    # (3)更新(Update)
    if user:
        user.email = "alice_new@example.com"
        db.commit()
        db.refresh(user)
        print(f"更新后邮箱:{user.email}")

    # (4)删除(Delete)
    if user:
        db.delete(user)
        db.commit()
        print("用户已删除")

    # 关闭会话
    db.close()

if __name__ == "__main__":
    crud_demo()

高频坑点

  • 忘记提交事务:新增/更新/删除操作后需调用 db.commit(),否则修改不会同步到数据库;
  • 会话未关闭:长期不关闭会话会导致数据库连接泄漏,建议使用上下文管理器(with SessionLocal() as db:);
  • 索引设计:频繁查询的字段(如 name/email)需添加 index=True,提升查询效率。

考点分析:考察数据库操作的实战能力,SQLAlchemy 是 Python 后端开发的必备工具,面试中常结合 CRUD 场景提问。

19. 如何处理 Python 项目中的配置管理?

答案解析

Python 项目配置管理的核心需求是"多环境适配(开发/测试/生产)、敏感信息保护、配置易维护",推荐方案如下:

方案 1:使用 Pydantic Settings(推荐,适合中大型项目)

结合 Pydantic 数据校验和环境变量,支持多环境配置、敏感信息注入(如数据库密码从环境变量读取):

python 复制代码
# 安装依赖:pip install pydantic-settings python-dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field

class Settings(BaseSettings):
    # 数据库配置
    DB_HOST: str = Field(default="localhost", description="数据库主机")
    DB_PORT: int = Field(default=3306, description="数据库端口")
    DB_USER: str = Field(description="数据库用户名")
    DB_PASS: str = Field(description="数据库密码")
    DB_NAME: str = Field(default="test_db", description="数据库名")

    # API 配置
    API_PORT: int = Field(default=8000, description="API 端口")
    DEBUG: bool = Field(default=False, description="调试模式")

    # 配置加载规则:优先环境变量,其次 .env 文件
    model_config = SettingsConfigDict(
        env_file=".env",  # 加载 .env 文件
        env_file_encoding="utf-8",
        case_sensitive=True  # 环境变量大小写敏感
    )

# 加载配置(单例模式,全局唯一)
settings = Settings()

# 使用配置
print(f"数据库连接:mysql+pymysql://{settings.DB_USER}:{settings.DB_PASS}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}")

.env 文件(开发环境配置):

env 复制代码
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASS=123456
DEBUG=True

生产环境:通过操作系统环境变量覆盖配置(避免敏感信息写入文件):

bash 复制代码
export DB_USER=prod_user
export DB_PASS=prod_secret
export DEBUG=False
python main.py
方案 2:使用 python-dotenv(适合小型项目)

简单直接,仅加载 .env 文件到环境变量:

python 复制代码
# 安装依赖:pip install python-dotenv
import os
from dotenv import load_dotenv

# 加载 .env 文件
load_dotenv()

# 读取配置
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PASS = os.getenv("DB_PASS")
DEBUG = os.getenv("DEBUG", "False").lower() == "true"

print(DB_HOST)  # 输出:localhost(从 .env 文件读取)

配置管理最佳实践

  1. 按环境拆分配置(.env.development/.env.production);
  2. 敏感信息(密码、Token)通过环境变量注入,不写入代码或配置文件;
  3. 配置项添加类型校验(如 Pydantic),避免类型错误;
  4. 配置项添加描述文档,便于团队协作。

考点分析:考察项目工程化能力,配置管理是生产环境项目的必备环节,体现开发者的工程素养。

六、性能优化与问题排查(高级/架构能力)

20. Python 代码性能优化的常见方法?

答案解析

Python 性能优化遵循"先定位瓶颈,再针对性优化"的原则,常见优化方法如下:

1. 代码层面优化
  • 避免循环嵌套(减少时间复杂度):用列表推导式、map/filter 替代多层 for 循环;

  • 减少全局变量使用:全局变量查询速度慢,优先使用局部变量(函数内变量);

  • 避免频繁字符串拼接:字符串是不可变对象,+= 拼接会生成新对象,用 join() 替代:

    python 复制代码
    # 低效
    s = ""
    for i in range(1000):
        s += str(i)
    # 高效
    s = "".join(str(i) for i in range(1000))
  • 合理使用数据结构:查询频繁用字典(O(1)),有序且需频繁插入用列表(O(1) 尾部插入),去重用集合(O(1) 查询)。

2. 内存优化
  • 用生成器表达式替代列表推导式(大数据量场景);
  • 避免创建不必要的对象(如循环内创建列表/字典);
  • 使用 sys.getsizeof() 分析对象内存占用,优化大对象存储;
  • 对于超大数据集,使用 pandas 分块处理(chunksize)而非一次性加载。
3. 并发优化
  • CPU 密集型:用多进程(multiprocessing)或 C 扩展(Cython);
  • IO 密集型:用多线程或异步 IO(asyncio);
  • 避免频繁创建线程/进程:使用线程池/进程池(concurrent.futures.ThreadPoolExecutor/ProcessPoolExecutor)。
4. 第三方库优化
  • 用高效库替代原生实现:
    • 数据处理:pandas 替代原生列表/字典;
    • 字符串处理:regex 替代原生 re 模块;
    • 数值计算:numpy 替代原生数学运算;
  • 避免过度依赖第三方库:减少不必要的依赖,降低打包体积和维护成本。
5. 工具辅助优化
  • 定位性能瓶颈:用 cProfile 分析代码执行时间(python -m cProfile -s cumulative main.py);
  • 内存泄漏排查:用 tracemalloc 追踪内存分配(Python 3.4+);
  • 代码静态分析:用 pylint/flake8 检查代码冗余和潜在优化点。

代码示例(用线程池优化 IO 密集型任务)

python 复制代码
from concurrent.futures import ThreadPoolExecutor
import requests
import time

def fetch_url(url):
    response = requests.get(url)
    return url, response.status_code

urls = ["https://www.baidu.com"] * 10  # 10 个 IO 任务

# 单线程执行
start = time.time()
for url in urls:
    fetch_url(url)
print(f"单线程耗时:{time.time() - start:.2f}s")  # 约3秒

# 线程池执行(5个线程)
start = time.time()
with ThreadPoolExecutor(max_workers=5) as executor:
    results = executor.map(fetch_url, urls)
print(f"线程池耗时:{time.time() - start:.2f}s")  # 约0.6秒

考点分析:考察性能优化的系统思维,高级开发岗位需具备"定位瓶颈→选择优化方案→验证效果"的完整能力。

21. 如何排查 Python 项目中的内存泄漏?

答案解析

内存泄漏是指程序运行过程中,无用对象无法被垃圾回收,导致内存占用持续升高,最终可能引发 OOM(内存溢出)。Python 中排查内存泄漏的步骤如下:

1. 确认内存泄漏
  • psutil 监控进程内存占用:

    python 复制代码
    import psutil
    import os
    
    process = psutil.Process(os.getpid())
    while True:
        print(f"内存占用:{process.memory_info().rss / 1024 / 1024:.2f} MB")
        time.sleep(1)
  • 若内存占用持续上升且无下降趋势,可确认存在内存泄漏。

2. 定位泄漏对象
  • tracemalloc 追踪内存分配(Python 3.4+):

    python 复制代码
    import tracemalloc
    import time
    
    tracemalloc.start()  # 启动内存追踪
    
    # 模拟内存泄漏代码(循环创建对象且未释放)
    leak_list = []
    def leak_func():
        for _ in range(1000):
            leak_list.append({"data": "memory leak"})  # 列表持续增长,对象无法回收
    
    for _ in range(10):
        leak_func()
        time.sleep(0.5)
        # 获取内存分配快照
        snapshot = tracemalloc.take_snapshot()
        top_stats = snapshot.statistics("lineno")
        print(f"\n第 {_+1} 次循环内存占用:")
        for stat in top_stats[:3]:
            print(stat)
  • 输出结果会显示内存占用最高的代码行,定位泄漏对象的创建位置。

3. 分析泄漏原因

Python 中常见的内存泄漏原因:

  • 全局变量累积:全局列表/字典持续添加元素,未及时清理;
  • 循环引用:两个或多个对象相互引用,且无外部引用(如 a = [1]b = [a]a.append(b));
  • 未关闭资源:文件句柄、数据库连接、网络连接未关闭,导致资源对象无法回收;
  • 缓存未设置过期时间:lru_cache 等缓存未限制大小,导致大量对象堆积。
4. 修复与验证
  • 针对原因修复:
    • 全局变量:避免滥用,及时清理无用数据(leak_list.clear());
    • 循环引用:手动解除引用(a = Noneb = None)或使用弱引用(weakref 模块);
    • 未关闭资源:使用上下文管理器(with 语句)自动关闭;
    • 缓存:设置缓存大小限制(如 lru_cache(maxsize=128))。
  • 修复后重新用 psutil/tracemalloc 验证内存占用是否稳定。

高频泄漏场景

python 复制代码
# 场景 1:全局列表泄漏
global_list = []
def add_data(data):
    global_list.append(data)  # 持续添加,未清理

# 场景 2:循环引用
class Node:
    def __init__(self):
        self.next = None

a = Node()
b = Node()
a.next = b
b.next = a  # 循环引用,无外部引用时无法回收

# 修复:使用弱引用
import weakref
a.next = weakref.ref(b)
b.next = weakref.ref(a)

考点分析:考察问题排查和系统调优能力,是高级开发/架构师岗位的核心考点,需掌握工具使用和常见泄漏场景。

相关推荐
前端不太难3 小时前
RN Hooks 设计规范与反模式清单
开发语言·php·设计规范
HyperAI超神经3 小时前
【vLLM 学习】vLLM TPU 分析
开发语言·人工智能·python·学习·大语言模型·vllm·gpu编程
爱笑的眼睛113 小时前
FastAPI 请求验证:超越 Pydantic 基础,构建企业级验证体系
java·人工智能·python·ai
拉姆哥的小屋3 小时前
基于深度学习的瞬变电磁法裂缝参数智能反演研究
人工智能·python·深度学习
ForteScarlet3 小时前
如何解决 Kotlin/Native 在 Windows 下 main 函数的 args 乱码?
开发语言·windows·kotlin
月殇_木言3 小时前
应用层自定义协议与序列化
开发语言
a努力。3 小时前
网易Java面试被问:偏向锁在什么场景下反而降低性能?如何关闭?
java·开发语言·后端·面试·架构·c#
前端达人4 小时前
CSS终于不再是痛点:2026年这7个特性让你删掉一半JavaScript
开发语言·前端·javascript·css·ecmascript
wjs20244 小时前
SVG 多边形
开发语言