[译] 不用 Class,如何写一个类

不用 Class,如何写一个类

前言

Python 的对象模型使人难以置信的强大;实际上,你能够重写全部(对象),或者向任何人分发奇怪的对象,并让他们像对待正常的对象的那样接受它。前端

Python 的面向对象是 smalltalk 面向对象的一个后裔。在 Python 中,一切都是对象,甚至对象集和对象类型都是如此;特别的,函数也是对象。这让我很好奇:不使用类建立一个类是否可能?android

代码

这个想法的关键性代码以下所示。这是一个很基础的实现,但它支持 __call__ 这样的边缘状况(但不支持其余魔术方法,由于他们须要加载依赖)。后文将会解说。ios

有没有搞错?

这是一些很先进的 Python 轮子,它用一种和对象的设计初衷毫不相同的方法使用了一些对象。咱们将分段解说代码。git

第一个 helper

def _suspend_self(namespace, suspended):  
复制代码

这是个让人有点惧怕的函数名。暂停?这可很差,但咱们是能够解决问题的。_suspend_self 函数是 functools.partial 的一个简单应用,它的工做原理是:经过从外部函数做用域中捕获 namespace,并把它悬停在内部函数中。github

def suspender(*args, **kwargs):
        return suspended(namespace, *args, **kwargs)
复制代码

接下来,这个内部的函数调用了和第一个参数 namespace 一块儿传递进来的函数 suspended,实际上这是将方法又包了一层,这样它就能够应用在一个普通的 Python 类上。_suspend_self 余下的部分就只是设置一些属性,这些属性在某些时候可能会被映射(reflection)用到(我可能漏掉一些内容)。后端

猛兽(beast)

下一个函数是 make_class。从它的签名中咱们能知道什么?bash

def make_class(locals: dict):  
    """ 在被调用者的本地建立一个类。 参数 locals:创建类的本地。 """
复制代码

若是其余方法请求或者直接取得了你的本地变量,可不是什么好事。一般状况下,这是为了在以前的栈中搜索什么东西,或者就是在黑你的本机。咱们当前的实例属于前面一种,搜索本地函数并加入到类中。函数

# 试着找到一个 `__call__` 来执行 call 函数
    # 它将做为一个函数,这样命名空间和被调用者能够引用彼此
    def call_maker():
        if '__call__' in locals and callable(locals['__call__']):
            return _suspend_self(namespace, locals['__call__'])

        def _not_callable(*args, **kwargs):
            raise TypeError('This is not callable')

        return _not_callable
复制代码

这个函数至关简单,它是一个将函数做为返回值的函数! 它实际上作了以下这些事:区块链

  • 在函数类中检查你是否已经定义过 __call__
  • 若是有,就像上文介绍过的那样,用 _suspend_self 函数“挂载” namespace 来用 __call__ 生成一个方法。
  • 若是没有,就和默认的 __call__ 同样,返回一个会发起错误的桩函数(stub function)。

命名空间 namespace

namespace 是关键的部分,然而我尚未解说。类中的每个(或者绝大部分)方法都会将 self 做为第一个参数,这个 self 就是函数运行的时候类的实例。ui

一个类的实例实际上就是一个你能够用 . 符号而不是数字索引访问其内容的字典。因此须要一个能够传入咱们指望的函数的对象来模仿这个字典。因而咱们就说,这个实例是一个 namespace,咱们在 namespace 上设置变量等等。后文提到 namespace 的地方,就把它看成咱们的实例。经过调用类的对象自身,你能够获取这个类的实例:obb = SomeClass()

标准的建立点式访问的字典的方法是 attrdict:

attrdict = type("attrdict", (dict,), {"__getattr__": dict.__getitem__, "__setattr__": dict.__setitem__})  
复制代码

可是既然它建立了一个类,这就有点欺骗性了。其余的方法包括 typing.SimpleNamespace,或者建立一个无哨兵(sentinel)的类。可是这两种方法都仍是欺骗性的建立了类,咱们都不能用。

解决方案

namespace 的解决方案是另外一个函数。函数的行为能够像可调用的点式访问字典,因此咱们就简单的建立一个 namespace 函数,假设它就是 self。

# 这个就充当了 self 对象
    # 全部的属性都创建在此之上
    def namespace():
        return called()
复制代码

须要注意调用 called() 的用法 - 这是为了正常模拟实例上 __call__ 的行为。

建立 __init__

Python 中的全部类都有 __init__(不包括默认提供空 init 的类),因此咱们须要去模仿这一点并确保用户定义的 init 被调用。

# 建立一个 init 的替代方法
    def new_class(*args, **kwargs):
        init = locals.get("__init__")
        if init is not None:
            init(namespace, *args, **kwargs)

        return namespace
复制代码

这段代码就是简单的从本地获取用户定义的 __init__,若是找到了,就调用它。而后,它返回 namespace(就是假的实例),有效地模拟了循环:(metaclass.)__call__ -> __new__ -> __init__

清理

接下来要作的就是在类的基础上建立方法,这能够用超级简单的循环扫描来完成:

# 更新 namespace
    for name, item in locals.items():
        if callable(item):
            fn = _suspend_self(namespace, item)
            setattr(namespace, name, fn)
复制代码

和上文提到的类似,全部可调用的函数都被 _suspend_self 包裹来将函数变成类的方法,在 namespace 完成设置。

获取到类

最后要作的就是简单的 return new_class。获取到类的实例的最后一轮循环是:

  • 用户的代码定义了一个类函数
  • 当类函数被调用,该函数调用 make_class 来设置 namespace(添加 @make 修饰符,这一步就能自动完成)
  • make_class 函数设置实例,使其为后续的初始化作好准备
  • make_class 函数返回另外一个函数,调用这个函数就能获取到实例并完成它的初始化。

如今咱们就获得它了,一个彻底没用类的类。打赌你会实际应用它。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索