python基础
大约 11 分钟
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
str.format()
# 位置参数
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
f-string
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
Template字符串
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(快、不保持顺序)
# 使用 set() 去重,但不保持原始顺序
unique_list = list(set(original_list))
dict(推荐)
# 使用 dict.fromkeys() 保持顺序
unique_list = list(dict.fromkeys(original_list))
7. PEP8规范
PEP8规范是python官方的代码风格指南,主要有:
- 4个空格作为代码缩紧
- 每行79字符
- 在一行里导入一个包 等
8. sort和sorted的区别
| 特性 | sort() | sorted() |
|---|---|---|
| 类型 | 列表方法 | 内置函数 |
| 返回值 | 原地修改 | 新的排序列表 |
| 原始数据 | 被修改 | 不变 |
| 适用对象 | 仅列表 | 任何可迭代的对象 |
| 内存使用 | 节省内存 | 额外内存 |