如何为 Python 类中的非函数成员属性创建别名?

发布于 2024-09-28 23:27:07 字数 783 浏览 1 评论 0原文

我正在编写 Python 库 API,经常遇到这样的情况:用户希望相同的函数和变量有多个不同的名称。

如果我有一个带有函数 foo() 的 Python 类,并且我想为其创建一个名为 bar() 的别名,那非常简单:

class Dummy:
   
   def __init__(self):
      pass

   def foo(self):
      pass

   bar = foo

现在我可以使用没问题:

d = Dummy()
d.foo()
d.bar()

我想知道使用常规变量(例如字符串)而不是函数的类属性执行此操作的最佳方法是什么?如果我有这段代码:

d = Dummy()
print(d.x)
print(d.xValue)

我希望 dx 和 d.xValue 始终打印相同的内容。如果dx发生变化,它也应该改变d.xValue(反之亦然)。

我可以想到很多方法来做到这一点,但没有一个看起来像我想要的那么顺利:

  • 编写自定义注释
  • 使用 @property 注释并使用 setter
  • 覆盖 __setattr__ 类函数

以下哪种方法最好?或者还有别的办法吗?我不禁觉得,如果为函数创建别名如此容易,那么为任意变量创建别名也应该同样容易......

I'm in the midst of writing a Python library API and I often run into the scenario where my users want multiple different names for the same functions and variables.

If I have a Python class with the function foo() and I want to make an alias to it called bar(), that's super easy:

class Dummy:
   
   def __init__(self):
      pass

   def foo(self):
      pass

   bar = foo

Now I can do this with no problem:

d = Dummy()
d.foo()
d.bar()

What I'm wondering is what is the best way to do this with a class attribute that is a regular variable (e.g. a string) rather than a function? If I had this piece of code:

d = Dummy()
print(d.x)
print(d.xValue)

I want d.x and d.xValue to always print the same thing. If d.x changes, it should change d.xValue also (and vice-versa).

I can think of a number of ways to do this, but none of them seem as smooth as I'd like:

  • Write a custom annotation
  • Use the @property annotation and mess with the setter
  • Override the __setattr__ class functions

Which of these ways is best? Or is there another way? I can't help but feel that if it's so easy to make aliases for functions, it should be just as easy for arbitrary variables...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

殤城〤 2024-10-05 23:27:07

这可以通过与类方法完全相同的方式来解决。例如:

class Dummy:
    def __init__(self):
        self._x = 17

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, inp):
        self._x = inp

    @x.deleter
    def x(self):
        del self._x

    # Alias
    xValue = x

d = Dummy()
print(d.x, d.xValue)
#=> (17, 17)
d.x = 0
print(d.x, d.xValue)
#=> (0, 0)
d.xValue = 100
print(d.x, d.xValue)
#=> (100, 100)

这两个值将始终保持同步。您可以使用您喜欢的属性名称编写实际的属性代码,然后使用您需要的任何旧名称对其进行别名。

This can be solved in exactly the same way as with class methods. For example:

class Dummy:
    def __init__(self):
        self._x = 17

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, inp):
        self._x = inp

    @x.deleter
    def x(self):
        del self._x

    # Alias
    xValue = x

d = Dummy()
print(d.x, d.xValue)
#=> (17, 17)
d.x = 0
print(d.x, d.xValue)
#=> (0, 0)
d.xValue = 100
print(d.x, d.xValue)
#=> (100, 100)

The two values will always stay in sync. You write the actual property code with the attribute name you prefer, and then you alias it with whatever legacy name(s) you need.

烟花易冷人易散 2024-10-05 23:27:07

您可以提供 __setattr__ 和引用别名映射的 __getattr__ :

class Dummy:
    aliases = {
        'xValue': 'x',
        'another': 'x',
    }

    def __init__(self):
        self.x = 17

    def __setattr__(self, name, value):
        name = self.aliases.get(name, name)
        object.__setattr__(self, name, value)

    def __getattr__(self, name):
        if name == "aliases":
            raise AttributeError  # http://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html
        name = self.aliases.get(name, name)
        return object.__getattribute__(self, name)


d = Dummy()
assert d.x == 17
assert d.xValue == 17
d.x = 23
assert d.xValue == 23
d.xValue = 1492
assert d.x == 1492

You can provide a __setattr__ and __getattr__ that reference an aliases map:

class Dummy:
    aliases = {
        'xValue': 'x',
        'another': 'x',
    }

    def __init__(self):
        self.x = 17

    def __setattr__(self, name, value):
        name = self.aliases.get(name, name)
        object.__setattr__(self, name, value)

    def __getattr__(self, name):
        if name == "aliases":
            raise AttributeError  # http://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html
        name = self.aliases.get(name, name)
        return object.__getattribute__(self, name)


d = Dummy()
assert d.x == 17
assert d.xValue == 17
d.x = 23
assert d.xValue == 23
d.xValue = 1492
assert d.x == 1492
怀念你的温柔 2024-10-05 23:27:07

当一半用户决定使用 dx 而另一半用户决定使用 d.xValue 时,您会怎么做?当他们尝试共享代码时会发生什么?当然,如果您知道所有别名,它会起作用,但是它会很明显吗?当你把代码搁置一年后,这对你来说是显而易见的吗?

最后,我认为这种美好或奢侈是一个邪恶的陷阱,最终会造成更多的混乱而不是好处。


这主要是因为我的脚本 API
跨多个子系统使用
域,因此默认词汇表
变化。所谓“X”合二为一
域在另一个域中被称为“Y”
域名。

您可以通过这种方式使用属性创建别名:

class Dummy(object):
   def __init__(self):
      self.x=1
   @property
   def xValue(self):
      return self.x
   @xValue.setter
   def xValue(self,value):
      self.x=value

d=Dummy()
print(d.x)
# 1
d.xValue=2
print(d.x)
# 2

但由于上述原因,我认为这不是一个好的设计。它
使 Dummy 更难阅读、理解和使用。对于每个用户,您将
用户为了理解 Dummy 必须知道的 API 的大小。

更好的替代方法是使用适配器设计模式
这使您可以保持 Dummy 美观、紧凑、简洁:

class Dummy(object):
   def __init__(self):
      self.x=1

而子域中希望使用不同词汇的用户可以这样做
通过使用 Adapter 类:

class DummyAdaptor(object):
   def __init__(self):
      self.dummy=Dummy()
   @property
   def xValue(self):
      return self.dummy.x
   @xValue.setter
   def xValue(self,value):
      self.dummy.x=value    

对于 Dummy 中的每个方法和属性,您只需连接相似的方法和属性
它将繁重的工作委托给 Dummy 的实例。

它可能需要更多行代码,但它允许您为 Dummy 保留干净的设计,更易于维护、记录和单元测试。人们会编写有意义的代码,因为类将限制可用的 API,并且根据他们选择的类,每个概念只有一个名称。

What are you going to do when half your users decide to use d.x and the other half d.xValue? What happens when they try to share code? Sure, it will work, if you know all the aliases, but will it be obvious? Will it be obvious to you when you put away your code for a year?

In the end, I think this kind of niceness or luxury is an evil trap that will eventually cause more confusion than good.


It's mostly because my scripting API
is used across multiple subsystems &
domains, so the default vocabulary
changes. What's known as "X" in one
domain is known as "Y" in another
domain.

You could make aliases with properties this way:

class Dummy(object):
   def __init__(self):
      self.x=1
   @property
   def xValue(self):
      return self.x
   @xValue.setter
   def xValue(self,value):
      self.x=value

d=Dummy()
print(d.x)
# 1
d.xValue=2
print(d.x)
# 2

But for the reasons mentioned above, I don't think this is a good design. It
makes Dummy harder to read, understand and use. For each user you've doubled the
size of the API the user must know in order to understand Dummy.

A better alternative is to use the Adapter design pattern.
This allows you to keep Dummy nice, compact, succinct:

class Dummy(object):
   def __init__(self):
      self.x=1

While those users in the subdomain who wish to use a different vocabulary can do so
by using an Adaptor class:

class DummyAdaptor(object):
   def __init__(self):
      self.dummy=Dummy()
   @property
   def xValue(self):
      return self.dummy.x
   @xValue.setter
   def xValue(self,value):
      self.dummy.x=value    

For each method and attribute in Dummy, you simply hook up similar methods and properties
which delegate the heavy lifting to an instance of Dummy.

It might be more lines of code, but it will allow you to preserve a clean design for Dummy, easier to maintain, document, and unit test. People will write code that makes sense because the class will restrict what API is available, and there will be only one name for each concept given the class they've chosen.

你穿错了嫁妆 2024-10-05 23:27:07

您可以使用标题为 使用描述符进行缓存和别名。这是其中显示的代码的简洁版本,它提供了您寻求的功能。

编辑:包含Alias属性的类可以在您删除一个属性时自动删除任何关联的目标属性(反之亦然)。我的答案的代码现在说明了一种简单的方法,可以使用方便的类装饰器来完成此操作,该装饰器添加自定义的 __delattr__() 来在属性 Alias's 可以时进行专门的删除管理参与其中。

class Alias(object):
    """ Descriptor to give an attribute another name. """
    def __init__(self, name):
        self.name = name
    def __get__(self, inst, cls):
        if inst is None:
            return self  # a class attribute reference, return this descriptor
        return getattr(inst, self.name)
    def __set__(self, inst, value):
        setattr(inst, self.name, value)
    def __delete__(self, inst):
        delattr(inst, self.name)


def AliasDelManager(cls):
    """ Class decorator to auto-manage associated Aliases on deletion. """
    def __delattr__(self, name):
        """ Deletes any Aliases associated with a named attribute, or
            if attribute is itself an Alias, deletes the associated target.
        """
        super(cls, self).__delattr__(name) # Use base class' method.
        for attrname in dir(self):
            attr = getattr(cls, attrname)
            if isinstance(attr, Alias) and attr.name == name:
                delattr(cls, attrname)

    setattr(cls, '__delattr__', __delattr__)
    return cls


if __name__=='__main__':
    @AliasDelManager
    class Dummy(object):
        def __init__(self):
            self.x = 17
        xValue = Alias('x')  # create an Alias for attr 'x'

    d = Dummy()
    assert d.x == 17
    assert d.xValue == 17
    d.x = 23
    assert d.xValue == 23
    d.xValue = 1492
    assert d.x == 1492
    assert d.x is d.xValue
    del d.x  # should also remove any associated Aliases
    assert 'xValue' not in dir(d)
    print('done - no exceptions were raised')

You could use some of ideas shown in the ActiveState Python recipe titled Caching and aliasing with descriptors. Here's a concise version of the code shown there which provides the functionality you seek.

Edit: A class containing Alias attributes could be made to automatically delete any associated target attributes when you del one (and vice-versa). The code for my answer now illustrates one easy way this could be done using a convenient class decorator which adds a custom __delattr__() to do the specialized deletion management when attribute Alias's could be involved.

class Alias(object):
    """ Descriptor to give an attribute another name. """
    def __init__(self, name):
        self.name = name
    def __get__(self, inst, cls):
        if inst is None:
            return self  # a class attribute reference, return this descriptor
        return getattr(inst, self.name)
    def __set__(self, inst, value):
        setattr(inst, self.name, value)
    def __delete__(self, inst):
        delattr(inst, self.name)


def AliasDelManager(cls):
    """ Class decorator to auto-manage associated Aliases on deletion. """
    def __delattr__(self, name):
        """ Deletes any Aliases associated with a named attribute, or
            if attribute is itself an Alias, deletes the associated target.
        """
        super(cls, self).__delattr__(name) # Use base class' method.
        for attrname in dir(self):
            attr = getattr(cls, attrname)
            if isinstance(attr, Alias) and attr.name == name:
                delattr(cls, attrname)

    setattr(cls, '__delattr__', __delattr__)
    return cls


if __name__=='__main__':
    @AliasDelManager
    class Dummy(object):
        def __init__(self):
            self.x = 17
        xValue = Alias('x')  # create an Alias for attr 'x'

    d = Dummy()
    assert d.x == 17
    assert d.xValue == 17
    d.x = 23
    assert d.xValue == 23
    d.xValue = 1492
    assert d.x == 1492
    assert d.x is d.xValue
    del d.x  # should also remove any associated Aliases
    assert 'xValue' not in dir(d)
    print('done - no exceptions were raised')

策马西风 2024-10-05 23:27:07

该函数将属性名称作为参数,并返回一个用作获取和设置别名的属性。

def alias_attribute(field_name: str) -> property:
    """
    This function takes the attribute name of field to make a alias and return
    a property that work to get and set.
    """
    field = property(lambda self: getattr(self, field_name))
    field = field.setter(lambda self, value: setattr(self, field_name, value))
    return field

例子:

>>> class A:
...     name_alias = alias_attribute('name')
...     def __init__(self, name):
...         self.name = name
... a = A('Pepe')

>>> a.name
'Pepe'

>>> a.name_alias
'Pepe'

>>> a.name_alias = 'Juan'

>>> a.name
'Juan'

This function takes a attribute name as a param and return a property that work as an alias for getting and setting.

def alias_attribute(field_name: str) -> property:
    """
    This function takes the attribute name of field to make a alias and return
    a property that work to get and set.
    """
    field = property(lambda self: getattr(self, field_name))
    field = field.setter(lambda self, value: setattr(self, field_name, value))
    return field

Example:

>>> class A:
...     name_alias = alias_attribute('name')
...     def __init__(self, name):
...         self.name = name
... a = A('Pepe')

>>> a.name
'Pepe'

>>> a.name_alias
'Pepe'

>>> a.name_alias = 'Juan'

>>> a.name
'Juan'
海拔太高太耀眼 2024-10-05 23:27:07

重写 __getattr__() 方法并返回适当的值。

Override the __getattr__() method and return the appropriate value.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文