学习日志04 python
坚持坚持坚持坚持坚持!!
今天的任务是学会写异常,文件读写操作,再学烦躁的时候做一下游戏进入像素动画(昨天居然闪退了还是误触了哦no...)
1 Python 函数参数解包、可变参数和参数匹配
1. 参数解包运算符 *
和 **
作用:
*
用于解包可迭代对象(如列表、元组、字符串)为位置参数。**
用于解包字典为关键字参数。
示例:
python
运行
def func(a, b, c):print(a, b, c)# 使用 * 解包列表/元组
args = [1, 2, 3]
func(*args) # 等价于 func(1, 2, 3)# 使用 ** 解包字典
kwargs = {'a': 1, 'b': 2, 'c': 3}
func(**kwargs) # 等价于 func(a=1, b=2, c=3)
2. 函数定义中的可变参数
*args
(可变位置参数):
- 收集所有未匹配的位置参数,打包成一个元组。
- 必须放在普通参数和默认参数之后。
**kwargs
(可变关键字参数):
- 收集所有未匹配的关键字参数,打包成一个字典。
- 必须放在所有参数的最后。
示例:
python
运行
def func(a, b=2, *args, **kwargs):print(f"a={a}, b={b}, args={args}, kwargs={kwargs}")func(1, 3, 4, 5, x=6, y=7)
# 输出:a=1, b=3, args=(4, 5), kwargs={'x': 6, 'y': 7}
3. 参数匹配规则(从左到右)
- 位置参数:按顺序匹配定义中的参数。
- 默认参数:若未传递值,则使用默认值。
*args
:收集剩余的位置参数。- 关键字参数:通过名称匹配到对应参数。
**kwargs
:收集剩余的关键字参数。
4. 常见用法场景
4.1 传递参数到其他函数
python
运行
def wrapper(*args, **kwargs):# 将所有参数传递给另一个函数result = target_function(*args, **kwargs)return result
4.2 扩展已有函数的参数
python
运行
def add(a, b):return a + bdef add_with_logging(*args, **kwargs):print(f"Calling add with args={args}, kwargs={kwargs}")return add(*args, **kwargs)add_with_logging(3, 5) # 输出:Calling add with args=(3, 5), kwargs={}
4.3 实现通用装饰器
python
运行
def my_decorator(func):def wrapper(*args, **kwargs):print("Before function call")result = func(*args, **kwargs)print("After function call")return resultreturn wrapper
5. 注意事项
-
参数顺序:
定义函数时必须遵循def func(positional, default, *args, **kwargs)
的顺序。 -
解包时的参数数量匹配:
python
运行
def func(a, b):passargs = [1, 2, 3] func(*args) # 报错:参数过多
-
字典键必须与参数名一致:
python
运行
kwargs = {'x': 1, 'y': 2} func(**kwargs) # 报错:func() 没有参数 'x' 和 'y'
总结
符号 | 位置 | 作用 |
---|---|---|
* | 函数定义 | 收集多个位置参数为元组 |
* | 函数调用 | 解包可迭代对象为多个位置参数 |
** | 函数定义 | 收集多个关键字参数为字典 |
** | 函数调用 | 解包字典为多个关键字参数 |
通过灵活使用 *
和 **
,可以编写出更通用、更灵活的函数。
2 Python 中对象赋值、浅拷贝和深拷贝
1. 赋值操作(b = a
)
- 本质:创建一个新变量
b
,指向同一个对象(内存地址相同)。 - 特点:对
a
的任何修改都会直接反映到b
,反之亦然。(会同步变化) - 示例:
python
运行
a = [1, 2, ['a']] b = a a.append(3) # a = [1, 2, ['a'], 3],b 同步变化
2. 浅拷贝(copy.copy()
或切片 a[:]
)
- 本质:创建一个新对象,但内部元素是原对象元素的引用。
- 不可变元素(如
int
,str
,tuple
):复制值。 - 可变元素(如
list
,dict
,set
):复制引用(不创建新对象)。
- 不可变元素(如
- 特点:
- 修改原对象的外层结构(如
a.append()
)不影响浅拷贝。 - 修改原对象的嵌套可变元素(如
a[0].append()
)会影响浅拷贝。
- 修改原对象的外层结构(如
- 示例:
python
运行
a = [1, 2, ['a']] c = copy.copy(a) a.append(3) # a = [1, 2, ['a'], 3],c 不变 a[2].append('b') # a[2] = ['a', 'b'],c[2] 同步变化
3. 深拷贝(copy.deepcopy()
)
- 本质:递归复制所有嵌套对象,创建一个完全独立的新对象。
- 特点:对原对象的任何修改(包括嵌套结构)都不影响深拷贝。
- 示例:
python
运行
a = [1, 2, ['a']] d = copy.deepcopy(a) a.append(3) # a = [1, 2, ['a'], 3],d 不变 a[2].append('b') # a[2] = ['a', 'b'],d 不变
4. 常见操作对比表
操作 | 是否创建新对象 | 嵌套对象是否独立 | 示例代码 |
---|---|---|---|
赋值 b = a | ❌ | ❌ | a[0] = 10 → b[0]=10 |
浅拷贝 copy.copy() | ✅ | ❌ | a[0].append() → c[0] 变化 |
深拷贝 copy.deepcopy() | ✅ | ✅ | a[0].append() → d[0] 不变 |
5. 易错点提醒
-
列表切片是浅拷贝:
a[:]
等价于copy.copy(a)
,仅复制一层。python
运行
a = [1, [2]] c = a[:] a[1].append(3) # c[1] 同步变为 [2, 3]
-
深拷贝的递归特性:
- 深拷贝会递归复制所有层级的嵌套对象,即使嵌套对象被多次引用。
python
运行
shared = [1] a = [shared, shared] # 两次引用同一列表 d = copy.deepcopy(a) print(d[0] is d[1]) # True:深拷贝保留内部引用关系
-
不可变对象的特殊性:
- 修改不可变对象(如
int
,str
)时,实际上创建了新对象,不影响原引用。
python
运行
a = [1, 2] c = copy.copy(a) a[0] = 10 # a[0] 指向新对象,c[0] 仍为 1
- 修改不可变对象(如
6. 应用场景
- 浅拷贝:适用于仅需复制顶层结构,节省内存(如数据过滤、重组)。
- 深拷贝:适用于需要完全隔离数据的场景(如配置克隆、递归数据结构操作)。
总结
理解赋值、浅拷贝和深拷贝的区别,关键在于区分对象引用和对象本身的复制。建议通过画图分析内存引用关系来加深理解!
我个人的理解:
就是....b赋值会跟着原来的a动态变化,但其实b不是在内存里面新的空间,但是copy和deepcopy都是在内存池开辟了新的空间,copy会动态变化,但是deepcopy不会动态变化,deepcopy仅仅复制了deepcopy操作前a的值
是的!你的理解非常准确。为了更直观地说明,我用内存示意图和代码示例补充说明:
内存示意图
假设初始列表 a = [1, 2, ['a', 'b']]
,执行赋值、浅拷贝、深拷贝后:
plaintext
内存状态:
┌────────────────────────────────────────────────────────────┐
│ 变量名 │ 内存地址 │ 指向的对象内容 │
├─────────┼──────────────┼───────────────────────────────────┤
│ a │ 0x1000 │ [1, 2, ['a', 'b']] │
│ │ │ ↑ ↑ ↑ │
│ │ │ │ │ └───────────┐ │
│ b │ 0x1000 │ (与a指向同一对象) │
│ │ │ │
│ c │ 0x2000 │ [1, 2, ['a', 'b']] │
│ │ │ ↑ ↑ ↑ │
│ │ │ │ │ └───────────┐ │
│ │ │ │ └──────────────┐ │ │
│ │ │ │ │ │ │
│ a[2] │ 0x3000 │ ['a', 'b'] <───────────┘ │ │
│ │ │ │
│ c[2] │ 0x3000 │ (与a[2]指向同一嵌套列表) │
│ │ │ │
│ d │ 0x4000 │ [1, 2, ['a', 'b']] │
│ │ │ ↑ ↑ ↑ │
│ │ │ │ │ └──────────┐ │
│ │ │ │ └──────────────┐ │ │
│ d[2] │ 0x5000 │ ['a', 'b'] <───────────┘ │ │
│ │ │ │ │
│ a[2] │ 0x3000 │ ['a', 'b'] │ │
└────────────────────────────────────────────────────────────┘
代码验证(内存地址)
通过 id()
函数查看对象内存地址:
python
运行
import copya = [1, 2, ['a', 'b']]
b = a # 赋值(同一内存地址)
c = copy.copy(a) # 浅拷贝(新对象,但嵌套列表共享引用)
d = copy.deepcopy(a) # 深拷贝(完全独立的新对象)# 验证顶层对象地址
print(id(a) == id(b)) # True:赋值共享同一地址
print(id(a) == id(c)) # False:浅拷贝创建新对象
print(id(a) == id(d)) # False:深拷贝创建新对象# 验证嵌套列表地址
print(id(a[2]) == id(b[2])) # True:赋值共享所有元素
print(id(a[2]) == id(c[2])) # True:浅拷贝共享嵌套列表
print(id(a[2]) == id(d[2])) # False:深拷贝独立创建嵌套列表
动态变化验证
修改 a
后观察各变量的变化:
python
运行
a.append(3) # 修改外层结构
a[2].append('c') # 修改嵌套列表print(a) # [1, 2, ['a', 'b', 'c'], 3]
print(b) # [1, 2, ['a', 'b', 'c'], 3] <- 赋值完全同步
print(c) # [1, 2, ['a', 'b', 'c']] <- 浅拷贝:嵌套列表同步变化
print(d) # [1, 2, ['a', 'b']] <- 深拷贝:完全独立
关键点总结
操作 | 是否开辟新空间 | 嵌套对象是否共享引用 | 修改原对象的外层 | 修改原对象的嵌套 |
---|---|---|---|---|
赋值 b=a | ❌ | ✅ | 影响 b | 影响 b |
浅拷贝 c | ✅ | ✅ | 不影响 c | 影响 c |
深拷贝 d | ✅ | ❌ | 不影响 d | 不影响 d |
常见误区
-
误以为浅拷贝是 "部分深拷贝":
浅拷贝仅复制顶层结构,嵌套的可变对象(如列表、字典)仍共享引用。 -
混淆 "可变对象" 和 "不可变对象":
修改不可变对象(如int
,str
)时,实际上创建了新对象,不影响原引用:python
运行
a = [1, 2] c = copy.copy(a) a[0] = 100 # a[0] 指向新的 int 对象,c[0] 仍为 1
-
深拷贝的递归特性:
深拷贝会递归复制所有层级的嵌套对象,即使嵌套对象被多次引用:python
运行
shared = [1] a = [shared, shared] # 两次引用同一列表 d = copy.deepcopy(a) print(d[0] is d[1]) # True:深拷贝保留内部引用关系
一句话总结
- 赋值:同一对象的不同别名,完全同步变化。
- 浅拷贝:新瓶装旧酒,外层独立,内层共享。
- 深拷贝:完全克隆,老死不相往来。
import copya = [1, 2, 3, 4, ['a', 'b']] b = a # 引用,除非直接给a重新赋值,否则a变则b变,b变则a变c = copy.copy(a) # 浅复制,只会拷贝父对象, 不会拷贝父对象中的子对象,所以若a的子对象变则c 变,但是父对象变c不会变d = copy.deepcopy(a)#深拷贝,完全拷贝,完全独立于原对象,a变也不变a.append(5) # 改变父对象a[4].append('c') #改变父对象中的 ['a', 'b']子对象 # a=[1, 2, 3, 4, ['a', 'b','c'],5] b=[1, 2, 3, 4, ['a', 'b','c'],5] c=[1, 2, 3, 4, ['a', 'b','c']] d=[1, 2, 3, 4, ['a', 'b']]
3 为什么copy要开辟新的内存空间呢?
这个问题触及了浅拷贝的核心设计目的:在节省内存和隔离修改之间找到平衡点。让我从三个角度解释:
1. 为什么需要开辟新空间?
(1)避免意外修改顶层结构
浅拷贝的主要用途之一是创建一个独立的容器,但保留对内部元素的引用。例如:
python
运行
original = [1, 2, [3]]
shallow = copy.copy(original)# 修改顶层结构:互不影响
original.append(4)
print(shallow) # [1, 2, [3]](不受影响)
如果浅拷贝不开辟新空间,shallow
会和 original
完全绑定,任何修改都会同步,这在很多场景下不符合需求。
(2)支持多态容器
Python 中不同容器可以包含相同元素,浅拷贝允许创建不同类型的容器:
python
运行
original = [1, 2]
shallow = tuple(original) # 元组浅拷贝列表
print(shallow) # (1, 2)
这里 shallow
和 original
必须是不同对象,否则类型转换就没有意义了。
2. 为什么只复制一层?
(1)性能与内存的权衡
深拷贝需要递归遍历所有嵌套对象,时间和空间复杂度都是 O(n),当对象层级很深时会非常慢。例如:
python
运行
# 假设有一个百万层级的嵌套列表
huge_list = [[]] * 1000000
deep = copy.deepcopy(huge_list) # 非常耗时
shallow = copy.copy(huge_list) # 几乎瞬间完成
浅拷贝的复杂度是 O(1),因为只复制顶层容器,适合处理大型数据结构。
(2)保留内部引用关系
在某些场景下,我们希望保留对象间的引用关系:
python
运行
shared = [1]
original = [shared, shared] # 同一列表被引用两次
shallow = copy.copy(original)print(shallow[0] is shallow[1]) # True(保留引用关系)
如果浅拷贝递归复制所有元素,会破坏这种引用关系,导致逻辑错误。
3. 典型应用场景
(1)过滤 / 重组数据
python
运行
data = [1, 2, [3, 4]]
filtered = copy.copy(data)
filtered.pop() # 移除最后一个元素(嵌套列表)print(data) # [1, 2, [3, 4]](原数据不变)
print(filtered) # [1, 2]
浅拷贝确保 filtered
是新容器,但内部元素(如 [3, 4]
)仍被共享,避免不必要的复制。
(2)安全传递参数
函数可能需要修改传入列表的结构,但不影响原始数据:
python
运行
def process_list(lst):copy_lst = copy.copy(lst)copy_lst.append(999) # 只修改拷贝return copy_lstoriginal = [1, 2]
result = process_list(original)
print(original) # [1, 2](不受影响)
总结:浅拷贝的本质
浅拷贝的设计哲学是:"复制容器,但共享内容"。它在以下场景中特别有用:
- 需要独立的容器结构(如增删元素),但内部元素不可变或无需隔离;
- 性能敏感的场景(避免深拷贝的开销);
- 需要保留对象间引用关系(如嵌套对象的循环引用)。
理解这一点后,你可以更精准地选择赋值、浅拷贝或深拷贝,让代码既安全又高效~
4 拷贝的代码实践
正确代码实现
python
运行
import copyoriginal = {"students": [{"name": "Alice", "scores": [85, 90]},{"name": "Bob", "scores": [70, 80]}],"class_avg": 82.5
}# 任务1:创建浅拷贝并修改第一个学生的第二个分数
shallow_copy = copy.copy(original)
shallow_copy["students"][0]["scores"][1] = 95 # 修改浅拷贝的嵌套列表# 任务2:创建深拷贝并修改第二个学生的第一个分数
deep_copy = copy.deepcopy(original) # 此时original的scores已变为[85, 95]
deep_copy["students"][1]["scores"][0] = 75 # 修改深拷贝的嵌套列表# 任务3:更新original的平均分(不可变对象,不影响浅拷贝)
original["class_avg"] = 85# 输出结果
print("Original:", original)
print("Shallow Copy:", shallow_copy)
print("Deep Copy:", deep_copy)
预期输出结果
python
运行
# 实际输出
Original: {'students': [{'name': 'Alice', 'scores': [85, 95]}, # 被浅拷贝修改影响{'name': 'Bob', 'scores': [70, 80]}],'class_avg': 85 # 被任务3修改
}Shallow Copy: {'students': [{'name': 'Alice', 'scores': [85, 95]}, # 与原对象共享嵌套列表{'name': 'Bob', 'scores': [70, 80]}],'class_avg': 82.5 # 未被任务3修改(不可变对象)
}Deep Copy: {'students': [{'name': 'Alice', 'scores': [85, 95]}, # 继承自原对象的修改{'name': 'Bob', 'scores': [75, 80]} # 深拷贝的独立修改],'class_avg': 82.5 # 深拷贝时继承原对象的82.5,不受后续修改影响
}
关键知识点回顾
-
浅拷贝共享嵌套对象:
浅拷贝只复制顶层字典,但内部的students
列表和scores
列表是共享的。因此,修改shallow_copy
的嵌套列表会直接影响original
。 -
深拷贝完全独立:
深拷贝递归复制所有层级,deep_copy
与original
没有任何共享引用。但由于深拷贝是在任务 1 之后创建的,它继承了任务 1 对原对象的修改(scores
变为[85, 95]
)。 -
不可变对象的修改逻辑:
修改original["class_avg"]
时,由于浮点数不可变,实际上是让original["class_avg"]
指向了新的浮点数85
,而shallow_copy["class_avg"]
仍指向旧的82.5
。
总结
(谢谢ai老师的鼓励!)
你成功掌握了浅拷贝和深拷贝的核心区别,以及不可变对象在赋值和修改时的行为!这种通过代码实践来验证概念的方式非常有效,继续保持!如果还有其他疑问,随时可以问~ 😊
5 Python的不可变对象
1. 什么是不可变对象?
不可变对象是指创建后其值不能被修改的对象。
如果尝试修改不可变对象,Python 会创建一个新对象,而不是改变原对象的值。
我个人的重述,理解:
意思就是,看起来好像是修改了,其实是在内存池里面新开辟了一个空间存储新的值,但原来的值对应的内存地址没有变化,所以就是没有修改成功,只不过是一个新的。
完全正确!你的理解非常精准。我用一个具体例子和内存示意图帮你强化这个概念:
示例:修改不可变对象(字符串)
python
运行
s = "hello" # 创建字符串对象 "hello",内存地址假设为 0x100
print(id(s)) # 输出: 0x100s += " world" # 看似修改 s,实际创建新字符串 "hello world",地址为 0x200
print(id(s)) # 输出: 0x200(地址改变,说明 s 指向了新对象)
内存示意图
plaintext
初始状态:
内存地址 对象值
0x100 "hello" ── s执行 s += " world" 后:
内存地址 对象值
0x100 "hello" (原对象未改变)
0x200 "hello world" ── s (s 指向新对象)
关键结论
- 原对象未被修改:
0x100
地址上的"hello"
始终存在,没有被改变。 - 变量指向新对象:执行
s += " world"
后,s
指向了新创建的字符串"hello world"
(地址0x200
)。 - 不可变对象的 “修改” 本质:所有 “修改” 操作(如
+=
,replace()
等)都会返回新对象,而非修改原对象。
对比:可变对象(列表)的修改
python
运行
lst = [1, 2] # 创建列表对象 [1, 2],内存地址假设为 0x300
print(id(lst)) # 输出: 0x300lst.append(3) # 直接修改原列表,添加元素 3
print(id(lst)) # 输出: 0x300(地址不变,说明 lst 仍指向原对象)
plaintext
内存示意图:
内存地址 对象值
0x300 [1, 2] ── lst (初始状态)
0x300 [1, 2, 3] ── lst (append 后,原对象被修改)
为什么这很重要?
理解不可变对象的特性,可以帮你避开许多编程陷阱:
(1)函数传参
python
运行
def add_exclamation(s):s += "!" # 对不可变对象的修改会创建新对象return stext = "hello"
add_exclamation(text)
print(text) # 输出: hello(原字符串未被修改)
(2)循环中的变量绑定
python
运行
funcs = []
for i in range(3):funcs.append(lambda: i) # 所有函数都绑定到同一个变量 ifor f in funcs:print(f()) # 输出: 2, 2, 2(因为循环结束后 i 的值为 2)
这里 i
是不可变对象,循环中每次迭代都创建新的 i
,但函数捕获的是变量的引用,而非值本身。
总结
不可变对象的核心原则:
“修改” = 创建新对象 + 变量重新绑定,原对象保持不变
掌握这个概念后,你就能更清晰地理解 Python 的内存管理、变量赋值和函数传参机制啦~
常见的不可变对象类型:
- 基本数据类型:整数(
int
)、浮点数(float
)、字符串(str
)、布尔值(bool
) - 容器类型:元组(
tuple
)、冻结集合(frozenset
) - 其他:
None
2. 不可变对象的特性
(1)修改操作会创建新对象
python
运行
a = 5 # 创建整数对象 5,a 指向它
print(id(a)) # 输出: 140722118184464a = a + 1 # 看似修改 a,实际是创建新对象 6,a 指向新对象
print(id(a)) # 输出: 140722118184496(地址改变)
(2)哈希性(Hashable)
不可变对象可以作为字典的键或集合的元素,因为它们的哈希值(hash()
)在生命周期内不会改变:
python
运行
d = { (1, 2): "tuple_key" } # 元组是不可变的,可以作为键
s = { "apple", "banana" } # 字符串是不可变的,可以存入集合
(3)变量赋值实际是引用复制
当多个变量指向同一个不可变对象时,修改其中一个变量不会影响其他变量:
python
运行
x = "hello"
y = x # x 和 y 指向同一个字符串对象
x = "world" # 创建新字符串 "world",x 指向新对象,y 仍指向 "hello"
print(y) # 输出: hello
3. 常见不可变对象类型
(1)整数(int
)
python
运行
a = 10
b = a
a += 5 # 创建新整数 15,a 指向新对象,b 仍为 10
print(b) # 输出: 10
(2)浮点数(float
)
python
运行
x = 3.14
y = x
x = 2.71 # 创建新浮点数 2.71,x 指向新对象,y 仍为 3.14
print(y) # 输出: 3.14
(3)字符串(str
)
python
运行
s1 = "hello"
s2 = s1
s1 += " world" # 创建新字符串 "hello world",s1 指向新对象
print(s2) # 输出: hello(原字符串未改变)
(4)元组(tuple
)
元组的元素不可修改,但如果元素是可变对象(如列表),则该元素的内容可以修改:
python
运行
t = (1, [2, 3])
# t[0] = 100 # 报错:元组元素不可修改
t[1].append(4) # 元组的元素2是列表(可变对象),可以修改
print(t) # 输出: (1, [2, 3, 4])
4. 不可变对象的优势
(1)线程安全
不可变对象在多线程环境下无需加锁,因为它们的值不会被修改。
(2)哈希表优化
字典和集合的查找效率依赖于键的不可变性(哈希值固定)。
(3)避免意外副作用
函数传递不可变对象时,不会意外修改原始数据:
python
运行
def add_one(x):x += 1 # 创建新对象,不影响原对象return xa = 5
add_one(a)
print(a) # 输出: 5(原对象未被修改)
5. 对比:可变对象(Mutable Objects)
与不可变对象相反,可变对象创建后可以直接修改其值,而无需创建新对象。
常见的可变对象类型:
- 列表(
list
) - 字典(
dict
) - 集合(
set
) - 自定义类的实例(默认)
可变对象的 “陷阱” 示例
python
运行
def append_zero(lst):lst.append(0) # 直接修改原列表return lstmy_list = [1, 2]
append_zero(my_list)
print(my_list) # 输出: [1, 2, 0](原列表被修改)
6. 面试常见问题
(1)如何判断对象是否可变?
- 使用
id()
函数观察修改前后的内存地址是否变化。 - 检查对象是否支持原地修改方法(如
list.append()
)。
(2)为什么元组是不可变的,但包含可变元素时可以修改元素内容?
元组的不可变性指的是元素的引用不可变,但如果元素本身是可变对象(如列表),则其内容可以修改。
(3)不可变对象和常量的区别?
- 不可变对象:值不可修改,但变量可以指向其他对象。
- 常量:Python 没有内置的常量机制,通常用全大写变量名(如
PI = 3.14
)表示约定俗成的常量。
总结
特性 | 不可变对象(如 int , str , tuple ) | 可变对象(如 list , dict , set ) |
---|---|---|
修改后是否创建新对象 | 是 | 否 |
是否可哈希 | 是 | 否 |
作为字典键 | 可以 | 不可以 |
函数传参 | 不影响原对象 | 可能影响原对象 |
理解不可变对象是 Python 内存管理的基础,也是避免编程中许多 “意外副作用” 的关键~
6 Python 中b = a
的赋值逻辑和 Java 的 “值传递”
a = 10
b = a
a += 5 # 创建新整数 15,a 指向新对象,b 仍为 10
print(b) # 输出: 10
#b等于a传递的是什么?我需要类比复习一下java的值传递
在 Python 中,b = a
的赋值逻辑和 Java 的 “值传递” 有相似之处,但需要结合 Python 的 “对象引用模型” 来理解。我们可以通过和 Java 的对比来清晰梳理:
1. Python 中 b = a
传递的是什么?
Python 中一切皆对象,变量本质是 “对象的引用”(可以理解为指向对象内存地址的指针)。
b = a
的过程是:将 a
指向的对象引用复制给 b
,让 b
和 a
指向同一个对象。
以你的例子来说:
python
运行
a = 10 # a 指向整数对象 10(内存地址假设为 0x100)
b = a # b 复制 a 的引用,也指向 0x100 的对象 10
a += 5 # 整数是不可变对象,修改时创建新对象 15(地址 0x200),a 指向新地址
print(b) # b 仍指向 0x100 的对象 10,输出 10
核心:b = a
传递的是 “对象的引用副本”,而非对象本身。
2. 和 Java 的 “值传递” 对比
Java 中存在 “基本类型” 和 “引用类型” 的区别,而 Python 中只有 “对象引用”,但可以类比:
(1)Java 基本类型(如 int
)的传递
Java 的基本类型是 “值传递”:传递的是变量的值的副本,修改副本不影响原变量。
java
int a = 10;
int b = a; // 传递 a 的值的副本(10)给 b
a += 5; // a 变为 15,b 仍为 10
System.out.println(b); // 输出 10
这和 Python 中不可变对象(如 int
)的 b = a
效果完全一致:修改原变量后,副本不受影响。
(2)Java 引用类型(如 String
)的传递
Java 的引用类型也是 “值传递”,但传递的是 “引用的副本”(和 Python 逻辑更像):
java
String a = "hello"; // a 指向字符串对象 "hello"
String b = a; // 传递引用副本,b 也指向 "hello"
a = a + " world"; // String 是不可变的,创建新对象,a 指向新对象
System.out.println(b); // 输出 "hello"(b 仍指向原对象)
这和 Python 中字符串的 b = a
完全一致:不可变对象的 “修改” 会创建新对象,原引用副本不受影响。
(3)Java 可变引用类型(如 List
)的传递
Java 中可变对象(如 ArrayList
)的引用传递后,修改对象内容会影响所有引用:
java
List<Integer> a = new ArrayList<>();
a.add(10);
List<Integer> b = a; // 传递引用副本,b 和 a 指向同一个列表
a.add(20); // 直接修改列表内容(可变对象)
System.out.println(b); // 输出 [10, 20](b 受影响)
这和 Python 中可变对象(如 list
)的 b = a
完全一致:
python
运行
a = [10]
b = a # b 和 a 指向同一个列表
a.append(20) # 修改列表内容(可变对象)
print(b) # 输出 [10, 20](b 受影响)
3. Python 和 Java 的核心区别
- Java:严格区分 “基本类型”(值传递值的副本)和 “引用类型”(值传递引用的副本)。
- Python:没有 “基本类型”,所有变量都是 “对象的引用”,赋值和传递的都是 “引用的副本”,但行为取决于对象是否可变:
- 不可变对象(如
int
,str
):修改时创建新对象,副本不受影响(类似 Java 基本类型)。 - 可变对象(如
list
,dict
):修改时直接改原对象,副本受影响(类似 Java 可变引用类型)。
- 不可变对象(如
总结
- Python 中
b = a
传递的是 “对象引用的副本”,让b
和a
指向同一个对象。 - 这和 Java 的 “值传递” 本质相通(都是传递副本),区别仅在于 Python 没有 “基本类型”,统一用 “对象引用” 模型。
- 理解的关键:不可变对象的修改会创建新对象,可变对象的修改会影响所有引用。
通过和 Java 的类比,你可以更快掌握 Python 的变量传递逻辑~
个人理解:
b=a赋值的时候传递的就是内存地址吧,后续要打印b的值也是对应到内存地址去。
你的理解非常准确!可以这样简化概括:
在 Python 中,b = a
本质上是将 a
存储的 “对象内存地址” 复制给了 b
。此后:
- 当你访问
b
时,Python 会根据b
存储的内存地址,找到对应的对象并返回其值; - 当你打印
b
时,输出的是该内存地址指向的对象的值,而非地址本身(除非用id(b)
查看地址)。
用内存地址视角再看之前的例子:
python
运行
a = 10 # a 存储 10 的内存地址(假设为 0x123)
b = a # b 复制 0x123,现在 a 和 b 都指向 0x123
a += 5 # 10 是不可变对象,创建新对象 15(地址 0x456),a 改为存储 0x456
print(b) # b 仍存储 0x123,所以输出 10
这里的逻辑和 “指针” 非常相似:a
和 b
就像两个指针变量,b = a
是指针的复制,让两个指针指向同一块内存;当 a
指向新内存时,b
的指向不变。
和 Java 的对比(再强化一次):
Java 中:
- 对于基本类型(如
int
),b = a
复制的是 “值”(但底层也可理解为值对应的内存地址复制,因为基本类型值也存储在内存中); - 对于对象(如
String
),b = a
复制的是 “对象的引用地址”(和 Python 完全一致)。
本质上,Python 的 “引用传递” 和 Java 的 “值传递” 在底层内存行为上是相通的:都是复制 “指向数据的标识”(地址或值),区别仅在于 Python 中所有数据都是对象,统一用地址标识,而 Java 区分了基本类型和对象。
理解 “内存地址的复制”,就抓住了变量赋值和传递的核心逻辑~
7 Python 模块导入机制
理解性记忆,先执行用户自己创建的模块,然后在去走python文件所在的路径,然后就是环境变量中的PYTHONPATH,然后再是python安装路径,因为自己创建的模块,有可能是python里面的机制是先保证用户所创建使用的,优先级较高。
一、核心知识点
-
模块搜索路径顺序(从高到低):
- 内置模块(如
sys
、math
):Python 解释器直接加载,不参与路径搜索 - 当前路径:执行脚本所在的目录(
sys.path[0]
) - PYTHONPATH 环境变量:用户自定义的搜索路径
- Python 安装路径:标准库和第三方库位置(如
site-packages
)
- 内置模块(如
-
sys.path 动态路径:
- Python 运行时通过
sys.path
列表确定搜索路径 - 可通过代码动态修改(如
sys.path.append('/custom/path')
)
- Python 运行时通过
-
命名空间优先级:
- 内置模块 > 当前路径模块 > PYTHONPATH 模块 > 标准库模块
- 避免自定义模块与内置模块重名(如创建
sys.py
会导致导入错误)
二、验证方法
-
查看搜索路径:
python
运行
import sys print('\n'.join(sys.path))
-
检查内置模块:
python
运行
print('math' in sys.builtin_module_names) # True表示内置模块
-
查看模块来源:
python
运行
import math print(math.__file__) # 内置模块显示None
三、常见问题及解决
问题类型 | 原因 | 解决方案 |
---|---|---|
模块找不到 | 路径不在 sys.path 中 | - 添加 PYTHONPATH 环境变量 - 使用 sys.path.append() 动态添加 |
内置模块被覆盖 | 自定义模块与内置模块同名 | 重命名自定义模块 |
相对导入失败 | 非包结构下使用相对导入 | - 将项目组织为包结构(添加__init__.py )- 使用绝对导入 |
四、最佳实践
-
项目结构:
plaintext
my_project/ ├── main.py # 执行脚本 ├── my_module/ # 自定义包 │ ├── __init__.py # 包标识 │ └── utils.py └── external/ # 外部模块└── helper.py
-
导入示例:
python
运行
# main.py from my_module.utils import func # 从包中导入 import external.helper as helper # 从外部路径导入
-
环境配置:
- Linux/macOS:
export PYTHONPATH="/custom/path:$PYTHONPATH"
- Windows:
set PYTHONPATH=C:\custom\path;%PYTHONPATH%
- Linux/macOS:
五、知识扩展
-
包与模块的区别:
- 模块:单个
.py
文件 - 包:包含
__init__.py
的目录
- 模块:单个
-
相对导入语法:
python
运行
from .utils import func # 从当前包导入 from ..shared import util # 从父包导入
-
模块缓存机制:
- 模块只加载一次(通过
sys.modules
缓存) - 使用
importlib.reload()
强制重新加载(开发调试场景)
- 模块只加载一次(通过
六、题目错误分析
题目:
当使用import导入模块时,按python查找模块的不同顺序是:
①环境变量中的PYTHONPATH
②内建模块
③python安装路径
④当前路径,即执行Python脚本文件所在的路径
题目中顺序错误点:
- 内置模块优先级最高,不参与路径搜索
- 当前路径先于 PYTHONPATH 搜索
- 正确顺序应为:②(内置)→ ④(当前路径)→ ①(PYTHONPATH)→ ③(安装路径)
通过掌握上述知识,可系统理解 Python 模块导入机制,有效解决导入相关问题。
8 Python 中 re 模块的基本用法
常用正则表达式元字符
元字符 | 描述 |
---|---|
. | 匹配除换行符以外的任意字符 |
^ | 匹配字符串的开始位置 |
$ | 匹配字符串的结束位置 |
* | 匹配前面的子表达式零次或多次 |
+ | 匹配前面的子表达式一次或多次 |
? | 匹配前面的子表达式零次或一次 |
{n} | 匹配前面的子表达式恰好 n 次 |
{n,} | 匹配前面的子表达式至少 n 次 |
{n,m} | 匹配前面的子表达式至少 n 次,最多 m 次 |
[] | 匹配方括号中指定的任意一个字符 |
| | 表示或的关系,匹配两个或多个模式中的任意一个 |
() | 标记一个子表达式的开始和结束位置 |
Python 中 re
模块的基本用法
1. 导入 re
模块
python
运行
import re
2. 编译正则表达式(可选但推荐)
python
运行
pattern = re.compile(r'正则表达式')
这里的 r
表示原始字符串,能避免 Python 对反斜杠的转义处理。
3. 常用的匹配函数
re.search(pattern, string)
:在字符串中搜索匹配正则表达式的第一个位置,返回一个匹配对象。re.match(pattern, string)
:从字符串的起始位置匹配正则表达式,返回一个匹配对象。re.findall(pattern, string)
:在字符串中找到所有匹配正则表达式的子串,返回一个列表。re.finditer(pattern, string)
:在字符串中找到所有匹配正则表达式的子串,返回一个迭代器。re.sub(pattern, repl, string)
:替换字符串中匹配正则表达式的子串。
示例代码
下面是一些具体的例子,展示了正则表达式在 Python 中的使用方法:
python
运行
import re# 示例 1:查找所有数字
text = "Hello 123 World 456"
pattern = re.compile(r'\d+') # \d 表示数字,+ 表示一次或多次
result = pattern.findall(text)
print(f"找到的数字:{result}") # 输出:['123', '456']# 示例 2:验证邮箱格式
email = "test@example.com"
pattern = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
if pattern.match(email):print(f"{email} 是有效的邮箱地址") # 输出:test@example.com 是有效的邮箱地址# 示例 3:替换字符串
text = "Hello World! Hello Python!"
pattern = re.compile(r'Hello')
new_text = pattern.sub('Hi', text)
print(f"替换后的文本:{new_text}") # 输出:Hi World! Hi Python!# 示例 4:使用分组
text = "Date: 2023-07-15"
pattern = re.compile(r'Date: (\d{4})-(\d{2})-(\d{2})')
match = pattern.search(text)
if match:year = match.group(1)month = match.group(2)day = match.group(3)print(f"提取的日期:{year}年{month}月{day}日") # 输出:提取的日期:2023年07月15日
匹配对象的常用方法
当使用 search()
、match()
或 finditer()
方法时,会返回一个匹配对象,它有以下常用方法:
group()
:返回匹配的子串。groups()
:返回一个包含所有分组匹配的元组。start()
:返回匹配开始的位置。end()
:返回匹配结束的位置。span()
:返回一个包含匹配开始和结束位置的元组。
如果你需要进一步学习正则表达式,可以参考 Python 官方文档中关于 re
模块的部分,或者使用在线正则表达式测试工具来练习。