如何在Python中创建一个透明的属性别名

179 阅读3分钟

当处理不断发展的API时,在一个类中重命名一个属性可能是有用的,但为了向后兼容而保留旧的名字。 这意味着使一个属性成为另一个属性的别名。 在这篇文章中,我们将看一下实现这一目的的两种方法。

使用@property

实现这一目标的一个方法是用 @property例如,这里有一个Widget 类,其中cyclesrotations 的别名:

    def __init__(self, rotations: int) -> None:
        self.rotations = rotations

    @property
    def cycles(self) -> int:
        return self.rotations

    @cycles.setter
    def cycles(self, value: int) -> None:
        self.rotations = value

用以下方法检查这个 ipython -i example.py:


In [1]: widget = Widget(1337)

In [2]: widget.cycles
Out[2]: 1337

In [3]: widget.cycles = 9001

In [4]: widget.cycles
Out[4]: 9001

In [5]: widget.rotations
Out[5]: 9001

好了,这就完成了任务,我们可以在这里结束这篇文章......但我们不

(一个完全透明的别名也会有一个删除方法来处理很少使用的del 语句)。

这种方法的缺点是它的重复性。 如果我们想要很多别名,我们必须为每个别名写几行类似的属性函数。 这感觉就像我们被困在Java土地上,浪费了我们写简单的getters和setters的时间。 然后如果我们想在测试中获得100%的覆盖率,我们必须独立测试每个方法。

Python 是否提供了一种机制来避免这种抄袭? 是的,是的,它提供了。

进入描述符

我们可以使用描述符来实现别名。 描述符是一种特殊的协议,可以在属性访问过程中运行额外的处理。 这通过三个特殊的方法使用。

  • __get__ 拦截访问
  • __set__ 拦截分配
  • __delete__ 拦截删除

即使你没有创建过描述符,如果你已经使用了一段时间的Python,你肯定已经使用了一个。 我们甚至在上面使用了一个。描述符协议为@property ,以及@classmethod@cached_property ,以及更多。

我们可以像这样创建一个别名描述符类:

    def __init__(self, source_name):
        self.source_name = source_name

    def __get__(self, obj, objtype=None):
        if obj is None:
            # Class lookup, return descriptor
            return self
        return getattr(obj, self.source_name)

    def __set__(self, obj, value):
        setattr(obj, self.source_name, value)

source_name 是要别名的属性名称,我们把它存储在描述符实例中。我们的 和 方法然后代理获取/设置实例上的底层属性 ( )。 也可以在类上调用,在这种情况下,我们通常返回描述符。__get__ __set__``obj``__get__

(同样,为了完整起见,我们也会通过添加一个__delete__ 方法来处理属性的删除)。

我们可以像这样使用我们的描述符:

    def __init__(self, rotations: int) -> None:
        self.rotations = rotations

    cycles = Alias("rotations")
    turns = Alias("rotations")

cyclesturns 中的每一个都是我们的描述符类的一个实例。当触摸Widget 类上的别名属性或其实例时,Python 看到它们是描述符并运行适当的方法。

在操作中,这看起来与上面的情况类似:


In [2]: widget.turns
Out[2]: 1024

In [3]: widget.cycles
Out[3]: 1024

In [4]: widget.turns = 2048

In [5]: widget.rotations
Out[5]: 2048

In [6]: widget.cycles
Out[6]: 2048

In [7]: widget.turns
Out[7]: 2048

看起来很合法!

这种方法为我们节省了很多行代码。而且我们可以单独测试我们的Alias 类一次,而不是为每个别名写很多测试。

但也有一个缺点。

精明的读者会注意到我们在每个例子中都使用了类型提示,除了我们的Alias 类。这是因为为我们的描述符编写正确的类型提示是......非同小可的。 它需要几个高级类型特征。Generic,TypeVar, 和@overload ,导致更多的代码行。

如果你使用类型提示,你可能更愿意坚持使用verbose@property 方法,因为类型检查器很容易识别和验证这些类型。