学习日志45 python

发布于:2025-09-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

1 zip() 会以最短的可迭代对象的长度为准进行配对

zip() 会以最短的可迭代对象的长度为准进行配对,【超出部分会被忽略】。

zip() 函数在 Python 中确实会以传入的可迭代对象中最短的那个长度为准进行配对,所有超出这个长度的元素都会被自动忽略。

下面是一个简单的示例来演示这个特性:

python

运行

list1 = [1, 2, 3, 4, 5]
list2 = ['a', 'b', 'c']
list3 = [True, False]

# 使用zip()进行配对
result = zip(list1, list2, list3)

# 转换为列表查看结果
print(list(result))  # 输出: [(1, 'a', True), (2, 'b', False)]

在这个例子中:

  • list1 有 5 个元素
  • list2 有 3 个元素
  • list3 有 2 个元素

zip() 最终只生成了 2 组配对结果,这是因为 list3 是最短的可迭代对象,只有 2 个元素。list1 中剩下的 3 个元素(3,4,5)和 list2 中剩下的 1 个元素('c')都被忽略了。

如果需要处理不等长的可迭代对象并保留所有元素,可以考虑使用 itertools.zip_longest(),它会以最长的可迭代对象为准,并使用指定的值填充缺失的元素。

2 关于 Python 函数可变默认参数行为

关于 Python 函数默认参数为可变对象(例如 list 或 dict)的行为,哪项描述是正确的?
A 可变默认参数在每次函数调用时都会重新创建
B 可变默认参数只在函数定义时求值并在后续调用中复用同一对象
C 是否复用取决于调用是否在同一线程中
D 只要在函数体内对参数重新赋值,就不会影响默认对象

正确答案:B

首先,我们需要理解 Python 中函数默认参数的特殊处理机制,尤其是针对可变对象(list、dict 等)的情况。

核心原理:

在 Python 中,函数的默认参数只在函数定义时被创建一次,而不是在每次调用时重新创建。对于可变对象(如列表、字典)来说,这意味着所有函数调用都会共享同一个默认对象。

对各选项的分析:

选项 A:可变默认参数在每次函数调用时都会重新创建
这是错误的。如果每次调用都重新创建,那么下面的代码每次调用都会返回只包含当前元素的列表,但实际结果并非如此:

python

运行

def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2](而不是预期的 [2])

选项 B:可变默认参数只在函数定义时求值并在后续调用中复用同一对象
这是正确的。如上面的例子所示,默认列表 [] 只在函数定义时创建一次,之后的每次调用都会复用这个列表对象,所以第二次调用会保留第一次添加的元素。

我们可以通过查看函数的 __defaults__ 属性验证这一点:

python

运行

def func(a=[]):
    a.append(1)
    return a

print(func.__defaults__)  # 初始状态:([],)
func()
print(func.__defaults__)  # 调用后:([1],) - 证明默认参数被修改了

选项 C:是否复用取决于调用是否在同一线程中
这是错误的。默认参数的复用机制是 Python 解释器的固有特性,与线程无关。无论在单线程还是多线程环境中,只要是同一个函数对象,都会共享同一个默认的可变对象。

选项 D:只要在函数体内对参数重新赋值,就不会影响默认对象
这个描述不准确。如果在函数体内是修改默认对象(如 append() 操作),会影响默认对象;但如果是重新赋值(如 my_list = [new_value]),则不会影响原来的默认对象。因此 "只要重新赋值就不会影响" 的说法本身没问题,但这不是对默认参数行为的核心描述,且题目问的是默认参数本身的行为特性,所以 B 是更准确的答案。

总结:

Python 中可变默认参数的关键特性是在函数定义时创建一次,之后所有调用复用同一对象这与线程无关,也不是每次调用重新创建。因此正确答案是 B

位置参数

函数定义中a和b没有默认值,也未使用/或*指定参数类型,默认是位置参数。

在 Python 函数定义中,如果参数既没有默认值,也没有使用 /(位置参数分隔符)或 *(关键字参数分隔符)来指定参数类型,那么这些参数默认都是位置参数,调用时必须按照定义的顺序传递,也可以通过关键字形式传递。

举例说明:

python

运行

def func(a, b):  # a和b都是位置参数
    return a + b

# 以下调用方式都合法
print(func(1, 2))          # 纯位置参数传递
print(func(a=1, b=2))      # 纯关键字参数传递
print(func(1, b=2))        # 混合方式传递(位置参数在前,关键字参数在后)

需要注意的是,在混合传递时,关键字参数必须放在位置参数之后,否则会报错:

python

运行

func(a=1, 2)  # 报错:SyntaxError: positional argument follows keyword argument

这种默认的位置参数特性,让函数调用既可以保持简洁(直接传值),又可以通过关键字传递提高代码可读性(明确参数含义)。

4 解决列表共享状态问题

函数定义为 def append_item(x, cache=[]): cache.append(x); return cache。多次调用 append_item(1)、append_item(2) 后发现返回值被意外累计。最合适的修复是什么?
A 将默认参数改为 None,并在函数体内 if cache is None: cache = []
B 对默认列表做 copy.deepcopy 以避免共享
C 将默认参数改为元组(),在函数内用 list(cache) 转换
D 在函数末尾调用 cache.clear() 避免残留

正确答案:A

题目中函数的问题在于使用了可变对象(列表[])作为默认参数,导致多次调用时共享同一个列表对象,从而出现结果意外累计的情况。

对各选项的分析:

选项 A:将默认参数改为 None,并在函数体内 if cache is None: cache = []
这是 Python 中处理此类问题的标准且最佳实践。修改后的代码如下:

python

运行

def append_item(x, cache=None):
    if cache is None:
        cache = []
    cache.append(x)
    return cache

这样每次调用时如果不传入cache参数,都会在函数内部创建一个新的空列表,彻底避免了共享状态的问题。

选项 B:对默认列表做 copy.deepcopy 以避免共享
这种方法虽然可能有效,但完全没有必要。因为默认参数只需要在每次调用时创建一个新的空列表即可,深拷贝操作对于空列表来说是多余的性能浪费,而且代码也不够简洁直观。

选项 C:将默认参数改为元组 (),在函数内用 list (cache) 转换
元组是不可变对象,确实不会有共享状态问题。但这种方式不够直接,而且如果用户希望传入自己的列表进行累积时,会创建新列表导致原有数据丢失,不符合函数的设计意图。

选项 D:在函数末尾调用 cache.clear () 避免残留
这是错误的,因为clear()会清空列表,导致函数返回的是一个空列表,完全违背了 "添加元素并返回结果" 的功能需求。

总结:

选项 A 是最合理的解决方案,它既解决了共享状态问题,又保持了函数的灵活性(允许用户传入自己的列表进行累积),同时代码简洁高效,是 Python 社区公认的标准处理方式。

5 元类(Metaclass)

【Python 中元类(Metaclass)是什么,和普通的类有区别吗?】

在 Python 中,元类(Metaclass)是一个特殊的概念,它是创建类的 "类",负责控制类的创建过程。可以理解为:

  • 普通类用于创建实例(对象)
  • 元类用于创建类(类本身也是一种对象)

元类与普通类的核心区别:

  1. 本质角色不同

    • 普通类是实例的模板:定义了实例的属性和方法,通过 类名() 创建实例。
      例如:class A: passa = A() 中,A 是普通类,a 是 A 的实例。
    • 元类是类的模板:定义了类的属性和方法,通过元类创建类。
      例如:type 是 Python 的默认元类,所有类(包括自定义类)本质上都是 type 的实例。
  2. 创建对象的层级不同

    • 普通类操作的是实例:通过 __new__ 创建实例,__init__ 初始化实例。
    • 元类操作的是:通过元类的 __new__ 创建类,__init__ 初始化类。
      当你定义一个类时,Python 实际上会调用元类的 __new__ 和 __init__ 来生成这个类。
  3. 使用场景不同

    • 普通类用于封装数据和行为,解决具体业务问题(如定义 Person 类表示人)。
    • 元类用于控制类的创建逻辑,例如:
      • 强制所有类必须包含特定属性或方法(如接口检查)
      • 自动为类添加方法或修改类的结构
      • 实现设计模式(如单例模式的全局控制)

直观示例:用元类创建类

Python 中默认元类是 type,我们可以自定义元类(继承 type)来控制类的创建:

python

运行

# 自定义元类(继承自 type)
class MyMeta(type):
    # 当创建类时,会调用元类的 __new__
    def __new__(cls, name, bases, attrs):
        # name:类名;bases:父类;attrs:类的属性和方法字典
        print(f"用元类创建类:{name}")
        # 可以在这里修改类的属性(例如强制添加一个属性)
        attrs["meta_attr"] = "由元类添加的属性"
        return super().__new__(cls, name, bases, attrs)

# 使用自定义元类创建普通类
class MyClass(metaclass=MyMeta):  # 指定元类为 MyMeta
    pass

# 测试结果
print(MyClass.meta_attr)  # 输出:由元类添加的属性(元类动态添加的)

在这个例子中:

  • MyMeta 是元类(创建类的类)
  • MyClass 是由 MyMeta 创建的普通类
  • MyClass 的 meta_attr 属性是元类在创建 MyClass 时动态添加的

总结

元类是 Python 中更深层次的抽象,它处于 "类的上层",负责类的诞生。

普通类解决的是 "实例如何创建",而元类解决的是 "类如何创建"。
在日常开发中,元类使用场景较少,但理解它能帮助你更深入地掌握 Python 的面向对象模型(例如为什么 type 既是函数又是元类,类与实例的本质关系等)。

6 关于元类的题目

对于以下使用元类(Metaclass)创建类的 Python 代码,其执行后的打印输出顺序是什么?
class MyMeta(type):
    def __new__(mcs, name, bases, dct):
        print("1. MyMeta __new__")
        return super().__new__(mcs, name, bases, dct)
    def __init__(cls, name, bases, dct):
        print("2. MyMeta __init__")
        super().__init__(name, bases, dct)
class MyClass(metaclass=MyMeta):
    def __init__(self):
        print("3. MyClass __init__")
    def __new__(cls):
        print("4. MyClass __new__")
        return super().__new__(cls)
instance = MyClass()

A 1, 2, 4, 3
B 1, 4, 2, 3
C 4, 3, 1, 2
D 2, 1, 3, 4

正确答案:A

我们可以把这个问题拆解成 "类的创建过程" 和 "实例的创建过程" 两个阶段,一步步看打印顺序:

阶段 1:创建 MyClass 这个类(在代码执行到 class MyClass... 时就会发生)

因为我们指定了 metaclass=MyMeta,所以 MyClass 这个类的创建过程会由 MyMeta 元类控制。
元类创建类的流程是:

  1. 先调用元类的 __new__ 方法:负责 "创建" 这个类本身
    → 执行 print("1. MyMeta __new__")
  2. 再调用元类的 __init__ 方法:负责 "初始化" 这个类
    → 执行 print("2. MyMeta __init__")

这一步完成后,MyClass 这个类就被创建出来了。

阶段 2:创建 MyClass 的实例(执行 instance = MyClass() 时发生)

当我们用 MyClass() 创建实例时,流程是:

  1. 先调用类的 __new__ 方法:负责 "创建" 实例对象
    → 执行 print("4. MyClass __new__")
  2. 再调用类的 __init__ 方法:负责 "初始化" 这个实例
    → 执行 print("3. MyClass __init__")

整体顺序总结

类的创建(元类操作)→ 实例的创建(类操作)
对应的打印顺序是:
1. MyMeta __new__ → 2. MyMeta __init__ → 4. MyClass __new__ → 3. MyClass __init__

所以答案是 A

简单记:先有 "类的诞生"(元类的 __new____init__),后有 "实例的诞生"(类的 __new____init__),且都是先创建(__new__)后初始化(__init__)。

【怎么会考查这个,听都没听过元类......】

元类(Metaclass)确实是 Python 中相对进阶且不常用的知识点,平时开发中很少直接用到,所以没听过很正常。

之所以会考查这个,主要有两个原因:

  1. 理解 Python 底层机制:元类是 Python 面向对象模型的底层基础(比如所有类都是通过元类 type 创建的),考查它能检验对 Python 类本质的理解深度。
  2. 区分进阶与基础:在一些高级面试或考试中,元类常被用来区分开发者对 Python 特性的掌握程度(虽然实际开发中用得少,但理解它能体现对语言的深入思考)。

其实元类的核心逻辑可以简单类比为:

  • 平时我们用类创建实例(比如 a = A()),而元类就是 “创建类的类”(比如用 MyMeta 创建 MyClass)。
  • 就像类有 __new__(创建实例)和 __init__(初始化实例),元类也有这两个方法,只不过操作对象是 “类” 而不是 “实例”。

如果暂时理解有困难也没关系,实际开发中 99% 的场景用不到元类。记住一个简单逻辑即可:先有元类创建类(元类的 __new____init__),再有类创建实例(类的 __new____init__,这道题的顺序就很好记了。

【__new__→__init__静态方法创造实例,创造了实例以后才能调用实例方法init,应该了解这个也行(?】

核心逻辑其实可以简化成你说的这一点,抓住这个关键,这类题目就很好理解了:

无论是 元类创建类,还是 普通类创建实例,都遵循「先通过 __new__ 创造对象,再通过 __init__ 初始化对象」的固定顺序 —— 因为 __init__ 是用来 “完善对象” 的,必须先有 __new__ 造出来的 “空对象”,__init__ 才有东西可初始化。

具体对应到题目里:

  1. 元类(MyMeta)的工作是 “造类(MyClass)”:先调用元类的 __new__ 造出 MyClass 这个 “类对象”,再调用元类的 __init__ 初始化这个 “类对象” → 对应打印 1、2。
  2. 普通类(MyClass)的工作是 “造实例(instance)”:先调用类的 __new__ 造出 instance 这个 “实例对象”,再调用类的 __init__ 初始化这个 “实例对象” → 对应打印 4、3。

你抓住的 “__new__ 造对象 → __init__ 初始化” 这个核心规律,其实已经覆盖了元类和普通类在创建对象时的共同逻辑。哪怕暂时不深究元类的复杂概念,只要记住这个顺序,这类题目就能做对,日常开发中也基本够用了~

7 处理超大CSV流

在处理超大 CSV 流时需要逐行解析并做轻量计算,期望尽可能降低内存峰值。以下哪种写法最合适?
A 使用生成器管道,例如 (proc(row) for row in stream) 搭配 itertools 逐步消费
B 先用列表推导式把所有行读入内存再统一处理
C 先把文件 read() 到内存再用 splitlines() 遍历
D 使用 map(lambda x: proc(x), list(stream)),以保证惰性求值
正确答案:A

官方解析:
A 使用生成器管道(如生成器表达式)搭配 itertools 逐步消费数据,逐行处理 CSV 流,避免一次性加载所有数据到内存,从而有效降低内存峰值,符合超大文件处理要求。B 和 C 选项将所有数据加载到内存后才处理,内存峰值高,不适用于超大文件。D 选项使用 map 函数但其输入 list(stream) 会先将整个流加载为列表,破坏惰性求值特性,导致内存峰值升高,不满足降低内存峰值的需求。
知识点:Python

【这个考查高频常见吗?】

这个知识点在 Python 数据处理、后端开发场景中属于高频且实用的考点,尤其是在面试数据分析师、Python 开发工程师(特别是处理大数据 / 流数据场景)时,出现概率很高。

核心原因是它直接对应了实际开发中的 “痛点”——超大文件 / 流数据处理的内存优化,这是 Python 开发者绕不开的基础能力(比如处理日志文件、用户行为数据、CSV 数据等),也是区分 “只会基础语法” 和 “懂工程优化” 的关键之一。

为什么它是高频考点?

  1. 场景通用性强
    实际工作中,“处理几 GB 甚至几十 GB 的 CSV / 日志文件” 很常见(比如分析服务器日志、清洗用户数据)。如果用 B、C、D 选项的方式(一次性加载所有数据到内存),很容易触发 MemoryError(内存溢出),而 A 选项的 “惰性求值” 是解决这类问题的标准方案,必须掌握。

  2. 能检验对 Python 核心特性的理解
    题目本质是考查对 “惰性求值” vs “立即求值” 的区分:

    • 生成器表达式((proc(row) for row in stream))、itertools 属于惰性求值:只在需要时才生成下一个元素,不占用额外内存;
    • 列表推导式([proc(row) for row in stream])、list(stream) 属于立即求值:会把所有元素一次性加载到内存,直接撑高内存峰值。

    理解这两种求值方式的差异,是 Python 进阶的基础,也是面试高频提问方向。

总结

这个考点看似是 “选写法”,实际是考查 “处理大数据时的内存优化思维”—— 这不是冷门知识点,而是 Python 开发者(尤其是数据 / 后端方向)的必备工程能力,所以在面试、笔试中出现频率很高,必须重点掌握。

【是不是流式管理?】

是的,你理解得很准!这种处理超大 CSV 流的思路,本质就是 “流式管理”(或叫 “流式处理”),核心逻辑和你说的完全一致 ——不一次性把所有数据 “吞” 进内存,而是像 “流水” 一样,让数据一行一行地 “流过” 处理环节,处理完一行就释放一行的内存,从而把内存峰值压到最低

我们可以结合题目里的选项,再把 “流式管理” 和它的对立面(“批量加载”)对比一下,就能更清楚它的本质:

处理方式 核心逻辑 内存占用特点 对应选项
流式管理 数据 “逐行 / 逐个” 生成、处理,用一个算一个 内存峰值极低(只存当前行) A(生成器管道)
批量加载 先把所有数据一次性读入内存,再统一处理 内存峰值极高(存全部数据) B、C、D

比如题目中的选项 A,生成器表达式 (proc(row) for row in stream) 就是典型的 “流式工具”:

  • stream 本身是 CSV 流(比如用 open('large.csv', 'r') 打开的文件对象,本质就是流式迭代器),它不会一次性读所有内容,而是迭代时才返回下一行;
  • 生成器表达式包裹后,proc(row) 也是处理完当前行才会去拿下一行,整个过程中内存里始终只保留 “正在处理的那一行数据”,完全符合 “流式管理” 的核心诉求。

所以简单说:题目考查的 “惰性求值”“逐行处理”,都是 “流式管理” 在 Python 中的具体实现方式,目的就是解决超大文件 / 流数据的内存占用问题 —— 你的这个总结非常精准,抓住了这类处理方案的核心本质~

【有点类似于编译型语言还是解释型语言?好比同声传译和通篇翻译的感觉?】

你的这个类比非常贴切!如果用 “编译型 vs 解释型”“同声传译 vs 通篇翻译” 来对应,流式处理(如选项 A)更像解释型语言 + 同声传译,而批量加载(如 B/C/D)更像编译型语言 + 通篇翻译—— 这个对比能帮你瞬间抓住两者的核心差异。

我们具体拆解开看:

对比维度 流式处理(选项 A,对应 “解释型 + 同声传译”) 批量加载(选项 B/C/D,对应 “编译型 + 通篇翻译”)
数据处理节奏 「拿到一行→处理一行→丢弃一行」,像 “同声传译”:听到一句翻一句,不提前记全所有内容 「先把所有数据读进内存→再统一处理」,像 “通篇翻译”:先把整本书看完 / 抄下来,再逐段翻译
内存占用逻辑 只保留 “当前正在处理的数据”,内存峰值极低(类似解释型语言 “逐行执行,不提前加载全部代码”) 必须占用能存下 “所有数据” 的内存(类似编译型语言 “先把整个源代码编译成可执行文件,再运行”)
核心特点 按需处理(惰性),适合 “数据量远超内存” 的场景 一次性加载(贪婪),只适合 “数据量小于内存” 的场景

举个更具体的例子:如果要处理一个 10GB 的 CSV 文件(而你的电脑只有 8GB 内存):

  • 批量加载(B/C/D):相当于先把 10GB 文件全塞进 8GB 内存 —— 直接 “撑爆”(内存溢出),就像 “通篇翻译” 时要先把 1000 页的书全背下来再翻,根本做不到;
  • 流式处理(A):相当于逐行读、逐行处理 —— 内存里始终只存 1 行数据(几十字节到几 KB),再大的文件也能处理,就像 “同声传译” 不管对方说多少,只专注当前一句,压力很小。

所以你的类比完全戳中了本质:流式处理的 “逐行按需” 逻辑,和解释型语言 “逐行执行”、同声传译 “逐句翻译” 的核心思路高度一致,都是通过 “不贪多、只处理当下” 来降低资源(内存 / 脑力)占用 —— 这个联想能帮你永久记住流式处理的优势~


网站公告

今日签到

点亮在社区的每一天
去签到