跳至主要內容

python基础

pptg大约 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]

还有一个小技巧是可以利用元组进行交换

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

使用场景:

  • Template: 安全输入场景,Template不会处理%、{}等特殊字符,只当成普通文本
  • f-string: python > 3.6 尽量选

延迟求值

  • f-stringstr.format是立即求值的
  • %是延迟求值的

对于logging模块,在有日志等级需要时,比如标注了warning以上才会输出。但是info等级的f-string、str.format也会执行表达式内部的计算。

6. 一行代码实现去重

set(快、不保持顺序)
# 使用 set() 去重,但不保持原始顺序
unique_list = list(set(original_list))

7. PEP8规范

PEP8规范是python官方的代码风格指南,主要有:

  • 4个空格作为代码缩紧
  • 每行79字符
  • 在一行里导入一个包 等

8. sort和sorted的区别

特性sort()sorted()
类型列表方法内置函数
返回值原地修改新的排序列表
原始数据被修改不变
适用对象仅列表任何可迭代的对象
内存使用节省内存额外内存