跳至主要內容

python基础

pptg大约 20 分钟

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()
类型列表方法内置函数
返回值原地修改新的排序列表
原始数据被修改不变
适用对象仅列表任何可迭代的对象
内存使用节省内存额外内存

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}'")
  • __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] 它的逻辑非常贱点,如果conditionTrue程序就会继续执行,如果False就会抛出一个 AssertionError 异常。如果提供了 message,这个异常会包含该信息;如果没有,则是一个普通的 AssertionError

用途:

  • 防御性编程:在函数开头检查参数是否满足前提条件。
  • 文档代码:作为一个活的注释,清晰地说明在代码的某个点上,哪些条件必须成立。
  • 单元测试:在测试用例中验证代码的行为是否符合预期。
  • 检查不可能发生的情况:用于捕捉程序中理论上不应该出现的逻辑错误。

assertException的区别

  • 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