Python 基础八股题
不背概念,用大白话真正理解每一个知识点
标识符就是你给变量、函数、类等起的名字。Python 里起名要守这几条规矩:
- 只能用字母、数字、下划线这三样,别的特殊字符都不行
- 不能以数字开头——
2name不行,但name2可以 - 不能跟 Python 的关键字重名,比如
if、for、class这些已经被 Python 占用了 - 区分大小写——
Age和age是两个不同的变量 - 虽然 Python 3 支持中文当变量名,但别这么干,容易出问题且不专业
命名风格上有个约定俗成的习惯:
- 变量名、函数名用蛇形命名法:
first_user_name - 类名用大驼峰命名法:
FirstUserName
Python 里常见的数据类型可以分两大类:
基本类型
int整数:10、-3float浮点数:3.14、0.5str字符串:"hello"bool布尔:True/FalseNoneType空值:None
容器类型
list列表:有序、可改、可重复 →[1, 2, 3]tuple元组:有序、不可改、可重复 →(1, 2, 3)set集合:无序、可改、不可重复 →{1, 2, 3}dict字典:键值对、键不可重复 →{"name": "张三"}
字符串拼接和格式化的方式,主要有这几种:
+号拼接:"你好" + "世界",简单粗暴但只适合少量拼接%格式化:"我叫%s,今年%d岁" % ("张三", 18),老写法,C 语言风格.format():"我叫{},今年{}岁".format("张三", 18),比 % 更灵活f-string:f"我叫{name},今年{age}岁",最推荐,简洁好读join():"-".join(["2024", "01", "15"]),适合把列表拼成字符串
用 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。
逻辑运算符就是用来把多个条件组合到一起的符号,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,后面就不看了,直接返回 Falseor前面为 True,后面也不看了,直接返回 True
这个特性经常用来做默认值:name = input_name or "默认名字",如果 input_name 为空就用默认的。
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。
核心区别就一句话:
- while 是"条件驱动"——只要条件为 True 就一直转,你不知道要转几次
- for 是"数据驱动"——从一个序列里一个一个取,取完就结束,次数是确定的
应用场景:
- while 适合不确定循环次数的场景:比如让用户反复输入密码,直到输对为止;比如游戏主循环,一直运行直到玩家退出
- for 适合遍历已知集合的场景:比如遍历列表里每个元素、遍历字典的键值对、配合 range 遍历固定次数
两个可以互相转换,但选对场景代码会更清晰。比如遍历列表你硬用 while 还得维护索引变量,多此一举。
range() 是生成整数序列的工具,三种用法:
range(n):从 0 到 n-1,步长为 1
比如range(5)→ 0, 1, 2, 3, 4range(a, b):从 a 到 b-1,步长为 1
比如range(2, 6)→ 2, 3, 4, 5range(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 返回的不是列表而是惰性序列,省内存。
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 在条件更新之前,可能导致死循环
- 不要滥用,循环逻辑太复杂时往往说明该拆分函数了
核心区别就一个字:改。
- 列表(list)可以修改——能增删改元素
- 元组(tuple)不可修改——创建后就不能动了
两者都有序、都可重复,长得确实像。但 Python 设计两种是有道理的:
- 列表适合数据会变化的场景:购物车商品列表(随时加删)、待办事项列表、读取的数据需要再处理
- 元组适合数据不该变、也不许别人变的场景:数据库一条记录的固定字段、函数返回多个值、字典的键(列表不能当键,元组可以)
元组还有个隐藏好处:因为不可变,所以创建速度比列表快、内存占用也小。如果你有一组数据确定不变,用元组更省。
切片语法:序列[起:止:步长]
- 步长为正:从左往右取,起点的索引要小于终点的索引
比如[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] 就是翻转。
组包就是把多个值自动打包成一个元组:
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 这个经典交换,背后发生的事:
- 先组包:右边的
b, a被打包成一个元组(b的值, a的值) - 再解包:把这个元组拆开,第一个值赋给 a,第二个值赋给 b
所以交换不需要临时变量,一步到位。Python 在底层帮你搞定了。
四大容器一张表:
容器 有序 可修改 可重复 查找速度
─────────────────────────────────────
list 是 是 是 慢(O(n)遍历)
tuple 是 否 是 慢
set 否 是 否 快(O(1)哈希)
dict 有序 是 键不可重复 快(O(1)哈希)
各自适合干什么:
list列表:最常用的万金油,需要按顺序存一堆东西、还要增删改的时候用它tuple元组:数据固定不变的时候用它,比如经纬度坐标、数据库记录set集合:需要去重的时候、需要做交集/并集/差集运算的时候dict字典:需要通过 key 快速查找 value 的时候,比如用户信息 {"name": "张三", "age": 18}
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"的用户
意思是:定义元组靠的是逗号,不是括号。
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 收集;比如只需要首尾元素,中间的不管;比如处理数据时把头部和尾部单独拿出来,中间的批量处理。
逐个说:
- list 列表:有序、可改、可重复。像一排可插拔的抽屉,随时加东西、拿东西、换东西。适合存一组同类数据,比如所有学生的成绩
- set 集合:无序、可改、不可重复。像一个自动去重的篮子,扔进去重复的东西会自动只留一个。适合去重和集合运算
- tuple 元组:有序、不可改、可重复。像密封的盒子,装好了就不能动了。适合存固定不变的数据,比如数据库配置信息
- dict 字典:键值对、键不可重复、Python 3.7+ 有序。像一本通讯录,通过名字找电话号码。适合通过 key 快速查 value
序列类型:list、tuple、str 都是序列类型。序列类型的特点是有序、可以通过索引访问(seq[0])、支持切片(seq[1:3])、支持 len()、支持 in 判断成员。
set 和 dict 不属于序列类型——set 无序不能索引,dict 虽然有序但它是通过 key 访问而不是位置索引。
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
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) # 报错!/ 左边不允许关键字
这主要是库作者用来防止参数名被依赖——以后改参数名也不会影响调用方。
两种方式:
*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={...})
作用域就是"变量在哪儿能被访问到",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。更好的做法是通过参数传进来、通过返回值传出去。
类型注解就是给变量和函数标注上"这是什么类型",方便人和工具看:
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)
用大白话说:
- 面向过程就是"按步骤来"——第一步干啥,第二步干啥,第三步干啥。关注的是过程和步骤,就像写菜谱:先切菜、再烧油、最后翻炒。代码就是一堆函数按顺序调用。
- 面向对象就是"找谁来干"——把数据和操作数据的方法绑在一起,变成一个"对象"。关注的是谁来做,就像餐厅里点菜:你跟厨师说"来份番茄炒蛋",具体怎么切怎么炒是厨师的事,你不用管。
举个例子,养猫:
- 面向过程:写函数
feed(cat)、play(cat),猫是参数传来传去 - 面向对象:定义 Cat 类,里面有
feed()和play()方法,猫自己就能吃能玩:cat.feed()
面向对象的好处是代码更贴近现实、更好复用和扩展。但不是说面向对象一定比面向过程好,简单脚本用面向过程反而更直接。
简单理解:
- 实例属性:每个对象自己持有的数据,各个对象各不相同。写在
__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) # 来福 犬科
什么时候用:
- 用实例属性:每个对象各不相同的特征——名字、年龄、价格
- 用类属性:所有对象都一样的特征,或者需要全局统计的东西——物种、默认配置、实例计数器
注意:通过实例修改类属性时要小心,可能只改了实例自己的副本而不是类级别的。改类属性建议直接用 类名.属性 = 新值。
魔法方法就是 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__,输出:学生:张三
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 是"约定俗成的私有",表示"不建议外部访问",但不强制。
能。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
实际开发中多继承要谨慎,容易出现"菱形继承"问题(两个父类继承同一个祖父类),设计时要想清楚。
用 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设为抽象的,让子类各自实现
抽象类的意义就是定规矩:强制子类必须实现某些方法,不然不让你实例化。
基本语法:
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。
文件操作三步走:打开 → 读写 → 关闭
# 基本写法
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。
三个概念打个比方:
- IP地址:就像小区的地址(某某路几号),定位互联网上某台电脑的具体位置,比如
110.242.68.66 - 域名:就是 IP 地址的好记版别名。IP 那串数字太难记了,所以用
www.baidu.com代替,就像小区的"阳光花园"代替"某某路123号" - 端口号:就像门牌号。一栋楼(一台电脑)里可能有好多个服务——网页服务住 80 号房间,数据库服务住 3306 号房间,你得知道去哪个门
浏览器访问 https://www.baidu.com 的完整过程:
- DNS 解析:浏览器问 DNS 服务器"www.baidu.com 的 IP 是多少?",DNS 回答"110.242.68.66"
- 建立连接:浏览器通过这个 IP 地址,连上百度服务器的 443 端口(HTTPS 默认端口)
- TLS 握手:因为用的是 https,浏览器和服务器先加密握手,建立安全通道
- 发送请求:浏览器发 HTTP 请求:"给我首页内容"
- 服务器处理:百度服务器收到请求,执行搜索页面的后端逻辑,把网页内容准备好
- 返回响应:服务器把 HTML 内容通过 HTTP 响应发回给浏览器
- 渲染页面:浏览器拿到 HTML,解析渲染成你看到的百度搜索页面
HTTP 就是浏览器和服务器之间沟通用的"语言"。浏览器说"我要什么"(请求),服务器说"给你什么"(响应),双方按固定格式说话,这个格式规范就是 HTTP 协议。
请求数据格式,分四部分:
- 请求行:方法 + 路径 + 协议版本,比如
GET /api/users HTTP/1.1 - 请求头:一些附加信息,比如浏览器类型、接收的内容格式、Cookie 等
- 空行:一个空行分隔头和体
- 请求体:要发给服务器的数据(GET 请求通常没有,POST 有)
响应数据格式,也分四部分:
- 状态行:协议版本 + 状态码 + 状态描述,比如
HTTP/1.1 200 OK - 响应头:返回数据的类型、长度、Cookie 等
- 空行
- 响应体:实际返回的数据,通常是 HTML 或 JSON
常见状态码(记住大类就行):
200OK:成功了301/302:重定向,页面搬家了304Not Modified:内容没变,用缓存就行400Bad Request:请求写得不对,服务器看不懂401Unauthorized:没登录,需要认证403Forbidden:登录了但没权限404Not Found:找不到这个资源500Internal Server Error:服务器内部出错了502Bad Gateway:网关出问题了503Service Unavailable:服务暂时不可用
核心区别:
- 参数位置:GET 参数拼在 URL 后面(
/api/users?name=张三&age=18),POST 参数放在请求体里 - 可见性:GET 参数在地址栏能看到,POST 在地址栏看不到
- 长度限制:GET URL 长度有浏览器限制(约2KB),POST 基本不限
- 缓存:GET 请求会被浏览器缓存,POST 不会
- 历史记录:GET 请求的参数会留在浏览器历史里,POST 不会
- 语义:GET 是"获取数据",POST 是"提交数据"
建议用 POST 的情况:
- 传敏感信息:密码、身份证号等,别放 URL 上被人看到
- 传大量数据:文件上传、长文本提交
- 会产生数据变更的操作:注册、登录、提交表单、删除数据——这些操作用 POST 是因为不希望被缓存或被重复执行
大多数 AI 大模型本身是没有记忆的——每次对话都是独立的,它不记得你上一句说了什么。你每次发消息对它来说都是"第一次见面"。
让大模型"有记忆"的通用做法是:把之前的对话记录一起带上发给模型。
# 伪代码示意
messages = [
{"role": "user", "content": "我叫张三"},
{"role": "assistant", "content": "你好,张三!"},
{"role": "user", "content": "我叫什么?"}, # 这次发的时候把上面的也带上
]
response = model.chat(messages) # 模型看到完整上下文就能"记住"了
# 返回:"你叫张三呀"
所以"记忆"的本质就是:每次请求时把历史对话拼接在一起发给模型。这也是为什么聊天越久 API 费用越高——因为每次都把前面的对话全发了一遍。
实际项目中通常做两件事:
- 用数据库存储每个用户的对话历史(session 管理)
- 设置一个对话窗口(比如只带最近10轮对话),太早的就不带了,控制成本
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 状态
以 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 错误。
用排队买饭来理解:
- 同步就是"排队等"——你点完餐就站在窗口等,等做好了拿到手才走开,然后下一个人才能点。虽然你等的时候啥也没干,但别人也用不了这个窗口
- 异步就是"点完就走,好了叫你"——你点完餐拿个号就去干别的了,厨房做好了通知你来取。窗口可以马上服务下一个人
为什么异步性能更高?因为在 IO 操作(等数据库、等网络、等文件)时,CPU 是闲着的。同步模式下 CPU 只能干等,异步模式下 CPU 可以去处理别的请求。一台服务器就能同时服务更多用户。
但异步不是万能的:
- 适合 IO 密集型任务(网络请求、数据库查询、文件读写)——等的时间长,异步能充分利用
- 不适合 CPU 密集型任务(大量计算)——CPU 一直在忙,异步没意义
FastAPI 之所以快,就是因为底层用了异步(async/await),在高并发场景下性能远高于同步框架。
接收 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}
日志级别从低到高:
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("接口调用失败") # 输出
约束就是给字段加的限制规则,防止脏数据存进数据库。比如年龄不能为负,用户名不能重复。
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 -- 既非空又唯一
);
两个都是存字符串的,区别在于定长和变长:
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。
-- 模糊查询: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 是包头包尾的(包含两端值)
事务就是把几条 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):事务一旦提交,数据就永久保存了,断电也不丢
什么场景需要事务控制:涉及多表或多步数据修改、且这些修改必须同时成功或同时失败的场景——转账、订单创建(扣库存+创建订单+记流水)、用户注册(写用户表+写权限表)等。
两者都是做条件过滤的,但时机和对象不同:
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 是"分完再筛"。