Есть ли преимущество использования декоратора свойств по сравнению с классом свойств? - PullRequest
0 голосов
/ 19 октября 2018

Я вижу два очень похожих способа иметь свойства в Python

(a) Класс свойства

class Location(object):

    def __init__(self, longitude, latitude):
        self.set_latitude(latitude)
        self.set_longitude(longitude)

    def set_latitude(self, latitude):
        if not (-90 <= latitude <= 90):
            raise ValueError('latitude was {}, but has to be in [-90, 90]'
                             .format(latitude))
        self._latitude = latitude

    def set_longitude(self, longitude):
        if not (-180 <= longitude <= 180):
            raise ValueError('longitude was {}, but has to be in [-180, 180]'
                             .format(longitude))
        self._longitude = longitude

    def get_longitude(self):
        return self._latitude

    def get_latitude(self):
        return self._longitude

    latitude = property(get_latitude, set_latitude)
    longitude = property(get_longitude, set_longitude)

(b) Декоратор свойства

class Location(object):

    def __init__(self, longitude, latitude):
        self.latitude = latitude
        self.longitude = latitude

    @property
    def latitude(self):
        """I'm the 'x' property."""
        return self._latitude

    @property
    def longitude(self):
        """I'm the 'x' property."""
        return self._longitude

    @latitude.setter
    def latitude(self, latitude):
        if not (-90 <= latitude <= 90):
            raise ValueError('latitude was {}, but has to be in [-90, 90]'
                             .format(latitude))
        self._latitude = latitude

    @longitude.setter
    def longitude(self, longitude):
        if not (-180 <= longitude <= 180):
            raise ValueError('longitude was {}, but has to be in [-180, 180]'
                             .format(longitude))
        self._longitude = longitude

Вопрос

Являются ли эти две части кода идентичными (например, с помощью байт-кода)?Они показывают то же самое поведение?

Существуют ли официальные руководства, которые "стиль" использовать?

Есть ли реальные преимущества одного над другим?

Что я пробовал

py_compile + uncompyle6

Я скомпилировал оба:

>>> import py_compile
>>> py_compile.compile('test.py')

, а затем декомпилировал оба с помощью uncompyle6 .Но это только что вернуло то, с чего я начал (с немного другим форматированием)

import + dis

Я попытался

import test  # (a)
import test2  # (b)
dis.dis(test)
dis.dis(test2)

Я очень запутался в выводеиз test2:

Disassembly of Location:
Disassembly of __init__:
 13           0 LOAD_FAST                2 (latitude)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (latitude)

 14           6 LOAD_FAST                2 (latitude)
              8 LOAD_FAST                0 (self)
             10 STORE_ATTR               1 (longitude)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE

, тогда как первый был намного больше:

Disassembly of Location:
Disassembly of __init__:
 13           0 LOAD_FAST                0 (self)
              2 LOAD_ATTR                0 (set_latitude)
              4 LOAD_FAST                2 (latitude)
              6 CALL_FUNCTION            1
              8 POP_TOP

 14          10 LOAD_FAST                0 (self)
             12 LOAD_ATTR                1 (set_longitude)
             14 LOAD_FAST                1 (longitude)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

Disassembly of set_latitude:
 17           0 LOAD_CONST               3 (-90)
              2 LOAD_FAST                1 (latitude)
              4 DUP_TOP
              6 ROT_THREE
              8 COMPARE_OP               1 (<=)
             10 JUMP_IF_FALSE_OR_POP    18
             12 LOAD_CONST               1 (90)
             14 COMPARE_OP               1 (<=)
             16 JUMP_FORWARD             4 (to 22)
        >>   18 ROT_TWO
             20 POP_TOP
        >>   22 POP_JUMP_IF_TRUE        38

 18          24 LOAD_GLOBAL              0 (ValueError)
             26 LOAD_CONST               2 ('latitude was {}, but has to be in [-90, 90]')
             28 LOAD_ATTR                1 (format)
             30 LOAD_FAST                1 (latitude)
             32 CALL_FUNCTION            1
             34 CALL_FUNCTION            1
             36 RAISE_VARARGS            1

 19     >>   38 LOAD_FAST                1 (latitude)
             40 LOAD_FAST                0 (self)
             42 STORE_ATTR               2 (latitude)
             44 LOAD_CONST               0 (None)
             46 RETURN_VALUE

Disassembly of set_longitude:
 22           0 LOAD_CONST               3 (-180)
              2 LOAD_FAST                1 (longitude)
              4 DUP_TOP
              6 ROT_THREE
              8 COMPARE_OP               1 (<=)
             10 JUMP_IF_FALSE_OR_POP    18
             12 LOAD_CONST               1 (180)
             14 COMPARE_OP               1 (<=)
             16 JUMP_FORWARD             4 (to 22)
        >>   18 ROT_TWO
             20 POP_TOP
        >>   22 POP_JUMP_IF_TRUE        38

 23          24 LOAD_GLOBAL              0 (ValueError)
             26 LOAD_CONST               2 ('longitude was {}, but has to be in [-180, 180]')
             28 LOAD_ATTR                1 (format)
             30 LOAD_FAST                1 (longitude)
             32 CALL_FUNCTION            1
             34 CALL_FUNCTION            1
             36 RAISE_VARARGS            1

 24     >>   38 LOAD_FAST                1 (longitude)
             40 LOAD_FAST                0 (self)
             42 STORE_ATTR               2 (longitude)
             44 LOAD_CONST               0 (None)
             46 RETURN_VALUE

Откуда эта разница?Где проверка диапазона значений для первого примера?

Ответы [ 2 ]

0 голосов
/ 23 октября 2018

Вы хотите использовать декоратор, всегда. нет никакого преимущества для другого синтаксиса и есть только недостатки.

Точка декораторов

Это потому, что синтаксис декоратора был придуман специально для избежать другой синтаксис.Любые примеры разнообразия name = property(...) обычно содержатся в коде, предшествующем декораторам.

Синтаксис декоратора: синтаксический сахар ;форма

@decorator
def functionname(...):
    # ...

выполняется так же, как

def functionname(...):
    # ...

functionname = decorator(functionname)

без присваивания functionname дважды (часть def functionname(...) создает функциональный объект и обычно присваивает functionname, но с декоратором объект функции создается и передается непосредственно объекту декоратора.)

Python добавил эту функцию, потому что, когда тело вашей функции имеет длину long , вы не можете легко увидеть этофункция была обёрнута декоратором.Вам нужно прокрутить вниз определение функции, чтобы увидеть это, и это не очень полезно, когда почти все остальное , которое вы хотите знать о функции, находится прямо вверху;Аргументы, имя, строка документа прямо здесь.

Из оригинального PEP 318 - Декораторы для функций и методов Спецификация:

Текущий метод применения преобразования к функции или методу размещает фактическое преобразование после тела функции.Для больших функций это отделяет ключевой компонент поведения функции от определения остальной части внешнего интерфейса функции.

[...]

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

и под Цели проектирования :

новый синтаксис должен

  • [...]
  • переместиться от конца функции, где он в данный момент скрыт, к передней части, где оно больше на вашем лице

Таким образом, использование

@property
def latitude(self):
    # ...

@latitude.setter
def latitude(self, latitude):
    # ...

гораздо более читабельно и самодокументируется , чем

def get_latitude(self):
    # ...

def set_latitude(self, latitude):
    # ...

latitude = property(get_latitude, set_latitude)

Нет загрязнения пространства имен

Далее, поскольку декоратор @property заменяет декорируемый вами функциональный объект результатом декорации (экземпляр property), вы также избегаете загрязнения пространства имен .Без @property и @<name>.setter и @<name>.deleter вы должны добавить 3 дополнительных, отдельных имени к определению вашего класса, которое никто и никогда не будет использовать:

>>> [n for n in sorted(vars(Location)) if n[:2] != '__']
['get_latitude', 'get_longitude', 'latitude', 'longitude', 'set_latitude', 'set_longitude']

Представьте себе класс с 5, 10 или даже более определениями свойств.Разработчики, менее знакомые с проектом и автозаполняющейся средой IDE, наверняка будут смущены разницей между get_latitude, latitude и set_latitude, и вы в конечном итоге получите код, который смешивает стили и затрудняет теперь отход отПредоставление этих методов на уровне класса.

Конечно, вы можете использовать del get_latitude, set_latitude сразу после назначения latitude = property(...), но это еще более дополнительный код для выполнения без реальной цели.

Запутываетимена методов

Несмотря на то, что вы можете избежать префикса имен доступа с помощью get_ и set_ или иным образом различать имена, чтобы создать из них объект property(), это все равно, как почти весь код, который не делаетиспользование синтаксиса декоратора @property приводит к именованию методов доступа.

И это может привести к некоторой путанице в трассировках;исключение, возникшее в одном из методов доступа, приводит к трассировке с get_latitude или set_latitude в имени, в то время как в предыдущей строке использовалось object.latitude.Новичку в свойствах Python может быть не всегда понятно, как они связаны, особенно если они пропустили линию latitude = property(...) дальше;см. выше.

Доступ к методам доступа, как наследовать

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

Но объект property при доступе к классу уже дает вам ссылки на средства доступа через атрибуты .fget, .fset и .fdel:

>>> Location.latitude
<property object at 0x10d1c3d18>
>>> Location.latitude.fget
<function Location.get_latitude at 0x10d1c4488>
>>> Location.latitude.fset
<function Location.set_latitude at 0x10d195ea0>

и вы можете повторно использовать синтаксис @<name>.getter / @<name>.setter / @<name>.deleter в подклассе без необходимости создавать новый объект property!

С помощьюстарый синтаксис, было обычным делом пытаться переопределить только один из методов доступа:

class SpecialLocation(Location):
    def set_latitude(self, latitude):
        # ...

и затем удивляться, почему он не будет подхвачен унаследованным объектом property.

с помощьюСинтаксис декоратора, который вы будете использовать:

class SpecialLocation(Location):
    @Location.latitude.setter
    def latitude(self, latitude):
        # ...

, а затем подклассу SpecialLocation дается новый экземпляр property() с получателем, унаследованным от Location, и с новым установщиком.

TLDR

Использовать синтаксис декоратора.

  • Самодокументируемый
  • Избегает загрязнения пространства имен
  • Делает наследование свойств от свойств более чистым и простым
0 голосов
/ 20 октября 2018

Результаты двух версий вашего кода будут практически одинаковыми.Дескриптор свойства, который у вас есть в конце, будет функционально идентичен в обоих случаях.Единственное отличие в дескрипторах заключается в именах функций, к которым вы можете получить доступ, если вы действительно попытаетесь (через Location.longitude.fset.__name__), и что вы можете увидеть в трассировке исключений, если что-то пойдет не так.

Единственное другоеРазница заключается в наличии методов get_foo и set_foo после того, как вы закончите.Когда вы используете @property, эти методы не будут загромождать пространство имен.Если вы создадите объект property самостоятельно, они останутся в пространстве имен класса, и вы сможете вызывать их напрямую, если действительно хотите, вместо использования обычного доступа к атрибутам через объект property.

Необычно синтаксис @property лучше, поскольку он скрывает методы, которые вам обычно не нужны.Единственная причина, по которой я могу подумать, что вы, возможно, захотите выставить их, заключается в том, что вы ожидаете передать методы как обратные вызовы какой-то другой функции (например, some_function(*args, callback=foo.set_longitude)).Вы могли бы просто использовать lambda хотя для обратного вызова (lambda x: setattr(foo, "longitude", x)), поэтому я не думаю, что стоит загрязнять хороший API посторонними методами getter и setter только для этого углового случая.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...