跳转至

Python 进阶教程系列 5:获取和修改被保护的属性

本文是 Python 进阶教程系列 5,主要介绍了 Python 私有化及 _ 下划线命名用途,以及使用 gettersetterproperty 来修改被保护的属性。

本文部分转载了 Python 私有化及 _ 下划线命名用途,已获得原作者授权。

Python 中没有真正的私有属性或方法,但有一些和命名有关的约定,让编程人员处理一些需要私有化的情况。我们常常需要区分私有方法、属性和公有方法、属性以方便管理和调用,在 Python 中如何做呢?

在变量、方法命名中有下列几种情况:

  • xx 公有变量/方法
  • _xx 前置单下划线
  • __xx 前置双下划线
  • __xx__ 前后双下划线
  • xx_ 后置单下划线

接下来分别介绍这几种带下划线命名的特性与区别。

几种带下划线命名的特性与区别

_ 单前置下划线

前置单下划线的意思是 提示 其他程序员,以单下划线开头的变量或方法只在内部使用。PEP 8 中定义了这个约定( PEP 8 是最常用的 Python 代码风格指南)。

前置单下划线定义了一个被保护的属性。关于 Public, Protected, Private Members 的区别,可以参考 Python - Public, Protected, Private Members

不过,这个约定对 Python 解释器并没有特殊含义。与 Java 不同,Python 在 “私有”“公共” 变量之间并没有很强的区别。在变量名之前添加一个下划线更像是有人挂出了一个小小的下划线警告标志:“注意,这并不是这个类的公共接口。最好不要使用它。”

一般 Python 约定前置单下划线 _ 的属性和方法为私有方法或属性,以提示该属性和方法 不应 在外部调用。

当然,在类中也可以用单下划线开头来命名属性或者方法,这只是表示类的定义者希望这些属性或者方法是 "私有的",但实际上并不会起任何作用。

这是因为 Python 中的前置单下划线 _ 只是一个公认的约定,而不是必须遵守的规则,至少在涉及变量名和方法名时是这样的。

但是前置下划线会影响从模块中导入名称的方式,不会被 from somemodule import * 导入。

Python
# demo.py

_key = "123"


def _set_key(key):
    global _key
    _key = key
Python
# test.py
from demo import *

print(_key)
_set_key("567")

对此解释器会抛出异常:NameError: name '_key' is not defined

使用 通配符导入 从这个模块中导入所有名称,Python 不会 导入带有前置单下划线的名称(除非模块中定义了__all__ 列表覆盖了这个行为。

但并非 demo.py 中的前置单下划线变量/方法在 test.py 中就不可以使用,完全可以 import module,然后通过 module.xxx 方式,test.py 代码做如下调整:

Python
# test.py
import demo

print(demo._key)  # 正常使用
demo._set_key("789")  # 正常调用
print(demo._key)  # 正常使用

__ 前置双下划线

用于对象的数据封装,以此命名的属性或者方法为类的私有属性或者私有方法。

Python
# coding:utf8


class Foo(object):
    def __init__(self):
        self__name = "private attribute"

    def getname():
        return self.__name

    def __method():
        print("private method")

    def run(self):
        self.__method()

在外部访问直接访问私有属性或方法:

Python Console Session
In [1]: # coding:utf8
   ...:
   ...:
   ...: class Foo(object):
   ...:
   ...:     def __init__(self):
   ...:         self.__name = "private attribute"
   ...:
   ...:     def getname(self):
   ...:         return self.__name
   ...:
   ...:     def __method(self):
   ...:         print("private method")
   ...:
   ...:     def run(self):
   ...:         self.__method()
   ...:
   ...:

In [2]: f = Foo()

In [3]: f.__name
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-332cfd3c796d> in <module>
----> 1 f.__name

AttributeError: 'Foo' object has no attribute '__name'

In [4]: f.__method()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-9cb5f81c61a6> in <module>
----> 1 f.__method()

AttributeError: 'Foo' object has no attribute '__method'

In [5]: f.getname()
Out[5]: 'private attribute'

In [6]: f.run()
private method

可以发现在外部访问直接访问私有属性或方法是不可行,这就起到了封装隐藏数据的作用。但是这种实现机制并不是很严格,这种机制是通过 名字重整 name mangling实现的,目的就是以防类意外重写基类的方法或属性。但类中所有以双下划线开头的名称都会自动变成 _Class_object 的新名称,如 __name >>> _Foo__name ,我们也可以用 dir()来查看类中成员详情

Python Console Session
python
In [7]: dir(f)
Out[7]:
['_Foo__method',
 '_Foo__name',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'getname',
 'run']

In [8]: f._Foo__name
Out[8]: 'private attribute'

In [9]: f._Foo__method()
private method

这种机制可以阻止继承类重新定义或者更改方法的实现,比如,定义一个 Foo 的字类 Goo

Python
class Goo(Foo):
    def __method(self):
        print("private method of Goo")

重写 __method 方法,运行:

Python
In [11]: class Goo(Foo):
    ...:   def __method(self):
    ...:     print('private method of Goo')
    ...:

In [12]: g = Goo()

In [13]: g.run() # (1)!
private method

In [14]: dir(g)
Out[14]:
['_Foo__method',
 '_Foo__name',
 '_Goo__method',
 ...
 ...
]
  1. 调用 run() 方法的时候依然执行的是 Foo 类的 __method() 方法。

调用 run() 方法的时候依然执行的是 Foo 类的 __method() 方法,因为在 run() 方法的实现中,self.__method() 已自动变形为 self._Foo__method()Goo 继承的 run() 方法也是如此,而Goo__method()方法就变成了 _Goo__method()

名字重整 name mangling 的技术,又叫 name decoration命名修饰。在很多现代编程语言中,这一技术用来解决 需要唯一名称而引起的问题,比如命名冲突/重载等。

__前后双下划线__

本节内容可以参考 Python 进阶教程系列 1:双下划线的魔法方法

前后均带双下划线的命名,一般用于特殊方法的命名,用来实现对象的一些行为或者功能,比如 __new__() 方法用来创建实例,__init__() 方法用来初始化对象,x + y操作被映射为方法 x.__add__(y),序列或者字典的索引操作 x[k] 映射为x.__getitem__(k)__len__()、__str__() 分别被内置函数 len()、str()调用等等。

Python
# coding:utf8


class Obj:
    def __init__(self, num):
        self.num = num
        self.li = list()
        self.li.append(num)

    def __add__(self, value):
        print("__add__() execute")
        return self.num + value

    def __getitem__(self, index):
        print("__getitem__() execute")
        return self.li[index]

    def __len__(self):
        print("__len__() execute")
        return len(self.li)

    def __str__(self):
        print("__str__() execute")
        return "< " + str(self.num) + " >"


def main():
    a = Obj(5)
    a = a + 2
    print(a)

    b = Obj(6)
    print(b[0])
    print(len(b))
    print(b)


if __name__ == "__main__":
    main()

测试结果:

Python Console Session
__add__() execute
7
__getitem__() execute
6
__len__() execute
1
__str__() execute
< 6 >
[Finished in 0.1s]

后置单下划线 _

后置单下划线,用于避免与 Python 关键词的冲突。如下:

Python
list_ = ["wang", "hui", "zack"]
dict_ = {"name": "hui", "age": 21}

总结

  • _xx 的变量、函数、类在使用 from xxx import * 时都不会被导入。
  • __xx的实例属性、方法会被 名字重整 name mangling >>> _类名__属性名
  • 父类中属性名为 __xx 的,子类不继承,子类不能访问。
  • 如果在子类中向 __xx 赋值,那么会在子类中定义的一个与父类相同名字的属性。
  • __xx__ 魔法对象或属性,有着特殊作用。不要随意起这种命名。
  • xx_ 用于避免与 Python 关键词的冲突。

获取和修改被保护的属性

实现 gettersetter

Python
# Python program showing a use
# of get() and set() method in
# normal function


class Geek:
    def __init__(self, age=0):
        self._age = age

    # getter method
    def get_age(self):
        return self._age

    # setter method
    def set_age(self, x):
        self._age = x


raj = Geek()

# setting the age using setter
raj.set_age(21)

# retrieving age using getter
print(raj.get_age())

print(raj._age)
Text Only
21
21

使用 property

Python
# Python program showing a
# use of property() function


class Geeks:
    def __init__(self):
        self._age = 0

    # function to get value of _age
    def get_age(self):
        print("getter method called")
        return self._age

    # function to set value of _age
    def set_age(self, a):
        print("setter method called")
        self._age = a

    # function to delete _age attribute
    def del_age(self):
        del self._age

    age = property(get_age, set_age, del_age)


mark = Geeks()

mark.age = 10

print(mark.age)
Text Only
setter method called
getter method called
10

使用 @property

在之前的方法中,为了实现 gettersetter 的行为,我们使用了 property() 函数。然而,正如在本帖中之前提到的,gettersetter 也用于验证属性值的获取和设置。还有一种实现 property 函数的方法,即使用装饰器。Python 的 @property 是内置的装饰器之一。任何装饰器的主要目的是以一种方式改变你的类方法或属性,使得你的类的用户不需要修改他们的代码。例如

Python
# Python program showing the use of
# @property


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


mark = Geeks()

mark.age = 19

print(mark.age)
Text Only
setter method called
getter method called
19

同时使用 @property@abc.abstractmethod

如果要同时使用 @property@abc.abstractmethod,必须将 @property 放在上方,将 @abc.abstractmethod 放在下方。因为 @abstractmethod 装饰器来定义一个没有实现的抽象方法,而 @property 会引入一些实现。如果把 @abc.abstractmethod 放在上方,则会报错:AttributeError: attribute 'isabstractmethod' of 'property' objects is not writable

总结

在 Python 中,Getter 和 Setter 是一种用于访问和修改对象属性的方法。它们提供了一种间接访问对象属性的方式,以便在访问或修改属性时执行其他操作。

Getter 是用于获取对象属性值的方法。它们通常用于访问私有属性或执行一些计算操作后返回属性值。在 Python 中,可以使用@property 装饰器来定义 Getter 方法。

Setter 是用于设置对象属性值的方法。它们通常用于验证属性值的有效性或执行一些额外的操作后设置属性值。在 Python 中,可以使用@property 装饰器和对应的 Setter 方法来定义 Setter。

以下是 Getter 和 Setter 的示例代码:

Python
class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value


# 创建 Person 对象
person = Person("Alice")

# 使用 Getter 获取属性值
print(person.name)  # 输出:Alice

# 使用 Setter 设置属性值
person.name = "Bob"
print(person.name)  # 输出:Bob

在上面的示例中,name属性使用了 Getter 和 Setter 方法。Getter 方法被@property装饰器修饰,允许通过person.name的方式获取属性值。Setter 方法被@name.setter装饰器修饰,允许通过person.name = value的方式设置属性值。

Getter 和 Setter 提供了一种封装属性访问的方式,使得可以在访问或修改属性时执行其他操作,例如验证输入、计算属性值等。这种封装可以增加代码的可维护性和灵活性。

请注意,Python 中还有其他实现属性访问控制的方式,例如使用property()函数或使用__getattr__()__setattr__()魔术方法。Getter 和 Setter 是其中一种常见的实现方式,但并不是唯一的方式。

评论