python

  • 地道解释
    • 框架——python
    • 描述——数据模型
    • 接口——序列、函数、迭代器、类、协程、上下文管理器等
  • 问题
      1. 为什么获取容器大小不使用 collection.len(),而是使用 len(collection) ?

数据模型

  • 特殊方法也叫作双下方法( __len__()),行话称之为魔术方法
    • 双下方法一般只有Python解释器会频繁调用
    • 一般建议不直接调用特殊方法,而是使用Python的内置方法( len(),int(),str()等)进而间接由python解释器调用特殊方法,这种方式对于python的内置类型来说,速度比直接调用方法更快
    • Python的一致性:使用者只需要统一调用不需要关心数据模型的背后如何实现,比如Python内置的 len(),不用关心函数的背后实现的是 __size__还是 __length__还是其他
  • 特殊方法示例
    • 能调用 len()方法的对象在其类的内部都定义了 __len__属性(或者叫方法)
      • 调用 len()函数时,对于自定义的对象,CPython会返回其内部实现的 __len__方法的值;对于Python自有的对象(list,str,bytes等)CPython会抄个近路直接获取ob_size属性的值
    • 对于for f in x,其背后调用的是 iter(x) (返回一个迭代器对象),接着又调用 x.__iter__()x.__getitem__()
    • 能使用点( obj.key)或者索引( obj[key])的方式获取对象的元素,在该对象的内部都实现了 __getitem__属性,python解释器调用的是 obj.__getitem__(key)
    • Python的 str(x)背后调用的是 x.__str___(),如果对象x的内部没有实现这个方法,那么Python会调用 x.__repr__()来替代它。在实现时如果必须二选一的话,请选择 __repr__ 方法
    • Python的 bool(x)背后调用的是 x.__bool__(),如果对象x没有实现这个方法,那么Python会调用 x.__len__(),为0则结果为False,否则为True
    • Python的 e in list_0 背后调用的是 __contains__() ,如果没有实现这个方法,那么python会顺序遍历这个对象从而进行判断
  • 如果想让对象支持以下基本的语言结构并与其交互,就需要实现特殊方法:
    • 容器;
    • 属性存取;
    • 迭代(包括使用 async for 的异步迭代);
    • 运算符重载;
    • 函数和方法调用;
    • 字符串表示形式和格式化;
    • 使用 await 的异步编程;
    • 对象创建和析构;
    • 使用 with 或 async with 语句管理上下文
    • 在 Python 中,为了让对象支持各种语言结构并与其交互,我们需要实现一些特殊方法。这些方法通常是以双下划线(__)开头和结尾的。以下是针对不同语言结构和交互方式所需实现的特殊方法的解释:

      1. 容器(Container)

      为了使对象能够像容器一样存储和访问元素,需要实现以下特殊方法:
      • __getitem__(self, key):允许使用方括号获取元素,如 obj[key]
      • __setitem__(self, key, value):允许使用方括号设置元素,如 obj[key] = value
      • __delitem__(self, key):允许使用 del 删除元素,如 del obj[key]
      • __len__(self):返回容器的长度,支持内置的 len() 函数。
      • __contains__(self, item):支持 in 操作符检查元素是否存在于容器中。

      2. 属性存取(Attribute Access)

      为了使对象的属性可以通过点操作符进行访问和操作,需要实现以下特殊方法:
      • __getattr__(self, name):当访问的属性不存在时调用。
      • __setattr__(self, name, value):设置属性的值。
      • __delattr__(self, name):删除属性。

      3. 迭代(Iteration)

      为了使对象支持迭代,需要实现以下特殊方法:
      • __iter__(self):返回一个迭代器对象,支持 for 循环。
      • __next__(self):返回下一个迭代项,支持迭代器协议。

      异步迭代(Asynchronous Iteration)

      为了使对象支持异步迭代,需要实现以下特殊方法:
      • __aiter__(self):返回一个异步迭代器对象,支持 async for 循环。
      • __anext__(self):返回一个 awaitable 对象,用于异步生成下一个迭代项。

      4. 运算符重载(Operator Overloading)

      为了使对象支持自定义的运算符行为,需要实现相关的二元或一元运算符方法,例如:
      • __add__(self, other):重载 + 运算符。
      • __sub__(self, other):重载  运算符。
      • __mul__(self, other):重载  运算符。
      • __truediv__(self, other):重载 / 运算符。
      • __eq__(self, other):重载 == 运算符。

      5. 函数和方法调用(Callables)

      为了使对象可以像函数一样被调用,需要实现以下特殊方法:
      • __call__(self, *args, **kwargs):使对象可调用,例如 obj(*args, **kwargs)

      6. 字符串表示形式和格式化(String Representation and Formatting)

      为了自定义对象的字符串表示形式和格式化行为,需要实现以下特殊方法:
      • __str__(self):返回非正式的字符串表示,支持 str() 和 print()
      • __repr__(self):返回正式的字符串表示,支持 repr() 和调试输出。
      • __format__(self, format_spec):自定义格式化行为,支持 format() 函数和格式化字符串。

      7. 使用 await 的异步编程(Asynchronous Programming with await)

      为了使对象支持 await 表达式,需要实现以下特殊方法:
      • __await__(self):返回一个可等待的对象,用于 await 表达式。

      8. 对象创建和析构(Object Creation and Destruction)

      为了控制对象的创建和销毁过程,需要实现以下特殊方法:
      • __new__(cls, *args, **kwargs):控制对象的创建。
      • __init__(self, *args, **kwargs):初始化对象的实例。
      • __del__(self):对象被垃圾回收时调用。

      9. 使用 with 或 async with 语句管理上下文(Context Management)

      为了使对象能够作为上下文管理器,需要实现以下特殊方法:
      • __enter__(self):进入上下文管理器,支持 with 语句。
      • __exit__(self, exc_type, exc_value, traceback):退出上下文管理器,处理异常。
      • __aenter__(self):异步进入上下文管理器,支持 async with 语句。
      • __aexit__(self, exc_type, exc_value, traceback):异步退出上下文管理器,处理异常。
      这些特殊方法使得我们可以灵活地定制对象的行为,从而更好地集成到 Python 的语言特性中。
序列
  • 特点:迭代、切片、排序、拼接
  • 类型
    • 按扁平类型划分
      • 容器序列——可以存放不同类型的项,包括嵌套容器。存放的是所包含对象的引用
        • 示例:list、tuple、collection.deque
        • 特点:
          • 内存空间存放的是所包含对象的引用,对象可以是任意类型
          • 对象也可以包含对其他对象的引用
      • 扁平序列——只能存放单一类型的序列
        • 示例:str、bytes、array.array
        • 特点:
          • 内存空间存放的是所包含内容的值,而不是各自不同的python对象
          • 更紧凑,只能存放原始机器值,比如字节数、整数、浮点数
      • 对比图
        • notion imagenotion image
      • 任何Python对象在内存中都有一个包含元数据的标头
        • 最简单的Python对象,例如一个 float ,内存标头中有一个值字段和两个元数据字段
        • ob_refcnt: 对象的引用计数
        • ob_type: 指向对象类型的指针
        • ob_fval: 一个C语言 double 类型值,存放 float 的值
      按可变类型划分
      • 不可变序列,示例:tuple、str、bytes
      • 可变序列,示例:list、bytesarray、collection.deque、array.array
      可变序列继承不可变序列的所有方法,并且还多实现了几个方法。关系图如下
      notion imagenotion image
列表推导式
  • 特点:生成列表序列
  • list_0 = [(c, s) for c in colors for s in sizes]
  • 如果不打算使用生成的列表,那就不要使用列表推导式
生成器表达式
  • 特点:逐项生成,节省内存空间
  • for tshirt in ((c, s) for c in colors for s in sizes): print(tshirt)
元组
  • 用作记录
  • 用作不可变列表
  • 元组是不可变数据类型,但不可变只针对元组的一级子元素,即对象的引用。如果引用的对象的值变了,则元组中非一级子元素的值也会跟着变
  • 只有值永不可变的对象才是可哈希的。不可哈希的元组不能作为字典的键,也不能作为集合的元素
序列和可迭代对象拆包
  • 拆包指的是将可迭代对象的元素分配给变量的过程
  • 并行赋值
  • 使用*获取余下的项
  • 在函数调用和序列字面量中使用*拆包
  • 嵌套拆包
模式匹配 match/case
  • 模式支持析构(如析构嵌套元组),但不析构序列以外的可迭代对象(例如迭代器)
  • 序列模式中
    • 方括号和圆括号的意义相同
    • _ 符号匹配相应位置上的任何一项但不绑定匹配项的值(占位符的作用),且是唯一可在模式中多次出现的变量
    • 模式中的任何一部分均可使用as关键字绑定到变量上
    • 可添加类型信息使模式更具体(如下第一项必须是str实例, 二元组中的两项必须是float实例)。在模式中str()、float()等的作用是在运行时检查类型,符合类型才算匹配成功
    • 示例 case [name, _, _, (lat, lon) as coord] case [str(name), _, _, (float(lat), float(lon))]
  • match/case的上下中,str、bytes、bytearray实例不作为序列处理
切片
  • 列表、元组、字符串等所有序列类型都支持切片操作
  • 问题:为什么切片和区间排除最后一项?
    • 方便计算切片或区间的长度
      • 在仅指定停止位置时,容易判断切片或区间的长度。例如 range(3)list_0[:3] 都只产生3项
      • 同时指定起始和停止位置时,容易计算切片或区间的长度,做个减法即可:stop-start
    • 方便在索引 x 处把一个序列分成两部分而不产生重叠,直接使用 list_0[:x]list_0[x:] 即可
  • 使用方式
    • 使用 []运算符,s[a:b:c] ,得到的结果是一个切片对象 slice(a, b, c)
    • 使用切片对象 SKU = slice(a, b, c)
  • 切片赋值——如果赋值目标是一个切片,那右边必须是一个可迭代对象,即使只有一项
使用 + 和 * 处理序列
构建嵌套列表
  • 推荐使用列表生成式
  • 方式一
    • board = [['_'] * 3 for i in range(3)] print(board) # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] board[1][2] = 'x' print(board) # [['_', '_', '_'], ['_', '_', 'x'], ['_', '_', '_'] # board 列表的生成过程等价如下代码 # 每一次迭代构建了一个新的 r ,追加到 board 中 board = [] for _ in range(3): r = ['_'] *3 board.append(r)
       
  • 方式二(错误方式,这种构建得到的子列表都引用了同一个对象)
    • weird_board = [['_'] * 3] * 3 print(weird_board) # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] weird_board[1][2] = 'X' print(weird_board) # [['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']] # weird_board 列表生成过程等价如下代码 # 同一个 r 向 weird_board 追加3次,即列表中的子列表都引用的是同一个对象 r = ['_'] *3 weird_board= [] for i in range(3): weird_board.append(r)
  • 使用增量赋值运算符处理序列
    • += 运算符
      • 背后提供支持的特殊方法是__iadd__(就地相加),如果没有实现该方法则调用__add__
      • 对于 a+=b
        • 如果 a 实现了__iadd__,则调用它。如果a是可变序列(例如list、bytearray、array.array),则就地修改(行为类似于a.extend(b))。
        • 倘若 a 没有实现__iadd__方法,表达式 a+=b 的作用等同于 a=a+b ,先求解 a+b ,再将得到的新的对象绑定到a上。
        • 也就是说,a绑定的对象可能变了,也可能没变,这取决于有没有实现__iadd__方法
      • 通常,对于可变序列,最好实现__iadd__方法,而且 += 运算符就地修改。对于不可变序列,显然不能就地修改,而是复制整个目标序列,创建一个新序列,包含要拼接的项,而不是简单的追加新项。但字符串例外,由于现实中的代码基经常在循环中使用+=运算构建字符串,因此CPython对这种情况做了优化,内存为str实例分配空间时会留些富余,因此拼接字符串时无须每次都复制整个字符串
      *= 运算符
      • 背后提供支持的是 __imul__方法
      • 作用效果同 += 运算符
      元组的 += 运算谜题
      t = (1, 2, [30, 40]) t[2] += [50, 60] 结果如何?请从以下 4 个选项中选出最佳答案。 A. t 变成 (1, 2, [30, 40, 50, 60])。 B. 抛出 TypeError,错误消息为 'tuple' object does not support item assignment。 C. A 和 B 都不对。 D. A 和 B 都对。
      正确答案
      • D
      解释
      notion imagenotion image
      • 可理解为这个过程执行了两步操作
          1. 通过元组的引用获取到源对象,将源对象与 b 进行 += 运算。【执行成功】
          1. 将经过+=操作后的源对象赋值给元组。【执行失败】
      • 报错是因为元组是不可变对象,即其内部的元素(对象的引用)不会改变。 从打印结果看貌似改变了元组中的元素,实际上并没有改变。改变的是元素所指向的对象。
    • 由此谜题可知——增量操作不是原子操作
list.sort和sorted
  • list.sort——就地排序列表,返回值为None sorted——构建一个新的排好序的列表,返回创建的新列表
  • list.sort和sorted均接受两个可选的关键字参数
    • reverse:值为True时,降序返回项。默认为False。
    • key:一种只接受一个参数的函数,应用到每一项上,作为排序依据。默认是恒等函数(即比较项本身)
  • python的排序算法是稳定的(即能够保留比较时相等的两项的相对顺序)
当列表不适用时
  • 列表(list)是一种动态数组,可以存储不同类型的对象。列表中的每一个元素实际上是指向一个对象的指针。由于它们可以指向任何类型的对象,所以Python需要为这些指针分配额外的内存空间,此外,为了存储各种对象的引用计数等信息,也需要额外的内存。
  • 数组(array.array、numpy.array)通常用于存储同一数据类型的元素,尤其是数据密集型的场景
 
对于本文内容有任何疑问, 可与我联系.