初学者会对这样的所谓『语法』感到困惑:
"Hello%s" % 'World'
而实际上,『字符串能够用%拼接』这个功能并不是Python本身的语法规定。本文将借此对Python的运算符加以讨论。
==================================================================================================================================
我们知道在C语言中,计算两个数的加法使用运算符『+』,C语言的编译器会将其转换为机器指令,由CPU的运算电路直接完成加法。而在C++等面向对象的高级语言中,又出现了『运算符重载』,使得字面意义上的『+』可以支持自定义的任何操作。最典型的应用就是字符串的拼接:使用 string1 + string2 的写法要比 concat_string(string1,string2) 的写法简单直观。
而Python作为一种面向对象的语言,它的所有『变量』都是一个对象的引用,即便是简单的数字都代表了一个对象。比如说我想知道数字100的二进制的表示有多少位:
print(100 .bit_length()) #结果为7,注意.前面需要有一个空格
自然,Python的运算符便不那么简单了。概括的说,Python的运算符没有『实质功能』,它们仅仅是调用被操作变量的某些成员函数罢了。换句话说,变量能不能『运算』,和Python是否支持相应的语法无关,而是要问这变量自己是否支持对应的运算。
1. 一元操作符
先用一元操作符『~』举例。这个操作符在文档中的含义是『返回对被操作数字二进制逐位取反的值』,例如:
print(~7) #-8
但实际上,『按位取反』这个操作,并不是『~』的功能。这个功能之所以实现,是因为7这个数字『可以被按位取反』:
print(7 .__invert__()) #-8
7是一个int类型的实例(的引用),故『__invert__』实际上是int类型的功能。
print(int.__invert__(7)) #-8
在Python的源代码(Objects/intobject.c)中可以找到这样几行:
static PyObject *
int_invert(PyIntObject *v)
{
return PyInt_FromLong(~v->ob_ival);
}
(注:本文中使用Python源代码皆为官方实现CPython)
在这里,invert函数调用C运算符~,完成最终的按位取反操作。
在Python代码被编译、执行的时候,『~7』这样的代码会最终转换为『int.__invert__(7)』这样的操作。具体说来,『~7』会编译成字节码指令『UNARY_INVERT』,而在Python虚拟机执行字节码时,会对UNARY_INVERT调用Python API函数PyNumber_Invert。而PyNumber_Invert会寻找被操作变量的__invert__成员函数,如果找到了就加以执行,否则抛异常。
Python还有一个内建的模块operator,它有一个函数invert。这个内建模块是C实现的(源代码:Modules/operator.c),它的invert函数也是调用了PyNumber_Invert这个API。
这样说来,如果想让一个变量支持『~』操作,只需要实现__invert__函数即可。比如我们随便写一个类:
class X(object):
def __invert__(self):
return 42
x = X()
print(~x) #42
__invert__是一个成员函数,它的第一个参数self即指向对象本身:
class X(object):
def __invert__(self):
return self.name
x = X()
x.name = 'fake result'
print(~x) # fake result
2. 二元运算符
和一元运算符一样,每个二元运算符实际上也会执行『被运算』变量的对应成员函数。与一元运算符不同的是,有算数意义的二元运算符有左右之分。
以加法『+』为例,它对应的函数是『左侧加法』__add__和『右侧加法』__radd__之分。
必须加以指出的是,在Python官方实现CPython中,如果『+』两侧的操作数都是整数、或者都是字符串的时候,并不会调用int.__add__或者str.__add__,而是直接进行快速计算,以提升性能。
除了快速计算的情形之外,无论是使用运算符『+』,还是调用operator.add()函数,最终会落到API函数PyNumber_Add(x,y)之上。它会尝试调用 x.__add__(y),如果抛出了NotImplemented异常,即对象x没有实现__add__函数,它会尝试调用y.__radd__(x)并作为最终结果。
作为理解和练习,读者可尝试运行下面的例子。
class A(object):
def __add__(self,other):
print ('A.__add__')
print (id(other))
class B(object):
def __radd__(self,other):
print ('B.__radd__')
print (id(other))
a = A()
b = B()
c = object()
print (id(a),id(b),id(c))
a + c
c + b
a + b
对于有算数含义的二元运算符/内建运算函数(+, -, *, /, %, divmod(), pow(), **, <>, &, ^, |) ,都有左侧、右侧运算之分。除了pow()(对应__pow__()和__rpow__())外,这些操作符在执行的时候都进行『先左后右』的尝试。其他的二元运算符,例如[](对应__setitem__)没有左右之分。
对于运算-赋值操作符(+=, -=, *=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=),以x += y 为例,在进行『+』之前,x.__iadd__(y) 这个函数会优先调用(在非快速计算的前提下)。如果__ixxx__出现了未实现异常,恢复为 x = x + y。
3. str.__mod__
回到开头的问题,之所以可以有 "Hello %s" % 'World' 这样的用法,是因为str这个类型『重载』了__mod__:
print ("Hello%s".__mod__('World'))
当然,利用运算符重载,你自己也可以写出(只有你自己才知道的)类型运算。