Python 只读属性:完整解决方案





5.00/5 (3投票s)
可靠的解决方案奏效了:它不依赖任何命名约定,同时适用于 Python 2 和 3,并提供了清晰简洁的使用语法。
目录
引言
此解决方案基于 rIZenAShes 在 GitHub 上发现的一个小型代码示例,该示例展示了一些非常有趣的想法。
至于代码,我发现它远不能令人满意。首先,它只与 Python 2 兼容,不与 Python 3 兼容。更糟糕的是,它基于某些命名约定。要暴露的属性由前导下划线标记,该下划线会被元类删除,以生成暴露的只读属性。当前解决方案与 Python 的两个版本都兼容,并提供了清晰简洁的语法。
那么,有什么大不了的呢?
实现只读属性相当容易
class Meta(type):
@property
def RO(self):
return 13
class DefinitionSet(Meta(str(), (), {})):
greetings = "Hello!"
myNameFormat = "My name is {}."
durationSeconds = 3.5
color = { "opacity": 0.7, "wavelength": 400 }
@property
def RO(self):
return 14
def __init__(self):
self.greetings = "Hello again!"
self.myNameFormat = "Let me introduce myself. My name is {}."
self.durationSeconds = 3.6
self.color = { "opacity": 0.8, "wavelength": 410 }
instance = DefinitionSet()
# instance.RO and DefinitionSet.RO are two different
# read-only attributes
在此代码示例中,instance.RO
作为实例属性,而 DefinitionSet.RO
作为类属性;它们被引入为只读属性
。
请注意开发中的某些不便之处:虽然在使用中,此类属性被用作“常规”属性的同等项(例如,instance.RO
与 instance.color
),但它是在更高级别设置的,即对象类型的级别。对于 instance
,这是实例类型 type(instance) == DefinitionSet
;对于 DefinitionSet
,这是它的元类 type(DefinitionSet) == Meta
。(通过继承设置 DefinitionSet
的元类的方法不那么明显,这里展示出来是为了显示 Python 2 和 3 的等效代码,见下文。)
类属性只读属性的定义比实例属性的定义看起来要复杂一些:对于实例,描述至少可以(几乎)放在一个地方(比较 DefinitionSet.RO
和 __init__
中的 self.greetings
)。对于类属性,属性定义应放在一个单独的类(我们示例中的 Meta
)中。
现在,这个问题通过一个小的设备,即 @property
装饰器来缓解,该装饰器可以被视为一种语法糖。如果我们想从头开始ab ovo,我们将展示更基本的描述符用法,基于 __get__
,这在 Python 2 和 Python 3 的文档中有描述。
那么,我们能否创建比这更甜、更短、更清晰简洁的语法糖?它有实际意义吗?
答案取决于我们对类属性的用法,因为将它们转换为只读属性看起来更令人困惑,也不那么清晰。
为什么需要类属性?
类属性有许多用途,但我想通过一个简单的用例来说明它们的重要性:定义集。假设我们需要定义一些字符串和整数常量。将它们全部放在一个地方是个好主意,以避免在任何其他地方使用立即定义的魔术数字或魔术字符串。
让我们暂时忘记只读属性,只比较两个选项
# using class attributes:
class DefinitionSet:
greetings = "Hello!"
myNameFormat = "My name is {}."
durationSeconds = 3.5
color = { "opacity": 0.7, "wavelength": 400 }
#...
print (DefinitionSet.durationSeconds)
和
# using instance attributes:
class DefinitionSet:
def __init__(self):
self.greetings = "Hello!"
self.myNameFormat = "My name is {}."
self.durationSeconds = 3.5
self.color = { "opacity": 0.7, "wavelength": 400 }
definitionSet = DefinitionSet()
#...
print (definitionSet.durationSeconds)
显而易见,使用类属性的选项更短、更方便。通常,只有在我们有多于一个实例时才需要实例,但在这种情况下,更无聊的部分是作为 __init__
参数传递值。对于一组单一的定义来说,这将完全没有意义。
类属性的解决方案:用法
首先,让我们看看如何使用它
class Foo(ReadonlyBase):
bar = 100
test = Readonly.Attribute(13)
print("Foo.bar: " + str(Foo.bar))
Foo.bar += 1
print("Modified Foo.bar: " + str(Foo.bar))
print("Foo.test: " + str(Foo.test))
try:
Foo.test = Foo.test + 1 # will raise exception
except Exception:
print ("Cannot set attribute Foo.test")
在这里,属性 test
仅通过使用 Readonly.Attribute
进行赋值来标记;所需任何类型的常量值被移至实际的调用参数。对象 Attribute
是类 Readonly
的内部类;整行是对其构造函数的调用和赋值。
这里有一个想法:整个技巧由元类完成:如果属性被分配给 Readonly.Attribute
对象,则类对象的实例化会删除该属性,并创建一个由另一个元类公开的相应只读属性。听起来可能很棘手,但……它确实很棘手。 下面,我们可以看到它是如何工作的。
事实上,不必使用 ReadonlyBase
基类。它在此代码示例中显示,因为 Python 2 和 Python 3 的语法不同。类 Foo
可以直接设置其元类,而无需任何基类。唯一的问题是语法不同。让我们考虑一下这个令人不快的 Python 问题及其解决方法。
Python 2 和 3 在演示中的统一
上面显示的用法示例缺少 ReadonlyBase
类的定义。没有这个类,Foo
类可以直接从用作其元类的 Readonly
类创建,使用以下语法
# Python 2.*.*:
class Foo(object, metaclass = Readonly):
# ...
# Python 3.*.*:
class Foo(object):
__metaclass__ = Readonly
# ...
或者,也可以用同样的方式创建基类 ReadonlyBase
。相反,“demo.py”文件使用元编程方法创建等效的类对象
ReadonlyBase = Readonly(str(), (), {})
这段代码与 Python 的两个版本都兼容。要理解它是如何工作的,只需知道元类就是从类 type
派生(直接或间接)的类。调用其构造函数会创建一个对象,该对象是一个类:它具有类的所有属性,可以用作类,并且可能根据第二个参数(bases
)用作元类。
此时,用法已解释。现在,该展示元类 Readonly
如何将由赋值标记的类属性转换为只读属性了。
它是如何工作的?
这就是完整的解决方案
class Readonly(type):
class Attribute(object):
def __init__(self, value):
self.value = value
def __new__(metaclass, classname, bases, classdict):
class NewMetaclass(metaclass):
attributeContainer = {}
def getAttrFromMetaclass(attr):
return lambda cls: type(cls).attributeContainer[attr]
clone = dict(classdict)
for name, value in clone.items():
if not isinstance(value, metaclass.Attribute):
continue;
getattr(NewMetaclass, DefinitionSet.attributeContainerName)[name] = value.value
aProperty = property(getAttrFromMetaclass(name))
setattr(NewMetaclass, name, aProperty)
classdict[name] = aProperty
classdict.pop(name, None)
return type.__new__(NewMetaclass, classname, bases, classdict)
容易展示但难解释。
首先,对于所有使用 Readonly
作为元类的类,此元类仅用于类对象的实例化。在实例化时,类对象使用一个名为 NewMetaclass
的不同元类创建,该元类是每个类实例的独立实例。它被称为“New”,因为它最终用于调用 type.__new__(NewMetaclass, classname, bases, classdict)
。
NewMetaclass
的每个实例都不同。首先,它用作正在初始化的类所使用的 Readonly.Attribute
所有实例的容器。其次,它用作一些属性的容器,每个属性的名称与原始类属性完全相同,将被重构为只读属性。
遍历类的原始属性集时,会创建 Readonly.Attribute
实例并将其放入字典 NewMetaclass.attributeContainer
中。对于每个此类属性,使用 property()
构造函数创建属性对象。对于每个不同的属性名,此类属性会使用基于名称生成的 lambda
表达式进行初始化,该表达式返回从 attributeContainer
检索的值。
在这些操作期间,传递给 type._new_
的原始类字典被修改,以删除原始的“想要成为只读”的类属性。在遍历之前,会克隆该字典,否则我们可能会遇到一个异常(在 Python 3 中),该异常是由尝试修改正在迭代的字典引起的。
这还不够吗?不。我们可以再向前迈出一大步。
实例属性该怎么办?
是否可以使用相同的机制来实现实例属性?
也许如果我们只需要实例属性而不是类属性,我们就不会费心了。但是,当 Readonly.Attribute
的使用机制已经可用时,拥有类属性和实例属性更简洁统一的外观将更自然。
class Foo(ReadonlyBase): # or make Readonly a metaclass of Foo, see above
bar = 100
test = Readonly.Attribute(13)
def __init__(self):
self.a = 1
self.b = Readonly.Attribute(3.14159)
那么,如何对实例属性(例如 b
)实现类似的只读效果?下面将展示这一点。
通用解决方案
令人惊讶的是,将类似的技术应用于实例属性比类属性要棘手得多。
主要问题在于处理类的多个实例。属性的实现,无论是否只读,都需要修改实例类。这很容易在元类的 __new__
方法中完成,但这只会对该类的一个实例化起作用。在尝试创建第二个实例时,将 Readonly.Attribute
分配给同一属性的构造函数将失败,因为已修改的类已经提供了该属性的只读功能。因此,我们陷入了需要为每个实例创建单独类的境地。
真正的技巧是将一个钩子注入类构造函数,这通过元类的 __call__
方法体内的 type.__call__
调用来实现。
当此调用创建 instance
时,我们需要类动态创建的另一个实例。这个新实例 newInstance
是从动态创建的类 NewClass
创建的,而无需构造函数。现在,使用两个实例和两个类(新旧类),我们可以操纵实例属性,将它们分配给 newInstance
— 用于读写实例属性,以及 NewClass
— 用于替换实例属性的只读属性。
class DefinitionSet:
attributeContainerName = "."
class Readonly(type):
class Attribute(object):
def __init__(self, value):
self.value = value
@classmethod
def Base(cls): # base class with access control of class attribute
return Readonly(str(), (), {})
def __new__(metaclass, className, bases, classDictionary):
def getAttrFromClass(attr):
return lambda cls: getattr(type(cls), DefinitionSet.attributeContainerName)[attr]
class NewMetaclass(metaclass):
setattr(metaclass, DefinitionSet.attributeContainerName, {})
def __call__(cls, *args, **kwargs):
instance = type.__call__(cls, *args, **kwargs)
newClass = metaclass(cls.__name__, cls.__bases__, {})
newInstance = type.__call__(newClass)
setattr(newClass, DefinitionSet.attributeContainerName, {})
names = dir(instance)
for name in names:
if hasattr(cls, name):
continue
value = getattr(instance, name)
if isinstance(value, metaclass.Attribute):
if hasattr(newInstance, name):
delattr(newInstance, name)
getattr(
newClass,
DefinitionSet.attributeContainerName)[name] = value.value
aProperty = property(getAttrFromClass(name))
setattr(newClass, name, aProperty)
else:
setattr(newInstance, name, getattr(instance, name))
return newInstance
clone = dict(classDictionary)
for name, value in clone.items():
if not isinstance(value, metaclass.Attribute):
continue;
getattr(NewMetaclass, DefinitionSet.attributeContainerName)[name] = value.value
aProperty = property(getAttrFromClass(name))
setattr(NewMetaclass, name, aProperty)
classDictionary[name] = aProperty
classDictionary.pop(name, None)
return type.__new__(NewMetaclass, className, bases, classDictionary)
请注意,getAttrFromClass
在不同类之间重用,即用于实现实例只读属性的实例类和用于实现类只读属性的元类。但是,替换的机制是不同的。
另一个技巧是“隐藏”存储在类中的实例字典,并将其命名为 DefinitionSet.attributeContainerName
。使用这样的名称,该属性不会作为“常规”点表示法操作 instance.attribute = value
的结果出现;它只能通过 getattr/setattr/delattr/hasattr
方法进行操作。这似乎非常重要,因为它有助于避免与基于点表示法的用户属性发生任何可能的冲突,即使用户使用了任意数量下划线的属性名。这样,实现就不依赖于 Python 开发者通常使用的任何命名约定。
版本
v.1.0.0:初始完全功能版本。
v.1.0.1:小修复。
v.2.0.0:稳定版本;演示版本实现了上面解释的 Python 2 和 3 统一。
v.3.0.0:将机制大规模地推广到类属性和实例属性。