Python魔术方法指南之二

反射

你可以通过魔术方法控制控制使用 isinstance()  issubclass() 内置方法的反射行为。这些魔术方法是:

  • __instancecheck__(self, instance):检查一个实例是不是你定义的类的实例
  • __subclasscheck__(self, subclass):检查一个类是不是你定义的类的子类

这些方法的用例似乎很少,这也许是真的。我不会花更多的时间在这些魔术方法上因为他们并不是很重要,但是他们的确反应了Python中的面向对象编程的一些基本特性:非常容易的去做一些事情,即使并不是很必须。这些魔术方法看起来并不是很有用,但是当你需要的时候你会很高兴有这种特性。

可以调用的对象

你也许已经知道,在Python中,方法也是一种高等的对象。这意味着他们也可以被传递到方法中就像其他对象一样。这是一个非常惊人的特性。在Python中,一个特殊的魔术方法可以让类的实例的行为表现的像函数一样,你可以调用他们,将一个函数当做一个参数传到另外一个函数中等等。这是一个非常强大的特性让Python编程更加舒适甜美。

  • __call__(self,[args...]):允许一个类的实例像函数一样被调用。实质上说,这意味着 x()  x.__call__() 是相同的。注意 __call__ 参数可变。这意味着你可以定义 __call__ 为其他你想要的函数,无论有多少个参数。

__call__ 在那些类的实例经常改变状态的时候会非常有效。调用这个实例是一种改变这个对象状态的直接和优雅的做法。用一个实例来表达最好不过了:

class Entity: 
    def __init__(self, size, x, y):     
        self.x, self.y = x, y     
        self.size = size     
    def __call__(self, x, y):     
        self.x, self.y = x, y

会话管理

在Python 2.5中,为了代码利用定义了一个新的关键词 with 语句。会话控制在Python中不罕见(之前是作为库的一部分被实现),直到 PEP343 被添加后。它被成为一级语言结构。你也许之前看到这样的语句:

with open('foo.txt') as bar:
# perform some action with bar

回话控制器通过包装一个 with 语句来设置和清理行为。回话控制器的行为通过两个魔术方法来定义:

  •  __enter__(self) 定义当使用 with 语句的时候会话管理器应该初始块被创建的时候的行为。注意 __enter__ 的返回值被 with 语句的目标或者 as 后的名字绑定。 
  • __exit__(self, exception_type, exception_value, traceback) 定义当一个代码块被执行或者终止后会话管理器应该做什么。它可以被用来处理异常,清楚工作或者做一些代码块执行完毕之后的日常工作。如果代码块执行成功, exception_type , exception_value , 和 traceback 将会是 None。否则的话你可以选择处理这个异常或者是直接交给用户处理。如果你想处理这个异常的话,确认 __exit__ 在所有结束之后会返回 True 。如果你想让异常被会话管理器处理的话,那么就这样处理。

__enter__  __exit__ 对于明确有定义好的和日常行为的设置和清洁工作的类很有帮助。你也可以使用这些方法来创建一般的可以包装其他对象的会话管理器。以下是一个例子。


class Closer:     
    '''通过with语句和一个close方法来关闭一个对象的会话管理器'''      
    def __init__(self, obj):
        self.obj = obj      
    def __enter__(self):         
        return self.obj # bound to target      
    def __exit__(self, exception_type, exception_val, trace):         
        try:             
            self.obj.close()         
        except AttributeError: 
            # obj isn't closable             
            print 'Not closable.'             
            return True # exception handled successfully


以下是一个使用 Closer 的例子,使用一个FTP链接来证明(一个可关闭的套接字):


>>> from magicmethods import Closer 
>>> from ftplib import FTP 
>>> with Closer(FTP('ftp.somesite.com')) as conn: 
...  conn.dir() 
... 
>>> conn.dir() 
>>> with Closer(int(5)) as i: 
...  i += 1 
... Not closable. 
>>> i 6 


你已经看到了我们的包装器如何静默的处理适当和不适当的使用行为。这是会话管理器和魔术方法的强大功能。

创建对象的描述器

描述器是通过得到,设置,删除的时候被访问的类。当然也可以修改其他的对象。描述器并不是鼓励的,他们注定被一个所有者类所持有。当创建面向对象的数据库或者类,里面含有相互依赖的属性时,描述器将会非常有用。一种典型的使用方法是用不同的单位表示同一个数值,或者表示某个数据的附加属性(比如坐标系上某个点包含了这个点到远点的距离信息)。

为了构建一个描述器,一个类必须有至少 __get__ 或者 __set__ 其中一个,并且 __delete__ 被实现。让我们看看这些魔术方法。 

  • __get__(self, instance, owner) 定义当描述器的值被取得的时候的行为, instance是拥有者对象的一个实例。 owner是拥有者类本身。
  • __set__(self, instance, value) 定义当描述器值被改变时候的行为。 instance是拥有者类的一个实例value是要设置的值。 
  • __delete__(self, instance) 定义当描述器的值被删除的行为。instance是拥有者对象的实例。 以下是一个描述器的实例:单位转换。
class Meter(object):     
    '''Descriptor for a meter.'''     
    def __init__(self, value=0.0):         
    	self.value = float(value)     
    def __get__(self, instance, owner):         
    	return self.value     
    def __set__(self, instance, value):         
    	self.value = float(value)  

class Foot(object):     
	'''Descriptor for a foot.'''     
	def __get__(self, instance, owner):         
		return instance.meter * 3.2808     

	def __set__(self, instance, value):         
		instance.meter = float(value) / 3.2808  

class Distance(object):     
	'''Class to represent distance holding two descriptors for feet and meters.'''     
	meter = Meter()     
	foot = Foot()

储存你的对象

如果你接触过其他的 Pythoner,你可能已经听说过 Pickle 了, Pickle 是用来序列化 Python 数据结构的模块,在你需要暂时存储一个对象的时候(比如缓存),这个模块非常的有用,不过这同时也是隐患的诞生地。

序列化数据是一个非常重要的功能,所以他不仅仅拥有相关的模块( Pickle , cPickle ),还有自己的协议以及魔术方法,不过首先,我们先讨论下关于序列化内建数据结构的方法。


Pickling: 简单例子

让我们深入研究 Pickle,比如说你现在需要临时储存一个字典,你可以把它写入到一个文件里,并且要小心翼翼的确保格式正确,之后再用 exec() 或者处理文件输入来恢复数据,实际上这是很不安全的,如果你使用文本存储了一些重要的数据,任何方式的改变都可能会影响到你的程序,轻则程序崩溃,重则被恶意程序利用,所以,让我们用 Pickle 代替这种方式:

import pickle 
data = {'foo': [1, 2, 3], 'bar': ('Hello', 'world!'), 'baz': True}  
jar = open('data.pkl', 'wb') 
pickle.dump(data, jar) # write the pickled data to the file jar  
jar.close() 

嗯,过了几个小时之后,我们需要用到它了,只需把它 unpickle 了就行了:

import pickle 
pkl_file = open('data.pkl', 'rb') # connect to the pickled data  
data = pickle.load(pkl_file) # load it into a variable 
print data pkl_file.close() 

正如你期望的,数据原封不动的回来了!

同时要给你一句忠告: pickle 并不是很完美, Pickle 文件很容易被不小心或者故意损坏, Pickle 文件比纯文本文件要稍微安全一点,但是还是可以被利用运行恶意程序。 Pickle 不是跨版本兼容的(译注:最近刚好在 《Python Cookbook》上看到相关讨论,书中描述的 Pickle 是跨版本兼容的,此点待验证),所以尽量不要去分发 Pickle 过的文本,因为别人并不一定能够打开。不过在做缓存或者其他需要序列化数据的时候, Pickle 还是很有用处的。


序列化你自己的对象

Pickle 并不是只支持内建数据结果,任何遵循 Pickle 协议的类都可以,Pickle 协议为 Python 对象规定了4个可选方法来自定义 Pickle 行为(对于 C 扩展的 cPickle 模块会有一些不同,但是这并不在我们的讨论范围内):

  • __getinitargs__(self):如果你希望在逆序列化的同时调用 __init__ ,你可以定义 __getinitargs__ 方法,这个方法应该返回一系列你想被 __init__ 调用的参数,注意这个方法只对老样式的类起作用。
  • __getnewargs__(self):对于新式的类,你可以定义任何在重建对象时候传递到 __new__ 方法中的参数。这个方法也应该返回一系列的被 __new__ 调用的参数。
  • __getstate__(self):你可以自定义当对象被序列化时返回的状态,而不是使用 __dict 方法,当逆序列化对象的时候,返回的状态将会被 __setstate__ 方法调用。
  • __setstate__(self, state):在对象逆序列化的时候,如果 __setstate__ 定义过的话,对象的状态将被传给它而不是传给 __dict__ 。这个方法是和 __getstate__ 配对的,当这两个方法都被定义的时候,你就可以完全控制整个序列化与逆序列化的过程了。


例子

我们以 Slate 为例,这是一段记录一个值以及这个值是何时被写入的程序,但是,这个 Slate 有一点特殊的地方,当前值不会被保存。

import time  

class Slate: 
	'''Class to store a string and a changelog, and forget its value when  pickled.'''      
	def __init__(self, value):                     
		self.value = value          
		self.last_change = time.asctime()         
		self.history = {}     

	def change(self, new_value): 
		# Change the value. Commit last value to history          
		self.history[self.last_change] = self.value         
		self.value = new_value          
		self.last_change = time.asctime()      

	def print_changes(self):         

		print 'Changelog for Slate object:'          
		for k, v in self.history.items():             
			print '%s\t %s' % (k, v)     

	def __getstate__(self): 
		# Deliberately do not return self.value or self.last_change. 
		# We want to have a "blank slate" when we unpickle.         
		return self.history     

	def __setstate__(self, state): 
		# Make self.history = state and last_change and value undefined         
		self.history = state         
		self.value, self.last_change = None, None 

结论

这份指南的希望为所有人都能带来一些知识,即使你是 Python 大牛或者对于精通于面向对象开发。如果你是一个 Python 初学者,阅读这篇文章之后你已经获得了编写丰富,优雅,灵活的类的知识基础了。如果你是一个有一些经验的 Python 程序员,你可能会发现一些能让你写的代码更简洁的方法。如果你是一个 Python 大牛,可能会帮助你想起来一些你已经遗忘的知识,或者一些你还没听说过的新功能。不管你现在有多少经验,我希望这次对于 Python 特殊方法的旅程能够带给你一些帮助(用双关语真的很不错 XD)(译注: 这里的双关在于标题为 Magic Methods 这里是 神奇的旅程 ,不过由于中英语序的问题,直译略显头重脚轻,所以稍微变化了下意思,丢掉了双关的含义)。

附录:如何调用魔术方法

一些魔术方法直接和内建函数相对,在这种情况下,调用他们的方法很简单,但是,如果是另外一种不是特别明显的调用方法,这个附录介绍了很多并不是很明显的魔术方法的调用形式。

魔术方法 调用方式 解释
__new__(cls [,...]) instance = MyClass(arg1, arg2) __new__ 在创建实例的时候被调用
__init__(self [,...]) instance = MyClass(arg1, arg2) __init__ 在创建实例的时候被调用
__cmp__(self, other) self == other, self > other, 等。 在比较的时候调用
__pos__(self) +self 一元加运算符
__neg__(self) -self 一元减运算符
__invert__(self) ~self 取反运算符
__index__(self) x[self] 对象被作为索引使用的时候
__nonzero__(self) bool(self) 对象的布尔值
__getattr__(self, name) self.name # name 不存在 访问一个不存在的属性时
__setattr__(self, name, val) self.name = val 对一个属性赋值时
__delattr__(self, name) del self.name 删除一个属性时
__getattribute(self, name) self.name 访问任何属性时
__getitem__(self, key) self[key] 使用索引访问元素时
__setitem__(self, key, val) self[key] = val 对某个索引值赋值时
__delitem__(self, key) del self[key] 删除某个索引值时
__iter__(self) for x in self 迭代时
__contains__(self, value) value in self, value not in self 使用 in 操作测试关系时
__concat__(self, value) self + other 连接两个对象时
__call__(self [,...]) self(args) “调用”对象时
__enter__(self) with self as x: with 语句环境管理
__exit__(self, exc, val, trace) with self as x: with 语句环境管理
__getstate__(self) pickle.dump(pkl_file, self) 序列化
__setstate__(self) data = pickle.load(pkl_file) 序列化

希望这个表格对你对于什么时候应该使用什么方法这个问题有所帮助。