Python 基础八股题

不背概念,用大白话真正理解每一个知识点

📚 11 大主题📝 共 44 道题💡 全配代码示例
1Python 基础语法
标识符、数据类型、输入输出、运算符
1Python 中的标识符定义规则是什么?

标识符就是你给变量、函数、类等起的名字。Python 里起名要守这几条规矩:

  • 只能用字母、数字、下划线这三样,别的特殊字符都不行
  • 不能以数字开头——2name 不行,但 name2 可以
  • 不能跟 Python 的关键字重名,比如 ifforclass 这些已经被 Python 占用了
  • 区分大小写——Ageage 是两个不同的变量
  • 虽然 Python 3 支持中文当变量名,但别这么干,容易出问题且不专业

命名风格上有个约定俗成的习惯:

  • 变量名、函数名用蛇形命名法first_user_name
  • 类名用大驼峰命名法FirstUserName
2Python 中常见的数据类型有哪些?字符串拼接或格式化有哪些方式?

Python 里常见的数据类型可以分两大类:

基本类型

  • int 整数:10-3
  • float 浮点数:3.140.5
  • str 字符串:"hello"
  • bool 布尔:True / False
  • NoneType 空值:None

容器类型

  • list 列表:有序、可改、可重复 → [1, 2, 3]
  • tuple 元组:有序、不可改、可重复 → (1, 2, 3)
  • set 集合:无序、可改、不可重复 → {1, 2, 3}
  • dict 字典:键值对、键不可重复 → {"name": "张三"}

字符串拼接和格式化的方式,主要有这几种:

  • + 号拼接:"你好" + "世界",简单粗暴但只适合少量拼接
  • % 格式化:"我叫%s,今年%d岁" % ("张三", 18),老写法,C 语言风格
  • .format()"我叫{},今年{}岁".format("张三", 18),比 % 更灵活
  • f-stringf"我叫{name},今年{age}岁"最推荐,简洁好读
  • join()"-".join(["2024", "01", "15"]),适合把列表拼成字符串
3如何接收键盘录入?数据是什么类型?如何进行类型转换?

input() 函数接收键盘输入:

name = input("请输入你的名字:")
print(name)

关键点:input() 拿到的永远是字符串,不管你输入的是数字还是文字。比如你输入 18,拿到的是字符串 "18" 而不是整数 18

类型转换就是用对应的函数把字符串转成你想要的类型:

age_str = input("请输入年龄:")  # 输入 "18"
age = int(age_str)              # 转成整数 18
height = float(input("身高:"))  # 转成浮点数
num_str = str(100)              # 把数字转成字符串 "100"

常用的转换函数:int() 转整数、float() 转浮点、str() 转字符串、bool() 转布尔、list() 转列表等。注意如果字符串内容不是合法数字,转换会报 ValueError

4什么是逻辑运算符?有哪几个?具体含义是什么?

逻辑运算符就是用来把多个条件组合到一起的符号,Python 有三个:

  • and(与):两边都为 True 才是 True。
    比如 age >= 18 and has_id == True,得成年 有身份证才能进
  • or(或):只要有一边为 True 就是 True。
    比如 is_vip or points > 1000,是VIP 或者 积分超过1000就能享受优惠
  • not(非):取反,True 变 False,False 变 True。
    比如 not is_deleted,没被删除的才显示

有个实用的特点叫短路逻辑

  • and 前面为 False,后面就不看了,直接返回 False
  • or 前面为 True,后面也不看了,直接返回 True

这个特性经常用来做默认值:name = input_name or "默认名字",如果 input_name 为空就用默认的。

2流程控制
条件判断、循环、range、break/continue
1if 条件判断和 match 模式匹配的语法是什么?各自的应用场景?

if 的写法大家都熟:

if score >= 90:
    print("优秀")
elif score >= 60:
    print("及格")
else:
    print("不及格")

match 是 Python 3.10 新加的,写法类似 switch:

match status:
    case 200:
        print("成功")
    case 404:
        print("没找到")
    case 500:
        print("服务器错误")
    case _:
        print("未知状态")

两者的应用场景区别:

  • if 适合范围判断和复杂条件:比如"分数在60到90之间"这种,用 if 60 <= score <= 90 很自然
  • match 适合精确匹配某个值:比如根据 status code 做不同处理,根据命令做不同操作。代码更清晰,不用写一堆 if-elif
  • match 还能做结构匹配,比如匹配列表或字典的形状,这个 if 做起来很麻烦

简单说:条件是"大于小于"用 if,条件是"等于哪个值"用 match。

2while 和 for 的核心差异是什么?各自的应用场景?

核心区别就一句话:

  • while 是"条件驱动"——只要条件为 True 就一直转,你不知道要转几次
  • for 是"数据驱动"——从一个序列里一个一个取,取完就结束,次数是确定的

应用场景:

  • while 适合不确定循环次数的场景:比如让用户反复输入密码,直到输对为止;比如游戏主循环,一直运行直到玩家退出
  • for 适合遍历已知集合的场景:比如遍历列表里每个元素、遍历字典的键值对、配合 range 遍历固定次数

两个可以互相转换,但选对场景代码会更清晰。比如遍历列表你硬用 while 还得维护索引变量,多此一举。

3range() 语句的常见用法有哪些?每种含义是什么?

range() 是生成整数序列的工具,三种用法:

  • range(n):从 0 到 n-1,步长为 1
    比如 range(5) → 0, 1, 2, 3, 4
  • range(a, b):从 a 到 b-1,步长为 1
    比如 range(2, 6) → 2, 3, 4, 5
  • range(a, b, step):从 a 到 b-1,每次跳 step
    比如 range(0, 10, 2) → 0, 2, 4, 6, 8
    比如 range(10, 0, -1) → 10, 9, 8, 7, 6, 5, 4, 3, 2, 1(倒着数)

注意两个细节:包头不包尾(包含起点不包含终点),range 返回的不是列表而是惰性序列,省内存。

4break 和 continue 的作用是什么?使用时有什么注意事项?
  • break直接跳出整个循环,后面不转了。比如在列表里找到目标元素后就不找了
  • continue跳过本次循环剩下的代码,直接进入下一次循环。比如过滤掉不需要处理的元素
# break: 找到3就停
for i in range(10):
    if i == 3:
        break
    print(i)  # 只打印 0 1 2

# continue: 跳过偶数
for i in range(5):
    if i % 2 == 0:
        continue
    print(i)  # 只打印 1 3

注意事项:

  • break 和 continue 只对最近一层循环生效,嵌套循环里内层的 break 跳不出外层
  • 在 while 循环里用 continue 要特别小心,如果 continue 在条件更新之前,可能导致死循环
  • 不要滥用,循环逻辑太复杂时往往说明该拆分函数了
3数据容器(一)
列表与元组、切片、组包解包、容器总览
1列表和元组的核心区别是什么?为什么需要两种相似的容器?

核心区别就一个字:

  • 列表(list)可以修改——能增删改元素
  • 元组(tuple)不可修改——创建后就不能动了

两者都有序、都可重复,长得确实像。但 Python 设计两种是有道理的:

  • 列表适合数据会变化的场景:购物车商品列表(随时加删)、待办事项列表、读取的数据需要再处理
  • 元组适合数据不该变、也不许别人变的场景:数据库一条记录的固定字段、函数返回多个值、字典的键(列表不能当键,元组可以)

元组还有个隐藏好处:因为不可变,所以创建速度比列表快、内存占用也小。如果你有一组数据确定不变,用元组更省。

2切片操作中"步长"为正和为负时有什么不同?[::-1] 是什么意思?

切片语法:序列[起:止:步长]

  • 步长为正:从左往右取,起点的索引要小于终点的索引
    比如 [0,1,2,3,4][1:4][1, 2, 3]
  • 步长为负:从右往左取,起点的索引要大于终点的索引
    比如 [0,1,2,3,4][3:0:-1][3, 2, 1]

[::-1] 就是把步长设为 -1,起止都空着,表示从头到尾倒着取,效果就是反转序列:

s = "hello"
print(s[::-1])  # "olleh"

lst = [1, 2, 3, 4, 5]
print(lst[::-1])  # [5, 4, 3, 2, 1]

这个写法非常常见,面试也常考,记住一句话:[::-1] 就是翻转

3什么是组包?什么是解包?a, b = b, a 发生了什么?

组包就是把多个值自动打包成一个元组:

t = 1, 2, 3  # 没加括号,Python 自动打包成元组 (1, 2, 3)

解包就是把容器里的值拆开分别赋给变量:

a, b, c = [1, 2, 3]      # 列表解包:a=1, b=2, c=3
x, y = (10, 20)          # 元组解包:x=10, y=20
m, n = {100, 200}        # 集合解包:m 和 n 分别拿到一个元素

a, b = b, a 这个经典交换,背后发生的事:

  1. 先组包:右边的 b, a 被打包成一个元组 (b的值, a的值)
  2. 再解包:把这个元组拆开,第一个值赋给 a,第二个值赋给 b

所以交换不需要临时变量,一步到位。Python 在底层帮你搞定了。

4说说 Python 中常见的数据容器及各自特点?

四大容器一张表:

容器    有序   可修改   可重复   查找速度
─────────────────────────────────────
list    是     是       是       慢(O(n)遍历)
tuple   是     否       是       慢
set     否     是       否       快(O(1)哈希)
dict    有序   是       键不可重复  快(O(1)哈希)

各自适合干什么:

  • list 列表:最常用的万金油,需要按顺序存一堆东西、还要增删改的时候用它
  • tuple 元组:数据固定不变的时候用它,比如经纬度坐标、数据库记录
  • set 集合:需要去重的时候、需要做交集/并集/差集运算的时候
  • dict 字典:需要通过 key 快速查找 value 的时候,比如用户信息 {"name": "张三", "age": 18}
4数据容器(二)
集合运算、元组解包进阶、容器对比、函数返回值
1集合 set 如何进行交集、并集、差集运算?应用场景?
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# 交集:两个集合都有的
print(a & b)        # 或 a.intersection(b) → {3, 4}

# 并集:两个集合合在一起去重
print(a | b)        # 或 a.union(b) → {1, 2, 3, 4, 5, 6}

# 差集:a 有但 b 没有的
print(a - b)        # 或 a.difference(b) → {1, 2}

应用场景:

  • 交集:找出两个列表共有的元素。比如找出同时关注了A和B的用户
  • 并集:把两个列表合并去重。比如两个班级的学生名单合并
  • 差集:找出"有但另一个没有的"。比如找出"买了A但没买B"的用户
2"逗号才是元组的灵魂,括号只是礼貌的装饰"——解释这句话。*在解包中扮演什么角色?

意思是:定义元组靠的是逗号,不是括号

t1 = 1, 2, 3        # 没括号,但这确实是元组 → (1, 2, 3)
t2 = (1, 2, 3)     # 加括号也行,但括号不是必须的
t3 = (1,)           # 只有一个元素时,逗号必须有!不加逗号就是普通整数
t4 = (1)            # 这不是元组!这就是整数 1

所以"括号是礼貌的装饰"——加了好看、清晰,但不加也行。逗号才是灵魂。

* 在解包中的作用是收集剩余元素

first, *other, last = (9, 10, 2, 23, 12)

结果:

  • first = 9(第一个)
  • last = 12(最后一个)
  • other = [10, 2, 23](中间所有的,类型是列表

实际用途:比如函数接收不定长参数时用 *args 收集;比如只需要首尾元素,中间的不管;比如处理数据时把头部和尾部单独拿出来,中间的批量处理。

3详细描述 list、set、tuple、dict 的特点和应用场景?哪些是序列类型?

逐个说:

  • list 列表:有序、可改、可重复。像一排可插拔的抽屉,随时加东西、拿东西、换东西。适合存一组同类数据,比如所有学生的成绩
  • set 集合:无序、可改、不可重复。像一个自动去重的篮子,扔进去重复的东西会自动只留一个。适合去重和集合运算
  • tuple 元组:有序、不可改、可重复。像密封的盒子,装好了就不能动了。适合存固定不变的数据,比如数据库配置信息
  • dict 字典:键值对、键不可重复、Python 3.7+ 有序。像一本通讯录,通过名字找电话号码。适合通过 key 快速查 value

序列类型:list、tuple、str 都是序列类型。序列类型的特点是有序、可以通过索引访问(seq[0])、支持切片(seq[1:3])、支持 len()、支持 in 判断成员。

set 和 dict 不属于序列类型——set 无序不能索引,dict 虽然有序但它是通过 key 访问而不是位置索引。

4Python 函数可以声明多个返回值吗?如何封装和接收?Docstring 的价值?

Python 函数本质上只能返回一个值,但可以通过返回元组的方式"假装"返回多个值:

def get_user_info():
    name = "张三"
    age = 18
    return name, age    # 这里其实是组包成 (name, age) 返回

# 接收方式1:解包接收
name, age = get_user_info()

# 接收方式2:整体接收,拿到的是元组
info = get_user_info()  # info = ("张三", 18)

函数说明文档(Docstring)就是写在函数体第一行的字符串,用三引号包裹。它的价值是:

  • 让别人(和未来的自己)一看就知道这个函数是干什么的、参数怎么传、返回什么
  • IDE 会自动提示,调用函数时能看到参数说明
  • help(函数名) 能直接查看

其中 :param 描述参数是什么意思,:return 描述返回值是什么:

def add(a, b):
    """两数相加

    :param a: 第一个加数
    :param b: 第二个加数
    :return: 两数之和
    """
    return a + b
5函数进阶
参数传递、不定长参数、作用域、类型注解
1函数调用时参数传递有哪几种方式?/ 在参数列表中表示什么?

Python 参数传递主要有这几种:

  • 位置参数:按顺序传,最基本的方式。def f(a, b): 调用时 f(1, 2),1 给 a,2 给 b
  • 关键字参数:按名字传,不关心顺序。f(b=2, a=1) 效果一样
  • 默认值参数:调用时可以不传,用默认值。def f(a, b=10):,调用 f(1) 时 b 就是 10
  • 可变位置参数*args 收集多余的位置参数成元组
  • 可变关键字参数**kwargs 收集多余的关键字参数成字典

参数列表中的 / 表示:它左边的参数只能用位置传递,不能用关键字传递

def score(math, english, /, *, round=2):
    ...

# math 和 english 只能位置传,不能写 math=90
# round 只能关键字传,不能写位置
score(90, 80)           # 正确
score(90, 80, round=3)  # 正确
score(math=90, 80)     # 报错!/ 左边不允许关键字

这主要是库作者用来防止参数名被依赖——以后改参数名也不会影响调用方。

2函数需要接收不确定数量的参数时,如何定义?

两种方式:

  • *args:收集多余的位置参数,打包成元组
  • **kwargs:收集多余的关键字参数,打包成字典
def func(*args, **kwargs):
    print(args)     # 元组
    print(kwargs)   # 字典

func(1, 2, 3, name="张三", age=18)
# args   = (1, 2, 3)
# kwargs = {"name": "张三", "age": 18}

应用场景:

  • *args:参数个数不确定但都是同类。比如 max(1, 2, 3, 4) 可以传任意个数
  • **kwargs:可选配置项很多,用字典传更灵活。比如 requests.get(url, timeout=5, headers={...})
3函数中变量的作用域?global 关键字的作用?

作用域就是"变量在哪儿能被访问到",Python 分两种主要作用域:

  • 局部变量:在函数内部定义的变量,只在函数内有效,函数执行完就没了
  • 全局变量:在函数外面定义的变量,所有函数都能读到
count = 0  # 全局变量

def add():
    count = 1  # 这是局部变量!不是修改全局的
    print(count)  # 1

add()
print(count)  # 0,全局变量没变

global 的作用:告诉函数"这个变量用的是外面的全局变量,不是新建局部变量":

count = 0

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

add()
print(count)  # 1,全局变量被修改了

实际开发中尽量少用 global,因为全局变量被到处改容易出 bug。更好的做法是通过参数传进来、通过返回值传出去。

4类型注解的作用?是强制性的吗?哪些场景该加?

类型注解就是给变量和函数标注上"这是什么类型",方便人和工具看:

def add(a: int, b: int) -> int:
    return a + b

name: str = "张三"
scores: list[int] = [90, 80, 70]

类型注解不是强制的——Python 不会因为你传了错误类型就报错,它只起提示作用。不加类型注解代码也能正常运行。

但加了的好处是:

  • IDE 能智能提示,写错类型会标黄提醒你
  • 别人看代码一眼就知道该传什么类型
  • 配合 mypy 等工具可以在运行前检查出类型错误

什么时候该加:

  • 该加:函数的参数和返回值、公共API接口、团队协作的项目代码
  • 可以省略:临时脚本、简单的局部变量、一看就知道类型的(i = 0 谁不知道是 int)
6面向对象(一)
编程思想、属性、魔法方法、封装
1"面向过程"与"面向对象"的核心区别?

用大白话说:

  • 面向过程就是"按步骤来"——第一步干啥,第二步干啥,第三步干啥。关注的是过程和步骤,就像写菜谱:先切菜、再烧油、最后翻炒。代码就是一堆函数按顺序调用。
  • 面向对象就是"找谁来干"——把数据和操作数据的方法绑在一起,变成一个"对象"。关注的是谁来做,就像餐厅里点菜:你跟厨师说"来份番茄炒蛋",具体怎么切怎么炒是厨师的事,你不用管。

举个例子,养猫:

  • 面向过程:写函数 feed(cat)play(cat),猫是参数传来传去
  • 面向对象:定义 Cat 类,里面有 feed()play() 方法,猫自己就能吃能玩:cat.feed()

面向对象的好处是代码更贴近现实、更好复用和扩展。但不是说面向对象一定比面向过程好,简单脚本用面向过程反而更直接。

2类属性与实例属性有什么区别?什么时候用哪个?

简单理解:

  • 实例属性:每个对象自己持有的数据,各个对象各不相同。写在 __init__ 里,用 self.xxx 赋值
  • 类属性:所有对象共享的数据,写在类里但不在 __init__ 里,直接写在 class 下面
class Dog:
    species = "犬科"  # 类属性:所有狗都是犬科

    def __init__(self, name):
        self.name = name  # 实例属性:每只狗名字不同

d1 = Dog("旺财")
d2 = Dog("来福")
print(d1.name, d1.species)  # 旺财 犬科
print(d2.name, d2.species)  # 来福 犬科

什么时候用:

  • 用实例属性:每个对象各不相同的特征——名字、年龄、价格
  • 用类属性:所有对象都一样的特征,或者需要全局统计的东西——物种、默认配置、实例计数器

注意:通过实例修改类属性时要小心,可能只改了实例自己的副本而不是类级别的。改类属性建议直接用 类名.属性 = 新值

3什么是魔法方法?常见的有哪些?运行时机是什么?

魔法方法就是 Python 里两边带双下划线的方法(也叫 dunder method),不需要你手动调用,Python 会在特定时机自动触发

常见的魔法方法:

  • __init__:对象创建时自动调用,用来初始化属性。运行时机:创建对象时
  • __str__:print 对象时自动调用,返回字符串给人看。运行时机:print(对象) 或 str(对象)
  • __repr__:给开发者看的对象表示。运行时机:直接在终端输入对象名时
  • __len__:让 len(对象) 能用。运行时机:调用 len() 时
  • __eq__:让 == 能比较两个对象。运行时机:用 == 比较时
  • __lt__:让 < 能比较。运行时机:用 < 比较时
  • __del__:对象被销毁时调用。运行时机:对象被回收时
class Student:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"学生:{self.name}"

s = Student("张三")
print(s)  # 自动调用 __str__,输出:学生:张三
4如何设置私有属性和方法?有什么特点?

Python 里设置私有的方式很朴素:在名字前面加两个下划线

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner        # 公开属性
        self.__balance = balance  # 私有属性(双下划线开头)

    def __check_password(self):   # 私有方法
        print("验证密码中...")

    def deposit(self, amount):    # 公开方法
        self.__check_password()   # 内部可以调用私有方法
        self.__balance += amount
        print(f"存入{amount},余额{self.__balance}")

acc = BankAccount("张三", 1000)
print(acc.owner)        # 正常访问
# print(acc.__balance)  # 报错!外部访问不了私有属性
acc.deposit(500)        # 正常,内部间接使用了私有属性

私有属性和方法的特点

  • 在类的外部不能直接访问,相当于"加了把锁"
  • 在类的内部可以正常访问
  • 如果想从外部读/改私有属性,应该提供公开的 getter/setter 方法来间接操作

不过说实话,Python 的私有不是绝对私有的——通过 对象._类名__属性名 还是能强行访问到。Python 的哲学是"大家都是成年人",靠自觉,不像 Java 那样严格限制。单下划线 _name 是"约定俗成的私有",表示"不建议外部访问",但不强制。

7面向对象(二)
多继承、抽象类、异常捕获、文件操作
1一个类能继承多个类吗?多继承同名方法哪个生效?方法解析顺序?

能。Python 支持多继承,一个类可以同时继承多个父类:

class A:
    def run(self):
        print("A的run")

class B:
    def run(self):
        print("B的run")

class C(A, B):
    pass

c = C()
c.run()  # 输出 "A的run"

多个父类有同名方法时,遵循 C3 线性化算法(MRO)——简单说就是"从左到右,深度优先"地找。上面 C 先继承 A 再继承 B,所以先在 A 里找到 run 就用了 A 的。

查看方法解析顺序用 __mro__

print(C.__mro__)
# (<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>)
# 查找顺序:C → A → B → object

实际开发中多继承要谨慎,容易出现"菱形继承"问题(两个父类继承同一个祖父类),设计时要想清楚。

2怎么定义抽象类和抽象方法?什么样的类/方法适合定义为抽象的?

abc 模块来定义:

from abc import ABC, abstractmethod

class Animal(ABC):           # 继承 ABC 就是抽象类
    @abstractmethod         # 装饰器标记抽象方法
    def make_sound(self):
        pass                 # 只定义不实现

    def sleep(self):         # 普通方法可以有实现
        print("zzz...")

# animal = Animal()  # 报错!抽象类不能直接实例化

class Dog(Animal):
    def make_sound(self):   # 子类必须实现抽象方法
        print("汪汪!")

dog = Dog()  # 这才行
dog.make_sound()  # 汪汪!

什么样的适合定义成抽象:

  • 抽象类:一类事物的"基类",本身不该被直接使用,只用来被继承。比如 Animal——你说"一个动物"太笼统,得具体到 Dog、Cat
  • 抽象方法:所有子类都应该有、但各自实现不同的方法。比如所有动物都会叫,但叫声不一样,所以 make_sound 设为抽象的,让子类各自实现

抽象类的意义就是定规矩:强制子类必须实现某些方法,不然不让你实例化。

3异常捕获方案的语法?捕获多种异常时的流程?

基本语法:

try:
    # 可能出错的代码
    result = 10 / 0
except ZeroDivisionError as e:
    # 捕获到特定异常
    print(f"出错了:{e}")
else:
    # try 没出错时执行
    print("一切正常")
finally:
    # 不管出没出错都执行
    print("收尾工作")

捕获多种异常时的流程:

try:
    num = int(input("输入数字:"))
    result = 10 / num
except ValueError:
    print("输入的不是数字")
except ZeroDivisionError:
    print("不能除以零")
except Exception as e:
    print("其他未知错误")

流程是这样的:从上到下依次匹配,哪个先匹配上就执行哪个,执行完就跳出整个 try-except 块,后面的 except 不再检查。所以要把具体的异常类型写在前面,通用的 Exception 写在最后兜底。

实际开发中,能预判什么异常就捕获什么异常,不要笼统地 except Exception 全包了——那样会掩盖真正的 bug。

4文件操作的基本步骤?如何保证资源正确释放?

文件操作三步走:打开 → 读写 → 关闭

# 基本写法
f = open("test.txt", "r", encoding="utf-8")
content = f.read()
f.close()  # 别忘了关

# 但上面的写法有隐患:如果中间出错了,close() 可能执行不到

保证资源释放的最佳实践——with 上下文管理器

with open("test.txt", "r", encoding="utf-8") as f:
    content = f.read()
# 出了 with 块自动关闭,不管中间有没有出错

# 写文件也一样
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("写入内容\n")

with 的原理是:进入 with 块时自动调用对象的 __enter__ 方法,离开时自动调用 __exit__ 方法——哪怕中间报了异常,__exit__ 也会被执行,文件一定会被关闭。

常见文件模式:"r" 读、"w" 写(覆盖)、"a" 追加、"rb"/"wb" 二进制读写。养成习惯:操作文件一律用 with,别手动 close。

8网络基础
IP/域名/端口、HTTP协议、GET与POST、AI记忆
1IP地址、域名、端口号的作用是什么?浏览器访问百度时发生了什么?

三个概念打个比方:

  • IP地址:就像小区的地址(某某路几号),定位互联网上某台电脑的具体位置,比如 110.242.68.66
  • 域名:就是 IP 地址的好记版别名。IP 那串数字太难记了,所以用 www.baidu.com 代替,就像小区的"阳光花园"代替"某某路123号"
  • 端口号:就像门牌号。一栋楼(一台电脑)里可能有好多个服务——网页服务住 80 号房间,数据库服务住 3306 号房间,你得知道去哪个门

浏览器访问 https://www.baidu.com 的完整过程:

  1. DNS 解析:浏览器问 DNS 服务器"www.baidu.com 的 IP 是多少?",DNS 回答"110.242.68.66"
  2. 建立连接:浏览器通过这个 IP 地址,连上百度服务器的 443 端口(HTTPS 默认端口)
  3. TLS 握手:因为用的是 https,浏览器和服务器先加密握手,建立安全通道
  4. 发送请求:浏览器发 HTTP 请求:"给我首页内容"
  5. 服务器处理:百度服务器收到请求,执行搜索页面的后端逻辑,把网页内容准备好
  6. 返回响应:服务器把 HTML 内容通过 HTTP 响应发回给浏览器
  7. 渲染页面:浏览器拿到 HTML,解析渲染成你看到的百度搜索页面
2什么是 HTTP 协议?请求数据和响应数据的格式?常见状态码含义?

HTTP 就是浏览器和服务器之间沟通用的"语言"。浏览器说"我要什么"(请求),服务器说"给你什么"(响应),双方按固定格式说话,这个格式规范就是 HTTP 协议。

请求数据格式,分四部分:

  • 请求行:方法 + 路径 + 协议版本,比如 GET /api/users HTTP/1.1
  • 请求头:一些附加信息,比如浏览器类型、接收的内容格式、Cookie 等
  • 空行:一个空行分隔头和体
  • 请求体:要发给服务器的数据(GET 请求通常没有,POST 有)

响应数据格式,也分四部分:

  • 状态行:协议版本 + 状态码 + 状态描述,比如 HTTP/1.1 200 OK
  • 响应头:返回数据的类型、长度、Cookie 等
  • 空行
  • 响应体:实际返回的数据,通常是 HTML 或 JSON

常见状态码(记住大类就行):

  • 200 OK:成功了
  • 301 / 302:重定向,页面搬家了
  • 304 Not Modified:内容没变,用缓存就行
  • 400 Bad Request:请求写得不对,服务器看不懂
  • 401 Unauthorized:没登录,需要认证
  • 403 Forbidden:登录了但没权限
  • 404 Not Found:找不到这个资源
  • 500 Internal Server Error:服务器内部出错了
  • 502 Bad Gateway:网关出问题了
  • 503 Service Unavailable:服务暂时不可用
3GET 与 POST 的区别?哪些情况建议用 POST?

核心区别:

  • 参数位置:GET 参数拼在 URL 后面(/api/users?name=张三&age=18),POST 参数放在请求体里
  • 可见性:GET 参数在地址栏能看到,POST 在地址栏看不到
  • 长度限制:GET URL 长度有浏览器限制(约2KB),POST 基本不限
  • 缓存:GET 请求会被浏览器缓存,POST 不会
  • 历史记录:GET 请求的参数会留在浏览器历史里,POST 不会
  • 语义:GET 是"获取数据",POST 是"提交数据"

建议用 POST 的情况:

  • 传敏感信息:密码、身份证号等,别放 URL 上被人看到
  • 传大量数据:文件上传、长文本提交
  • 会产生数据变更的操作:注册、登录、提交表单、删除数据——这些操作用 POST 是因为不希望被缓存或被重复执行
4AI 大模型本身具备会话记忆能力吗?如何让它具备记忆?

大多数 AI 大模型本身是没有记忆的——每次对话都是独立的,它不记得你上一句说了什么。你每次发消息对它来说都是"第一次见面"。

让大模型"有记忆"的通用做法是:把之前的对话记录一起带上发给模型。

# 伪代码示意
messages = [
    {"role": "user", "content": "我叫张三"},
    {"role": "assistant", "content": "你好,张三!"},
    {"role": "user", "content": "我叫什么?"},  # 这次发的时候把上面的也带上
]

response = model.chat(messages)  # 模型看到完整上下文就能"记住"了
# 返回:"你叫张三呀"

所以"记忆"的本质就是:每次请求时把历史对话拼接在一起发给模型。这也是为什么聊天越久 API 费用越高——因为每次都把前面的对话全发了一遍。

实际项目中通常做两件事:

  • 用数据库存储每个用户的对话历史(session 管理)
  • 设置一个对话窗口(比如只带最近10轮对话),太早的就不带了,控制成本
9API 接口设计
RESTful风格、JSON参数封装、同步与异步
1什么是 RESTful 风格的 API?请求方式对应增删改查的哪种?

RESTful 是一种 API 设计风格,核心理念是:用 URL 表示资源,用 HTTP 方法表示操作

资源就是名词,比如"用户"(/users)、"文章"(/articles)。操作就是动词,用 HTTP 的方法来表示:

  • GET(查询):GET /users 获取用户列表,GET /users/1 获取id为1的用户
  • POST(新增):POST /users 新建一个用户
  • PUT / PATCH(修改):PUT /users/1 更新id为1的用户信息
  • DELETE(删除):DELETE /users/1 删除id为1的用户

RESTful 的几个特点:

  • URL 里只有名词,没有动词(/users 对,/getUsers 不对)
  • 用 HTTP 方法区分操作类型,而不是在 URL 里写动作
  • 返回统一的状态码和 JSON 格式数据
  • 无状态:每次请求都包含所有信息,服务器不依赖 session 状态
2前端传 JSON 格式参数,服务端如何封装接收?

以 FastAPI 为例,前端传这样的 JSON:

{"name": "王林", "age": 80, "gender": "男"}

服务端的做法是:定义一个数据模型(Pydantic Model),用它来接收 JSON

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 定义数据模型,字段类型和 JSON 对应
class UserCreate(BaseModel):
    name: str
    age: int
    gender: str

@app.post("/api/users")
def create_user(user: UserCreate):
    # user 对象自动封装好了
    print(user.name)    # 王林
    print(user.age)     # 80
    print(user.gender)  # 男
    return {"code": 200, "msg": "创建成功"}

流程就是:前端发 JSON → FastAPI 自动把 JSON 反序列化 → 填入你定义的 Model → 函数里直接用 user.xxx 访问。类型不对还会自动报错,比如 age 传了字符串就会返回 422 错误。

3同步与异步的特点?为什么异步性能更高?

用排队买饭来理解:

  • 同步就是"排队等"——你点完餐就站在窗口等,等做好了拿到手才走开,然后下一个人才能点。虽然你等的时候啥也没干,但别人也用不了这个窗口
  • 异步就是"点完就走,好了叫你"——你点完餐拿个号就去干别的了,厨房做好了通知你来取。窗口可以马上服务下一个人

为什么异步性能更高?因为在 IO 操作(等数据库、等网络、等文件)时,CPU 是闲着的。同步模式下 CPU 只能干等,异步模式下 CPU 可以去处理别的请求。一台服务器就能同时服务更多用户。

但异步不是万能的:

  • 适合 IO 密集型任务(网络请求、数据库查询、文件读写)——等的时间长,异步能充分利用
  • 不适合 CPU 密集型任务(大量计算)——CPU 一直在忙,异步没意义

FastAPI 之所以快,就是因为底层用了异步(async/await),在高并发场景下性能远高于同步框架。

10框架与数据库
FastAPI参数接收、日志、MySQL约束、char与varchar
1FastAPI 中如何接收 JSON 参数和路径参数?

接收 JSON 参数用 Pydantic Model:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
    name: str
    age: int
    gender: str

# 接收 JSON body
@app.post("/api/users")
def create_user(user: UserCreate):
    return {"name": user.name, "age": user.age}

接收路径参数用大括号 {} 占位:

# 路径里 {user_id} 就是路径参数
@app.get("/api/users/{user_id}")
def get_user(user_id: int):    # 参数名和路径里的一致,自动接收
    return {"user_id": user_id}

# 请求 /api/users/1 → user_id = 1
# 请求 /api/users/2 → user_id = 2

还可以同时用路径参数 + 查询参数 + JSON body:

@app.put("/api/users/{user_id}")
def update_user(user_id: int,                # 路径参数
                user: UserCreate):           # JSON body
    return {"id": user_id, "data": user.name}
2logging 模块的日志级别?从低到高?常用两种的场景?

日志级别从低到高:

  • DEBUG:调试信息,最详细的日志
  • INFO:正常运行信息,确认程序按预期工作
  • WARNING:警告,有问题但不影响运行
  • ERROR:出错,某功能失败了
  • CRITICAL:严重错误,整个程序可能挂了

设了某个级别后,只输出该级别及以上的日志。比如设成 WARNING,那 DEBUG 和 INFO 就不输出了。

常用的两种及场景:

  • INFO:记录正常流程——"用户登录成功"、"订单创建成功"。日常运行看这些就知道系统在干什么
  • ERROR:记录出错的详情——"数据库连接失败"、"支付接口超时"。出了问题靠这些排查
import logging
logging.basicConfig(level=logging.INFO)

logging.debug("调试信息")     # 不输出(低于 INFO)
logging.info("用户张三登录")   # 输出
logging.warning("磁盘空间不足") # 输出
logging.error("接口调用失败")   # 输出
3什么是约束?MySQL 有哪些约束?一个字段能加多个约束吗?

约束就是给字段加的限制规则,防止脏数据存进数据库。比如年龄不能为负,用户名不能重复。

MySQL 常见约束:

  • PRIMARY KEY 主键:唯一标识一条记录,不能重复、不能为空
  • NOT NULL 非空:这个字段必须有值,不能为空
  • UNIQUE 唯一:值不能重复(比如手机号、邮箱)
  • DEFAULT 默认值:不填时自动用默认值
  • AUTO_INCREMENT 自增:自动递增,常配合主键用
  • FOREIGN KEY 外键:关联其他表,保证数据一致性
  • CHECK 检查:自定义条件(MySQL 8.0+ 才真正生效)

一个字段可以加多个约束,空格隔开就行:

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,  -- 既非空又唯一
    age INT NOT NULL DEFAULT 0,            -- 既非空又有默认值
    phone CHAR(11) NOT NULL UNIQUE         -- 既非空又唯一
);
4char 与 varchar 的区别?应用场景?

两个都是存字符串的,区别在于定长和变长

  • CHAR(n):固定长度。存的时候不管你实际多长,都占 n 个字符的空间,不够的用空格补齐。比如 CHAR(10) 存 "abc" 也占 10 个字符
  • VARCHAR(n):可变长度。实际多长就占多长,外加 1-2 字节存长度信息。比如 VARCHAR(10) 存 "abc" 只占 3 个字符

应用场景:

  • CHAR 适合长度固定的字段:手机号(11位)、身份证号(18位)、性别代码(1位)。固定长度用 CHAR 查询效率略高
  • VARCHAR 适合长度不固定的字段:用户名、邮箱、地址、文章标题。不确定多长就用 VARCHAR,省空间

实际项目中 90% 的情况用 VARCHAR,只有确定长度固定时才用 CHAR。

11SQL 进阶
条件查询、事务、分组查询
1DQL 条件查询的常见构建方式?模糊查询、范围查询、NULL值判断?
-- 模糊查询:LIKE + 通配符
SELECT * FROM users WHERE name LIKE '张%';    -- 张开头的
SELECT * FROM users WHERE name LIKE '%三';    -- 三结尾的
SELECT * FROM users WHERE name LIKE '%张%';  -- 包含张的

-- 范围查询
-- 方式1:BETWEEN...AND...
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
-- 方式2:IN
SELECT * FROM users WHERE city IN ('北京', '上海', '广州');

-- NULL值判断(注意不能用 = )
SELECT * FROM users WHERE email IS NULL;        -- 邮箱为空
SELECT * FROM users WHERE email IS NOT NULL;    -- 邮箱不为空

-- 比较运算符
SELECT * FROM products WHERE price >= 100 AND price <= 500;
SELECT * FROM orders WHERE status != '已完成';

几个容易踩的坑:

  • NULL 判断必须用 IS NULL / IS NOT NULL,不能用 = NULL(永远查不出来)
  • LIKE 通配符:% 代表任意多个字符,_ 代表一个字符
  • BETWEEN 是包头包尾的(包含两端值)
2什么是事务?如何控制?四大特性?什么场景需要事务?

事务就是把几条 SQL 打包成一个整体,要么全成功,要么全失败

经典例子:转账。A 给 B 转 100 块,两步操作——A 扣 100、B 加 100。如果第一步成功第二步失败,钱就凭空消失了。事务保证两步要么都成功,要么都回滚。

事务控制:

BEGIN;    -- 开启事务
UPDATE account SET balance = balance - 100 WHERE name = 'A';
UPDATE account SET balance = balance + 100 WHERE name = 'B';
COMMIT;   -- 提交:两步都成功,确认写入

-- 如果中间出错了:
ROLLBACK; -- 回滚:撤销刚才所有操作

四大特性(ACID):

  • A 原子性(Atomicity):事务里的操作不可分割,要么全做要么全不做
  • C 一致性(Consistency):事务执行前后数据是一致的,比如转完账总金额不变
  • I 隔离性(Isolation):多个事务同时执行互不干扰
  • D 持久性(Durability):事务一旦提交,数据就永久保存了,断电也不丢

什么场景需要事务控制:涉及多表或多步数据修改、且这些修改必须同时成功或同时失败的场景——转账、订单创建(扣库存+创建订单+记流水)、用户注册(写用户表+写权限表)等。

3分组查询中 WHERE 和 HAVING 的区别?常用聚合函数?

两者都是做条件过滤的,但时机和对象不同

  • WHERE:在分组之前过滤,过滤的是原始行数据。不能在 WHERE 里用聚合函数
  • HAVING:在分组之后过滤,过滤的是分组后的结果。可以配合聚合函数使用
-- 需求:查出各部门平均工资大于10000的部门
SELECT dept, AVG(salary) as avg_salary
FROM employees
WHERE status = '在职'          -- 先过滤:只看在职的
GROUP BY dept                   -- 再分组:按部门分组
HAVING AVG(salary) > 10000;     -- 最后过滤:只留平均工资>10000的部门

执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

常用聚合函数:

  • COUNT():统计行数——COUNT(*) 数所有行,COUNT(字段) 数非空行
  • SUM():求和
  • AVG():平均值
  • MAX():最大值
  • MIN():最小值

一句话总结:WHERE 是"先筛再分",HAVING 是"分完再筛"。