Как я могу создать псевдоним для нефункционального атрибута члена в классе Python? - PullRequest
17 голосов
/ 25 октября 2010

Я нахожусь в процессе написания 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__

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

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

Спасибо!

Ответы [ 5 ]

16 голосов
/ 25 октября 2010

Вы можете предоставить __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
8 голосов
/ 11 июля 2015

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

Например,

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

5 голосов
/ 25 октября 2010

Что вы будете делать, когда половина ваших пользователей решит использовать d.x, а другая половина d.xValue? Что происходит, когда они пытаются поделиться кодом? Конечно, это будет работать, , если вы знаете все псевдонимы , но будет ли это очевидно? Будет ли это очевидно для вас, когда вы отложите свой код на год?

В конце концов, я думаю, что этот вид милости или роскоши - это злая ловушка, которая в конечном итоге вызовет больше путаницы, чем пользы.


Это в основном потому, что мой скрипт API используется в нескольких подсистемах домены, поэтому словарь по умолчанию изменения. То, что известно как «Х» в одном домен известен как "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

Но по причинам, упомянутым выше, я не думаю, что это хороший дизайн. Это делает манекен труднее читать, понимать и использовать. Для каждого пользователя вы удвоили Размер API, который пользователь должен знать, чтобы понять 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, проще в обслуживании, документировании и модульном тестировании. Люди будут писать код, который имеет смысл, потому что класс будет ограничивать доступный API, и для каждого концепта будет только одно имя, учитывая класс, который они выбрали.

3 голосов
/ 26 октября 2010

Вы можете использовать некоторые идеи, показанные в рецепте 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'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'
0 голосов
/ 25 октября 2010

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

...