views:

112

answers:

4

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(object):

   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...

FYI: I'm using Python 2.7.x, not Python 3.0, so I need a Python 2.7.x compatible solution (though I would be interested if Python 3.0 does something to directly address this need).

Thanks!

A: 

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

Ignacio Vazquez-Abrams
I don't think I see how just overriding `__getattr__` is going to solve the problem of propagating changes when the user says `d.x=5` versus `d.xValue=6`
Brent Nash
You never mentioned anything about *setting* `d.xValue`. When they ask for the value of `d.xValue`, just look up and return `d.x` instead.
Ignacio Vazquez-Abrams
The original questions says `If d.x changes, it should change d.xValue also (and vice-versa).` I was trying to get at the fact that I wanted to be able to do `d.x=5` or `d.xValue=5`. Apologies if I was unclear.
Brent Nash
+7  A: 

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

class Dummy(object):
    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 getattr(self, name) #Causes infinite recursion on non-existent attribute
        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
Ned Batchelder
@Ned Batchelder: Thanks for http://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html; it's a very interesting read!
unutbu
That's something close to what I was picturing, though I didn't come up with the alias dictionary the first time around. That makes for a nice way to avoid if/else ladders and long, verbose setattr/getattr functions.
Brent Nash
Important Addendum: Using `getattr` inside of `__getattr__` actually causes an infinite recursion for non-existent attribtues (e.g. for the example above, try doing `print d.asdf`). I believe using `object.__getattribute__` instead solves the problem.
Brent Nash
@Brent: ouch, that was a bad mistake on my part. Thanks for fixing it!
Ned Batchelder
+1  A: 

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.

unutbu
Brent Nash
+1  A: 

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's method
        for attrname in dir(self):
            attr = getattr(Dummy, attrname)
            if isinstance(attr, Alias) and attr.name == name:
                delattr(Dummy, 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'
martineau
That is really clever. The only thing that bugs me about it is that if I do `del d.x` and then do `dir(d)`, `xValue` will still show up in the resulting list.
Brent Nash
@Brent Nash: I've added a class decorator to deal with the attribute deletion issue you pointed out.
martineau