How can I make an alias to a non-function member attribute in a Python class?

后端 未结 6 1685
Happy的楠姐
Happy的楠姐 2021-02-01 05:05

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

相关标签:
6条回答
  • 2021-02-01 05:35

    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.

    0 讨论(0)
  • 2021-02-01 05:38

    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.

    0 讨论(0)
  • 2021-02-01 05:40

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

    0 讨论(0)
  • 2021-02-01 05:47

    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
    
    0 讨论(0)
  • 2021-02-01 05:53

    This function takes a attribute name and return a property that work as an alias to get and set.

    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'
    
    0 讨论(0)
  • 2021-02-01 06:00

    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')
    
    
    0 讨论(0)
提交回复
热议问题