常量
常用的内置常量有True
、False
和None
三个,都是实例对象并且比较特殊:
-
None
是单例模式,也就是无法通过调用其类型对象的方式实例化其它实例。None
的类型对象在Python中没有显式的绑定名字,但是我们可以用以下代码来验证:NoneType = type(None) AnotherNone = NoneType() print(id(None)) print(id(AnotherNone)) # 运行结果: # 140724991098072 # 140724991098072
这也是判断一个对象是不是
None
的时候可以用is
的原因(并且推荐用is
而不是==
)。 -
True
和False
的类型都是bool
,不是严格的单例模式,但也有特殊之处:- 任何比较表达式(
a > b
等)必返回二者其一,不会返回新的bool
类型实例。 - 以
bool(<obj>)
的形式调用时,也不会实例化新的实例,而是对<obj>
进行布尔检测,并且返回二者其一。
所以我们判断表达式真假的时候也是使用
is
而不是==
。 - 任何比较表达式(
这三个名字都是不可赋值的,尝试None = <obj>
等操作时会触发SyntaxError
。
操作符
逻辑检测
逻辑检测即调用bool
对象,参数为待检测对象。任何对象都可以进行逻辑检测,逻辑检测的结果是True
或者False
。显式使用bool(<obj>)
进行逻辑检测的方式并不常用,更多的是用在if
和while
语句中。if <condition>
和while <condition>
其实就是对<condition>
进行逻辑检测,当<condition>
是一个表达式时,则是对其返回值进行逻辑检测。
bool
是一个类型对象,调用流程和其他类型对象实例化对象时相同。但是它的__new__()
方法比较特殊。假设被检测对象是a
,它的__new__()
方法主要逻辑如下(按顺序进行):
- 如果
a
是True
或False
之一,返回a
。 - 如果
a
是None
,返回False
。 - 如果
a
有__bool__()
方法,则根据该方法的执行结果返回True
或False
。 - 如果
a
有__len__()
方法,则执行该方法,执行结果是非零值时返回True
,否则返回False
。 - 返回
True
。
object
没有定义__bool__()
方法和__len__()
方法,所以自定义对象如果也没有定义这两个方法,布尔检测的结果总是True
。
逻辑运算
逻辑运算有三个and
、or
和not
。and
和or
都是短路运算,并且结果是二者其一而不会转换成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
是不可变对象(如int
和tuple
),名字'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(*objects, sep='', end='\n', file=sys.stdout, flush=False)
函数逻辑如下:
string = sep.join([str(obj) for obj in objects]) + end
file.write(string)
sep
和end
都需要是字符串。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
。