Going Further with functions 函数也是对象
Python里的所有东西都是对象,函数也不例外。但和其他对象不同,函数不是由调用类生成的,而是由def 语句,告诉编译器来把接下来的代码编译好并绑定到函数名上。
函数属性
和Python里的其他对象一样,函数也有一个具体的类型(type)
。你可以利用dir()
检查函数的命名空间(namesapce
).
函数和方法调用
在函数的命名空间中可检查到__call__
方法。从名字上就可以看出这个方法与函数调用有关。Python解释器通过调用可调用对象(callable
)的__call__
方法来完成调用。实际上,我们可以直接调用可调用对象callable
的该方法来完成调用。
1 | >>>def f1(x): |
我们也可以自己定义包含__call__
方法的类。如此,该类的实例都可以被直接调用(像函数一样调用)。实际上,这是Python的一种机制,即:
1 | f(*args, **kargs) == f.__call__(*args, **kargs) |
我们来试试
1 | class Func: |
可以看出,当我们在类中定义了__call__
方法,它的实例就可以被调用。(Amos题外话,Python里面在object后跟上()表示对object的调用,跟上[],表示get object’s attributes[待议] )这些调用导致类中__call__
方法被调用,实例本身作为第一个变量,紧接着实例调用时的位置参数和关键字参数。方法一般被定义在类中。当然,我们也可以在实例的命名空间中绑定可调用对象callable
,但解释器并不会认为这是一个真正的方法,因而不会将实例本身作为第一个参数。因此,实例字典__dict__
中的可调用对象__callable__
接收的参数只有实例调用时的那些参数(实例本身并不会作为第一个参数)。
NOTE:
以双下划线开始和结束的方法永远不会在实例里面被查询,解释器会直接到类中去查询这些方法。因此尽管实例字典__dict__
中有键__call__
,它也会被忽略而去调用类的__call__
方法。
接着前面的Python会话:
1 | def userfunc(arg): |
Instance’s ‘magic’ method was ignored.
既然所有的可调用对象有__call__
方法,__call__
方法也是可调用的,那我们会怀疑它本身是否也含有__call__
方法。答案是肯定的。
1 | f2.__call__.__call__("Andrey") |
函数组合
因为函数是一级对象first-class
,他们可以作为函数的参数。假设$f$ 、$g$ 是两个函数,数学家定义了一种组合运算:$f*g$ ,即$f(g(x))$ 。换句话说,组合两个函数为一个新的函数,其效果就是将第二个函数的输出作为第一个函数的输入。
如果给定了两个函数,我们能否构造出他们的组合呢?当然可以,我们可以写一个组装函数,以这两个函数为参数,在函数内部完成新函数的功能。然后组装函数返回这个组合函数。
1 | def compose(g, h): |
尽管这样做能够非常直接地将两个函数组合起来,但对数学家而言,用星号将两个函数组合起来才比较自然。然而,尝试函数相乘注定失败,因为Python没有设计两个函数相乘。如果我们为函数添加一个__mul__
方法,或许我们还有点机会。但是对函数实例而言不可能,因为函数类是C写的内建对象:*不可能更改、很难被继承。尽管我们可以写代码来继承函数类型,但是我们没有办法创建函数实例,因为def
语句一定会忽略你的子类而创建常规类型的函数。
虽然不可以继承函数类型,但是有方法可以创建可以 调用(call)的类实例。于是乎可以将类实例作为函数代理,在内部完成我们想要的功能。这个类要定义一个__mul__
方法,接受另外一个类似的类实例作为参数并返回一个新的可以调用的类。
1 | class Composable: |
另外一种实现__mul__()
方法的方式是 return self(other(x))
,哪一种更好呢?
这不是一个好的实现方式。
假设以$f*g(x)$的方式调用。所给的方式调用顺序为:以参数$x$调用$g$ ,然后以其输出调用
self
即$f$ ,并将结果返回。除了变量$x$必须要提前预知外,每次调用的时候$f$ 、$g$ 都必须要显示地给出,或者说并没有组合函数$f$、 $g$为一个新的函数C。如果以代码块中地方式,$x$不必提前给出,而且获得了一个$f$、 $g$的组合函数C,$f$ 、$g$不再需要了😊。
λ 匿名函数
Python有一个特性是允许将简单函数定义为表达式。lambda表达式就是一种无须def
关键字而描述函数的方式。它作为表达式的特点限制了可以在lambda上进行的操作。萝卜青菜,各有所爱。有的人用得多,有的人用得少。理解他们是必要的,因为不知道哪一天会撞见喜欢萝卜的人。
1 | f = lambda x, y: x*y === def f(x, y): return x*y |
尽管上面这种等价不完全,但就实际应用理解来说足够了。lambda关键字后紧跟着由逗号分开的参数的名字,再接着是表达式。参数和表达式由冒号分开。调用时会返回表达式的值。
1 | def compose(g, h): |
用lambda写出和前面compose()函数相同的功能很容易。最后一行表明对解释器而言,lambda表达式和函数完全等同。(lambda表达式和函数有相同的类型”
同时,lambda表达式没有名字(或者准确一点,它们有相同的名字)。当以def
的形式定义函数的时候,解释器会将def
语句中的名字存储为它的__name__
属性。所有的lambda表达式在创建的时候有相同的名字:’
最后,记住lambda表达式是故意限制在函数体只有一个表达式的函数。如果多行,不如写成一个具名函数,这样还可以用文档做注释。如果你确实想在多行中继续lambda表达式,最好的方式时用小括号将lambda表达式扩起来。
1 | def f1(x): |
上面这些东西会让你在写可调用对象时有更多的伸展空间:能更好地和解释器交互和更高效地解决问题。
Some Small Tips
(, /) ‘/‘ means preceding arguments are position only.
(, …, ) … means