python基础
1. 可变对象(list)和不可变对象(tuple)的区别
“可变”指的是能否随意修改内部元素,python中常见的可变对象有: list、dict,不可变对象有tuple、str、int
list和tuple都可以存储序列,但在使用场景上有所不同:
- list
- 需要一个经常变化的数据集合时。比如,记录一个不断有新用户加入的名单
- 需要使用各种方法来操作数据,如 .sort(), .reverse(), .pop() 等
- tuple
- 数据安全:当你希望数据不被意外修改时。例如,函数的参数、数据库查询返回的一条记录。因为它是不可变的,所以可以当作字典的键(而List不行)
- 性能:创建元组比创建列表更快,占用的内存也更小。对于大量只读数据,使用元组更高效
- 作为字典的键:因为不可变,所以可哈希
# 列表 List 的演示
my_list = [1, 2, 3, 'hello']
print("原始列表:", my_list) # 输出:[1, 2, 3, 'hello']
# 列表是可变的
my_list[0] = 100 # 修改第一个元素
my_list.append(4) # 添加一个新元素
my_list.remove('hello') # 移除一个元素
print("修改后的列表:", my_list) # 输出:[100, 2, 3, 4]
# 元组 Tuple 的演示
my_tuple = (1, 2, 3, 'hello')
print("原始元组:", my_tuple) # 输出:(1, 2, 3, 'hello')
# 元组是不可变的,尝试修改会报错
# my_tuple[0] = 100 # 这行代码如果取消注释,会抛出 TypeError: 'tuple' object does not support item assignment
# my_tuple.append(4) # 同样,没有 append 方法
# 但是,如果元组内部包含可变元素(比如列表),那么这个列表本身是可以被修改的
complex_tuple = (1, 2, [3, 4])
print("包含列表的元组:", complex_tuple) # 输出:(1, 2, [3, 4])
complex_tuple[2][0] = 'modified' # 修改元组中列表的元素
print("修改内部列表后的元组:", complex_tuple) # 输出:(1, 2, ['modified', 4])
- 不可变的实现:不可变对象在创建后,其内存地址和内容就固定了。如果你尝试“修改”它,Python实际上会创建一个新的对象。这使得它在多线程环境下是安全的,因为不会被其他线程改变
a=(1) 和 a=(1,)的区别: 这是一个经典陷阱。a = (1)只是一个普通的整数1,括号被当作数学运算符。而要创建只有一个元素的元组,必须在元素后面加一个逗号:a = (1,)
2. is 和 == 的区别
- ==: 值比较,检查两个对象的值/内容是否相同
- is: 身份相等比较 - 检查两个变量是否指向内存中的同一个对象
# 示例 1:列表比较
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1 # list3 是 list1 的引用
print("== 比较(值相等):")
print(f"list1 == list2: {list1 == list2}") # True - 值相同
print(f"list1 == list3: {list1 == list3}") # True - 值相同
print("\nis 比较(身份相等):")
print(f"list1 is list2: {list1 is list2}") # False - 不同对象
print(f"list1 is list3: {list1 is list3}") # True - 同一个对象
print(f"\n内存地址:")
print(f"id(list1): {id(list1)}")
print(f"id(list2): {id(list2)}") # 与 list1 不同
print(f"id(list3): {id(list3)}") # 与 list1 相同
小整数和字符串驻留
- 小整数驻留:在python会预先创建并缓存一个范围内的整数对象(通常是-5到256),当程序需要使用这些整数时,直接返回缓存中的对象引用,而不是每次都创建新对象。(小整数比较常见,为了避免频繁创建常见的循环计数器、索引的对象)
- 字符串驻留:Python会自动缓存一些字符串,当创建相同内容的字符串时,会直接返回已缓存字符串的引用。(比如字典的key)
- 标识符:变量名、函数名、类名等
- 长度为0或1的字符串
- 仅包含字母、数字、下划线的字符串
- 编译时确定的字符串
# 示例 2:整数比较(小整数池)
a = 100
b = 100
c = 1000
d = 1000
print("小整数(-5 到 256):")
print(f"a is b: {a is b}") # True - Python缓存了小整数
print(f"id(a) == id(b): {id(a) == id(b)}") # True
print("\n大整数:")
print(f"c is d: {c is d}") # False - 大整数不缓存
print(f"c == d: {c == d}") # True - 值相等
# 示例 3:字符串驻留
s1 = "hello"
s2 = "hello"
s3 = "hello world!"
s4 = "hello world!"
print(f"\n短字符串: s1 is s2 = {s1 is s2}") # True - 字符串驻留
print(f"长字符串: s3 is s4 = {s3 is s4}") # 可能True,但不保证
# 标识符自动驻留
name1 = "my_variable"
name2 = "my_variable"
print(f"标识符: name1 is name2 = {name1 is name2}") # True
# 包含特殊字符可能不驻留
path1 = "/usr/local/bin"
path2 = "/usr/local/bin"
print(f"包含特殊字符: path1 is path2 = {path1 is path2}") # 可能False
手动字符串驻留:在大量字符串处理的时候,可以手动驻留
import sys
from collections import Counter
def process_text(text):
# 处理大量重复字符串时,手动驻留可以节省内存
words = text.split()
interned_words = [sys.intern(word) for word in words]
return Counter(interned_words)
# 手动驻留在处理大量重复数据时特别有用
large_text = "hello world " * 1000
word_count = process_text(large_text)
None、True、False比较
这三个都是单例对象,直接用is比较的效率更高,只需要比较内存地址(一个整数比较),用==可能需要递归比较所有内容
# 正确的方式 用 is
x = None
if x is None: # ✅ 推荐
print("x is None")
if x == None: # ❌ 不推荐,虽然能工作
print("x == None")
# 因为 None 是单例,内存中只有一个 None
print(f"None is None: {None is None}") # 总是 True
is 和 id()
A is B 等同于 id(A) == id(B)
== 和 eq
使用 == 时,可能会产生意外,比如两个同值的对象,==的结果是False,因为因为 == 的行为依赖于类的 __eq__ 方法。如果没有实现 __eq__,Python会回退到使用 is 比较
class Person:
def __init__(self, name):
self.name = name
p1 = Person("Alice")
p2 = Person("Alice")
p3 = p1
print(p1 == p2) # False - p1、p2的name相同,但没有实现 __eq__,所以回退到 is 比较
print(p1 == p3) # True - 同一个对象
3. 引用、深拷贝和浅拷贝
区别
- 引用:使用某对象的内存,共享修改
- 浅拷贝(
.copy):只复制最外层对象,内层对象仍然共享引用 - 深拷贝(
.deepcopy):递归复制所有层级的对象,完全独立
代码演示
修改外层元素,浅拷贝和深拷贝都不受影响,修改内存元素浅拷贝受影响。
import copy
# 原始对象(包含嵌套列表)
original = [1, 2, [3, 4]]
shallow = copy.copy(original) # 浅拷贝
deep = copy.deepcopy(original) # 深拷贝
print("初始状态:")
print(f"original: {original}")
print(f"shallow: {shallow}")
print(f"deep: {deep}")
# 初始状态:
# original: [1, 2, [3, 4]]
# shallow: [1, 2, [3, 4]]
# deep: [1, 2, [3, 4]]
# 修改外层元素 - 所有拷贝都独立
original[0] = "修改的外层"
print("\n修改外层元素后:")
print(f"original: {original}") # ['修改的外层', 2, [3, 4]]
print(f"shallow: {shallow}") # [1, 2, [3, 4]] - 不受影响
print(f"deep: {deep}") # [1, 2, [3, 4]] - 不受影响
# 修改内层嵌套列表 - 关键区别出现!
original[2][0] = "修改的内层"
print("\n修改内层元素后:")
print(f"original: {original}") # ['修改的外层', 2, ['修改的内层', 4]]
print(f"shallow: {shallow}") # [1, 2, ['修改的内层', 4]] - 也被修改了!
print(f"deep: {deep}") # [1, 2, [3, 4]] - 完全不受影响
拷贝方式
深拷贝仅支持: .deepcopy, 浅拷贝支持多种方式
import copy
original = [1, 2, [3, 4]]
# 方法1: copy模块的copy函数
shallow1 = copy.copy(original)
# 方法2: 列表的copy方法(Python 3.3+)
shallow2 = original.copy()
# 方法3: 切片操作
shallow3 = original[:]
# 方法4: 列表构造函数
shallow4 = list(original)
# 验证它们都是浅拷贝
original[2][0] = "修改"
print(f"shallow1: {shallow1}") # [1, 2, ['修改', 4]]
print(f"shallow2: {shallow2}") # [1, 2, ['修改', 4]]
print(f"shallow3: {shallow3}") # [1, 2, ['修改', 4]]
print(f"shallow4: {shallow4}") # [1, 2, ['修改', 4]]
- 什么时候需要浅拷贝: 问自己"如果内层对象被修改,我希望拷贝的对象也受到影响吗?"如果答案是"是",用浅拷贝;如果"否",用深拷贝
- 为什么深拷贝耗时: 需要递归创建整个对象的结构
4. *args 和 **kwargs 的作用?
*args:接收任意数量的位置参数,打包成元组**kwargs:接收任意数量的关键字参数,打包成字典
再函数定义中,必需按照以下顺序:
def correct_order(required, default="value", *args, **kwargs):
pass
# ✅ 正确顺序:
# 1. 必需参数 (required)
# 2. 默认参数 (default="value")
# 3. 可变位置参数 (*args)
# 4. 可变关键字参数 (**kwargs)
# ❌ 错误顺序会报语法错误
# def wrong_order(*args, required, **kwargs): # 语法错误!
# pass
# 例子
def flexible_function(required_arg, *args, **kwargs):
print(f"必需参数: {required_arg}")
print(f"额外位置参数: {args}")
print(f"额外关键字参数: {kwargs}")
print("-" * 30)
# 测试各种调用方式
flexible_function("hello")
flexible_function("hello", 1, 2, 3)
flexible_function("hello", name="Alice", age=30)
flexible_function("hello", 1, 2, 3, name="Alice", age=30)
必需参数: hello
额外位置参数: ()
额外关键字参数: {}
------------------------------
必需参数: hello
额外位置参数: (1, 2, 3)
额外关键字参数: {}
------------------------------
必需参数: hello
额外位置参数: ()
额外关键字参数: {'name': 'Alice', 'age': 30}
------------------------------
- *args 和 **kwargs 的区别是什么:*args 用于接收任意数量的位置参数,打包成元组;**kwargs 用于接收任意数量的关键字参数,打包成字典
- 什么时候使用 *args 和 **kwargs:在编写装饰器、继承中的super()调用、包装其他函数等需要传递不确定参数的情况下必须使用
- 参数解包和打包的区别:
# 打包(定义时)
def func(*args, **kwargs): ...
# 解包(调用时)
func(*[1,2,3], **{'a':1, 'b':2})
# *
def function(a, b, c):
print(f"a={a}, b={b}, c={c}")
# 正常调用
function(1, 2, 3)
# 使用解包
numbers = [1, 2, 3]
function(*numbers) # 等价于 function(1, 2, 3)
# 解包元组
coordinates = (10, 20, 30)
function(*coordinates) # 等价于 function(10, 20, 30)
# 部分解包
first, *rest = [1, 2, 3, 4, 5]
print(f"第一个: {first}, 其余: {rest}") # 第一个: 1, 其余: [2, 3, 4, 5]
def create_person(name, age, city):
return f"{name}, {age}岁, 来自{city}"
# 正常调用
print(create_person("Alice", 25, "New York"))
# 使用字典解包
person_data = {"name": "Bob", "age": 30, "city": "London"}
print(create_person(**person_data)) # 等价于 create_person(name="Bob", age=30, city="London")
# 配置合并的例子
base_config = {"host": "localhost", "port": 8080}
custom_config = {"port": 9000, "debug": True}
final_config = {**base_config, **custom_config}
print(final_config) # {'host': 'localhost', 'port': 9000, 'debug': True}
还有一个小技巧是可以利用元组进行交换
a = 10
b = 20
# 看似一行代码,实际分为两个步骤:
# 步骤1:右侧先求值,创建元组 (b, a) → (20, 10)
# 注: 实际这里会被CPython优化为 ROT_TWO,不实际的创建对象
# 步骤2:左侧解包,将元组元素分别赋值 a, b = (20, 10)
a, b = b, a
5. 字符串格式化方法
字符串有四种格式化方法:
# 基本类型
name = "Alice"
age = 25
height = 165.5
is_student = True
result = "姓名: %s, 年龄: %d, 身高: %.1f, 是否学生: %s" % (name, age, height, is_student)
print(result)
# 姓名: Alice, 年龄: 25, 身高: 165.5, 是否学生: True
# 位置参数
print("{} {} {}".format("Hello", "World", "!")) # Hello World !
# 索引参数
print("{2} {0} {1}".format("World", "!", "Hello")) # Hello World !
# 关键字参数
print("姓名: {name}, 年龄: {age}".format(name="Alice", age=25))
# 姓名: Alice, 年龄: 25
# 访问类属性
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Charlie", 30)
print("姓名: {p.name}, 年龄: {p.age}".format(p=person))
# 姓名: Charlie, 年龄: 30
name = "Alice"
age = 25
score = 95.5
# 直接嵌入变量
print(f"姓名: {name}, 年龄: {age}, 分数: {score}")
# 姓名: Alice, 年龄: 25, 分数: 95.5
# 表达式计算
print(f"明年年龄: {age + 1}") # 明年年龄: 26
print(f"是否成年: {'是' if age >= 18 else '否'}") # 是否成年: 是
# 调用方法
text = "hello world"
print(f"大写: {text.upper()}") # 大写: HELLO WORLD
print(f"长度: {len(text)}") # 长度: 11
from string import Template
# 基本替换
template = Template("姓名: $name, 年龄: $age")
result = template.substitute(name="Alice", age=25)
print(result) # 姓名: Alice, 年龄: 25
# 安全替换(缺少变量时不报错)
safe_result = template.safe_substitute(name="Bob")
print(safe_result) # 姓名: Bob, 年龄: $age
# 字典替换
data = {"name": "Charlie", "age": 30}
result = template.substitute(data)
print(result) # 姓名: Charlie, 年龄: 30
使用场景:
- Template: 安全输入场景,Template不会处理
%、{}等特殊字符,只当成普通文本 - f-string: python > 3.6 尽量选
延迟求值
f-string和str.format是立即求值的%是延迟求值的
对于logging模块,在有日志等级需要时,比如标注了warning以上才会输出。但是info等级的f-string、str.format也会执行表达式内部的计算。
6. 一行代码实现去重
# 使用 set() 去重,但不保持原始顺序
unique_list = list(set(original_list))
# 使用 dict.fromkeys() 保持顺序
unique_list = list(dict.fromkeys(original_list))
7. PEP8规范
PEP8规范是python官方的代码风格指南,主要有:
- 4个空格作为代码缩紧
- 每行79字符
- 在一行里导入一个包 等
8. sort和sorted的区别
| 特性 | sort() | sorted() |
|---|---|---|
| 类型 | 列表方法 | 内置函数 |
| 返回值 | 原地修改 | 新的排序列表 |
| 原始数据 | 被修改 | 不变 |
| 适用对象 | 仅列表 | 任何可迭代的对象 |
| 内存使用 | 节省内存 | 额外内存 |
9. __init__ 和 __new__的区别
执行顺序: 先执行__new__创建对象,后执行__init__初始化对象
| 特性 | __new__ | __init__ |
|---|---|---|
| 作用 | 创建对象 | 初始化对象 |
| 调用时机 | 在对象创建时 | 在对象创建后 |
| 返回值 | 必须返回对象实例 | 不返回任何值(None) |
| 参数 | cls(类) | self(实例) |
| 必需性 | 可选(默认继承) | 可选 |
| 场景 | 控制对象创建过程 | 设置对象初始状态 |
应用场景
class Singleton:
"""单例模式 - 确保一个类只有一个实例"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
print("创建单例实例")
cls._instance = super().__new__(cls)
else:
print("返回已存在的单例实例")
return cls._instance
def __init__(self, name):
# 注意:每次调用都会执行__init__
if not hasattr(self, 'initialized'):
self.name = name
self.initialized = True
print(f"初始化单例: {self.name}")
else:
print(f"单例已初始化,当前名称: {self.name}")
print("=== 单例模式演示 ===")
s1 = Singleton("第一个")
s2 = Singleton("第二个")
s3 = Singleton("第三个")
print(f"s1 is s2: {s1 is s2}")
print(f"s1 is s3: {s1 is s3}")
print(f"所有实例的名称: s1.name='{s1.name}', s2.name='{s2.name}'")
class ConnectionPool:
"""连接池 - 重用对象以减少创建开销"""
_pool = []
_max_size = 2
def __new__(cls, connection_id):
# 如果池中有可用连接,则重用
if cls._pool:
print(f"从池中获取连接")
instance = cls._pool.pop()
instance.connection_id = connection_id # 更新连接ID
return instance
else:
print(f"创建新连接")
instance = super().__new__(cls)
return instance
def __init__(self, connection_id):
self.connection_id = connection_id
print(f"初始化连接: {self.connection_id}")
def close(self):
"""关闭连接,返回池中"""
if len(self._pool) < self._max_size:
print(f"连接 {self.connection_id} 返回池中")
self._pool.append(self)
else:
print(f"连接池已满,销毁连接 {self.connection_id}")
print("=== 对象池模式演示 ===")
# 创建连接
conn1 = ConnectionPool("CONN_001")
conn2 = ConnectionPool("CONN_002")
conn3 = ConnectionPool("CONN_003") # 超过最大池大小
# 关闭连接,使其返回池中
conn1.close()
conn2.close()
# 再次创建连接,应该从池中获取
conn4 = ConnectionPool("CONN_004")
conn5 = ConnectionPool("CONN_005")
class Shape:
"""形状基类"""
def draw(self):
raise NotImplementedError
class Circle(Shape):
def draw(self):
return "绘制圆形"
class Square(Shape):
def draw(self):
return "绘制方形"
class ShapeFactory:
"""形状工厂 - 通过__new__实现工厂模式"""
def __new__(cls, shape_type, *args, **kwargs):
if shape_type == "circle":
return Circle(*args, **kwargs)
elif shape_type == "square":
return Square(*args, **kwargs)
else:
raise ValueError(f"未知形状类型: {shape_type}")
print("=== 工厂模式演示 ===")
circle = ShapeFactory("circle")
square = ShapeFactory("square")
print(circle.draw()) # 绘制圆形
print(square.draw()) # 绘制方形
print(f"circle类型: {type(circle)}") # <class '__main__.Circle'>
print(f"square类型: {type(square)}") # <class '__main__.Square'>
__new__是一个静态方法,但Python会自动传递类作为第一个参数,所以它看起来像类方法。不需要使用@staticmethod装饰器。- 需要控制对象创建过程、继承不可变类型、实现设计模式时用
__new__,其余用__init__
10. 反射
反射(Reflection)指的是程序在运行时(Runtime)检查、访问和修改其自身状态(如属性、方法、类信息)的能力。它是一种元编程(metaprogramming)的形式。
Python提供了四个用于反射的核心内置函数,它们都以字符串形式接收属性/方法名:
hasattr(object, 'name'):检查对象object是否拥有名为'name'的属性或方法。getattr(object, 'name'[, default]):获取对象object中名为'name'的属性或方法。如果找不到,则返回提供的default值;若未提供default,则抛出AttributeError。setattr(object, 'name', value):将对象object中名为'name'的属性设置为value。如果属性不存在,则会创建它。delattr(object, 'name'):删除对象object中名为'name'的属性
除了上述核心函数外,还有一些函数,比如dir(object)用于返回对象所有属性和方法名,isinstance(obj, cls)检查对象和类的继承关系
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def get_user_info(self):
return f"Name: {self.name}, Age: {self.age}"
def greet(self, message):
return f"{self.name} says: {message}"
# 创建一个对象
user = User("Alice", 30)
# --- 使用反射的核心函数 ---
# 1. 检查属性/方法是否存在
print("hasattr for 'name':", hasattr(user, 'name')) # True
print("hasattr for 'get_user_info':", hasattr(user, 'get_user_info')) # True
print("hasattr for 'non_existent':", hasattr(user, 'non_existent')) # False
# 2. 获取属性/方法
name_value = getattr(user, 'name')
print("getattr for 'name':", name_value) # Alice
# 获取方法,注意这里返回的是“绑定方法”本身,不是调用结果
method = getattr(user, 'get_user_info')
print("Method object:", method)
# 需要调用它
print("Calling the method:", method()) # Name: Alice, Age: 30
# 使用default参数避免异常
salary = getattr(user, 'salary', 0) # 'salary'属性不存在,返回默认值0
print("Salary with default:", salary)
# 3. 设置属性
setattr(user, 'location', 'Beijing') # 动态添加一个新属性
print("Dynamically added location:", user.location)
setattr(user, 'age', 31) # 修改已存在的属性
print("Updated age from reflection:", user.age)
# 4. 动态方法调用 (一个非常强大的应用场景)
method_to_call = 'greet'
if hasattr(user, method_to_call) and callable(getattr(user, method_to_call)):
# 获取方法并调用,同时传递参数
result = getattr(user, method_to_call)("Hello, Reflection!")
print(result) # Alice says: Hello, Reflection!
# --- 实际应用场景:根据配置调用不同方法 ---
class DataExporter:
def export_to_json(self):
return "Exporting data to JSON..."
def export_to_csv(self):
return "Exporting data to CSV..."
def export_to_xml(self):
return "Exporting data to XML..."
# 假设这个格式来自配置文件或用户输入
export_format = "json" # 可以是 'json', 'csv', 'xml'
exporter = DataExporter()
method_name = f"export_to_{export_format}"
# 动态决定调用哪个导出方法
if hasattr(exporter, method_name):
export_method = getattr(exporter, method_name)
print(export_method()) # 输出: Exporting data to JSON...
else:
print(f"Unsupported export format: {export_format}")
11. assert
assert 是Python中的一个关键字,用于断言。它是一个调试辅助工具,用于在代码中设置一个检查点(条件),语法是assert condition, [message] 它的逻辑非常贱点,如果condition为True程序就会继续执行,如果False就会抛出一个 AssertionError 异常。如果提供了 message,这个异常会包含该信息;如果没有,则是一个普通的 AssertionError。
用途:
- 防御性编程:在函数开头检查参数是否满足前提条件。
- 文档代码:作为一个活的注释,清晰地说明在代码的某个点上,哪些条件必须成立。
- 单元测试:在测试用例中验证代码的行为是否符合预期。
- 检查不可能发生的情况:用于捕捉程序中理论上不应该出现的逻辑错误。
assert和Exception的区别
assert:主要用于调试和开发阶段。它传达的意思是“这是一个程序内部自检,这个条件在正确的情况下必须为真”。它的一个关键特性是可以用 -O 标志全局禁用。if...raise ValueError/TypeError/...:用于正常的、可预见的运行时错误检查,尤其是与外部输入或系统状态相关的错误(如用户输入了错误格式、文件不存在、网络断开)。这些检查永远不应该被禁用。
python -O生成优化的字节码文件,移除了assert
12. 循环导入破解方案
循环导入的根本原因:
python的模块导入机制是执行式的。当执行 import ... 时,解释器会:
- 在 sys.modules 中查找该模块是否已被加载。
- 如果未加载,则创建一个新的模块对象,并执行该模块文件中的所有顶层代码。
- 如果在执行过程中遇到了另一个 import 语句,它会暂停当前模块的执行,转去加载被导入的模块。
破解方案:
- 重构代码:找出公共依赖拆解为公共模块
- 局部导入:在函数内部导入
- DI:
injector库
13. Exception体系
Python的异常层次如下:
BaseException
├── KeyboardInterrupt # Ctrl+C 中断
├── SystemExit # sys.exit() 退出
├── GeneratorExit # 生成器关闭
└── Exception # 除了在最外层和对接外部系统的时候,一般都不用Exception
├── ArithmeticError
├── AttributeError
├── EOFError
├── ImportError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── NameError
├── RuntimeError
├── SyntaxError
├── TypeError
├── ValueError
└── ... 其他很多异常
处理异常的原则:
- 只补获预期可能知道如何处理的异常
- 对于未预期的异常,让它们向外层传播
- 在最外层崩溃或统一处理未知异常
14. @property描述符
描述符(Descriptor)是Python中一个强大的特性,它允许对象自定义属性访问的行为。描述符协议由三个特殊方法组成:
__get__(self, obj, type=None) -> value:获取属性值时调用__set__(self, obj, value) -> None:设置属性值时调用__delete__(self, obj) -> None:删除属性时调用
实现了其中至少一个方法的类就是描述符。描述符分为:
- 数据描述符:实现了
__set__或__delete__方法 - 非数据描述符:只实现了
__get__方法
属性查找顺序
当访问obj.attribute时,Python按照以下顺序查找:
- 数据描述符(在类或父类中)
- 实例属性(在obj.__dict__中)
- 非数据描述符(在类或父类中)
- 类属性
- 父类属性
- 调用__getattr__(如果存在)
15. map、filter、reduce
- map(function, iterable):对可迭代对象中的每个元素应用函数,返回一个map对象(迭代器)
- filter(function, iterable):过滤可迭代对象,只保留使函数返回True的元素,返回filter对象
- reduce(function, iterable[, initial]):对可迭代对象进行累积计算,从左到右依次将函数应用于元素(需要从functools导入)
map、filter vs 列表推导式
- 为什么列表推导式通常比map/filter更快?
- 解释器优化:列表推导式在Python解释器中有专门的优化,执行路径更短。
- 函数调用开销:map和filter需要为每个元素调用一次函数(特别是lambda函数),而函数调用在Python中是有开销的。列表推导式的操作在解释器内部直接执行,避免了这部分开销。
- 什么情况下map/filter会比列表推导式更有优势?
- 使用内置函数:当使用C实现的内置函数时(如str.upper),map可能比列表推导式稍快。
- 内存敏感场景:map和filter返回迭代器,在处理超大数据集时可以节省内存。
- 代码复用:当已经存在合适的函数时,使用map/filter可以避免重复代码。
16. __slots__是什么
__slots__ 是Python类的一个特殊属性,它允许我们显式地声明类实例可以拥有哪些属性,从而替代默认的 __dict__ 字典存储机制。使用 __slots__ 可以显著减少内存占用并提高属性访问速度。
python的默认实例属性存储机制
默认情况下,Python类的每个实例都有一个 __dict__ 字典来存储所有实例属性。这种方式非常灵活,允许动态添加任意新属性,但有以下代价:
- 内存开销:每个字典都有额外的内存开销
- 访问速度:字典查找比直接属性访问慢
__slots__工作原理
当类定义了 slots 时:
- Python会为每个实例创建一个更紧凑的固定大小的数组来存储属性
- 实例不再拥有
__dict__属性(除非显式包含在__slots__中) - 不能动态添加
__slots__中未声明的属性
class RegularPerson:
"""普通类 - 使用 __dict__ 存储属性"""
def __init__(self, name, age):
self.name = name
self.age = age
class SlotsPerson:
"""使用 __slots__ 的类"""
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age