1 fromkeys()函数是什么
在 Python 中,fromkeys()
是字典(dict
)的一个类方法,用于创建一个新字典。
它的作用是:根据指定的可迭代对象(如列表、元组等)中的元素作为键(key),并为所有键设置一个默认的初始值(value),从而快速创建一个新字典。
语法格式:
python
运行
dict.fromkeys(iterable, value=None)
参数说明:
iterable
:必需参数,一个可迭代对象(如列表、元组、字符串等),其元素将作为新字典的键。value
:可选参数,为所有键设置的默认值,默认为None
。
返回值:一个新的字典。
示例:
python
运行
# 用列表作为可迭代对象,创建新字典,默认值为 None
keys = ['name', 'age', 'gender']
new_dict = dict.fromkeys(keys)
print(new_dict) # 输出: {'name': None, 'age': None, 'gender': None}# 为所有键设置相同的初始值
new_dict2 = dict.fromkeys(keys, 'unknown')
print(new_dict2) # 输出: {'name': 'unknown', 'age': 'unknown', 'gender': 'unknown'}# 用字符串作为可迭代对象
str_keys = 'abc'
new_dict3 = dict.fromkeys(str_keys, 0)
print(new_dict3) # 输出: {'a': 0, 'b': 0, 'c': 0}
注意事项:
如果 value
是可变对象(如列表、字典等),则所有键会共享同一个对象的引用,修改其中一个键的值会影响其他键。例如:
python
运行
keys = ['a', 'b', 'c']
value = [] # 可变对象
my_dict = dict.fromkeys(keys, value)my_dict['a'].append(1)
print(my_dict) # 输出: {'a': [1], 'b': [1], 'c': [1]}
此时所有键的值都会被修改,因为它们指向同一个列表对象。如果需要避免这种情况,通常需要用其他方式(如循环)为每个键创建独立的可变对象。
=> 如果希望每个键对应不同的初始值(尤其是可变对象)的做法
如果希望每个键对应不同的初始值(尤其是可变对象),可以通过循环遍历键列表,为每个键单独赋值。这样能确保每个值都是独立的对象,修改其中一个不会影响其他。
方法 1:使用普通 for 循环
python
运行
keys = ['a', 'b', 'c']
my_dict = {}# 为每个键设置不同的初始值(这里用索引区分)
for i, key in enumerate(keys):my_dict[key] = [i] # 每个键对应一个独立的列表print(my_dict) # 输出: {'a': [0], 'b': [1], 'c': [2]}# 修改其中一个值,其他不受影响
my_dict['a'].append(100)
print(my_dict) # 输出: {'a': [0, 100], 'b': [1], 'c': [2]}
方法 2:使用字典推导式(更简洁)
字典推导式可以一行代码实现上述功能,适合简单场景:
python
运行
keys = ['a', 'b', 'c']# 为每个键设置不同的值(例如基于键名的处理)
my_dict = {key: [key.upper()] for key in keys}print(my_dict) # 输出: {'a': ['A'], 'b': ['B'], 'c': ['C']}# 修改一个值,其他不变
my_dict['a'].append('first')
print(my_dict) # 输出: {'a': ['A', 'first'], 'b': ['B'], 'c': ['C']}
方法 3:根据条件动态生成不同值
如果值的差异更复杂(比如依赖外部数据),可以结合条件判断:
python
运行
keys = ['a', 'b', 'c']
values = [10, 20, 30] # 提前准备好对应的值列表# 用zip配对键和值
my_dict = {k: [v] for k, v in zip(keys, values)}print(my_dict) # 输出: {'a': [10], 'b': [20], 'c': [30]}
核心原理:
通过循环或推导式,为每个键单独创建新对象(如[i]
、[key.upper()]
),而不是让所有键共享同一个对象引用。这样无论值是列表、字典等可变对象,修改时都不会相互影响。
2 pop()
pop() 函数的作用是默认删除最后一个元素,并返回删除的元素的值。
在 Python 中,pop()
是列表(list
)的一个常用方法,其主要作用是:
- 默认删除列表中的最后一个元素
- 返回被删除的元素的值
基本语法:
python
运行
list.pop(index=-1)
index
是可选参数,默认值为-1
(即最后一个元素的索引)- 如果指定了有效的索引,就会删除该索引位置的元素并返回它
示例:
python
运行
# 示例1:默认删除最后一个元素
fruits = ['apple', 'banana', 'cherry']
last_fruit = fruits.pop()
print(last_fruit) # 输出: 'cherry'
print(fruits) # 输出: ['apple', 'banana']# 示例2:指定索引删除元素
numbers = [10, 20, 30, 40]
removed = numbers.pop(1) # 删除索引为1的元素(20)
print(removed) # 输出: 20
print(numbers) # 输出: [10, 30, 40]
注意:
- 如果列表为空,调用
pop()
会抛出IndexError
错误 - 如果指定的索引超出列表范围,也会抛出
IndexError
错误
除了列表,字典(dict
)也有 pop()
方法,但功能不同:字典的 pop(key)
是根据指定的键删除键值对,并返回对应的值。
3 正则表达式(re
模块)
正则表达式(re
模块)是 Python 中非常重要且实用的工具,尤其在文本处理场景中几乎不可或缺。如果你经常需要处理字符串(比如提取信息、验证格式、替换内容等),系统学习re
模块会让你效率翻倍。
为什么re
模块重要?
文本处理的 “瑞士军刀”
无论是爬虫提取数据、日志分析、表单验证(如邮箱 / 手机号格式),还是字符串清洗,正则表达式都能通过简洁的模式完成复杂操作。
例如:用一行正则快速提取文本中所有邮箱地址,比用字符串方法(split
/find
等)简洁得多。跨语言通用能力
正则表达式的语法在 Python、Java、JavaScript 等主流语言中高度一致,学会后能迁移到其他场景。简化复杂逻辑
很多字符串处理如果不用正则,可能需要写十几行循环判断,而正则只需一个模式字符串。
如何入门re
模块?
不需要一开始就掌握所有细节,先学核心功能即可:
基础匹配规则
.*?
非贪婪匹配(如题目中的用法)\d
匹配数字,\w
匹配字母 / 数字 / 下划线^
开头,$
结尾(用于严格验证格式)()
捕获组(用于提取特定部分,如题目中的group(2)
)
常用方法
re.search()
:查找第一个匹配(如题目中的用法)re.findall()
:查找所有匹配并返回列表re.sub()
:替换匹配的内容re.match()
:从字符串开头匹配(与search
的区别)
实战练习
从简单例子入手,比如:- 提取字符串中的所有数字
- 验证手机号格式(
^1[3-9]\d{9}$
) - 替换文本中的敏感词
举个简单例子理解 “捕获组”
题目中的(.*) are (.*?) .*
其实是用()
定义了两个 “捕获组”,就像给需要提取的部分 “打标签”:
- 第一个
(.*)
标签 1:匹配 “are” 前面的内容(Cats) - 第二个
(.*?)
标签 2:匹配 “are” 后面的内容(smarter)
通过group(1)
、group(2)
就能直接拿到这些 “标签” 对应的内容,这就是正则提取信息的核心逻辑。
如果后续经常用到文本处理,建议花 1-2 天系统学一下基础语法,之后遇到具体问题再查文档补充。入门后你会发现,很多之前觉得复杂的字符串操作会变得非常简单~
4 Python的多态
【Python的多态可通过鸭子类型(Duck Typing) 实现,即不依赖继承,这一点是不是和强类型语言c,java是反的?】
你的理解有一定道理,但更准确地说,这是动态类型语言(如 Python)与静态强类型语言(如 C、Java)在多态实现方式上的差异,而非完全 “相反”。
核心区别在于:
Python(动态类型)的鸭子类型
不要求显式的继承关系或接口实现,只关注对象是否具有所需的方法 / 属性。
例如,只要一个对象有fly()
方法,就可以被当作 “会飞的东西” 来使用,无论它是否继承自Bird
类:python
运行
class Duck:def fly(self):print("鸭子飞")class Plane:def fly(self):print("飞机飞")def make_fly(obj):obj.fly() # 只要有fly()方法就能调用make_fly(Duck()) # 可行 make_fly(Plane()) # 也可行
Java/C#(静态强类型)的多态
依赖继承关系或接口实现,编译器会严格检查类型兼容性。
例如,要让make_fly
方法接收不同对象,必须让它们实现同一个接口:java
运行
// 必须定义接口 interface Flyable {void fly(); }class Duck implements Flyable {public void fly() { ... } }class Plane implements Flyable {public void fly() { ... } }// 方法参数必须声明为接口类型 void makeFly(Flyable obj) {obj.fly(); }
C 语言
本身没有面向对象的多态特性,需要通过函数指针、结构体模拟,本质是手动实现类型适配,与上述两种方式都不同。(想当然的以为也有,都不是opp了......倒是像文盲了哈哈哈)
本质差异:
- 动态类型语言:多态是 “隐式” 的,由运行时对象的能力决定(鸭子类型)。
- 静态强类型语言:多态是 “显式” 的,由编译时的类型声明 / 继承关系保证。
这两种方式没有绝对优劣:
- 鸭子类型更灵活,代码简洁,但可能在运行时才暴露类型错误;
- 接口 / 继承方式更严谨,编译期即可发现问题,但需要写更多声明代码。
5 什么叫可调用对象?关于对内存空间的理解
类似的开辟了几个内存空间,我也没有搞得很清楚....../(ㄒoㄒ)/
一、什么是可调用对象?
在 Python 中,可调用对象(Callable) 指的是可以像函数一样被调用的对象,即可以在对象后面加括号 ()
并传入参数来执行的对象。
常见的可调用对象类型:
函数(def 定义的普通函数)
python
运行
def add(a, b):return a + b add(1, 2) # 调用函数,返回 3 → add 是可调用对象
lambda 表达式(匿名函数)
python
运行
multiply = lambda x, y: x * y multiply(3, 4) # 调用 lambda,返回 12 → multiply 是可调用对象
类(实例化时会调用
__init__
方法)python
运行
class Person:def __init__(self, name):self.name = name p = Person("Alice") # 调用类创建实例 → Person 是可调用对象
类的实例(如果类定义了
__call__
方法)python
运行
class Counter:def __init__(self):self.count = 0def __call__(self): # 定义 __call__ 方法后,实例可被调用self.count += 1return self.count c = Counter() print(c()) # 调用实例,返回 1 → c 是可调用对象 print(c()) # 再调用,返回 2
其他(如内置函数
print
、方法list.append
等)python
运行
print("Hello") # print 是可调用对象 [1,2].append(3) # append 方法是可调用对象
判断是否为可调用对象:
用 callable()
函数判断:
python
运行
print(callable(add)) # True(函数可调用)
print(callable(Person)) # True(类可调用)
print(callable(123)) # False(数字不可调用)
二、关于 “内存空间” 的理解
变量、对象、函数等在内存中都会占用空间,不同类型的对象占用的空间和存储方式不同,这里用简单例子说明:
1. 变量与对象的内存关系
- 对象:在内存中实际存储数据的实体(如数字、字符串、列表、函数等)。
- 变量:指向对象的 “标签”,本身不存储数据,只记录对象在内存中的地址。
python
运行
a = 100 # 100 是一个整数对象,在内存中占据一块空间;a 是变量,指向这块空间
b = a # b 也指向 100 所在的内存空间(不新开辟空间)
2. 不同对象的内存占用示例
简单类型(int、str 等):创建时开辟一块内存。
python
运行
x = "hello" # 开辟一块内存存 "hello",x 指向它 y = "world" # 再开辟一块内存存 "world",y 指向它
容器类型(list、dict 等):容器本身和内部元素都占内存。
python
运行
lst = [1, 2, 3] # 1. 开辟一块内存存列表本身(记录元素的地址)# 2. 分别开辟内存存 1、2、3 三个整数对象
函数 / 类(可调用对象):定义时就会在内存中创建对应的对象。
python
运行
def func(): # 定义函数时,内存中创建一个函数对象,func 指向它pass
3. 关键结论
- 每个独立的对象(如不同的字符串、列表、函数)都会开辟独立的内存空间。
- 多个变量可以指向同一个对象(此时共享一块内存,不重复开辟)。
- 可调用对象(如函数、类)也是对象的一种,定义时会在内存中分配空间,变量指向它们。
总结
- 可调用对象:能加
()
调用的对象(函数、类、带__call__
的实例等)。 - 内存空间:每个独立对象占一块空间,变量只是指向对象的 “指针”,多个变量可共享同一对象的内存。
通过 id()
函数可以查看对象的内存地址(唯一标识),帮助理解:
python
运行
a = 10
b = a
print(id(a), id(b)) # 输出相同的地址(a 和 b 指向同一对象)
6 链式比较和逻辑运算or(以及其他Python 中容易混淆的运算符特性)
下列Python3代码的运行结果是()
print('a' < 'b' < 'c')
A a
B b
C c
D True
E False
正确答案:D
你的答案:C#我之前做or不是返回操作数本身吗??!!
你混淆了两种不同的运算符行为:
比较运算符的链式判断:
Python 支持类似a < b < c
的链式比较,等价于a < b and b < c
,返回的是布尔值(True
或False
)。
题目中'a' < 'b' < 'c'
是字符的 ASCII 码比较(a
的 ASCII 码是 97,b
是 98,c
是 99),显然97 < 98 < 99
成立,所以结果为True
。逻辑运算符
or
的特性:
你提到的 “返回操作数本身” 是or
的行为:or
会返回第一个为 “真” 的值(如果所有值都为假,则返回最后一个值)。例如:python
运行
print(0 or 'a' or 'b') # 输出 'a'(第一个真值) print('a' or 'b') # 输出 'a'
这是两种完全不同的语法:
- 链式比较(
a < b < c
)返回布尔值 - 逻辑运算
or
返回第一个真值
所以题目中是比较运算,结果为 True
(选项 D)。
为了避免混淆各类运算符的行为,这里整理了 Python 中容易混淆的运算符特性及对比小结:
一、比较运算符(==
、!=
、>
、<
、>=
、<=
)
基本作用:比较两个值的关系,返回布尔值(
True
/False
)。python
运行
print(2 == 2) # True print(3 > 5) # False
特殊:链式比较
支持a < b < c
这样的连续比较,等价于a < b and b < c
,最终返回布尔值。python
运行
print(1 < 2 < 3) # 等价于 1<2 and 2<3 → True print('x' > 'y' > 'z') # 字符按ASCII码比较 → False
二、逻辑运算符(and
、or
、not
)
not
:逻辑非,返回布尔值。python
运行
print(not True) # False print(not 0) # True(0为假值)
and
:逻辑与- 若第一个值为假,返回第一个值;
- 若第一个值为真,返回第二个值(不会返回布尔值,除非操作数本身是布尔值)。
python
运行
print(0 and 5) # 0(第一个值为假,返回0) print(3 and 5) # 5(第一个值为真,返回第二个值) print(True and False) # False(操作数是布尔值,返回布尔值)
or
:逻辑或- 若第一个值为真,返回第一个值;
- 若第一个值为假,返回第二个值(同样可能返回非布尔值)。
python
运行
print(0 or 5) # 5(第一个值为假,返回第二个值) print(3 or 5) # 3(第一个值为真,返回第一个值) print(False or True) # True(操作数是布尔值,返回布尔值)
三、常见混淆点对比
表达式类型 | 运算逻辑 | 返回值类型 | 示例 | 结果 |
---|---|---|---|---|
单个比较 ( | 判断关系 | 布尔值(True /False ) | 'a' < 'b' | True |
链式比较 ( | 等价于 a < b and b < c | 布尔值 | 1 < 3 < 2 | False |
and 运算 | 取第一个假值,无假值则取最后一个值 | 操作数本身 (或布尔值) | 'a' and '' and 'b' | '' |
or 运算 | 取第一个真值,无真值则取最后一个值 | 操作数本身 (或布尔值) | '' or 'a' or 'b' | 'a' |
四、记忆技巧
- 比较运算(包括链式):最终都是 “判断对错”,所以返回布尔值。
and
/or
:更像 “筛选值”,返回的是参与运算的原始值(除非原始值本身是布尔值)。and
可以理解为 “找第一个不对劲的值”(假值),找不到就返回最后一个。or
可以理解为 “找第一个对劲的值”(真值),找不到就返回最后一个。
7 list.extend()
方法的返回值是 None
以及 列表扩容与内存地址
执行以下程序,输出结果为()l1 = [1,2]
l2 = [3,4]
print(id(l1) == id(l1.extend(l2)))A False
B True
C None
D 抛出异常正确答案:A
id(l1.extend(l2)) 得到 None 对象的内存地址?(官方题解)我以为l1这种list列表内存地址是离散的,从底层理解的话extend以后会占更多的内存空间所以内存地址肯定不一样(歪打正着,本意就是认为列表扩容一定会更改内存地址。
你的思路中有部分正确(结果确实是 False),但对底层逻辑的理解可以再精确一些。我们来详细拆解:
核心原因:list.extend()
方法的返回值是 None
l1.extend(l2)
的作用:
它会在原列表l1
上修改,将l2
的元素添加到l1
末尾(不会创建新列表)。
执行后l1
变成[1,2,3,4]
,但l1
本身的内存地址不变(因为是原地修改)。l1.extend(l2)
的返回值:
所有列表的 “原地修改” 方法(如extend
、append
、sort
等)都返回None
(这是 Python 的设计规范)。
所以id(l1.extend(l2))
等价于id(None)
,即获取None
对象的内存地址。比较的本质:
id(l1) == id(l1.extend(l2))
等价于:id(原列表l1的地址) == id(None的地址)
显然两者完全不同,因此结果为False
。
关于 “列表内存地址” 的补充
- 列表是动态数组,当元素增加导致内存不足时,Python 会重新分配更大的内存空间并复制元素(此时
id(l1)
会变化)。 - 但本题中
l1
从[1,2]
扩展为[1,2,3,4]
,通常不会触发内存重分配(小列表有预留空间),所以l1
的id
不变。 - 即使触发了内存重分配,
l1
的id
变化了,l1.extend(l2)
返回的依然是None
,id
比较结果还是False
(因为None
的地址和任何列表的地址都不同)。
关键结论
这道题的核心不是 “列表是否扩容”,而是 **extend
方法返回 None
**,导致比较的是 “原列表地址” 和 “None
的地址”,必然不等,因此选 A
。
【列表扩容影响内存地址吗?】
列表扩容可能会影响内存地址,具体取决于扩容时是否需要重新分配新的内存空间。
在 Python 中,列表(list
)是一种动态数组,其底层会预先分配一定的「预留内存空间」。当添加元素时:
- 如果当前预留空间足够容纳新元素(即未超过当前容量),则直接在原有内存空间中添加,列表的内存地址(
id
)不会改变。 - 如果当前预留空间不足(即超过当前容量),则需要重新分配一块更大的内存空间,并将原列表中的元素复制到新空间中,此时列表的内存地址(
id
)会发生改变。
示例验证:
可以通过 id()
函数观察列表扩容时的内存地址变化:
python
运行
l = [1, 2]
print("初始id:", id(l)) # 记录初始内存地址# 逐步添加元素,观察id是否变化
l.append(3)
print("添加第3个元素后id:", id(l)) # 通常不变(预留空间足够)l.append(4)
print("添加第4个元素后id:", id(l)) # 可能不变l.extend([5, 6, 7, 8, 9]) # 一次性添加多个元素,可能触发扩容
print("添加多个元素后id:", id(l)) # 此时可能变化(预留空间不足)
运行结果可能类似(具体地址因环境而异):
plaintext
初始id: 140525422476416
添加第3个元素后id: 140525422476416 # 地址不变
添加第4个元素后id: 140525422476416 # 地址不变
添加多个元素后id: 140525422509056 # 地址改变(触发扩容)
总结:
- 列表扩容时,若无需重新分配内存,地址不变;若需要重新分配内存,地址改变。
- 这种机制是 Python 为了平衡性能设计的:预留空间减少了频繁分配内存的开销,而必要时的扩容则保证了列表可以动态增长。
因此,不能认为列表扩容一定会改变内存地址,具体取决于当前容量是否足够。
【我怎么判断够不够?】
在 Python 中,列表的「当前容量」(即底层已分配的内存空间能容纳的元素总数)和「实际长度」(len()
返回的值)是两个不同的概念。可以通过一些技巧判断当前容量是否足够容纳新元素:
方法:借助 sys.getsizeof()
间接计算容量
sys.getsizeof(list)
会返回列表对象占用的内存总字节数。由于列表存储的是元素的引用(指针),可以通过总字节数减去列表自身固定开销,再除以单个指针的字节数(通常与系统位数有关,64 位系统为 8 字节),得到当前容量。
步骤如下:
- 导入
sys
模块 - 计算列表的固定开销(空列表的内存大小)
- 用
(列表总字节数 - 固定开销) // 单个指针字节数
得到当前容量
python
运行
import sysdef get_list_capacity(lst):# 空列表的内存大小(固定开销)empty_size = sys.getsizeof([])# 列表总内存大小total_size = sys.getsizeof(lst)# 单个指针的字节数(64位系统通常为8)pointer_size = sys.getsizeof(id(lst)) # 间接获取指针大小(id返回的是内存地址,其大小即指针大小)# 计算容量:(总大小 - 固定开销) / 指针大小capacity = (total_size - empty_size) // pointer_sizereturn capacity# 测试示例
l = [1, 2]
print("长度:", len(l)) # 实际元素个数:2
print("容量:", get_list_capacity(l)) # 输出当前容量(通常大于等于2,比如可能是4)l.append(3)
print("添加1个元素后,长度:", len(l)) # 3
print("添加1个元素后,容量:", get_list_capacity(l)) # 若容量仍为4,说明还能容纳l.append(4)
print("添加第2个元素后,长度:", len(l)) # 4
print("添加第2个元素后,容量:", get_list_capacity(l)) # 仍为4,刚好装满l.append(5)
print("添加第3个元素后,长度:", len(l)) # 5
print("添加第3个元素后,容量:", get_list_capacity(l)) # 容量可能变为8(触发扩容)
结论:如何判断容量是否足够?
- 当
len(列表) < 容量
时:容量足够,添加元素不会改变列表的内存地址(id
不变)。 - 当
len(列表) == 容量
时:容量已满,再添加元素会触发扩容,此时列表的内存地址(id
会改变)。
注意:
- 容量是 Python 内部管理的,不同版本 / 环境可能有不同的扩容策略(通常是翻倍或按比例增长),不需要手动干预。
- 实际开发中很少需要关心容量,因为 Python 会自动处理扩容,这个知识点主要用于理解列表的底层机制。
通过上面的方法,你可以直观地看到列表在添加元素过程中「长度」和「容量」的变化,从而判断是否会触发扩容。
【那我怎么做到比较extend以后的id地址?】
要比较 extend()
操作前后列表的内存地址(id
),可以在调用 extend()
方法之前和之后分别获取列表的 id
,再进行比较。
关键在于:!!!!!!!extend()
是原地修改列表(不会返回新列表),所以需要单独获取 extend()
执行前后的 id
。
示例代码:
python
运行
l1 = [1, 2]
l2 = [3, 4]# 记录 extend 执行前 l1 的 id
id_before = id(l1)# 执行 extend(原地修改 l1,无返回值)
l1.extend(l2)# 记录 extend 执行后 l1 的 id
id_after = id(l1)# 比较两个 id 是否相同
print("extend 前 id:", id_before)
print("extend 后 id:", id_after)
print("id 是否相同:", id_before == id_after)
可能的运行结果:
plaintext
extend 前 id: 140632345869056
extend 后 id: 140632345869056
id 是否相同: True
为什么会这样?
extend()
是原地修改列表(直接在l1
原有内存空间中添加元素),如果原列表的容量足够(len(l1) + len(l2) ≤ 容量
),则不需要重新分配内存,id
保持不变。- 即使容量不足需要扩容(
id
改变),也能通过这种方式清晰对比extend()
前后的id
。
对比:如果用 +
拼接列表(会创建新列表)
python
运行
l1 = [1, 2]
l2 = [3, 4]id_before = id(l1)
l1 = l1 + l2 # 用 + 拼接会创建新列表,l1 指向新地址
id_after = id(l1)print("+ 拼接前 id:", id_before)
print("+ 拼接后 id:", id_after)
print("id 是否相同:", id_before == id_after) # 结果为 False
总结:
- 要比较
extend()
前后的id
,只需在调用extend()
方法前和后分别获取列表的id
即可。 extend()
通常不会改变列表的id
(除非扩容触发内存重分配),而+
拼接一定会改变id
(因为创建了新列表)。