Как я могу создать псевдоним для атрибута non-function member в классе Python?

Я в середине написания API библиотеки Python, и я часто сталкиваюсь с тем сценарием, когда мои пользователи хотят иметь несколько разных имен для тех же функций и переменных.

Если у меня есть класс Python с функцией foo(), и я хочу сделать его псевдоним под названием bar(), это супер просто:

class Dummy(object):

   def __init__(self):
      pass

   def foo(self):
      pass

   bar = foo

Теперь я могу сделать это без проблем:

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

Что мне интересно, так это лучший способ сделать это с атрибутом класса, который является обычной переменной (например, строкой), а не функцией? Если бы у меня была эта часть кода:

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

Я хочу d.x и d.xValue в ВСЕГДА печатать то же самое. Если d.x изменяется, он также должен изменить d.xValue (и наоборот).

Я могу придумать несколько способов сделать это, но ни один из них не выглядит таким гладко, как хотелось бы:

  • Напишите пользовательскую аннотацию
  • Используйте аннотацию @property и беспорядок с установщиком
  • Переопределить функции класса __setattr__

Какой из этих способов лучше? Или есть другой способ? Я не могу не чувствовать, что, если так легко создавать псевдонимы для функций, это должно быть так же легко для любых переменных...

FYI: Я использую Python 2.7.x, а не Python 3.0, поэтому мне нужно решение Python 2.7.x(хотя мне было бы интересно, если Python 3.0 сделает что-то прямое, чтобы решить эту проблему).

Спасибо!

Ответ 1

Вы можете указать __setattr__ и __getattr__, которые ссылаются на карту псевдонимов:

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

Ответ 2

Что вы собираетесь делать, когда половина ваших пользователей решит использовать d.x, а другую половину 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 труднее читать, понимать и использовать. Для каждого пользователя вы удвоили размер API, который должен знать пользователь, чтобы понять Dummy.

Лучшей альтернативой является использование шаблона проектирования адаптера. Это позволяет сохранить Dummy красивым, компактным, сжатым:

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

Хотя те пользователи в субдомене, которые хотят использовать другой словарь, могут это сделать используя класс адаптера:

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, упростить обслуживание, документ и unit test. Люди будут писать код, который имеет смысл, потому что класс будет ограничивать доступность API, и будет только одно имя для каждой концепции, если выбрать класс, который они выбрали.

Ответ 3

Если я не понимаю вопрос, это можно решить почти так же, как с методами класса.

Например,

class Dummy(object):

    def __init__(self):
        self._x = 17

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

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

    # 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)

Два значения всегда будут синхронизироваться. Вы пишете фактический код свойства с именем атрибута, который вы предпочитаете, а затем вы используете его с любым старым именем (ами), которое вам нужно.

На мой взгляд, этот код намного легче читать и понимать, чем все перезаписывающие __setattr__ и __getattr__.

Ответ 4

Вы можете использовать некоторые идеи, показанные в рецепте ActiveState Python под названием Кэширование и сглаживание с помощью дескрипторов. Здесь представлена ​​краткая версия кода, который показывает, какие функции вы ищете.

Изменить:. Класс, содержащий атрибуты Alias, может быть сделан для автоматического удаления любых связанных целевых атрибутов, когда вы del один (и наоборот). Код для моего ответа теперь иллюстрирует один простой способ: это можно сделать с помощью удобного декоратора класса, который добавляет пользовательский __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(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'

Ответ 5

Переопределите метод __getattr__() и верните соответствующее значение.