Python中的“私有”属性
我对Python还比较陌生,希望我没有遗漏什么,但我来试试...
我想写一个Python模块,想创建一个“私有”的属性,这个属性只能通过模块中的一个或多个函数来修改。这样做是为了让模块更稳健,因为如果在这些函数之外修改这个属性,可能会导致一些不想要的行为。例如,我可能会有:
- 一个存储散点图的x和y值的类,叫做
Data
- 一个从文件读取x和y值并存储到类中的函数,叫做
read()
- 一个绘制这些值的函数,叫做
plot()
在这种情况下,我希望用户不能做这样的事情:
data = Data()
read("file.csv", data)
data.x = [0, 3, 2, 6, 1]
plot(data)
我知道在属性名前加一个下划线可以告诉用户这个属性不应该被修改,也就是说可以改名为_x
并添加一个属性装饰器,这样用户可以访问这个值而不会觉得内疚。但是,如果我还想添加一个设置器属性呢:
class Data(object):
_x = []
_y = []
@property
def x(self):
return self._x
@x.setter
def x(self, value):
# Do something with value
self._x = value
现在我又回到了之前的情况——用户不能直接访问属性_x
,但他们仍然可以通过以下方式设置它:
data.x = [0, 3, 2, 6, 1]
理想情况下,我想把属性函数的定义改成_x()
,但这会让人困惑,因为self._x
到底是什么意思(根据声明的顺序,这似乎会导致设置器被递归调用,或者设置器被忽略而使用属性)。
我想到了一些解决方案:
- 在属性名前加两个下划线
__x
,这样名字会被修改,不会和设置器函数混淆。根据我的理解,这应该保留给那些类不想与可能的子类共享的属性,所以我不确定这是否是一个合理的用法。 - 重命名属性,比如
_x_stored
。虽然这完全解决了问题,但会让代码更难读,并且引入命名约定的问题——我应该重命名哪些属性?只是相关的那些?还是只有有属性的那些?还是仅限于这个类中的那些?
以上两种解决方案可行吗?如果不可行,还有没有更好的方法来解决这个问题?
编辑
感谢大家的回复。评论中提到的一些要点:
- 我想保留设置器属性带来的额外逻辑——上面例子中的
# Do something with value
部分,所以通过直接访问self._x
来设置属性并不能解决问题。 - 去掉设置器属性并创建一个单独的函数
_set_x()
确实解决了问题,但这不是一个很整洁的解决方案,因为它允许通过两种不同的方式来设置_x
——要么调用那个函数,要么直接访问self._x
。我得跟踪哪些属性应该通过自己的(非属性)设置器函数来设置,哪些应该通过直接访问来修改。我可能更愿意使用我上面提到的某个解决方案,因为尽管它们在类内部搞乱了命名约定,但在类外部的使用至少是一致的,也就是说,它们都使用了属性的语法糖。如果没有更好的方法来做到这一点,那我想我只能选择一个造成最小干扰的方案。
2 个回答
如果你想要一些简单明了的属性,这些属性能够自己管理存储,不会被随意修改,你可以定义一个类(类似于属性),然后用它来声明你的类成员:
我把它叫做“Field”:
class Field:
def __init__(self,default=None):
self.valueName = None # actual attribute name
self.default = default # type or value or lambda
if not callable(default): self.default = lambda:default
self._didSet = None # observers
self._willSet = None
def findName(self,owner): # find name of field
if self.valueName: return # once per field for class
for name,attr in owner.__dict__.items():
if attr is self:
self.valueName = f"<{name}>" # actual attribute name
break
def __get__(self,obj,owner=None): # generic getter
if not obj: return self
self.findName(owner or type(obj))
value = getattr(obj,self.valueName,self) # attribute from instance
if value is self:
value = self.default() # default value
setattr(obj,self.valueName,value) # set at 1st reference
return value
def __set__(self,obj,value): # generic setter
self.findName(type(obj))
if self._willSet: value = self._willSet(obj,value)
if self._didSet: oldValue = self.__get__(obj)
setattr(obj,self.valueName,value) # attribute in instance
if self._didSet: self._didSet(obj,oldValue)
def willSet(self,f): self._willSet = f
def didSet(self,f): self._didSet = f
使用方法:
class myClass:
lastName = Field("Doe")
firstName = Field("")
age = Field(int)
gender = Field("M")
relatives = Field(list)
@lastName.willSet
def _(self,newValue): # no function name needed
return newValue.capitalize()
@lastName.didSet
def _(self,oldValue): # no function name needed
print('last name changed from',oldValue,'to',self.lastName)
c = myClass()
c.firstName = "John"
c.lastName = "Smith"
# last name changed from Doe to Smith
c.relatives.extend(['Lucy','Frank'])
print(c.gender)
# M
print(c.__dict__)
# {'<lastName>': 'Smith', '<firstName>': 'John',
'<relatives>': ['Lucy', 'Frank'], '<gender>': 'M'}
添加到实例上的属性在Python中是无法直接访问的,因为它们使用的标识符在代码中是无效的。
因为你在类级别定义了默认值,所以在构造函数中不需要设置字段值(当然你还是可以根据需要这样做)
字段值只有在被引用时才会作为实例属性添加,这样可以让实例创建的过程更高效。
请注意,我的实际Field类要复杂得多,支持变更跟踪、更多的观察者函数、类型检查,以及只读/计算字段。为了这个回答,我把它简化到了最基本的内容
关于私有/公共方法保护,你可能想看看这个 回答
如果你想让用户不轻易改变某个属性,但又想让他们知道可以读取这个属性,我建议使用 @property
,但不提供设置器,就像你之前提到的那样:
class Data(object):
def __init__(self):
self._x = []
self._y = []
@property
def x(self):
return self._x
@property
def y(self):
return self._x
我知道你提到过“如果我想给这个属性加个设置器怎么办?”,但我想反问一下:如果你不希望用户能设置这个属性,那为什么还要加设置器呢?在内部,你可以直接访问 self._x
。
至于用户直接访问 _x
或 _y
,在Python中,任何以'_'开头的变量都被认为是“私有”的,所以你应该相信用户会遵守这个规则。如果他们不遵守,结果搞砸了,那就是他们自己的问题。这种想法和很多其他语言(比如C++、Java等)不同,那些语言非常重视数据的私密性,但Python在这方面的文化就是不一样。
补充说明
还有一点需要注意,因为在这个特定情况下,你的私有属性是列表,而列表是可变的(不同于字符串或整数,它们是不可变的),所以用户可能会不小心改变它们:
>>> d = Data()
>>> print d.x
['1', '2']
>>> l = d.x
>>> print l
['1', '2']
>>> l.append("3")
>>> print d.x
['1', '2', '3'] # Oops!
如果你想避免这种情况,你需要让你的属性返回列表的一个副本:
@property
def x(self):
return list(self._x)