学Java,Java书籍的最佳阅读顺序
724 2023-04-03 03:52:53
好久不见,再一次回到 treevalue
系列。本文将基于上一篇treevalue讲解,继续对函数的树化机制进行详细解析,并且会更多的讲述其衍生特性及应用。
首先,基于之前的树化函数,我们可以对一般意义上的函数进行树化扩展。而对“函数”这一范畴来说,其中自然也包含方法、类方法这两种特殊的函数,它们在本质上和一般函数是类似的(关于这部分可以阅读Python科普系列——类与方法(下篇)中“对象方法的本质”章节作进一步的了解)。也正是因为它们之间的相似性,所以无论是对象方法还是类方法,同样都是可以被扩展的。
基于上面所述的方法、类方法的性质,我们可以对其进行类似的树化扩展。让我们来看一个例子
from treevalue import TreeValue, method_treelize, classmethod_treelizeclass MyTreeValue(TreeValue): @method_treelize() def plus(self, x): return self + x # with the usage of rise option, final return should be a tuple of 2 trees @classmethod @classmethod_treelize(rise=True) def add_all(cls, a, b): return cls, a + b
由此,我们构建了一个属于自己的TreeValue类—— MyTreeValue
类,并且可以使用内部的方法与类方法进行面向对象程序的编写。例如对于 MyTreeValue
类,我们可以执行以下的运算(代码接上文)
t1 = MyTreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})t2 = MyTreeValue({'a': 5, 'b': 6, 'x': {'c': 7, 'd': 8}})print(t1.plus(2))# <MyTreeValue 0x7fe023375ee0># ├── a --> 3# ├── b --> 4# └── x --> <MyTreeValue 0x7fe023375eb0># ├── c --> 5# └── d --> 6print(t1.plus(t2))# <MyTreeValue 0x7fe023375eb0># ├── a --> 6# ├── b --> 8# └── x --> <MyTreeValue 0x7fe021dd16a0># ├── c --> 10# └── d --> 12print(MyTreeValue.add_all(t1, t2))# (<MyTreeValue 0x7effa62c6250># ├── a --> <class '__main__.MyTreeValue'># ├── b --> <class '__main__.MyTreeValue'># └── x --> <MyTreeValue 0x7effa62a0790># ├── c --> <class '__main__.MyTreeValue'># └── d --> <class '__main__.MyTreeValue'># , <MyTreeValue 0x7effa629df70># ├── a --> 6# ├── b --> 8# └── x --> <MyTreeValue 0x7effa62c6d90># ├── c --> 10# └── d --> 12# )
此外,对于对象方法,显然存在一个运算主体,也就是 self
,并且常常会出现需要进行“原地运算”的情况,类似于torch
库里面的sin_
。在针对对象方法的树化函数中,我们提供了 self_copy
选项,当开启此选项时,计算完毕后会将各个节点上的运行结果挂载至当前的树对象上,并将其作为返回值传出。一个简单的例子如下
from treevalue import TreeValue, method_treelizeclass MyTreeValue(TreeValue): @method_treelize(self_copy=True) def plus_(self, x): return self + xt1 = MyTreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})print(t1)# <MyTreeValue 0x7f543c83cd60># ├── a --> 1# ├── b --> 2# └── x --> <MyTreeValue 0x7f543c83cd00># ├── c --> 3# └── d --> 4print(t1.plus_(2))# <MyTreeValue 0x7f543c83cd60># ├── a --> 3# ├── b --> 4# └── x --> <MyTreeValue 0x7f543c83cd00># ├── c --> 5# └── d --> 6
在上述代码中,可以看到 plus_
方法的返回值仍是之前的树对象,且内部的节点值均被替换为了计算结果值,此时如果访问树 t1
,得到的也将会是这一对象。
延伸思考1:对于静态方法,应该如何进行树化?请通过编写代码验证你的猜想。
延伸思考2:对于属性(property),仅考虑读取( __get__
)功能的话,该如何进行树化?请通过代码验证你的猜想。
欢迎评论区讨论!
如果你对算术运算的原理有所了解的话,应该知道在python中,算术运算也同样是由一类特殊的对象方法支持的,例如加法运算是由 __add__
(self + x)、 __radd__
(x + self)和 __iadd__
(self += x)运算所共同支持的,而对运算符的重载也往往是通过此类魔术方法实现的。关于这部分,可以阅读Python科普系列——类与方法(下篇)中“魔术方法的妙用”章节作更进一步的了解。
既然如此,不妨想一想,如果将树化函数用在这类特殊的方法上,会产生什么样的奇妙效果呢?没错,如你所想,这类运算一样是可以被扩展的,而效果就会像是如下的代码所示
from treevalue import TreeValue, method_treelizeclass AddTreeValue(TreeValue): @method_treelize() def __add__(self, other): return self + other @method_treelize() def __radd__(self, other): return other + self @method_treelize(self_copy=True) def __iadd__(self, other): return self + other
运行起来的效果如下所示
t1 = AddTreeValue({'a': 1, 'x': {'c': 3}})t2 = AddTreeValue({'a': 5, 'x': {'c': 7}})print(t1)# <AddTreeValue 0x7ff25d729e50># ├── a --> 1# └── x --> <AddTreeValue 0x7ff25d729e20># └── c --> 3print(t1 + 2)# <AddTreeValue 0x7ff25d72caf0># ├── a --> 3# └── x --> <AddTreeValue 0x7ff25c17aa90># └── c --> 5print(3 + t1)# <AddTreeValue 0x7ff25d72caf0># ├── a --> 4# └── x --> <AddTreeValue 0x7ff25c17aa90># └── c --> 6print(t1 + t2)# <AddTreeValue 0x7ff25d72caf0># ├── a --> 6# └── x --> <AddTreeValue 0x7ff25c17aa90># └── c --> 10t1 += t2 + 10print(t1)# <AddTreeValue 0x7ff25d729e50># ├── a --> 16# └── x --> <AddTreeValue 0x7ff25d729e20># └── c --> 20
不仅如此,笔者作为treevalue的开发者也同样是这么想的。于是这里提供了一个基于TreeValue
,并提供了更多常用功能和运算,使之更加快捷易用的子类——FastTreeValue
。这个类从本系列的第一弹以来已经多次出场,在这里我们终于得以揭晓其真正的奥秘。在FastTreeValue
类中,诸如上述的各类算术运算已经以类似的方式进行了实现,并可供使用。例如下面的这段代码
from treevalue import FastTreeValuet1 = FastTreeValue({'a': 1, 'x': {'c': 3}})t2 = FastTreeValue({'a': 5, 'x': {'c': 7}})print(t1 * (1 - t1 + t2) % 10 + (t2 // t1)) # complex calculation# <FastTreeValue 0x7f973be1eaf0># ├── a --> 10# └── x --> <FastTreeValue 0x7f973be1ea00># └── c --> 7t3 = FastTreeValue({'a': 1, 'b': 'sdjkfh', 'x': {'c': [1, 2], 'd': 1.2}})t4 = FastTreeValue({'a': 4, 'b': 'anstr', 'x': {'c': [4, 5, -2], 'd': -8.5}})print(t3 + t4) # add all together, not only int or float# <FastTreeValue 0x7f973be1e970># ├── a --> 5# ├── b --> 'sdjkfhanstr'# └── x --> <FastTreeValue 0x7f973be1eac0># ├── c --> [1, 2, 4, 5, -2]# └── d --> -7.3t5 = FastTreeValue({'a': {2, 3}, 'x': {'c': 8937}})t6 = FastTreeValue({'a': {1, 2, 4}, 'x': {'c': 910}})print(t5 | t6) # | and &, between sets and ints# <FastTreeValue 0x7f973be1e640># ├── a --> {1, 2, 3, 4}# └── x --> <FastTreeValue 0x7f973be1e8e0># └── c --> 9199print(t5 & t6)# <FastTreeValue 0x7f973be1e640># ├── a --> {2}# └── x --> <FastTreeValue 0x7f973be1e8e0># └── c --> 648
至此,常规的算术运算已经被覆盖,而且由于python对算术运算的支持方式,算术运算也并不受限于值的类型,而是可以广泛地支持各种类型的运算。
延伸思考3:结合Python科普系列——类与方法(下篇)中“魔术方法的妙用”部分,想一想此类算术运算魔术方法各自应该被如何实现?然后去翻阅一下treevalue的源码验证你的猜想。
欢迎评论区讨论!
实际上,python中以下划线开头和结尾的特殊运算并不只有上述的算术运算,还有一系列的操作类也一样可以被用类似的方式扩展。其中最为典型的就是对于功能性的魔术方法所做的扩展,比如,我们可以对 __getitem__
、 __setitem__
进行扩展,如下所示
from treevalue import TreeValue, method_treelizeclass MyTreeValue(TreeValue): @method_treelize() def __getitem__(self, item): return self[item] @method_treelize() def __setitem__(self, key, value): self[key] = value
在 FastTreeValue
中也有类似的实现,由此可以产生的一个效果是这样的,通过索引即可快速对下属的所有对象进行访问,代码如下
import torchfrom treevalue import FastTreeValuet1 = FastTreeValue({ 'a': torch.randn(2, 3), 'x': { 'c': torch.randn(3, 4), }})print(t1)# <FastTreeValue 0x7f93f19b9c40># ├── a --> tensor([[-0.5878, 0.8615, -0.1703],# │ [ 1.5826, -0.5806, 1.5869]])# └── x --> <FastTreeValue 0x7f93f19b9d00># └── c --> tensor([[-0.3380, -0.6968, 0.7013, -0.8895],# [-0.2798, 0.6196, 0.8141, -2.5651],# [ 0.0113, -2.0468, 0.1121, 0.3606]])print(t1[0])# <FastTreeValue 0x7f93f19b9d30># ├── a --> tensor([-0.5878, 0.8615, -0.1703])# └── x --> <FastTreeValue 0x7f93901c1fd0># └── c --> tensor([-0.3380, -0.6968, 0.7013, -0.8895])print(t1[:, 1:-1])# <FastTreeValue 0x7f93f19b9d30># ├── a --> tensor([[ 0.8615],# │ [-0.5806]])# └── x --> <FastTreeValue 0x7f93901c1fd0># └── c --> tensor([[-0.6968, 0.7013],# [ 0.6196, 0.8141],# [-2.0468, 0.1121]])
除此之外,在TreeValue
类中,预留了一个_attr_extern
方法,当尝试获取TreeValue
对象包含的值时,一般通过直接访问属性实现,而当当前树节点无此键时,则会进入_attr_extern
方法。在原生的 TreeValue
类中,这一方法被实现为直接抛出 KeyError
异常,而在 FastTreeValue
中进行了类似这样的扩展(仅示意,与真实实现略有差异)
from treevalue import TreeValue, method_treelizeclass MyTreeValue(TreeValue): @method_treelize() def _attr_extern(self, key): return getattr(self, key)
于是便可以实现类似这样的效果
import torchfrom treevalue import FastTreeValuet1 = FastTreeValue({ 'a': torch.randn(2, 3), 'x': { 'c': torch.randn(3, 4), }})print(t1.shape)# <FastTreeValue 0x7fac48ac66d0># ├── a --> torch.Size([2, 3])# └── x --> <FastTreeValue 0x7fac48ac6700># └── c --> torch.Size([3, 4])print(t1.sin)# <FastTreeValue 0x7f0fcd0e36a0># ├── a --> <built-in method sin of Tensor object at 0x7f0fcd0ea040># └── x --> <FastTreeValue 0x7f0fcd0e3df0># └── c --> <built-in method sin of Tensor object at 0x7f0fcd0ea080>
可以看到,不仅一般意义上的属性(例如 shape
)可以被获取并构建成树,连对象的方法也被以同样的方式进行了提取构造。这是因为在Python中,实际上属性这一概念(更准确的说法是字段,英文为Field)包含的内容有很多,其中包括方法(具体可以参考Python科普系列——类与方法(上篇)中“如何手动制造一个对象”章节作进一步了解),基于这一点,通过与上述代码类似的方式,我们可以获得一棵由对象方法构成的树,即如上述的 sin
方法一样。
说到这里,我们可以继续去扩展一个魔术方法—— __call__
方法,这个方法的作用是让对象可以被以类似函数调用的方式直接运行。重载的方式如下所示
from treevalue import TreeValue, method_treelizeclass MyTreeValue(TreeValue): @method_treelize() def __call__(self, *args, **kwargs): return self(*args, **kwargs)
在 FastTreeValue
中也作了类似的实现,因此上面获取到的那棵由对象方法构成的树,实际上是可以被执行的。而将对 _attr_extern
与 __call__
的扩展相结合,则可以形成这样一种更为奇妙的用法——直接对树对象执行其内部对象所包含的方法,如下所示
import torchfrom treevalue import FastTreeValuet1 = FastTreeValue({ 'a': torch.randn(2, 4), 'x': { 'c': torch.randn(3, 4), }})print(t1)# <FastTreeValue 0x7f7e7534bc40># ├── a --> tensor([[ 1.4246, 0.4117, -1.1805, 0.1825],# │ [ 0.5865, -0.8895, -0.8055, 0.9112]])# └── x --> <FastTreeValue 0x7f7e7534bd00># └── c --> tensor([[ 1.6239e+00, -2.3074e+00, -2.8613e-01, 1.3310e+00],# [-1.8917e-01, 1.6694e+00, -8.2944e-01, 2.8590e-01],# [-4.0992e-01, -5.8827e-01, 2.0444e-03, 7.0647e-01]])print(t1.sin())# <FastTreeValue 0x7f7e7534bd30># ├── a --> tensor([[ 0.9893, 0.4002, -0.9248, 0.1814],# │ [ 0.5534, -0.7768, -0.7212, 0.7902]])# └── x --> <FastTreeValue 0x7f7e7534bd60># └── c --> tensor([[ 0.9986, -0.7407, -0.2822, 0.9714],# [-0.1880, 0.9951, -0.7376, 0.2820],# [-0.3985, -0.5549, 0.0020, 0.6491]])print(t1.reshape((4, -1)))# <FastTreeValue 0x7f7e13b43fa0># ├── a --> tensor([[ 1.4246, 0.4117],# │ [-1.1805, 0.1825],# │ [ 0.5865, -0.8895],# │ [-0.8055, 0.9112]])# └── x --> <FastTreeValue 0x7f7e7534bd30># └── c --> tensor([[ 1.6239e+00, -2.3074e+00, -2.8613e-01],# [ 1.3310e+00, -1.8917e-01, 1.6694e+00],# [-8.2944e-01, 2.8590e-01, -4.0992e-01],# [-5.8827e-01, 2.0444e-03, 7.0647e-01]])# different sizesnew_shapes = FastTreeValue({'a': (1, -1), 'x': {'c': (2, -1)}})print(t1.reshape(new_shapes))# <FastTreeValue 0x7f98d95241f0># ├── a --> tensor([[ 2.0423, -0.5339, -0.4458, -0.3386, 0.1002, 0.6809, -0.3839, 1.9945]])# └── x --> <FastTreeValue 0x7f993b3e3d30># └── c --> tensor([[ 0.9726, 0.2787, 1.2419, -0.4118, 2.2535, -0.7826],# [-0.9467, 0.3230, -0.6319, -0.2424, 0.4348, 1.3872]])
可能读者还会有些懵,这里以上面的 reshape
为例,解释一下其运行机理:
t1.reshape
,进入已经被树化的 _attr_extern
方法,获取到一棵由方法对象组成的树,设为 t1_m
。t1_m((4, -1))
,进入已经被树化的 __call__
方法,通过对树内各个方法的运行与对返回值的组装,形成一棵由最终结果组成的树,即为 t1.reshape((4, -1))
。有了这样的功能,实际上整个 treevalue
已经足以实现非常丰富且灵活的功能,并且简单易懂,易于维护。而针对torch进行了专用树化封装库 treetensor
,目前也已经发布,感兴趣可以去作进一步的了解:opendilab / DI-treetensor。
延伸思考4:除了上述例子中的 reshape
、 sin
,以及 numpy
、 torch
等计算库,还有哪些常见的库以及对象可以通过上述动态特性实现类似的效果?
延伸思考5:如果上述例子中不是 reshape
,而是类似sum
这样的方法,并且在部分情况下可能希望获取到整棵树所有对象之和,这一需求该如何设计以满足?
延伸思考6:对于类似 sum
方法这样的情况,还有哪些运算是与之类似的?这些运算在逻辑上存在什么共同点?与 reshape
、 sin
这样的方法在逻辑上的区别又在哪里?
欢迎评论区讨论!
本文主要针对treevalue的核心特性——树化函数,对其在类方法、魔术方法等的具体应用进行了展示,受限于篇幅,只能对这些颇有亮点的特性进行展示。在下一篇中,我们将针对treevalue在numpy、torch等计算模型库的应用展开详解,并与同类产品进行对比与分析,敬请期待。
此外,欢迎欢迎了解OpenDILab的开源项目: