当前位置: 首页 > news >正文

学习日志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. 参数匹配规则(从左到右)

  1. 位置参数:按顺序匹配定义中的参数。
  2. 默认参数:若未传递值,则使用默认值。
  3. *args:收集剩余的位置参数。
  4. 关键字参数:通过名称匹配到对应参数。
  5. **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. 注意事项

  1. 参数顺序
    定义函数时必须遵循 def func(positional, default, *args, **kwargs) 的顺序。

  2. 解包时的参数数量匹配

    python

    运行

    def func(a, b):passargs = [1, 2, 3]
    func(*args)  # 报错:参数过多
    
  3. 字典键必须与参数名一致

    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[:]

  • 本质:创建一个新对象,但内部元素是原对象元素的引用
    • 不可变元素(如 intstrtuple):复制值。
    • 可变元素(如 listdictset):复制引用(不创建新对象)。
  • 特点
    • 修改原对象的外层结构(如 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 = aa[0] = 10 → b[0]=10
浅拷贝 copy.copy()a[0].append() → c[0] 变化
深拷贝 copy.deepcopy()a[0].append() → d[0] 不变

5. 易错点提醒

  1. 列表切片是浅拷贝a[:] 等价于 copy.copy(a),仅复制一层。

    python

    运行

    a = [1, [2]]
    c = a[:]
    a[1].append(3)  # c[1] 同步变为 [2, 3]
    
  2. 深拷贝的递归特性

    • 深拷贝会递归复制所有层级的嵌套对象,即使嵌套对象被多次引用。

    python

    运行

    shared = [1]
    a = [shared, shared]  # 两次引用同一列表
    d = copy.deepcopy(a)
    print(d[0] is d[1])  # True:深拷贝保留内部引用关系
    
  3. 不可变对象的特殊性

    • 修改不可变对象(如 intstr)时,实际上创建了新对象,不影响原引用。

    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

常见误区

  1. 误以为浅拷贝是 "部分深拷贝"
    浅拷贝仅复制顶层结构,嵌套的可变对象(如列表、字典)仍共享引用。

  2. 混淆 "可变对象" 和 "不可变对象"
    修改不可变对象(如 intstr)时,实际上创建了新对象,不影响原引用:

    python

    运行

    a = [1, 2]
    c = copy.copy(a)
    a[0] = 100  # a[0] 指向新的 int 对象,c[0] 仍为 1
    
  3. 深拷贝的递归特性
    深拷贝会递归复制所有层级的嵌套对象,即使嵌套对象被多次引用:

    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,不受后续修改影响
}

关键知识点回顾

  1. 浅拷贝共享嵌套对象
    浅拷贝只复制顶层字典,但内部的 students 列表和 scores 列表是共享的。因此,修改 shallow_copy 的嵌套列表会直接影响 original

  2. 深拷贝完全独立
    深拷贝递归复制所有层级,deep_copy 与 original 没有任何共享引用。但由于深拷贝是在任务 1 之后创建的,它继承了任务 1 对原对象的修改(scores 变为 [85, 95])。

  3. 不可变对象的修改逻辑
    修改 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 指向新对象)

关键结论

  1. 原对象未被修改0x100 地址上的 "hello" 始终存在,没有被改变。
  2. 变量指向新对象:执行 s += " world" 后,s 指向了新创建的字符串 "hello world"(地址 0x200)。
  3. 不可变对象的 “修改” 本质:所有 “修改” 操作(如 +=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)表示约定俗成的常量。

总结

特性不可变对象(如 intstrtuple可变对象(如 listdictset
修改后是否创建新对象
是否可哈希
作为字典键可以不可以
函数传参不影响原对象可能影响原对象

理解不可变对象是 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:没有 “基本类型”,所有变量都是 “对象的引用”,赋值和传递的都是 “引用的副本”,但行为取决于对象是否可变:
    • 不可变对象(如 intstr):修改时创建新对象,副本不受影响(类似 Java 基本类型)。
    • 可变对象(如 listdict):修改时直接改原对象,副本受影响(类似 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里面的机制是先保证用户所创建使用的,优先级较高。

一、核心知识点
  1. 模块搜索路径顺序(从高到低):

    • 内置模块(如sysmath):Python 解释器直接加载,不参与路径搜索
    • 当前路径:执行脚本所在的目录(sys.path[0]
    • PYTHONPATH 环境变量:用户自定义的搜索路径
    • Python 安装路径:标准库和第三方库位置(如site-packages
  2. sys.path 动态路径

    • Python 运行时通过sys.path列表确定搜索路径
    • 可通过代码动态修改(如sys.path.append('/custom/path')
  3. 命名空间优先级

    • 内置模块 > 当前路径模块 > PYTHONPATH 模块 > 标准库模块
    • 避免自定义模块与内置模块重名(如创建sys.py会导致导入错误)
二、验证方法
  1. 查看搜索路径

    python

    运行

    import sys
    print('\n'.join(sys.path))
    
  2. 检查内置模块

    python

    运行

    print('math' in sys.builtin_module_names)  # True表示内置模块
    
  3. 查看模块来源

    python

    运行

    import math
    print(math.__file__)  # 内置模块显示None
    
三、常见问题及解决
问题类型原因解决方案
模块找不到路径不在 sys.path 中- 添加 PYTHONPATH 环境变量
- 使用sys.path.append()动态添加
内置模块被覆盖自定义模块与内置模块同名重命名自定义模块
相对导入失败非包结构下使用相对导入- 将项目组织为包结构(添加__init__.py
- 使用绝对导入
四、最佳实践
  1. 项目结构

    plaintext

    my_project/
    ├── main.py          # 执行脚本
    ├── my_module/       # 自定义包
    │   ├── __init__.py  # 包标识
    │   └── utils.py
    └── external/        # 外部模块└── helper.py
    
  2. 导入示例

    python

    运行

    # main.py
    from my_module.utils import func  # 从包中导入
    import external.helper as helper  # 从外部路径导入
    
  3. 环境配置

    • Linux/macOS:export PYTHONPATH="/custom/path:$PYTHONPATH"
    • Windows:set PYTHONPATH=C:\custom\path;%PYTHONPATH%
五、知识扩展
  1. 包与模块的区别

    • 模块:单个.py文件
    • 包:包含__init__.py的目录
  2. 相对导入语法

    python

    运行

    from .utils import func  # 从当前包导入
    from ..shared import util  # 从父包导入
    
  3. 模块缓存机制

    • 模块只加载一次(通过sys.modules缓存)
    • 使用importlib.reload()强制重新加载(开发调试场景)
六、题目错误分析

题目:

当使用import导入模块时,按python查找模块的不同顺序是:

①环境变量中的PYTHONPATH

②内建模块

③python安装路径

④当前路径,即执行Python脚本文件所在的路径

题目中顺序错误点:

  1. 内置模块优先级最高,不参与路径搜索
  2. 当前路径先于 PYTHONPATH 搜索
  3. 正确顺序应为:②(内置)→ ④(当前路径)→ ①(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 模块的部分,或者使用在线正则表达式测试工具来练习。

http://www.lryc.cn/news/581247.html

相关文章:

  • 解决kali Linux在VMware中的全局缩放问题
  • Linux:多线程---深入互斥浅谈同步
  • jvm架构原理剖析篇
  • Python之--基本知识
  • App爬虫实战篇-以华为真机手机爬取集换社的app为例
  • 11_架构演进:从单体到云原生的蜕变
  • 【Docker基础】Docker数据卷管理:docker volume prune及其参数详解
  • Apache 配置文件提权的实战思考
  • Feign调用报“请求方法POST不支持“错误
  • 在sf=0.1时测试fireducks、duckdb、polars的tpch
  • 《设计模式之禅》笔记摘录 - 4.抽象工厂模式
  • pagecache过多导致oom的排查记录
  • 单用户模式、紧急模式、救援模式有什么区别
  • LeetCode 第89题:格雷编码
  • PostgreSQL表操作
  • 深度剖析:OPENPPP2 libtcpip 实现原理与架构设计
  • python缓存装饰器实现方案
  • python中执行前置操作,后置操作的几种方法
  • 【QT】事件(鼠标、按键、定时器、窗口)
  • JVM的位置和JVM的结构体系
  • Java创建型模式---工厂模式
  • PVE DDNS IPV6
  • 基于Elasticsearch的短视频平台个性化推荐系统设计与实现
  • SwiftUI 7(iOS 26)中玻璃化工具栏的艺术
  • 介绍electron
  • 基于spark的奥运会奖牌变化数据分析
  • 国产 OFD 标准公文软件数科 OFD 阅读器:OFD/PDF 双格式支持,公务办公必备
  • day44打卡
  • cmd 的sftp传输;Conda出现环境问题: error: invalid value for --gpu-architecture (-arch)
  • 浅度解读-(未完成版)浅层神经网络-多个隐层神经元