Python 从入门到入土之奇技淫巧(二)

· · 科技·工程

在绝大多数情况下,内置函数能实现的,就不要用手写(除非你想学习它的原理)。也许你也能写出来,但效率往往会差一些——因为它们往往已经被一群天才优化到了极致。

不过在 Python 这里,手写也许会慢非常多,为什么呢?

众所周知,Python 是一门高级的解释性语言,例如 CPython 解释器使用 C 语言实现的。显然,高级语言的效率远低于低级语言,而 Python 的内置函数正是由解释器实现的,所以会比手写快得多。

因此,学习内置函数还是挺重要的,好比学 C++ 要学 STL,你也不想为了一个 map 手写红黑树吧

Python 中内置的函数、类、常量等等,一共有 161 个 *(3.13.1 版本),但我们这一期只讲常用的函数。

* 由 import builtins; print(len(dir(builtins))) 得到

all & any

all 和 any 都接受一个可迭代对象,并返回一个 bool 值。

函数 判断条件
all 可迭代对象中不存在False
any 可迭代对象中存在True

:::warning[注意]{open} 所谓的 TrueFalse,是指将元素转为 bool 值(bool(x))的结果。

特别地,当可迭代对象没有任何元素时,all 和 any 分别返回 TrueFalse。 :::

进制转换

进制转换有 bin、oct 和 hex 函数,它们接受一个整数,返回一个对应进制下这个整数的表示,包含前缀。

函数 进制 前缀
bin 二进制 0b
oct 八进制 0o
hex 十六进制 0x

int 函数(准确来说是 int 类的构造函数)单列,它比较特殊,有多种用法,但均返回一个整数。

chr & ord

chr 接受一个整数,返回这个码点在 Unicode 中对应的字符。

ord 则相反,将一个字符转换为 Unicode 码点。

divmod

小学学的带余除法,接受两个数,返回一个元组,包含它们相除的商和余数,即 (x // y, x % y)

enumerate

enumerate 是一个可迭代对象,构造函数接受一个可迭代对象,可以在遍历时返回一个包含下标和值的元组(这点比 C++ 方便多了)。

l = 'abcde'
for i, val in enumerate(l):
    print(i, val)
# 0 a
# 1 b
# 2 c
# 3 d
# 4 e

eval & exec

eval 和 exec 均接受一个字符串或字节串(bytes),还有一种用法较罕见故不提及。

eval,即 evaluate,就是将字符串看作表达式,返回它的值,可以水一些表达式计算。

而 exec,就是当作正常代码执行,无返回值(None)。

eval:

print(eval('1+1'))  # 2
print(eval('[1, 2, 3]'))  # [1, 2, 3]
eval('print("Hello World")')  # Hello World

print(eval('print("Hello World")'))
# Hello World
# None

# 别忘了 eval 是表达式求值
eval('a = 114514') # SyntaxError: invalid syntax
print(a)  # NameError: name 'a' is not defined

exec:

print(exec('1+1'))  # None
print(exec('[1, 2, 3]'))  # None
exec('print("Hello World")')  # Hello World

print(exec('print("Hello World")'))
# Hello World
# None

exec('a = 114514')
print(a)  # 114514

exit

与 C++ 类似,接受一个整数作为返回值,并退出程序。

filter

filter 实际上是一个类,顾名思义它是一个过滤器,它的构造函数有且仅有两个参数:一个可调用对象(callable)和一个可迭代对象(iterable)。它会保留可调用对象处理后为 True 的元素。

l = [i for i in range(10)]
f = filter(lambda x: x & 1, l)
print(f)  # <filter object at 十六进制地址>
print(list(f))  # [1, 3, 5, 7, 9]

hash

哈希函数,接受一个不可变对象,返回整数哈希值。

print(hash(114514))  # 114514
print(hash(114514.0))  # 114514
print(hash(114514.1919810))  # 442678046743445330
print(hash('114514'))  # 2469581522774740131

print(hash([1, 2, 3, 4, 5]))  # TypeError: unhashable type: 'list'
print(hash((1, 2, 3, 4, 5)))  # -5659871693760987716

print(hash({1, 2, 3, 4, 5}))  # TypeError: unhashable type: 'set'
print(hash(frozenset({1, 2, 3, 4, 5})))  # -3779889356588604112
# frozenset 是 set 的不可变版本,除了不能修改以外与 set 均相同。

id

获取对象的地址。

上期中我们讲到 Python 中对象常常以引用形式传递,那么我们怎么验证一个对象是否是另一个对象的引用呢?

a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(id(a) == id(b))  # True,说明b是a的引用
print(id(a) == id(c))  # False,说明c不是a的引用

另外,Python 会将小整数存储到一个池子里,所以你会发现:

a = 1
b = 1
print(id(a) == id(b))  # True
a = 114514
b = 114514
print(id(a) == id(b))  # False
print((id(256) - id(0)) / 32)  # 256.0
print((id(257) - id(0)) / 32)  # -4319257184724.75(因人而异)
print((id(0) - id(-5)) / 32)  # 5.0
print((id(0) - id(-6)) / 32)  # 4348471359369.75(因人而异)
t = 114514, 114514
print(id(t[0]) == id(t[1]))  # True
# ?
t = (1, 2), (1, 2)
print(id(t[0]) == id(t[1]))  # True
# ??
t = [1, 2], [1, 2]
print(id(t[0]) == id(t[1]))  # False
# ???

iter & next

iter 有两种用法,第一种是获取可迭代对象的一个迭代器。

i = iter([1, 2, 3])
print(next(i))  # 1
print(next(i))  # 2
print(next(i))  # 3
print(next(i))  # StopIteration
print(next(i))  # StopIteration

第二种接受一个可调用对象(callable)和一个值(sentinel),可以一直 next,直到可调用对象的返回值等于 sentinel。

i = 0
def f():
    global i
    i += 1
    print(i)
    return i
it = iter(f, 5)
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
print(next(it))  # 4
print(next(it))  # StopIteration
print(next(it))  # StopIteration
print(i)  # 5

实际上,for 的本质就是一个 while 循环在不断地执行 next,直到遇到 StopIteration。

for i in l:
    print(i)
# 等价于
it = iter(l)
while 1:
    try:
        i = next(it)
        print(i)
    except StopIteration:
        break

:::info[小寄巧]{open}

for i in iter(input, ''):
    print(i)

这段代码会不断读取输入,直到输入为空行。 :::

map

上一期中我们多次看到了 map,这东西到底有啥用呢?

首先,我们需要澄清一下,map 不是 C++ 中的有序键值对容器(map),也不是字典(dict),它就是「映射」,而且它看起来像函数,实则是一个类

map 的构造函数有且仅有两个参数:一个可调用对象(callable)和一个可迭代对象(iterable)。它会将可迭代对象中的每个元素都映射到经过可调用对象处理的元素

:::warning{open} 不要忘了 map 是一个类,也是可迭代对象,它的类型并不与你扔进去的可迭代对象相同,必要时需要进行转换。 :::

举个例子:

l = [i for i in range(1, 6)]
l = map(lambda x: x * 2, l)
print(l)
# <map object at 十六进制地址>
# ???
print(list(l))
# [2, 4, 6, 8, 10]
# Wonderful Answer!

再举个更经典的例子:

l = list(map(int, input().split()))

上一期中讲到,input 函数只能读入整行字符串,所以遇到一行多个整数时,我们需要先使用 str.split 方法将读入的字符串按空格分隔,得到一个包含多个字符串的列表,再使用 map 将每个字符串都映射到它的整数形式,最后用 list 将其转换为列表。

max & min

This does what you think it does. —— C++ STL 中 max / min 函数的文档

max / min 可以传入一个可迭代对象或者多个参数,与 sort 类似,可以加上一个 key 函数。

print(max(1, 2, 3, 4, 5))  # 5
print(max('abcdefg'))  # g
print(max('abc', 'abcdef'))  # abcdef

print(hash('abc'))  # -2837500452362813077
print(hash('abcdef'))  # 6448430210802371289
print(max('abc', 'abcdef', key=lambda x: hash(x)))  # abcdef

pow

通常情况下等效于 ** 运算符,但不同的是 pow 函数可以传入一个模数。

P = 998244353
# 乘法逆元
def inverse(x):
    return pow(x, P - 2, P)
print(114514 * inverse(114514) % P)  # 1

range

每次循环你几乎都会用到 range,但你真的了解它吗?

print(list(range(5)))  # [0, 1, 2, 3, 4]
print(list(range(1, 6)))  # [1, 2, 3, 4, 5]
print(list(range(0, 10, 2)))  # [0, 2, 4, 6, 8]

print(list(range(10, 0)))
# []
# ???
print(list(range(10, 0, -1)))
# [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
# Wonderful Answer!

range(start, stop, step) 相当于 C++ 中的:

template<typename T>
vector<T> range(T start, T stop, T step = 1) {
  vector<T> result;
  for (T i = start; i < stop; i += step) {
    result.push_back(i);
  }
  return result;
}

:::error[警钟撅烂]{open}

for i in range(5):
    print(i)
    i += 114514
    print(i)
# 0
# 114514
# 1
# 114515
# 2
# 114516
# 3
# 114517
# 4
# 114518

不要学其他语言学傻了,range 是一个生成器。上文中有讲过,i 只是对其调用 next 的结果,对 i 的修改只持续到这一轮循环结束。

:::

repr

将对象转换为开发者友好的字符串。

print(repr('Hello World\n'))
# 'Hello World\n'
print('Hello World\n')
# Hello World
# [滚木]

reversed

这是一个普普通通的可迭代对象。

a = [1, 2, 3, 4, 5]

别急,有反转。

print(reversed(a))
# <list_reverseiterator object at 十六进制地址>
print(list(reversed(a)))
# [5, 4, 3, 2, 1]
for i in reversed(a):
    print(i)
# 5
# 4
# 3
# 2
# 1

round

顾名思义,四舍五入。但 Python 很有良心的一点在于它可以自定义保留几位小数。

print(round(1.14514))  # 1
print(round(1.919810))  # 2
print(round(1.14514, 2))  # 1.15
print(round(1.919810, 2))  # 1.92

zip

print(list(
    zip('abcdefg', range(3), range(4))
))
# [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]

你问我这有什么用?……它可以用来压缩键值对。

print(dict(zip('abcde', range(5))))
# {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}

__import__

import numpy as np

相当于

np = __import__('numpy')

结语

这一期,我们一共讲了 23 个内置函数,接下来还会讲内置的类以及标准库。但看过 Python 官方文档的都知道,内置及标准库的目录整整有有 12 页(相当于按 11 下 PageDown)。这么多内容我讲不完,你们也记不住,但事实上你只要学会其中的 20\%,就能实现 80\% 的功能。至于剩下 20\% 的功能?管他嘞,真用到了再说吧。

由于这个系列属于梦到什么说什么的那种,大家有什么想听的内容,也欢迎在评论区提出,希望这个系列能真正地呼应标题。