65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2016 年 4 月 21 日

CPOL

8分钟阅读

viewsIcon

21829

downloadIcon

104

面向 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,好吧,我希望这一切都讲得通,你学到了一些有趣的东西!

© . All rights reserved.