学习日志29 python
1 __slots__
在 Python 中,__slots__
是一个特殊的类属性,用于显式指定类的实例可以拥有的属性名称,从而限制实例动态添加其他属性,并带来一定的性能优化。
主要作用和特点:
限制属性添加
当类定义了__slots__
后,实例只能拥有__slots__
中列出的属性,无法动态添加其他属性。例如:class Person:__slots__ = ('name', 'age') # 只允许这两个属性def __init__(self, name, age):self.name = nameself.age = agep = Person("Alice", 30) p.gender = "female" # 报错:AttributeError,因为 gender 不在 __slots__ 中
优化内存占用
Python 中,默认情况下类的实例会用一个字典(__dict__
)存储属性,字典会占用额外内存。而定义__slots__
后,实例会使用固定大小的数组存储属性,不再创建__dict__
,从而节省内存,尤其在创建大量实例时效果明显。提升属性访问速度
由于属性存储在数组中而非字典,访问__slots__
中定义的属性时,速度会比普通属性更快。
注意事项:
__slots__
中未列出的属性,实例无法添加,但类本身仍可添加属性。- 如果子类未定义
__slots__
,则子类实例可以拥有__dict__
,并能动态添加属性(不受父类__slots__
限制)。 - 若子类也定义了
__slots__
,则其允许的属性是自身__slots__
与父类__slots__
的合集。 __slots__
中可以包含特殊属性,如__dict__
(允许动态添加属性)或__weakref__
(允许实例被弱引用),例如:python
运行
class Person:__slots__ = ('name', 'age', '__dict__') # 允许动态添加其他属性
适用场景:
- 需要创建大量实例(如数据处理中的对象),希望减少内存占用时。
- 希望限制类的属性,避免误添加无关属性时。
- 对属性访问速度有较高要求的场景。
总之,__slots__
是一个用于优化性能和约束类属性的实用工具,但并非所有类都需要使用,通常在对内存和性能有明确要求时才考虑使用。
当子类定义了__slots__时,子类实例允许的属性是父类的__slots__加上子类的__slots__的并集。
当子类定义了 __slots__
时,子类实例允许拥有的属性是父类 __slots__
与子类 __slots__
的并集。
这意味着:
- 子类实例可以使用自身
__slots__
中定义的属性 - 也可以使用父类
__slots__
中定义的属性 - 但不能使用两者都未定义的属性
示例如下:
class Parent:__slots__ = ('a', 'b') # 父类允许的属性class Child(Parent):__slots__ = ('c', 'd') # 子类允许的属性# 子类实例可以使用父类和子类的 slots 属性
child = Child()
child.a = 1 # 父类 slots 中的属性,允许
child.c = 3 # 子类 slots 中的属性,允许# 但不能使用两者都未定义的属性
child.e = 5 # 报错:AttributeError: 'Child' object has no attribute 'e'
这种设计既保留了父类的属性约束,又允许子类扩展自己需要的属性,同时继续维持 __slots__
带来的内存和性能优势。
需要注意的是:如果子类未定义 __slots__
,则子类实例会自动拥有 __dict__
,可以动态添加任何属性(不受父类 __slots__
限制)。
2 Python 中没有真正的「私有变量」
这种说法并不完全准确。在 Python 中,以双下划线 __
开头的变量确实会触发「名称修饰」(name mangling)机制,但这并非严格意义上的「私有变量」,也不是必须通过定义同名变量才能在子类中访问。
具体说明:
名称修饰机制
当类中定义__xxx
形式的变量时,Python 会在内部将其重命名为_类名__xxx
,以避免子类意外覆盖父类的变量。例如:class Parent:def __init__(self):self.__value = 10 # 实际会被重命名为 _Parent__valuep = Parent() print(p._Parent__value) # 输出:10(可以通过这种方式访问) print(p.__value) # 报错:AttributeError(直接访问会失败)
并非真正的私有
这种机制更多是一种「命名约定」,用于提示开发者该变量应被视为内部实现,而非强制禁止访问。通过_类名__变量名
的形式,依然可以在类外部或子类中访问。继承中的访问方式
子类不需要定义同名的双下划线变量,而是可以通过父类的名称修饰后的变量名访问:class Child(Parent):def get_parent_value(self):return self._Parent__value # 直接访问父类被修饰的变量c = Child() print(c.get_parent_value()) # 输出:10(无需定义同名__value)
与单下划线
_
的区别- 单下划线
_xxx
是一种约定,表示「建议私有」,但不会触发名称修饰,子类可以直接访问。 - 双下划线
__xxx
会触发名称修饰,目的是避免子类中的同名变量与父类冲突。
- 单下划线
总结:
- Python 中没有真正的「私有变量」,双下划线变量的限制是通过名称修饰实现的,而非语法级别的禁止访问。
- 子类继承时,无需定义同名双下划线变量,只需通过
_父类名__变量名
的形式即可访问父类的双下划线变量。 - 实际开发中,更推荐使用单下划线
_xxx
作为「私有变量」的约定,除非确实需要避免子类变量冲突,才使用双下划线。
也就是说:
关于 Python 中以下划线开头的变量(尤其是双下划线变量)的正确理解可以总结为:
Python 中没有严格意义上的「私有变量」
与其他语言(如 Java、C++)的private
关键字不同,Python 中不存在真正禁止外部访问的私有变量,一切都是「约定优于配置」的设计思路。单下划线
_xxx
:约定性私有- 仅作为一种编程约定,表明该变量 / 方法是类的内部实现,建议外部不要直接访问 / 修改。
- 不会触发任何语法级别的限制,外部仍可直接访问(如
obj._xxx
)。 - 子类可以直接继承并访问父类的
_xxx
变量。
双下划线
__xxx
:名称修饰(name mangling)- 会触发 Python 的名称修饰机制,变量会被自动重命名为
_类名__xxx
(如__value
在Parent
类中会变为_Parent__value
)。 - 目的不是「禁止访问」,而是避免子类中定义同名变量时发生意外冲突。
- 外部或子类仍可通过修饰后的名称访问(如
obj._Parent__value
),但不建议这么做(破坏封装性)。
- 会触发 Python 的名称修饰机制,变量会被自动重命名为
继承中的规则
- 子类不会自动继承父类的双下划线变量的「原始名称」(因为已被修饰)。
- 子类若要访问父类的
__xxx
变量,需使用修饰后的名称(如self._Parent__xxx
),而非重新定义同名的__xxx
。 - 若子类定义了自己的
__xxx
,会被修饰为_Child__xxx
,与父类的_Parent__xxx
完全隔离,互不影响。
核心结论:
Python 中以下划线开头的变量是「封装意图的表达」,而非强制的访问限制:
- 单下划线是「温和建议」:请把它当私有变量对待。
- 双下划线是「强提醒」:除了类自身,其他地方访问需谨慎(可能破坏设计)。
这种设计体现了 Python 的「信任开发者」理念 —— 不强行限制,而是通过约定让代码更易读、更易维护。
3 代码的执行顺序分析
执行以下程序,输出结果为()
def outer():def inner():print('inner',end=" ")print('outer',end = " ")return inner
outer()A inner outer
B inner
C outer
D outer inner正确答案:C
这段代码的执行逻辑其实很清晰,我们一步步拆解来看:
- 首先定义了一个
outer()
函数,它内部又定义了inner()
函数 - 在
outer()
函数中,先执行print('outer', end=" ")
,这会输出 "outer" - 然后
outer()
函数返回inner
函数本身(注意:这里返回的是函数对象,并没有调用它) - 最后执行
outer()
,但没有对返回的inner
函数做任何操作
关键点在于:outer()
函数返回了inner
函数,但我们没有调用这个返回的inner
函数。
所以整个程序只会执行:
outer()
函数内部的print('outer', end=" ")
不会执行inner()
函数里的打印语句,因为inner()
从未被调用过(调用函数需要加括号,如inner()
)。
如果想让inner()
也执行,需要这样写:
python
运行
def outer():def inner():print('inner', end=" ")print('outer', end=" ")return inner# 先调用outer()得到inner函数,再调用它
func = outer()
func() # 这才会执行inner()
这时才会输出 "outer inner"
因此原程序的正确输出是 "outer",对应选项 C。
关键知识点拆解:
函数的定义与调用
- 定义函数:
def 函数名(): ...
只是声明一个函数,此时函数内部的代码不会执行。 - 调用函数:必须通过
函数名()
(加括号)才能执行函数内部的代码。
例如:
python
运行
def hello():print("你好") # 仅定义,不执行hello() # 加括号调用,才会执行打印
- 定义函数:
嵌套函数
- 在一个函数内部定义另一个函数(如
inner()
定义在outer()
内部),称为嵌套函数。 - 内部函数(
inner
)的作用域仅限于外部函数(outer
)内部,外部无法直接调用inner()
。
- 在一个函数内部定义另一个函数(如
函数可以作为返回值
- Python 中,函数是「一等公民」,可以像普通变量一样被返回。
- 代码中
return inner
表示返回inner
这个函数对象本身(注意没有加括号,不是调用),而不是返回inner()
的执行结果。
原代码的执行顺序分析:
python
运行
def outer():def inner(): # 步骤1:定义inner函数(不执行内部代码)print('inner', end=" ")print('outer', end=" ") # 步骤3:执行outer内部的打印return inner # 步骤4:返回inner函数对象(不执行inner)outer() # 步骤2:调用outer函数(触发outer内部代码执行)
执行步骤:
- 先定义
outer
函数(不执行内部代码)。 - 执行
outer()
(调用outer
),此时进入outer
内部:- 首先定义
inner
函数(仅定义,不执行print('inner')
)。 - 执行
print('outer', end=" ")
,输出outer
。 - 返回
inner
函数对象(但没有对这个返回值做任何操作,比如调用)。
- 首先定义
- 整个程序结束,
inner
函数从未被调用,因此print('inner')
从未执行。
如何判断执行顺序?
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!记住一个核心原则:只有加了括号的函数(如 func()
)才会被执行,单纯的函数名(如 func
)只是一个对象,不会触发执行。
在这个例子中:
outer()
加了括号 → 执行outer
内部代码(包括打印outer
)。inner
没加括号(只是return inner
)→ 不执行inner
内部代码。
因此最终只输出 outer
。
4 混淆了列表的「加法运算」和「元素级乘法运算」的区别
在Python3中,下列程序结果为( )
res = lambda x: x+x
print(res([1, 2, 3, 4]))A [1, 2, 3, 4]
B [2, 4, 6, 8]
C [1, 2, 3, 4, 1, 2, 3, 4]
D [1, 2, 3, 4, 2, 4, 6, 8]正确答案:C
你的答案:B
你之所以会做错,是因为混淆了列表的「加法运算」和「元素级乘法运算」的区别。
在 Python 中,lambda x: x + x
对于列表来说,x + x
表示的是列表拼接(将两个相同的列表连接起来),而不是对列表中的每个元素进行乘法运算。
所以当 x = [1, 2, 3, 4]
时,x + x
的结果是 [1, 2, 3, 4, 1, 2, 3, 4]
,对应选项 C。
如果要实现选项 B(即对列表中的每个元素乘以 2,得到 [2, 4, 6, 8]
),可以使用列表推导式或 map()
函数,代码如下:
# 方法1:使用列表推导式
res = lambda x: [i * 2 for i in x]
print(res([1, 2, 3, 4])) # 输出 [2, 4, 6, 8]# 方法2:使用map()函数
res = lambda x: list(map(lambda i: i * 2, x))
print(res([1, 2, 3, 4])) # 输出 [2, 4, 6, 8]
这两种方法的核心都是遍历列表中的每个元素,并对单个元素执行 *2
操作,而不是对整个列表进行拼接。
5 iter()函数是什么?
在 Python 中,iter()
是一个内置函数,用于创建一个迭代器对象(iterator)。迭代器是一种可以逐个访问集合元素的对象,它遵循迭代协议(即包含 __next__()
方法,用于返回下一个元素,当没有元素时会抛出 StopIteration
异常)。
iter()
的基本用法
iter()
函数有两种常用调用方式:
iter(可迭代对象)
接收一个「可迭代对象」(如列表、字符串、字典、集合等),返回对应的迭代器。
示例:python
运行
# 对列表创建迭代器 my_list = [1, 2, 3] it = iter(my_list) # 创建迭代器print(next(it)) # 输出 1(调用迭代器的 __next__() 方法) print(next(it)) # 输出 2 print(next(it)) # 输出 3 print(next(it)) # 抛出 StopIteration 异常(没有更多元素)
iter(函数, 哨兵值)
接收一个函数和一个「哨兵值」(sentinel),返回一个迭代器。每次调用迭代器的__next__()
时,会执行该函数,直到函数返回值等于哨兵值时停止。
示例(读取文件直到遇到空行):python
运行
with open("file.txt", "r") as f:# 每次调用 f.readline(),直到返回空字符串 "" 时停止for line in iter(f.readline, ""):print(line.strip())
核心作用
- 将可迭代对象转换为迭代器,支持通过
next()
函数逐个获取元素,实现「惰性访问」(需要时才生成下一个元素,节省内存)。 - 迭代器只能遍历一次,遍历结束后需重新创建迭代器才能再次遍历。
注意点
- 迭代器和可迭代对象的区别:
可迭代对象(如列表)是「可以被迭代的对象」,但本身不是迭代器;迭代器是「正在迭代的状态对象」,保存了当前遍历的位置。 - 所有迭代器都是可迭代对象,但并非所有可迭代对象都是迭代器(如列表是可迭代对象,但不是迭代器)。
通过 iter()
函数,Python 可以统一处理各种可迭代对象的遍历逻辑,是实现 for
循环的底层基础(for
循环本质上就是通过迭代器逐个获取元素)。
6 Python 中字典的迭代默认行为
字典在迭代时,默认遍历的是它的「键(key)」,而不是值(value)
下面代码执行后的结果是( )
#!/usr/bin/python3
dict={'a': '1', 'b': 2, 'c': '3'}
it = iter(dict)
for x in it:print (x, end=" ")
A a b c
B 1 2 3
C a': '1', 'b': 2, 'c': '3'
D a': '1' 'b': 2 'c': '3'正确答案:A
你的答案:B#我不知道这种字典键值对是迭代的什么,键还是值
具体来看这段代码:
python
运行
dict={'a': '1', 'b': 2, 'c': '3'}
it = iter(dict) # 创建字典的迭代器,迭代对象是键
for x in it:print(x, end=" ") # 逐个打印键
- 当我们用
iter(dict)
创建字典的迭代器时,迭代器会指向字典的键('a'
、'b'
、'c'
)。 - 因此
for
循环遍历的是这些键,最终输出a b c
,对应选项 A。
如果想迭代字典的值(得到选项 B 的结果),需要显式使用字典的 .values()
方法,代码如下:
python
运行
dict={'a': '1', 'b': 2, 'c': '3'}
it = iter(dict.values()) # 迭代值
for x in it:print(x, end=" ") # 输出:1 2 3
此外,若想同时迭代键和值,可以用 .items()
方法:
python
运行
for k, v in dict.items():print(k, v, end="; ") # 输出:a 1; b 2; c 3;
总结:字典默认迭代键,迭代值用 .values()
,迭代键值对用 .items()
。