好久不见,再一次回到 treevalue 系列。本文将基于上一篇treevalue讲解,继续对函数的树化机制进行详细解析,并且会更多的讲述其衍生特性及应用。

树化方法与类方法

首先,基于之前的树化函数,我们可以对一般意义上的函数进行树化扩展。而对“函数”这一范畴来说,其中自然也包含方法、类方法这两种特殊的函数,它们在本质上和一般函数是类似的(关于这部分可以阅读Python科普系列——类与方法(下篇)中“对象方法的本质”章节作进一步的了解)。也正是因为它们之间的相似性,所以无论是对象方法还是类方法,同样都是可以被扩展的

基于上面所述的方法、类方法的性质,我们可以对其进行类似的树化扩展。让我们来看一个例子

from treevalue import TreeValue, method_treelize, classmethod_treelize

class 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 --> 6 print(t1.plus(t2))
# <MyTreeValue 0x7fe023375eb0>
# ├── a --> 6
# ├── b --> 8
# └── x --> <MyTreeValue 0x7fe021dd16a0>
#     ├── c --> 10
#     └── d --> 12 print(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_treelize

class MyTreeValue(TreeValue):
    @method_treelize(self_copy=True)
    def plus_(self, x):
        return self + x t1 = MyTreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}}) print(t1)
# <MyTreeValue 0x7f543c83cd60>
# ├── a --> 1
# ├── b --> 2
# └── x --> <MyTreeValue 0x7f543c83cd00>
#     ├── c --> 3
#     └── d --> 4 print(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_treelize

class 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 --> 3
print(t1 + 2)
# <AddTreeValue 0x7ff25d72caf0>
# ├── a --> 3
# └── x --> <AddTreeValue 0x7ff25c17aa90>
#     └── c --> 5
print(3 + t1)
# <AddTreeValue 0x7ff25d72caf0>
# ├── a --> 4
# └── x --> <AddTreeValue 0x7ff25c17aa90>
#     └── c --> 6
print(t1 + t2)
# <AddTreeValue 0x7ff25d72caf0>
# ├── a --> 6
# └── x --> <AddTreeValue 0x7ff25c17aa90>
#     └── c --> 10 t1 += t2 + 10
print(t1)
# <AddTreeValue 0x7ff25d729e50>
# ├── a --> 16
# └── x --> <AddTreeValue 0x7ff25d729e20>
#     └── c --> 20

不仅如此,笔者作为treevalue的开发者也同样是这么想的。于是这里提供了一个基于TreeValue,并提供了更多常用功能和运算,使之更加快捷易用的子类——FastTreeValue 。这个类从本系列的第一弹以来已经多次出场,在这里我们终于得以揭晓其真正的奥秘。FastTreeValue类中,诸如上述的各类算术运算已经以类似的方式进行了实现,并可供使用。例如下面的这段代码

from treevalue import FastTreeValue

t1 = 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 --> 7 t3 = 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.3 t5 = 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 --> 9199
print(t5 & t6)
# <FastTreeValue 0x7f973be1e640>
# ├── a --> {2}
# └── x --> <FastTreeValue 0x7f973be1e8e0>
#     └── c --> 648

至此,常规的算术运算已经被覆盖,而且由于python对算术运算的支持方式,算术运算也并不受限于值的类型,而是可以广泛地支持各种类型的运算。

延伸思考3:结合Python科普系列——类与方法(下篇)中“魔术方法的妙用”部分,想一想此类算术运算魔术方法各自应该被如何实现?然后去翻阅一下treevalue的源码验证你的猜想。

欢迎评论区讨论!

基于树化运算的应用

实际上,python中以下划线开头和结尾的特殊运算并不只有上述的算术运算,还有一系列的操作类也一样可以被用类似的方式扩展。其中最为典型的就是对于功能性的魔术方法所做的扩展,比如,我们可以对 __getitem____setitem__ 进行扩展,如下所示

from treevalue import TreeValue, method_treelize

class MyTreeValue(TreeValue):
    @method_treelize()
    def __getitem__(self, item):
        return self[item]     @method_treelize()
    def __setitem__(self, key, value):
        self[key] = value

FastTreeValue 中也有类似的实现,由此可以产生的一个效果是这样的,通过索引即可快速对下属的所有对象进行访问,代码如下

import torch

from treevalue import FastTreeValue

t1 = 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_treelize

class MyTreeValue(TreeValue):
    @method_treelize()
    def _attr_extern(self, key):
        return getattr(self, key)

于是便可以实现类似这样的效果

import torch

from treevalue import FastTreeValue

t1 = 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_treelize

class MyTreeValue(TreeValue):
    @method_treelize()
    def __call__(self, *args, **kwargs):
        return self(*args, **kwargs)

FastTreeValue 中也作了类似的实现,因此上面获取到的那棵由对象方法构成的树,实际上是可以被执行的。而将对 _attr_extern__call__ 的扩展相结合,则可以形成这样一种更为奇妙的用法——直接对树对象执行其内部对象所包含的方法,如下所示

import torch

from treevalue import FastTreeValue

t1 = 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 sizes
new_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:除了上述例子中的 reshapesin ,以及 numpytorch 等计算库,还有哪些常见的库以及对象可以通过上述动态特性实现类似的效果?

延伸思考5:如果上述例子中不是 reshape ,而是类似sum这样的方法,并且在部分情况下可能希望获取到整棵树所有对象之和,这一需求该如何设计以满足?

延伸思考6:对于类似 sum 方法这样的情况,还有哪些运算是与之类似的?这些运算在逻辑上存在什么共同点?与 reshapesin 这样的方法在逻辑上的区别又在哪里?

欢迎评论区讨论!

后续预告

本文主要针对treevalue的核心特性——树化函数,对其在类方法、魔术方法等的具体应用进行了展示,受限于篇幅,只能对这些颇有亮点的特性进行展示。在下一篇中,我们将针对treevalue在numpy、torch等计算模型库的应用展开详解,并与同类产品进行对比与分析,敬请期待。

此外,欢迎欢迎了解OpenDILab的开源项目:

Treevalue(0x03)——函数树化详细解析(下篇)的更多相关文章

  1. Treevalue(0x02)——函数树化详细解析(上篇)

    本文将对 func_treelize 这一treevalue库中的核心功能进行详细的原理解析. 关于treevalue的概述,可以参考之前的文章:Treevalue(0x01)--功能概述 树化函数基 ...

  2. ClickHouse(10)ClickHouse合并树MergeTree家族表引擎之ReplacingMergeTree详细解析

    目录 建表语法 数据处理策略 资料分享 参考文章 MergeTree拥有主键,但是它的主键却没有唯一键的约束.这意味着即便多行数据的主键相同,它们还是能够被正常写入.在某些使用场合,用户并不希望数据表 ...

  3. Thrift之代码生成器Compiler原理及源码详细解析1

    我的新浪微博:http://weibo.com/freshairbrucewoo. 欢迎大家相互交流,共同提高技术. 又很久没有写博客了,最近忙着研究GlusterFS,本来周末打算写几篇博客的,但是 ...

  4. (MTT)连续能量函数最小化方法

    (MTT)连续能量函数最小化方法 Multitarget tracking Multi-object tracking 连续能量函数 读"A.Milan,S. Roth, K. Schind ...

  5. java类生命周期详细解析

    (一)详解java类的生命周期 引言 最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前 ...

  6. springmvc 项目完整示例06 日志–log4j 参数详细解析 log4j如何配置

    Log4j由三个重要的组件构成: 日志信息的优先级 日志信息的输出目的地 日志信息的输出格式 日志信息的优先级从高到低有ERROR.WARN. INFO.DEBUG,分别用来指定这条日志信息的重要程度 ...

  7. C++多态的实现及原理详细解析

    C++多态的实现及原理详细解析 作者: 字体:[增加 减小] 类型:转载   C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型 ...

  8. 单表扫描,MySQL索引选择不正确 并 详细解析OPTIMIZER_TRACE格式

    单表扫描,MySQL索引选择不正确 并 详细解析OPTIMIZER_TRACE格式     一 表结构如下:  万行 CREATE TABLE t_audit_operate_log (  Fid b ...

  9. Linux多线程编程详细解析----条件变量 pthread_cond_t

    Linux操作系统下的多线程编程详细解析----条件变量 1.初始化条件变量pthread_cond_init #include <pthread.h> int pthread_cond_ ...

随机推荐

  1. Python 实现断网自动重连

    为了实现 断网了,自动连接网络原理:每隔一段时间ping一下百度,判断网络状态,没有联网的话,就模仿浏览器发一条Post给服务器import urllibimport hashlibimport su ...

  2. scala基础篇---- Try finally不加catch的使用情形

    普通的try-catch-finally Try{ } catch{//不加catch向上抛出异常 case  _=> } finally{//一般是资源关闭 } 普通的try-finally ...

  3. 剑指offer:JZ8 二叉树的下一个结点

    JZ8 二叉树的下一个结点 描述 给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回.注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针.下图为一棵有9个节点的二叉 ...

  4. [Beta]the Agiles Scrum Meeting 9

    会议时间:2020.5.24 21:00 1.每个人的工作 今天已完成的工作 成员 已完成的工作 issue yjy 撰写技术博客 tq 实现评测机获取评测状态功能 评测部分增加更多评测指标 wjx ...

  5. 【二食堂】Alpha - Scrum Meeting 7

    Scrum Meeting 7 例会时间:4.17 11:40 - 12:00 进度情况 组员 昨日进度 今日任务 李健 1. 继续文本区域的开发,先完成目前简陋的添加方式,再区实现勾选功能issue ...

  6. 对mongo文档的增删改操作

    在mongo db 中增加.删除.修改文档有好多方法,这里简单记录一下我所知道的一些方法. 前置条件: 1.创建study数据库  use study; 2.创建persons集合,当第一次向pers ...

  7. STL模板

    目录 栈stack 队列queue 列表List 集合set 映射map 多重映射multimap 对pair 元组tuple 容器containers 算法algorithms 仿函数/函数对象fu ...

  8. Python课程笔记(三)

    1.python定义类.创建对象 class Myclass: # 定义Myclass类 def sum(self,x,y): self.x = x self.y = y return self.x+ ...

  9. 洛谷 P5658 [CSP-S2019] 括号树

    链接: P5658 分析: 显然我们应该在dfs树的同时维护每个点的答案. 注意到第 \(u\) 个点的答案可以分成两部分,不包含 \(u\) 点时的答案,和加入 \(u\) 点后新增的答案,前者可以 ...

  10. Less3

    继续第三关的学习 1.根据第一关的记录,我们判断出是什么注入 id=1' and '1'='1 id=1' and '1'='2 返回不同,所以存在字符型的注入 2. 这时候我们再用正常的报错猜解准备 ...