学习日志21 python
1 Python中可以作为字典键的数据类型
Python中可以作为字典键的数据类型包括:
- 数字(整数、浮点数)
- 字符串
- 元组
- 布尔值
而列表、字典、集合等可变类型都不能作为字典的键。
在 Python 中,字典的键必须是可哈希(hashable)的数据类型。
具体来说,以下类型可以作为字典的键:
1. 不可变数据类型(可哈希)
整数(int)
python
运行
my_dict = {1: "one", 2: "two"}
浮点数(float)
python
运行
my_dict = {3.14: "pi", 2.71: "e"}
字符串(str)
python
运行
my_dict = {"name": "Alice", "age": 30}
布尔值(bool)
python
运行
my_dict = {True: "yes", False: "no"}
元组(tuple)
但元组内的所有元素也必须是可哈希的:python
运行
my_dict = {(1, 2): "coordinate", ("a",): "letter"}
None
python
运行
my_dict = {None: "default"}
2. 自定义类的实例(默认可哈希)
自定义类的对象默认是可哈希的,因为它们的哈希值基于内存地址:
python
运行
class Person:passp = Person()
my_dict = {p: "person object"}
3. 不可变集合(frozenset)
python
运行
my_dict = {frozenset([1, 2]): "frozen set"}
不可作为键的类型(不可哈希)
列表(list)
因为列表是可变的,无法保证哈希值的唯一性。
❌my_dict = {[1, 2]: "invalid"} # 报错
字典(dict)
同理,字典也是可变的。
❌my_dict = {{1: "a"}: "invalid"} # 报错
集合(set)
集合是可变的,不可哈希。
❌my_dict = {{1, 2}: "invalid"} # 报错
判断对象是否可哈希
使用 hash()
函数测试:
python
运行
hash("hello") # 正常返回哈希值 → 可哈希
hash([1, 2]) # 报错 → 不可哈希
为什么要求键必须可哈希?
字典通过哈希表实现,键的哈希值用于快速定位存储位置。如果键是可变的,其哈希值可能改变,导致无法正确找到对应的值。因此,Python 要求键必须是不可变类型,以保证哈希值的稳定性。
2 可哈希是什么?
在 Python 中,可哈希(hashable)是一个对象的属性,用于判断该对象是否可以作为字典的键或集合(set
/frozenset
)的元素。可哈希性的核心在于对象的哈希值在其生命周期内不可变。
1. 什么是哈希值?
- 哈希值是一个由对象通过
hash()
函数计算得到的整数。python
运行
print(hash("hello")) # 输出: 4276290592126914302
- Python 内置的哈希函数会根据对象的内容生成哈希值(对于不可变类型),或根据对象的内存地址生成哈希值(对于自定义对象)。
2. 可哈希对象的条件
一个对象要被视为可哈希,必须满足以下条件:
- 不可变类型(如
int
,str
,tuple
等)。 - 实现了
__hash__()
方法,返回一个固定的哈希值。 - 实现了
__eq__()
方法,用于比较对象是否相等。
3. 常见可哈希类型
类型 | 可哈希性 | 原因 |
---|---|---|
int , float , bool | ✅ 可哈希 | 数值不可变。 |
str | ✅ 可哈希 | 字符串不可变。 |
tuple | ✅ 可哈希 | 元组本身不可变,但要求所有元素也必须是可哈希的。 |
frozenset | ✅ 可哈希 | 不可变集合。 |
自定义对象 | ✅ 可哈希 | 默认根据对象的内存地址生成哈希值(除非重写 __hash__ )。 |
4. 常见不可哈希类型
类型 | 不可哈希原因 |
---|---|
list | 列表可变,可通过 append() 等方法修改。 |
dict | 字典的键必须是可哈希的,但字典本身可变。 |
set | 集合可变,可通过 add() 等方法修改。 |
5. 为什么字典键和集合元素需要可哈希?
- 字典和集合的工作原理:
它们使用哈希表来存储数据,通过哈希值快速定位元素。如果键或元素的哈希值可变,会导致数据无法正确定位。 - 示例:
python
运行
my_dict = {[1, 2]: "value"} # 报错!列表不可哈希
6. 自定义可哈希类
如果需要让自定义类的对象可哈希,需要重写 __hash__()
和 __eq__()
方法:
python
运行
class Point:def __init__(self, x, y):self.x = xself.y = ydef __hash__(self):return hash((self.x, self.y)) # 使用元组的哈希值def __eq__(self, other):return self.x == other.x and self.y == other.yp1 = Point(1, 2)
p2 = Point(1, 2)
my_set = {p1, p2} # 由于 __eq__ 判断相等,集合中只会存储一个元素
总结
- 可哈希 ≈ 不可变:大多数不可变类型都是可哈希的。
- 用途:可哈希对象可以作为字典的键或集合的元素,用于高效查找和去重。
- 判断方法:使用
hash()
函数测试,若不报错则可哈希:python
运行
hash("hello") # 正常 hash([1, 2]) # TypeError: unhashable type 'list'
3 file.seek(0)是什么意思?
file.seek(0)
是 Python 文件操作中的一个方法,用于移动文件指针到指定位置。具体来说:
seek()
方法:
用于改变文件当前的位置(即文件指针)。文件指针决定了后续读写操作的起始位置。参数
0
:
表示将文件指针移动到文件的开头。数值参数代表相对于某个参考点的字节偏移量,0
即文件起始处。常见用途:
- 重读文件:当你已经读取完文件内容后,使用
seek(0)
可以回到开头重新读取。 - **覆盖写入**:在写入文件前调用
seek(0)
,可以从文件开头开始覆盖原有内容。
- 重读文件:当你已经读取完文件内容后,使用
示例代码
python
运行
# 读取文件
with open("example.txt", "r") as file:first_line = file.readline() # 读取第一行print(f"第一行: {first_line}")file.seek(0) # 回到文件开头all_content = file.read() # 读取整个文件print(f"全文内容: {all_content}")# 写入文件(覆盖模式)
with open("example.txt", "w") as file:file.write("新内容")file.seek(0) # 回到开头file.write("覆盖原有内容") # 从开头覆盖写入
参数说明
offset
:偏移量(如0
表示开头)。whence
(可选):参考点,默认值为0
(文件开头):0
:从文件开头计算偏移量。1
:从当前位置计算偏移量。2
:从文件末尾计算偏移量(此时offset
通常为负数)。
例如:
file.seek(0, 2)
:移动到文件末尾。file.seek(-10, 2)
:从文件末尾向前移动 10 个字节。
题目
在Python中,以下代码片段的功能是?
with open("data.txt", "w") as file:file.write("Hello\nWorld\n")file.seek(0)file.write("Hi")A 创建文件data.txt,最终内容为Hillo\nWorld\n
B 创建文件data.txt,最终内容为Hi
C 创建文件data.txt,最终内容为Hi\nWorld\n
D 抛出io.UnsupportedOperation异常正确答案:A
你的答案:B
代码执行步骤解析
python
运行
with open("data.txt", "w") as file:file.write("Hello\nWorld\n") # 步骤1:写入内容,指针移到末尾file.seek(0) # 步骤2:指针回到文件开头file.write("Hi") # 步骤3:从开头覆盖写入"Hi"
第一次写入:
- 写入内容:
Hello\nWorld\n
(共 12 个字符)。 - 文件指针位置:移动到末尾(位置 12)。
- 写入内容:
seek(0)
操作:- 文件指针被移动到文件开头(位置 0)。
- 注意:此时文件内容并未被清空,只是指针位置改变。
第二次写入:
- 从位置 0 开始写入
Hi
,覆盖原内容的前两个字符。 - 原内容:
Hello\nWorld\n
- 覆盖后:
Hillo\nWorld\n
(He
被替换为Hi
)。
- 从位置 0 开始写入
为什么答案是 A 而非 B?
选项 B(最终内容为Hi
)的错误原因:
当使用"w"
模式打开文件时,文件会在打开瞬间被清空。但后续的写入操作是按指针位置进行的。
- 若第二次写入后立即关闭文件,内容为
Hillo\nWorld\n
(覆盖前两个字符)。 - 若要完全覆盖为
Hi
,需在seek(0)
后追加truncate()
操作清空文件:python
运行
file.seek(0) file.truncate() # 清空文件 file.write("Hi") # 写入新内容
常见误区
认为
seek(0)
会清空文件:seek(0)
仅移动指针,不改变文件内容。只有truncate()
或重新打开文件会清空内容。忽略覆盖写入的特性:
写入操作从当前指针位置开始,直接覆盖原有字符,而非插入或替换整行。
验证方法
运行以下代码可验证结果:
python
运行
with open("test.txt", "w") as f:f.write("Hello\nWorld\n")f.seek(0)f.write("Hi")with open("test.txt", "r") as f:print(f.read()) # 输出:Hillo\nWorld\n
结论
正确答案为 A,因为第二次写入从文件开头覆盖了前两个字符,最终内容为:
plaintext
Hillo
World
4 关于python的作用域
关于python的作用域,以下描述正确的是()A 在函数内部创建的变量属于该函数的作用域,并且只能在该函数内部使用B python的作用域规则的查找顺序LEGB,Builtion,Local(函数),GLOBALC global关键字可以在函数内部创建一个全局变量D 函数外部创建的变量的作用,函数内部可以直接使用正确答案:ACD
正确选项解析
A. 在函数内部创建的变量属于该函数的作用域,并且只能在该函数内部使用
正确。函数内部定义的变量(局部变量)仅在函数内部可见。
python
运行
def test():x = 10 # 局部变量print(x) # 正确:函数内部可访问test()
print(x) # 错误:NameError,外部无法访问
C. global 关键字可以在函数内部创建一个全局变量
正确。global
声明可将函数内的变量绑定到全局作用域。
python
运行
def create_global():global y # 声明y为全局变量y = 20 # 创建全局变量ycreate_global()
print(y) # 输出:20(全局作用域已存在y)
D. 函数外部创建的变量的作用,函数内部可以直接使用
正确。函数内部可直接读取全局变量,但不能直接修改(需用 global
声明)。
python
运行
z = 30def read_global():print(z) # 正确:读取全局变量read_global() # 输出:30
错误选项解析
B. Python 的作用域规则的查找顺序 LEGB,Builtion,Local(函数),GLOBAL
错误。正确的 LEGB 顺序为:
- Local(局部作用域,函数内)
- Enclosing(闭包作用域,嵌套函数的外层)
- Global(全局作用域)
- Built-in(内置作用域)
选项 B 混淆了顺序,且拼写错误(Builtion
应为 Built-in
)。
总结
正确答案:A、C、D
- A:局部变量仅在函数内可见。
- C:
global
可在函数内创建 / 修改全局变量。 - D:函数可直接读取全局变量。
- B:LEGB 顺序错误。
5 python好像大部分都是左闭右开吗?(指的是range)
在 Python 中,并非所有涉及范围的操作都是左闭右开,不是所有的都是左闭右开!但左闭右开是一种非常常见的设计模式,尤其在序列相关操作中。以下是具体分析:
1. 左闭右开的典型场景
这些操作遵循 “包含起始值,不包含结束值” 的规则,是 Python 中最常见的情况:
range()
函数
如range(1, 5)
生成[1, 2, 3, 4]
,包含 1 但不包含 5,这是左闭右开的核心体现。序列切片(列表、字符串、元组等)
例如:python
运行
s = "abcdef" print(s[1:4]) # 输出 "bcd"(包含索引1,不包含索引4)
切片
[start:end]
始终包含start
对应的元素,不包含end
对应的元素。某些库的区间操作
如pandas
中对DataFrame
的切片、numpy
数组的索引等,大多延续了 Python 原生的左闭右开规则。
2. 不遵循左闭右开的场景
以下情况不适用左闭右开,需要特别注意:
字符串方法
str.find()
的返回值
该方法返回子串首次出现的起始索引,若查找范围为str.find(sub, start, end)
,则end
是 “包含” 的(即查找范围是[start, end]
)。例如:python
运行
s = "abcabc" print(s.find("ab", 0, 3)) # 输出 0(在索引0-3范围内查找,包含索引3)
循环中的比较条件
手动编写的循环条件可能灵活定义范围,例如:python
运行
for i in [1, 2, 3, 4, 5]:if 2 <= i <= 4: # 这里是闭区间 [2,4]print(i) # 输出 2,3,4
数学计算相关函数
如numpy.linspace(start, stop, num)
生成的序列 包含stop
(闭区间),例如:python
运行
import numpy as np print(np.linspace(1, 5, 3)) # 输出 [1. 3. 5.](包含1和5)
3. 为什么左闭右开更常见?
Python 中大量操作采用左闭右开,主要是为了:
- 计算方便:区间长度直接为
end - start
(如range(1,5)
长度为 4)。 - 避免重叠:多个连续区间可无缝衔接(如
[0:2]
和[2:4]
刚好覆盖[0:4]
)。 - 一致性:切片、
range
等操作规则统一,降低学习成本。
总结
Python 中 左闭右开是主流设计(如 range
、切片),但并非所有场景都遵循,需根据具体操作或函数的定义判断。记住核心场景的规则,同时对特殊情况(如 str.find()
、numpy.linspace
)保持留意,即可避免混淆。
6 Python装饰器是不是类似于java里面的方法重写?
Python 装饰器和 Java 方法重写在功能上有部分相似性(如增强原有方法),但本质上是两种不同的机制,应用场景和实现方式差异较大。以下是详细对比:
1. 核心概念对比
Python 装饰器
- 本质:是一种高阶函数,接收一个函数作为参数,并返回一个新的函数(通常包装原函数)。
- 目的:在不修改原函数代码的前提下,为其添加额外功能(如日志、权限校验、性能统计等)。
- 语法:使用
@decorator
语法糖简化调用。
示例:
python
运行
def log_decorator(func):def wrapper(*args, **kwargs):print(f"调用函数: {func.__name__}")result = func(*args, **kwargs)print(f"函数返回: {result}")return resultreturn wrapper@log_decorator
def add(a, b):return a + badd(1, 2) # 输出:调用函数: add → 函数返回: 3
Java 方法重写(Override)
- 本质:是面向对象编程的特性,子类通过重写父类的方法来改变其行为。
- 目的:实现多态,允许子类根据自身需求定制父类方法的实现。
- 语法:使用
@Override
注解(可选,但推荐)。
示例:
java
运行
class Animal {public void speak() {System.out.println("动物发出声音");}
}class Dog extends Animal {@Overridepublic void speak() {System.out.println("汪汪汪");}
}Animal dog = new Dog();
dog.speak(); // 输出:汪汪汪
2. 相似点
- 增强功能:
装饰器和方法重写都可以改变原有方法的行为,但装饰器是通过包装实现,而方法重写是通过继承和覆盖实现。 - 不修改原代码:
两者都不需要修改原方法的源代码(符合开闭原则)。
3. 关键区别
特性 | Python 装饰器 | Java 方法重写 |
---|---|---|
实现机制 | 函数式编程(高阶函数) | 面向对象编程(继承与多态) |
作用对象 | 可以装饰任何函数或类方法 | 只能重写父类的虚方法(非 final、非 private) |
调用方式 | 静态绑定(编译时确定) | 动态绑定(运行时根据对象类型确定) |
灵活性 | 可动态组合多个装饰器,随时启用 / 禁用 | 必须通过继承关系实现,结构固定 |
典型场景 | 日志、缓存、权限校验、事务管理等横切关注点 | 定制子类行为、实现多态 |
4. 装饰器在 Java 中的等价实现
Java 中没有直接等同于 Python 装饰器的语法,但可以通过以下方式实现类似功能:
(1)包装器模式(Wrapper Pattern)
通过组合而非继承来增强对象功能,类似 Python 装饰器的思路。
示例:
java
运行
interface Component {void operation();
}class ConcreteComponent implements Component {@Overridepublic void operation() {System.out.println("执行基本操作");}
}class LoggingDecorator implements Component {private Component component;public LoggingDecorator(Component component) {this.component = component;}@Overridepublic void operation() {System.out.println("开始操作前记录日志");component.operation();System.out.println("操作完成后记录日志");}
}// 使用
Component decorated = new LoggingDecorator(new ConcreteComponent());
decorated.operation();
(2)AOP(面向切面编程)
Java 的 Spring 框架通过 AOP 实现横切关注点(如日志、事务),功能类似 Python 装饰器。
示例:
java
运行
@Aspect
@Component
public class LoggingAspect {@Before("execution(* com.example.service.*.*(..))")public void beforeAdvice(JoinPoint joinPoint) {System.out.println("方法调用前: " + joinPoint.getSignature().getName());}
}
总结
- Python 装饰器更灵活,可动态增强任意函数 / 方法,适用于横切关注点。
- Java 方法重写是面向对象的核心特性,用于子类定制父类行为,实现多态。
- 若 Java 需要类似装饰器的功能,可使用包装器模式或 AOP。
两者虽然都能增强功能,但设计思想和应用场景不同,选择时需根据语言特性和需求来决定。
7 嵌套循环和分析
题目
res = 0
for i in range(1, 4):for j in range(1, 4):for k in range(1, 4):if i != j and i != k and j != k:res += 1
print(res)在Python3中,三层循环后res的结果为:A 6
B 12
C 3
D 5正确答案:A
你的答案:C
正确答案是 A. 6。让我详细解释为什么:
代码分析
python
运行
res = 0
for i in range(1, 4): # i取值1,2,3for j in range(1, 4): # j取值1,2,3for k in range(1, 4): # k取值1,2,3if i != j and i != k and j != k: # 三个数互不相等res += 1 # 符合条件则计数+1
print(res)
这段代码的核心逻辑是:找出所有由 1、2、3 组成的互不重复的三元组,并统计其数量。
关键步骤解析
三层循环的所有可能组合:
每个变量i
,j
,k
都能取1, 2, 3
,总共有3×3×3 = 27
种组合。筛选条件:
要求i ≠ j ≠ k
,即三个数必须互不相同。这实际上是数学中的排列问题:从 3 个不同元素中选 3 个进行排列,排列数为3! = 3×2×1 = 6
。符合条件的组合:
具体列出所有符合条件的三元组:plaintext
(1, 2, 3) (1, 3, 2) (2, 1, 3) (2, 3, 1) (3, 1, 2) (3, 2, 1)
共 6 种。
常见误区
选项 C(3)的错误原因:
可能误将循环次数(每个变量的取值范围是 3)直接作为结果,但题目要求的是符合条件的组合数量。选项 B(12)的错误原因:
可能忽略了条件i ≠ j ≠ k
,直接计算3×3×3 = 27
,或错误地应用了排列组合公式(如3×4 = 12
)。
验证方法
运行代码或手动枚举所有符合条件的组合,都可确认结果为 6。
答案:A. 6
8 Python中列表推导式的执行顺序
在 Python 中,列表推导式(List Comprehension)的执行顺序与嵌套循环的逻辑一致,但语法上是从左到右书写的。理解其执行顺序对于正确编写和解读复杂的列表推导式至关重要。
1. 基本结构与执行顺序
列表推导式的通用语法为:
python
运行
[表达式 for 变量1 in 可迭代对象1 for 变量2 in 可迭代对象2 ... if 条件]
执行顺序:
- 最外层循环:从左到右第一个
for
语句。 - 内层循环:后续的
for
语句依次嵌套。 - 条件判断:
if
语句在所有循环的最内层执行,过滤不符合条件的元素。 - 表达式求值:满足条件时,计算表达式并添加到结果列表中。
2. 示例分析
示例 1:单层循环
python
运行
result = [x**2 for x in range(5)]
# 等价于:
result = []
for x in range(5):result.append(x**2)
执行顺序:遍历 range(5)
,对每个 x
计算 x**2
,结果为 [0, 1, 4, 9, 16]
。
示例 2:双层循环
python
运行
result = [x + y for x in [1, 2] for y in [10, 20]]
# 等价于:
result = []
for x in [1, 2]:for y in [10, 20]:result.append(x + y)
执行顺序:
- 外层循环
x
取1
,内层循环y
依次取10, 20
,生成[11, 21]
。 - 外层循环
x
取2
,内层循环y
依次取10, 20
,生成[12, 22]
。
最终结果:[11, 21, 12, 22]
。
示例 3:带条件判断的双层循环
python
运行
result = [x + y for x in [1, 2] for y in [10, 20] if x != y]
# 等价于:
result = []
for x in [1, 2]:for y in [10, 20]:if x != y: # 实际上所有x和y都不相等result.append(x + y)
执行顺序:循环嵌套与示例 2 相同,但每次循环都会检查 if
条件,仅当条件为真时才执行表达式。
3. 多层循环的执行顺序
对于更复杂的多层循环,执行顺序依然是从左到右的嵌套关系。例如:
python
运行
result = [f"{i},{j},{k}" for i in "ab" for j in "xy" if j != 'x' for k in "12" if k == '2']
等价嵌套循环:
python
运行
result = []
for i in "ab":for j in "xy":if j != 'x': # 仅保留j='y'for k in "12":if k == '2': # 仅保留k='2'result.append(f"{i},{j},{k}")
执行结果:
i='a'
,j='y'
,k='2'
→"a,y,2"
i='b'
,j='y'
,k='2'
→"b,y,2"
最终结果:["a,y,2", "b,y,2"]
。
4. 关键点总结
- 嵌套顺序:列表推导式中的
for
语句从左到右对应嵌套循环的外层到内层。 - 条件过滤:
if
语句在其所在循环层的最内层执行,过滤不符合条件的元素。 - 表达式计算:仅当所有
for
循环和if
条件都满足时,才计算表达式并添加到结果列表。
理解这种执行顺序有助于正确编写复杂的列表推导式,避免逻辑错误。