内置对象
 2024-01-01
 4326字
 Python

常量

常用的内置常量有TrueFalseNone三个,都是实例对象并且比较特殊:

  • None是单例模式,也就是无法通过调用其类型对象的方式实例化其它实例。None的类型对象在Python中没有显式的绑定名字,但是我们可以用以下代码来验证:

    NoneType = type(None)
    AnotherNone = NoneType()
    
    print(id(None))
    print(id(AnotherNone))
    
    # 运行结果:
    # 140724991098072
    # 140724991098072

    这也是判断一个对象是不是None的时候可以用is的原因(并且推荐用is而不是==)。

  • TrueFalse的类型都是bool,不是严格的单例模式,但也有特殊之处:

    • 任何比较表达式(a > b等)必返回二者其一,不会返回新的bool类型实例。
    • bool(<obj>)的形式调用时,也不会实例化新的实例,而是对<obj>进行布尔检测,并且返回二者其一。

    所以我们判断表达式真假的时候也是使用is而不是==

这三个名字都是不可赋值的,尝试None = <obj>等操作时会触发SyntaxError

操作符

逻辑检测

逻辑检测即调用bool对象,参数为待检测对象。任何对象都可以进行逻辑检测,逻辑检测的结果是True或者False。显式使用bool(<obj>)进行逻辑检测的方式并不常用,更多的是用在ifwhile语句中。if <condition>while <condition>其实就是对<condition>进行逻辑检测,当<condition>是一个表达式时,则是对其返回值进行逻辑检测。

bool是一个类型对象,调用流程和其他类型对象实例化对象时相同。但是它的__new__()方法比较特殊。假设被检测对象是a,它的__new__()方法主要逻辑如下(按顺序进行):

  1. 如果aTrueFalse之一,返回a
  2. 如果aNone,返回False
  3. 如果a__bool__()方法,则根据该方法的执行结果返回TrueFalse
  4. 如果a__len__()方法,则执行该方法,执行结果是非零值时返回True,否则返回False
  5. 返回True

object没有定义__bool__()方法和__len__()方法,所以自定义对象如果也没有定义这两个方法,布尔检测的结果总是True

逻辑运算

逻辑运算有三个andornotandor都是短路运算,并且结果是二者其一而不会转换成True或者False;但not会返回True或者False

操作 结果
a and b if a return b else return a
a or b if a return a else return b
not a if a return True else return False

比较运算

运算符 特殊方法或含义
< __lt__()
<= __le__()
> __gt__()
>= __ge__()
== __eq__()
!= __ne__()
is id(a) == id(b)*
is not id(a) != id(b)*

*这只是逻辑意义上的。因为id(<obj>)返回的是一个int对象,再进行==其实是调用这个int对象的__eq__()方法,实际判定时自然不会这么绕弯。因为底层使用对象的时候是先得到指向对象的指针,所以直接比较两个指针值是否相等就可以,甚至不用获取这个对象本身。

PyTypeObject中有一个richcmpfunc tp_richcompare字段,这个指针指向的函数会根据运算符调用对应的函数,如果想要修改这些被调用函数,在Python层面就是重写这些特殊方法。

单目运算

使用示例 特殊方法 注释
+x __pos__
-x __neg__ 算数取反
abs(x) __abs__ 绝对值
~x __invert__ 逻辑取反
round(x) __round__ 舍入
math.floor(x) __floor__ 退1
math.ceil(x) __ceil__ 进1
math.trunc(x) __trunc__ 截断

双目运算和原地运算

对于a + b,如果a没有实现__add__方法,将会调用b.__radd__(a)(称为反射运算),其它运算同理。

对于a += b将调用a.__iadd__(b)实现原地运算,但是如果a是不可变对象(如inttuple),名字'a'将会绑定到一个新的对象上或者报错,取决于该对象的具体实现。

使用示例 特殊方法 反射运算 原地运算
a + b __add__ __radd__ __iadd__
a - b __sub__ __rsub__ __isub__
a * b __mul__ __rmul__ __imul__
a @ b __matmul__ __rmatmul__ __imatmul__
a // b __floordiv__ __rfloordiv__ __ifloordiv__
a / b __div__ __rdiv__ __idiv__
a % b __mod__ __rmod__ __imod__
divmod(a, b) __divmod__ __rdivmod__ -
pow(a, b) __pow__ __rpow__ __ipow__
a << b __lshift__ __rlshift__ __ilshift__
a >> b __rshift__ __rrshift__ __irshift__
a & b __and__ __rand__ __iand__
`a b` __or__ __ror__
a ^ b __xor__ __rxor__ __ixor__

数值对象

Python中包括三种数值类型:整数类型(int)、浮点类型(float)和复数类型(complex)。

这里主要介绍整数类型和浮点类型。

浮点类型

浮点类型的结构比较简单,就是C中的double加上一个公共对象头部。其结构体(是浮点型实例对象的结构体,而非float这个类型对象的结构体)定义如下(位于Include/floatobject.h):

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

需要注意的是,浮点对象是不可变对象,执行a += b时先计算a + b得到一个新的浮点对象,然后把名字'a'和这个浮点对象绑定。

整数类型

和浮点对象一样,整数对象也是不可变对象,+=操作会将名字绑定到一个新的整数对象上。

内存布局

由于Python的整数是“无限精度”的,所以其实现相当复杂且巧妙,是大整数课程作业的pro max版。

整数实例对象的结构体定义如下(位于Include/longintrepr.h):

//该语句在Include/longobject.h中
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

在大多数平台上该结构体和如下定义等价:

struct _longobject {
    long long ob_refcnt;
    PyTypeObject *ob_type;
    long long ob_size;
    unsigned int ob_digit[1]; //
};

C语言的数组长度是无关紧要,数组本质上只是指针的语法糖,重要的是数组名就是一个常量指针。所以ob_digit就是一个指向ob_size这个字段之后的一个常量指针,至于ob_size之后的有多少个unsigned int,则交给ob_size记录。

如果是定义成unsigned int *ob_digit,则还需要再分配内存之后手动给ob_digit赋值。

ob_size之后的内存(暂且记为ob_digit数组)记录该整数的绝对值;ob_size的符号和该整数的符号相同,绝对值则等于数组长度。当该整数为0时,ob_size为0,ob_digit也是一个无效指针。

数组记录整数绝对值的方式与$N$进制下整数表示一致: $$ x = (a_ka_{k-1}…a_1a_0)N=\sum^{k}{i=0}a_iN^i $$ 其中$k$为数组长度,$a_i$为数组中的对应元素,$N=2^{30}$。之所以不采用$2^{32}$进制是为了方便处理进位。

小整数内存池

为了避免频繁的创建销毁整数对象,CPython把常用的较小的整数设置了内存池,默认的范围为$[-5, 256]$,在这个范围内的整数,不会重新构造整数对象,而是绑定到内存池中已存在的整数对象。

>>> a = 1 * 1
>>> b = 2 - 1
>>> c = int(1.1)
>>> a is b and b is c
True
>>> a = 256 + 1
>>> b = 258 - 1
>>> a is b
False

函数

输入输出

print

函数签名:print(*objects, sep='', end='\n', file=sys.stdout, flush=False)

函数逻辑如下:

string = sep.join([str(obj) for obj in objects]) + end
file.write(string)
  • sepend都需要是字符串。sep的默认值是一个空格而不是空字符串。
  • file必须是一个流式文本文件。默认的sys.stdout表示标准输出流,大部分情况下它都是指屏幕。也可以传入一个文件对象,用来向文件中写入字符。文件对象需要以文本模式打开,详见open()函数。
  • flush用来强制刷新file的缓冲区,常用于不关闭文件而持续性写入日志。上面的函数逻辑忽略了这个参数的作用。

input

函数签名:input(prompt='') -> str

  • prompt的内容输出到sys.stdout
  • 然后从sys.stdin读取内容,直到遇到换行符,然后丢弃末尾的换行符,将读取内容以字符串的形式返回。

open

open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

打开file指向的文件并返回一个文件对象,如果文件不能打开则引发OSError

  • file:文件路径或文件描述符。

  • mode:打开模式。分为打开格式打开目的。打开格式分为文本形式二进制形式,分别以字符串't'(text)和'b'(binary)表示。打开目的有以下几种:

    参数 含义
    'r' 读取。
    'w' 写入,如果文件存在则清空,不存在则新建。
    'x' 排他性写入,文件存在则引发FileExistsError
    'a' 写入,如果文件存在则在末尾追加。
    '+' 打开以用于更新,读取与写入均可。

    需要以单个字符串的形式同时指定打开格式和打开目的。例如二进制读取就是'rb'

数值转换

ord

函数签名:ord(c)

  • c必须是单字符的字符串,可以是非ASCII字符。
  • 以整数形式返回该字符的UTF-8编码的值。

chr

函数签名:chr(i)

  • i必须是能表示字符编码的整数。
  • 以字符串形式返回该整数对应的字符。

序列处理

这些函数的参数虽然都是可迭代对象,但是在逻辑上是把它当做序列或者是容器来使用。

sorted

函数签名:sorted(iterable, *, key=None, reverse=False) -> list

函数逻辑(仅做逻辑上的说明):

ls = list(iterable) if key is None else [key(x) for x in interable]
# 然后对ls原地排序
# 如果reverse为False,返回排序后的列表;否则翻转列表后再返回
  • 可以对任意可迭代对象排序,不只是列表。
  • 返回值是一个新的列表,不会改变原对象的数据。
  • key需要是一个可调用对象。
  • ls排序采用的是一种叫做TimSort的稳定排序算法,稳定指的是如果两个元素的值相同,排序后它们的相对位置不变。该算法可以做到最坏情况的时间复杂度是$O(n\text{log}n)$,最好情况的时间复杂度是$O(n)$。主要思想是对短序列使用插入排序,长序列使用预处理+归并排序。

max & min

两者使用方法一致,这里以max为例。

函数签名:

max(iterable, key=None, default=NULL)  # 由于是C写的,所以default不是None而是真正的空指针
max(*args, key=None)
  • 该函数有两种调用方式。一种是只传入一个位置参数,那么它必须是可迭代对象,函数结果是返回可迭代对象中的最大值。如果传入多个位置参数,则返回传入的对象之中的最大值。

    print(max([1, 2, 3]))
    print(max(1, 2, 3))
    print(max([1, 2], [2, 3]))
    
    # 运行结果:
    # 3
    # 3
    # [2, 3]
  • 不管哪种形式调用,如果有多个最大值,则返回最先遍历到的那个。

  • 参数key含义和sorted相同。

  • 当使用第一种方式调用且iterable为空时,如果default指定,则返回default;如果未指定,引发ValueError

sum

函数签名:sum(iterable, /, start=0) /之前的参数只能以位置参数的形式传入,之后的参数只能关键字参数的形式传入。

start给定的值开始累加iterable中的元素并返回,所以如果iterable为空,返回的是start的值。因为它的内部实现机制就是进行len(iterable)次加法,所以对于某些加法导致开辟新内存的情形应该避免使用:

  • 如果要拼接字符串,最好使用''.join(list(iterable)),可参考数据结构>序列类型>字符串
  • 如果在多个序列上进行迭代,最好使用itertools.chain()

any & all

函数签名:any(iterable)

iterable内的元素进行逻辑检测的结果至少有一个为True时返回True,否则返回False

函数签名:all(iterable)

iterable内的元素进行逻辑检测的结果全部为True时返回True,否则返回False

迭代枚举

对象构造

类型系统

编译运行

对象操作