为Python添加类似C#的属性事件






4.50/5 (2投票s)
面向 C# 开发者,了解 Python。面向 Python 开发者,也许有些有用的东西。
前导码
我写这篇文章主要面向对 Python 感兴趣的 C# 程序员,所以对于 Python 开发者来说,你会遇到一些我说的东西可能会让你觉得“显而易见!”。 忽略它们就好。 另外,我不太喜欢 Python 中命名函数的方式,比如 do_something_here
。 我仍然是一名 C# 程序员,我更喜欢,并且觉得同样易读,将我的函数命名为:doSomethingHere
。 接受吧。 我在 Python 库中见过这两种约定——有时很不一致——是的,我明白下划线格式是更 Pythonic 的做法。
另外,为把它放在 C# 部分道歉——Code Project 没有 Python 语言板块!
引言
有时,你确实希望在设置类中的属性时产生一个副作用。 用户界面就是一个很好的例子,例如,在 C# 的 Form
类中,你会这样做(给定一个 Form
实例 form
)
form.Location = new Point(100, 100);
或者更简单的,比如设置宽度
form.Width = 500;
窗体立即更新,意味着屏幕上的更改立即可见。
要在 Python 中实现属性 getter/setter 的副作用,我们必须这样做
class TheUsualWay(object): def setWidth(self, w): self._width = w print("Call side-effect") width = property(lambda self: self._width, lambda self, value: self.setWidth(value))
一个简单的示例,说明如何使用 width
属性
q = TheUsualWay() q.width = 1 print("Width = " + str(q.width))
注意到几点
- 底层属性(或者说,用 C# 的术语来说,“字段”)有一个前导下划线,这是防止在设置值时出现递归所必需的,否则它会再次调用属性的 setter!
- 我们需要键入很多内容,使用两个 lambda 表达式创建属性
不喜欢 lambda? 你也可以这样写
class TheUsualWay(object): def getWidth(self): return self._width def setWidth(self, w): self._width = w print("Call side-effect") width = property(getWidth, setWidth)
你仍然需要键入很多内容——在这种情况下,是显式的 getter 和 setter。
现在,Python 的优点在于,如果你最初有一个简单的属性 width
,但后来决定想为同名属性添加一些带有副作用的行为,你可以在不修复其他类中 width
的所有引用的情况下修改你的类。 这就是为什么在 C# 中直接访问字段而使用属性是一种非常糟糕的做法,原因相同。
对于一小部分你认为需要副作用的属性,上面两个例子是一个很好的选择。 然而,如果你有大量希望带有副作用的字段(UI 编程是一个很好的例子),这会变得非常繁琐,所以我将展示另一种(不声称更好)的方法来将“事件”与属性的 get 和 set 调用相关联。
基本实现
为了支持属性事件,我们将要求需要它们的类继承自 PropertyEvents
。 在 Python 中这不成问题,因为 Python 支持多重继承(这是为 C# 开发者准备的,也是 Python 开发者“显而易见”的时刻)。
首先,我们将子类化 Python 的字典 dict
(原因稍后解释)
class CallbackDictionary(dict): pass
对于 C# 开发者来说,pass
关键字基本上是一个“什么都不做”的语句,这意味着在这种情况下,CallbackDictionary
并没有用额外的功能扩展 dict
——至少目前还没有。
基础 PropertyEvents 类
此类提供了将回调(“事件”)连接到属性的 get 和 set 活动的功能。 首先,我们初始化两个字典,一个用于与一个或多个属性关联的“get”事件,另一个用于“set”事件
class PropertyEvents(object): def __init__(self): self._getCallbacks = CallbackDictionary(self) self._setCallbacks = CallbackDictionary(self)
对于 C# 开发者来说,__init__
类似于类构造函数,但又不完全是(稍后会详细介绍)。 变量名 self
是对象实例——每个类方法都会获得它的实例,你必须使用这个实例来调用其他类方法或访问类属性和属性。 它有点像 C# 的 this
关键字。 使用“self”这个名字只是一个约定,但这是一个普遍采纳的约定。
这些字典旨在作为键值对,其中键是属性名,值是要触发的回调函数数组
class PropertyEvents(object): def __init__(self): self._getCallbacks = CallbackDictionary() self._setCallbacks = CallbackDictionary()
内部绑定和解绑函数回调
内部,我们使用“私有”类方法——带有前导下划线的方法——来绑定和解绑回调函数
def _bind(self, name, callback, callbacks): # We use setdefault here because lambdas cannot have assigment expressions. callbacks[name].append(callback) if (callbacks.has_key(name)) else callbacks.setdefault(name, [callback]) def _unbind(self, name, callback, callbacks): if (callbacks.has_key(name) and callback in callbacks[name]): callbacks[name].remove(callback)
几个辅助属性
这些内部函数和定义的两个属性稍后会很有用
def _get_getters(self): return self._getCallbacks def _get_setters(self): return self._setCallbacks getters = property(_get_getters, lambda self, value: ()) setters = property(_get_setters, lambda self, value: ())
请注意,属性的“set”函数是一个什么都不做函数(这里不能使用 pass
)。
用于绑定和解绑回调函数的公开方法
对于类的“用户”,我们公开类方法(我在这里交替使用“函数”和“方法”)来将 getter 和 setter 方法绑定到/从属性
def bindGetter(self, name, callback): self._bind(name, callback, self.getters) def unbindGetter(self, name, callback): self._unbind(name, callback, self.setters) def bindSetter(self, name, callback): self._bind(name, callback, self.setters) def unbindSetter(self, name, callback): self._unbind(name, callback, self.setters)
这里可以看到这些函数中的细微差别,即它们决定调用哪个“私有”方法以及修改哪个集合。
调用属性 Get/Set 回调
最后,我们有用于实际处理 get 属性值和 set 属性值行为的类方法
def get(self, name): """ Calls any getter callbacks for the attribute [name] and then returns the value of the attribute [name]. """ self._doCallbacks(name, self.getters) return getattr(self, self._privateName(name)) def set(self, name, value): """ Sets the value of attribute [name] and then calls any setter callbacks for the attribute [name]. """ setattr(self, self._privateName(name), value) self._doCallbacks(name, self.setters) def _privateName(self, name): """ Prepends the attribute [name] with '_', firstly to indicate that it is "private", secondly to avoid infinite recursion. """ return '_' + name def _doCallbacks(self, name, callbacks): if (callbacks.has_key(name)): for callback in callbacks[name]: callback(self)
让我们看看到目前为止它是如何工作的
这是一个定义属性“x”的测试类
class Test(PropertyEvents): def __init__(self): PropertyEvents.__init__(self) x = property(lambda self: self.get('x'), lambda self, value: self.set('x', value))
对于 C# 用户,请注意初始化程序必须显式调用基类初始化程序。 在 Python 中,构造(实例化对象)和初始化被分成两个步骤,Python 程序员几乎不需要考虑。
同时注意到我们如何使用 lambda 表达式来定义属性的“get”和“set”函数。
顺便说一句,(可能)无法摆脱硬编码属性名的字符串字面量(可能有一些魔法技术可以检查代码并找出属性名,但这远远超出了本文的范围)。
我们定义一个 getter 和 setter 回调
def x_getter(obj): print("Getter called") def x_setter(obj): print("Setter called")
以下是我们如何测试代码(不进行单元测试)
t = Test() t.bindSetter('x', x_setter) t.bindGetter('x', x_getter) t.x = 5 print(t.x)
结果输出为
一些所谓的改进
本节讨论了各种可能的改进(例如,减少输入量和其他语法糖)。
改进属性定义
我仍然认为输入太多了
x = property(lambda self: self.get('x'), lambda self, value: self.set('x', value))
但选项(实际上,据我所知只有一个)有点奇怪。 我们可以做的是在类实例化时动态创建属性,如下所示
def __new__(cls): PropertyEvents.defineProperty(cls, 'y') return PropertyEvents.create(Test, cls)
在这里,我们定义了类创建时应该发生什么——C# 构造函数的另一半。 因为我们还没有实例,只有正在构造的对象的类型,所以我们不能进行任何成员属性的初始化,也不能调用成员函数等。 为了说明区别,调试器说“cls”是这样的
它的类型是“type”! create
函数(见下文)只是一个薄包装,所以我们不必输入太多。
将此与 __init__(self)
函数中 self
的类型进行对比
在这里,“self”是“Test”类型,这是我们的测试类——这里是实际的实例。
我们还需要几个静态辅助方法,添加到 PropertyEvents
类中
@staticmethod def defineProperty(cls, name): setattr(cls, name, property(fget = lambda self: self.get(name), fset = lambda self, value: self.set(name, value))) @staticmethod def create(sub, base): obj = super(sub, base).__new__(base) return obj
你可能会问,如果 defineProperty
在类实例尚未存在时被调用,lambda 表达式如何使用 self
? 这是因为(这对 C# 和 Python 程序员来说都应该是“显而易见”的)lambda 函数是一个闭包,并且直到调用时才被评估。
绑定 setter 和 getter 仍然完全相同
t.bindSetter('y', y_setter) t.bindGetter('y', y_getter)
这种方法更好吗? 嗯,如果我们创建许多属性,输入量会少一些,并且可以避免输入错误的可能性,如下所示
y = property(lambda self: self.get('x'), lambda self, value: self.set('x', value))
糟糕,我们将一个属性赋给了 y,但属性名是 x!
大多数事件都连接到属性的 setter
大多数时候,我们希望只将回调连接到属性的 setter。 我们可以通过实现 += 运算符的重写来以更 C# 的方式来实现这一点。 这需要定义 += 和 -=(后者以便我们可以解绑属性)的运算符函数
def __add__(self, callback): PropertyEvents._assertIsInstanceOfDict(callback) self.bindSetter(callback.keys()[0], callback.values()[0]) return self def __sub__(self, callback): PropertyEvents._assertIsInstanceOfDict(callback) self.unbindSetter(callback.keys()[0], callback.values()[0]) return self @staticmethod def _assertIsInstanceOfDict(src): if (not isinstance(src, dict)): raise ParameterException("Expected dictionary, got " + str(type(src)))
以及异常类
class ParameterException(Exception): pass
我们现在可以使用 += 和 -= “语法糖”来添加和删除 setter
t += {'x': x_setter}
注意对传入类型是字典的断言。 这是因为,如果你这样连接属性 setter
t += {'x', x_setter}
你正在创建一个无序的 set
,而不是一个字典! 你看到区别了吗? 一个逗号与一个冒号。 我经常犯这个错误(好吧,这暴露了我仍然是 Python 新手!)
属性 Getter 的运算符重载
要将 += 和 -= 语法用于属性 getter,我们必须明确说明我们是向 setter 还是 getter 添加回调。 还记得文章开头的 CallbackDictionary 类吗? 我们现在定义它的作用
class CallbackDictionary(dict): def __init__(self, events): self.events = events def __add__(self, callback): PropertyEvents._assertIsInstanceOfDict(callback) self.events._bind(callback.keys()[0], callback.values()[0], self) return self def __sub__(self, callback): PropertyEvents._assertIsInstanceOfDict(callback) self.events._unbind(callback.keys()[0], callback.values()[0], self) return self
并稍微修改 PropertyEvents 初始化,传入它本身
def __init__(self): self._getCallbacks = CallbackDictionary(self) self._setCallbacks = CallbackDictionary(self)
现在我们可以这样写
t.getters += {'x': x_getter}
所以,有效地,我们有三种创建setter回调的方法
t += {'x': x_setter} t.setters += {'x': x_setter} t.bindSetter('x', x_setter)
以及两种创建getter回调的方法
t.getters += {'x': x_getter} t.bindGetter('x', x_getter)
解绑 getter 和 setter 也是如此。
只读属性
最后,我们可以创建一个只读属性。 使用动态属性创建技术,我们可以这样定义一个只读属性
PropertyEvents.defineReadOnlyProperty(cls, 'z')
利用一个新的静态方法
@staticmethod def defineReadOnlyProperty(cls, name): setattr(cls, name, property(fget = lambda self: self.get(name), fset = lambda self, value: self._readOnlyException(name)))
以及“私有”异常函数(因为我们不能在 lambda 表达式中 raise
)
def _readOnlyException(self, name): raise ReadOnlyPropertyException(name + " is read only")
以及异常类本身
class ReadOnlyPropertyException(Exception): pass
结论
嗯,为了避免一点 getter 和 setter 的输入量,这做了很多工作。 另一方面,我们有了一个可重用的模块,用于连接属性的 get/set 回调,并且语法已经尽可能好了。
如果你是 C# 程序员,你现在应该会欣赏该语言的事件功能。 如果你是 Python 程序员,希望我没有犯太多不 Pythonic 的错误。 如果你是 C# 程序员正在看 Python,好吧,我希望这一切都讲得通,你学到了一些有趣的东西!