Декоратор property#

Python позволяет создавать и изменять переменные экземпляров:

In [1]: class Robot:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:

In [2]: bb8 = Robot('BB-8')

In [3]: bb8.name
Out[3]: 'BB-8'

In [4]: bb8.name = 'R2D2'

In [5]: bb8.name
Out[5]: 'R2D2'

Однако иногда нужно сделать так чтобы при изменении/установке значения переменной, проверялся ее тип или диапазон значений, также иногда необходимо сделать переменную неизменяемой и сделать ее доступной только для чтения. В некоторых языках программирования для этого используются методы get и set, например:

In [9]: class IPAddress:
   ...:     def __init__(self, address, mask):
   ...:         self._address = address
   ...:         self._mask = int(mask)
   ...:
   ...:     def set_mask(self, mask):
   ...:         if not isinstance(mask, int):
   ...:             raise TypeError("Маска должна быть числом")
   ...:         if not mask in range(8, 32):
   ...:             raise ValueError("Маска должна быть в диапазоне от 8 до 32")
   ...:         self._mask = mask
   ...:
   ...:     def get_mask(self):
   ...:         return self._mask
   ...:

In [10]: ip1 = IPAddress('10.1.1.1', 24)

In [12]: ip1.set_mask(23)

In [13]: ip1.get_mask()
Out[13]: 23

По сравнению со стандартным синтаксисом обращения к атрибутам, этот вариант выглядит очень громоздко. В Python есть более компактный вариант сделать то же самое - property.

Property как правило, используется как декоратор метода и превращает метод в переменную экземпляра с точки зрения пользователя класса.

Пример создания property:

In [14]: class IPAddress:
    ...:     def __init__(self, address, mask):
    ...:         self._address = address
    ...:         self._mask = int(mask)
    ...:
    ...:     @property
    ...:     def mask(self):
    ...:         return self._mask
    ...:

Теперь можно обращаться к mask как к обычной переменной:

In [15]: ip1 = IPAddress('10.1.1.1', 24)

In [16]: ip1.mask
Out[16]: 24

Один из плюсов property - переменная становится доступной только для чтения:

In [17]: ip1.mask = 30
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-17-e153170a5893> in <module>
----> 1 ip1.mask = 30

AttributeError: can't set attribute'

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

In [19]: class IPAddress:
    ...:     def __init__(self, address, mask):
    ...:         self._address = address
    ...:         self._mask = int(mask)
    ...:
    ...:     @property
    ...:     def mask(self):
    ...:         return self._mask
    ...:
    ...:     @mask.setter
    ...:     def mask(self, mask):
    ...:         if not isinstance(mask, int):
    ...:             raise TypeError("Маска должна быть числом")
    ...:         if not mask in range(8, 32):
    ...:             raise ValueError("Маска должна быть в диапазоне от 8 до 32")
    ...:         self._mask = mask
    ...:

In [20]: ip1 = IPAddress('10.1.1.1', 24)

In [21]: ip1.mask
Out[21]: 24

In [23]: ip1.mask = 30

In [24]: ip1.mask = 320
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-24-8573933afac9> in <module>
----> 1 ip1.mask = 320

<ipython-input-19-d0e571cd5e2b> in mask(self, mask)
     13             raise TypeError("Маска должна быть числом")
     14         if not mask in range(8, 32):
---> 15             raise ValueError("Маска должна быть в диапазоне от 8 до 32")
     16         self._mask = mask
     17

ValueError: Маска должна быть в диапазоне от 8 до 32

Пример использования property для динамического получения значения:

from base_ssh import BaseSSH
import time


class CiscoSSH(BaseSSH):
    def __init__(self, ip, username, password, enable_password,
                 disable_paging=True):
        super().__init__(ip, username, password)
        self._ssh.send('enable\n')
        self._ssh.send(enable_password + '\n')
        if disable_paging:
            self._ssh.send('terminal length 0\n')
        time.sleep(1)
        self._ssh.recv(self._MAX_READ)
        self._cfg = None

    @property
    def cfg(self):
        if not self._cfg:
            self._cfg = self.send_show_command('sh run')
        return self._cfg

При обращении к переменной cfg первый раз, на оборудовании выполняется команда sh run и записывается в переменную self._cfg, второй раз значение просто берется из переменной:

In [6]: r1 = CiscoSSH('192.168.100.1', 'cisco', 'cisco', 'cisco')

In [7]: r1.cfg # тут возникает пауза
Out[7]: 'sh run\r\nBuilding configuration...\r\n\r\nCurrent configuration : 2286 bytes\r\n!\r\nversion 15.2\r\n...'

In [8]: r1.cfg
Out[8]: 'sh run\r\nBuilding configuration...\r\n\r\nCurrent configuration : 2286 bytes\r\n!\r\nversion 15.2\r\n...'

В этом примере property используется для создания переменной, которая отвечает за чтение/изменение основного IP-адреса:

import re
import time
from base_ssh import BaseSSH


class CiscoSSH(BaseSSH):
    def __init__(self, ip, username, password, enable_password,
                 disable_paging=True):
        super().__init__(ip, username, password)
        self._ssh.send('enable\n')
        self._ssh.send(enable_password + '\n')
        if disable_paging:
            self._ssh.send('terminal length 0\n')
        time.sleep(1)
        self._ssh.recv(self._MAX_READ)
        self._mgmt_ip = None

    def config_mode(self):
        self._ssh.send('conf t\n')
        time.sleep(0.5)
        result = self._ssh.recv(self._MAX_READ).decode('ascii')
        return result

    def exit_config_mode(self):
        self._ssh.send('end\n')
        time.sleep(0.5)
        result = self._ssh.recv(self._MAX_READ).decode('ascii')
        return result

    def send_config_commands(self, commands):
        result = self.config_mode()
        result += super().send_config_commands(commands)
        result += self.exit_config_mode()
        return result

    @property
    def mgmt_ip(self):
        if not self._mgmt_ip:
            loopback0 = self.send_show_command('sh run interface lo0')
            self._mgmt_ip = re.search('ip address (\S+) ', loopback0).group(1)
        return self._mgmt_ip

    @mgmt_ip.setter
    def mgmt_ip(self, new_ip):
        if self.mgmt_ip != new_ip:
            self.send_config_commands([f'interface lo0',
                                       f'ip address {new_ip} 255.255.255.255'])
            self._mgmt_ip = new_ip

Теперь при чтении переменной mgmt_ip считывается конфиг или читается переменная _mgmt_ip, а при записи адрес перенастраивается на оборудовании:

In [19]: r1 = CiscoSSH('192.168.100.1', 'cisco', 'cisco', 'cisco')

In [22]: r1.mgmt_ip
Out[22]: '4.4.4.4'

In [23]: r1.mgmt_ip = '10.4.4.4'

In [24]: r1.mgmt_ip
Out[24]: '10.4.4.4'

In [27]: print(r1.send_show_command('sh run interface lo0'))
sh run interface lo0
Building configuration...

Current configuration : 64 bytes
!
interface Loopback0
 ip address 10.4.4.4 255.255.255.255
end

R1#