Python 开发指南:关键字 VS 对不可变的理解
Python 关键字
or & and & not
为了提高代码的可读性,Python 分别使用 or
代替了 "或",and
代替了 "与",not
代替了 "非",这些运算符常用于条件判断式。比如:
print(not False) # True
print(False or False) # False
print(False or True) # True
print(True and False) # Fale
print(True and True) # True
print(1 not in [1, 2, 3]) # False
除此之外,or
和 and
还有一个延伸用法。比如,对于两个数值型而言,x or y
可以返回两者中的较小值,而 x and y
可以返回两者中的较大值。如:
print(2 or 3) # 2
print(3 and 5) # 5
pass
pass
关键字充当 Python 语法上的占位符。比如:
if x >= 10:
pass
else:
print("x >= 10")
或者将一些仅声明但未给出实现的函数 foo
加上 pass
以保持语法完整。比如:
def foo(): # TODO waiting for implementing
pass
None
None
在 Python 中作为一个特殊的常量,它的类型为 NoneType
。它代表语义上的空值,但自身既不是 0
,也不是 False
。
None
可以用于设计部分函数 ( partial function )。比如说,当某个函数 f
选择不去对某些输入进行处理时,它可以不选择抛出异常或者是其它预设好的默认值,而是简单地以 None
代替。
def div(x, y):
if y == 0:
return None
else:
return x / y
print(div(3, 0))
这种设计思想被广泛用于函数式编程。比如:Scala:函数式编程下的异常处理 - 掘金 (juejin.cn)
*is & ==
is
关键字常和 ==
放到一起去讨论。两者的主要区别是:
==
进行的是值比较,强调相等。is
进行的是引用比较,强调相同。
在 Python 中,可以通过内置的 id()
函数获得某个对象的全局标识号,该标识号的作用相当于 C 语言的地址。若两者的标识号相同,则认为两者的引用相同。此时使用 is
比较的结果为 True
,否则为 False
。
而对象的 ==
操作符底层指向 __eq__()
方法,它和 __hash__()
方法 成对出现。同理,还可以为对象定义 >=
,<=
等运算符重载。
class Obj:
# __init__ 相当于其它语言中的对象构造器
# self.x 表示声明对象的内部属性。
def __init__(self, v_):
self.v = v_
def __eq__(self, other): return self.v == other.v
def __hash__(self): return hash(self.v,)
o1 = Obj(1)
o2 = Obj(1)
print(o1 == o2) # True
print(o1 is o2) # False
数值之间的比较应使用 ==
,另外,比较一个值是否为 None
时使用 is
。因为 None
相当于是一个全局的单例对象,所有被赋值为 None
的变量总会指向同一处引用。
*in
in
是一个实用的关键字。我们可以快速地利用该关键字验证某个元素是否在可迭代的数据结构,如列表,切片,元组,集合,字典。而查找机制的底层仍然离不开比较,即 相等性判断。如果查找的元素是 对象,Python 会优先尝试调用用户重写的 __eq()__
方法,否则仍然按照引用进行比较,见下面的例子。
class Foo:
def __init__(self, v_):
self.v = v_
def __eq__(self, other): return self.v == other.v
def __hash__(self): return hash(self.v,)
class Goo:
def __init__(self, v_):
self.v = v_
cond1 = Foo(1) in [Foo(1)]
print(cond1) # True
cond2 = Goo(1) in [Goo(1)]
print(cond2) # False
由此可见,重写 __eq__()
方法对明确类的语义很重要。否则,看似高可读的代码实际上会返回完全相悖的结果。
yield from*
懒加载是一个偏 Functional 的话题。
首先,yield
关键字可用于生成一个 懒加载 的数据流,避免一次性将要处理的数据全部读入内存,从而减少资源浪费。比如,下面的 seq()
函数用于生成无限流:
def seq(start: int = 0):
while True:
yield start
start += 1
gen = seq(0)
一旦某个函数使用 yield
作为返回值,Python 就会将其翻译为生成器 ( Generator )。
上述的代码通过调用 seq(0)
创建了一个生成器实例并赋值给了 gen
。next()
函数能够调用一次生成器并得到一个返回值。生成器每被调用一次,就会执行函数体到 下一条 yield
语句,产生出一个值返回给外界,随后停下来等待被下一次调用,直到执行最后一条 yield
之后退出。
如你所见,
yield
可以使函数在运行到某一段代码处后被 "暂停"。这种特性可以被用来设计协程。感兴趣的同学可以参考:Python 的关键字 yield 有哪些用法和用途? - 知乎 (zhihu.com)
比如,利用上面的生成器 gen
,我们可以不断地生成递增的连续序列:
"""
这里的 for 循环是为了反复调用 next(gen) 生成 10 个连续自然数
xs = [0, 1, 2, ... , 9]
ys = [10, 11, 12, ... , 19]
"""
n = 10
xs = [next(gen) for _ in range(n)]
ys = [next(gen) for _ in range(n)]
print(*xs)
print(*ys)
由于 seq()
函数本身是一个死循环,因此 gen
总是能够源源不断地返回值。下面是一个更容易被理解的简单生成器,它没有包含任何循环语句:
def finite_seq():
yield 1
yield 3
yield 5
finite_gen = finite_seq()
print(next(finite_gen)) # 返回第一个 yield 值 1
print(next(finite_gen)) # 返回第二个 yield 值 3
print(next(finite_gen)) # 返回第一个 yield 值 5
print(next(finite_gen)) # StopIteration
生成器 finite_gen
会在依次产生数据 1 3 5
之后关闭。如果此时企图再生成更多的数据,则程序会抛出 StopIteration
异常。
生成器也是可遍历对象。在这个例子中,可以直接使用 for
循环将流内的元素全部提取出来,因为 finite_gen
不会无休止地生成元素。
def finite_seq():
yield 1
yield 3
yield 5
finite_gen = finite_seq()
for x in finite_gen:
print(x,end=", ")
不要在无限流中这么做,否则程序会陷入死循环。
有些高阶的生成器会依赖其它生成器 ( 或者递归调用自身 ) 生成元素,此时需要引入 yield from
关键字。回到最开始的例子:我们现在能够以递归的形式定义一个不断累增的无限流:
# 这种无限流也称之为共递归。
def seq(start):
yield start
yield from seq(start+1)
gen = seq(0)
xs = [next(gen) for _ in range(10)]
print(*xs)
下面是一个稍稍复杂的案例:
def flatten(xs: list):
for i in range(len(xs)):
if isinstance(xs[i], list):
yield from flatten(xs[i])
else:
yield xs[i]
xxs = [1,[2,3,[4,5]],6,[7,8]]
xs = [x for x in flatten(xxs)]
print(*xs) # 1 2 3 4 5 6 7 8
flattten
生成器会检测 xs
的元素是否还包含列表。若是,则递归地创建一个子生成器提取该子列表的元素。因此,flatten
可以将任意复杂的列表展平成一维列表。
*小结
在 Python 的设计理念中,对象的相等性是重复性的子问题:
__eq__()
定义了相等性,这决定了==
和in
操作符的结果。__eq__()
和__hash__()
定义了哈希计算中的重复性,这进一步决定了它是否可作为集合set
的元素,或者是字典dict
的 key。
其次,在遍历列表或切片时,避免意外的引用共享,抑或无意中破坏了它,导致设计出的程序与预期不符。
最后是对不可变的理解。数值和字符串的不可变,相等性,重复性都是直观的,而元组的不可变指引用不可变,但元素内部的状态仍然是可变的。为了避免意外的麻烦,如果要将某个元组作为字典的 key
,则需使内部所有对象元素都是 hashable type。
额外地,和 Java 不同,Python 的引用相等性,是靠 id()
全局标识决定的,这决定了 is
操作符的结果。它和哈希 __hash__()
函数是两回事。
作者:花花子
来源:稀土掘金