Python 装饰器之 Property: Setter 和 Getter

Property vs. Getter and Setter in Python decorators

An optional description of the image for screen readers.

Getters(also known as ‘accessors’) and setters (aka. ‘mutators’) are used in many object oriented programming languages to ensure the principle of data encapsulation. Data encapsulation - as you can learn in a introduction on Object Oriented Programming of the tutorial - is seen as the bundling of data with the methods that operate on them. These methods are of course the getter for retrieving the data and the setter for changing the data. According to this principle, the attributes of a class are made private to hide and protect them from the other codes.

Unfortunately, it is widespread belief that a proper Python class should encapsulate private attributes by using getters and setters. As soon as one of these programmers introduces a new attribute, he or she will make it a private variable and creates “automatically” a getter and a setter for this attributes. Such programmers may even use an editor or an IDE, which automatically creates getters and setters for all private attributes. These tools even warn the programmer if she or he uses a public attribute! Java programmers will wrinkle their brows, screw up their noses, or even scream with horror when they read the following: The Pythonic way to introduce attributes is to make them public.

Source: Properties vs. Getters and Setters

Using @property decorators to achieve getters and setters behaviour.


Demo

用一个简单例子来开局,体会一般:

class Person:
    def __init__(self, name):
        self.name1 = name
        self.name2 = '小白'

    # 利用property装饰器将获取name方法转换为获取对象的属性
    @property
    def name(self):
        return self.name1 + '!'

    # 利用property装饰器将设置name方法转换为获取对象的属性
    @name.setter  # @属性名.setter
    def name3(self, n):
        self.name1 = '小绿' if n == '小灰' else '小宝'


p = Person('小黑')
print(p.name, p.name1, p.name2, p.name3)
p.name3 = '小灰' 
print(p.name, p.name1, p.name2, p.name3)
p.name3 = '小2' 
print(p.name, p.name1, p.name2, p.name3)
p.name = '123'

Output:

小黑! 小黑 小白 小黑!
小绿! 小绿 小白 小绿!
小宝! 小宝 小白 小宝!
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-110-90af6f048a4f> in <module>
----> 1 p.name = '123'

AttributeError: can't set attribute

上图中的例子,我们可以直观的感受到 @property 装饰器将调用方法改为了调用对象,即 p.name() 改为了 p.name。另外,@name.setter 装饰器不仅将调用方法改为了获取指定对象的属性,即 p.name3 对应于 p.name()p.name。此外,对其赋值时相当于调用了方法,即有 p.name3 = n 对应于 p.name3(n)

值得留意的是,上述例子背后其实是在操作私有属性 p.name,使用者是透过 setter 方法来管理输入的值,并对 p.name 等属性参数进行赋值影响,直接对私有属性 p.name 进行赋值是不被允许的。

了解了其背后的执行逻辑和规律以后,下面给几个标准写法和实例:


Case 1

我们可以用其来对属性的赋值做判断和异常检测。

# Python program showing the use of 
# @property from https://www.geeksforgeeks.org/getter-and-setter-in-python/
  
class Geeks: 
     def __init__(self): 
          self._age = 0
       
     # using property decorator 
     # a getter function 
     @property
     def age(self): 
         print("getter method called") 
         return self._age 
       
     # a setter function 
     @age.setter 
     def age(self, a): 
         if(a < 18): 
            raise ValueError("Sorry you age is below eligibility criteria") 
         print("setter method called") 
         self._age = a 

Case 2

另一种写法就是可以将 settergetter 作为私有方法隐藏起来:

# https://www.datacamp.com/community/tutorials/property-getters-setters
class FinalClass:

    def __init__(self, var):
        ## calling the set_a() method to set the value 'a' by checking certain conditions
        self.__set_a(var)

    ## getter method to get the properties using an object
    def __get_a(self):
        return self.__a

    ## setter method to change the value 'a' using an object
    def __set_a(self, var):

        ## condition to check whether var is suitable or not
        if var > 0 and var % 2 == 0:
            self.__a = var
        else:
            self.__a = 2

    a = property(__get_a, __set_a)

Case 3

这个例子来自 stackoverflow 上的回答,可以参考其是如何避免 delete 受保护的属性。

# https://stackoverflow.com/a/36943813/8656360
class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

Output:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

Case 4

最后一个例子非常有趣,发现可以利用 Property 下的 Setter 和 Getter 决定多种属性之间的动态依赖关系

FYI:在信号处理中,时域下的采样率 sampling_rate,时长 time_duration 和采样点总数 Nt 三个变量中是任意两个可以推导出第三个变量。

# https://github.com/stephengreen/lfi-gw/blob/11cd4f650af793db45ebc78892443cc2b0b60f40/lfigw/waveform_generator.py#L250
class DSP():
    def __init__(self,):
        self.sampling_rate = 1024
        self.time_duration = 8
    @property
    def f_max(self):
        """Set the maximum frequency to half the sampling rate."""
        return self.sampling_rate / 2.0

    @f_max.setter
    def f_max(self, f_max):
        self.sampling_rate = 2.0 * f_max

    @property
    def delta_t(self):
        return 1.0 / self.sampling_rate

    @delta_t.setter
    def delta_t(self, delta_t):
        self.sampling_rate = 1.0 / delta_t

    @property
    def delta_f(self):
        return 1.0 / self.time_duration

    @delta_f.setter
    def delta_f(self, delta_f):
        self.time_duration = 1.0 / delta_f

    @property
    def Nt(self):
        return int(self.time_duration * self.sampling_rate)

    @property
    def Nf(self):
        return int(self.f_max / self.delta_f) + 1
def OUTPUT():
print('-'*20+'''\nsampling_rate: {}
time_duration: {}
f_max: {}
delta_t: {}
delta_f: {}
Nt: {}
Nf: {}\n'''.format(t.sampling_rate, t.time_duration, t.f_max, t.delta_t, t.delta_f, t.Nt, t.Nf)+'-'*20)    

Output:

>>> t = DSP()
>>> OUTPUT()
--------------------
sampling_rate: 1024
time_duration: 8
f_max: 512.0
delta_t: 0.0009765625
delta_f: 0.125
Nt: 8192
Nf: 4097
--------------------
>>>t.f_max = 256
>>>OUTPUT()
--------------------
sampling_rate: 512.0
time_duration: 8
f_max: 256.0
delta_t: 0.001953125
delta_f: 0.125
Nt: 4096
Nf: 2049
--------------------
>>>t.delta_t = 1/8192
>>>OUTPUT()
--------------------
sampling_rate: 8192.0
time_duration: 8
f_max: 4096.0
delta_t: 0.0001220703125
delta_f: 0.125
Nt: 65536
Nf: 32769
--------------------

Reference

此文探讨非常浅显,仅简明扼要,更多的讨论素材和教程细节,可参阅下方的参考文献:

Next
Previous

Related