前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >非常全的通俗易懂 Python 魔法方法指南(下)

非常全的通俗易懂 Python 魔法方法指南(下)

作者头像
咸鱼学Python
发布2020-02-26 18:04:10
6680
发布2020-02-26 18:04:10
举报
文章被收录于专栏:咸鱼学Python咸鱼学Python

点击上方“咸鱼学Python”,选择“加为星标”

第一时间关注Python技术干货!

作者:Rafe Kettler

翻译:hit9

来源:https://pyzh.readthedocs.io/en/latest/python-magic-methods-guide.html

06. 反射

你可以通过定义魔法方法来控制用于反射的内建函数 isinstance 和 issubclass 的行为。下面是对应的魔法方法:

  • __instancecheck__(self, instance) 检查一个实例是否是你定义的类的一个实例(例如 isinstance(instance, class) )。
  • __subclasscheck__(self, subclass) 检查一个类是否是你定义的类的子类(例如 issubclass(subclass, class) )。

这几个魔法方法的适用范围看起来有些窄,事实也正是如此。我不会在反射魔法方法上花费太多时间,因为相比其他魔法方法它们显得不是很重要。但是它们展示了在Python中进行面向对象编程(或者总体上使用Python进行编程)时很重要的一点:不管做什么事情,都会有一个简单方法,不管它常用不常用。这些魔法方法可能看起来没那么有用,但是当你真正需要用到它们的时候,你会感到很幸运,因为它们还在那儿(也因为你阅读了这本指南!)

07. 抽象基类

请参考 http://docs.python.org/2/library/abc.html

08. 可调用的对象

你可能已经知道了,在Python中,函数是一等的对象。这意味着它们可以像其他任何对象一样被传递到函数和方法中,这是一个十分强大的特性。

Python中一个特殊的魔法方法允许你自己类的对象表现得像是函数,然后你就可以“调用”它们,把它们传递到使用函数做参数的函数中,等等等等。这是另一个强大而且方便的特性,让使用Python编程变得更加幸福。

  • __call__ (self, [args…]) 允许类的一个实例像函数那样被调用。本质上这代表了 x() 和 x.__call__() 是相同的。注意 __call__ 可以有多个参数,这代表你可以像定义其他任何函数一样,定义 __call__ ,喜欢用多少参数就用多少。

__call__ 在某些需要经常改变状态的类的实例中显得特别有用。“调用”这个实例来改变它的状态,是一种更加符合直觉,也更加优雅的方法。一个表示平面上实体的类是一个不错的例子:

代码语言:javascript
复制
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

09. 上下文管理器

在Python 2.5中引入了一个全新的关键词,随之而来的是一种新的代码复用方法—— with 声明。上下文管理的概念在Python中并不是全新引入的(之前它作为标准库的一部分实现),直到PEP 343被接受,它才成为一种一级的语言结构。可能你已经见过这种写法了:

代码语言:javascript
复制
with open('foo.txt') as bar:
    # 使用bar进行某些操作

当对象使用 with 声明创建时,上下文管理器允许类做一些设置和清理工作。上下文管理器的行为由下面两个魔法方法所定义:

  • __enter__(self) 定义使用 with 声明创建的语句块最开始上下文管理器应该做些什么。注意 __enter__ 的返回值会赋给 with 声明的目标,也就是 as 之后的东西。
  • __exit__(self, exception_type, exception_value, traceback) 定义当 with 声明语句块执行完毕(或终止)时上下文管理器的行为。它可以用来处理异常,进行清理,或者做其他应该在语句块结束之后立刻执行的工作。如果语句块顺利执行, exception_type , exception_value 和 traceback 会是 None 。否则,你可以选择处理这个异常或者让用户来处理。如果你想处理异常,确保 __exit__ 在完成工作之后返回 True 。如果你不想处理异常,那就让它发生吧。

对一些具有良好定义的且通用的设置和清理行为的类,__enter____exit__会显得特别有用。你也可以使用这几个方法来创建通用的上下文管理器,用来包装其他对象。下面是一个例子:

代码语言:javascript
复制
class Closer:
    '''一个上下文管理器,可以在with语句中
    使用close()自动关闭对象'''

    def __init__(self, obj):
        self.obj = obj

    def __enter__(self, obj):
        return self.obj # 绑定到目标

    def __exit__(self, exception_type, exception_value, traceback):
        try:
                self.obj.close()
        except AttributeError: # obj不是可关闭的
                print 'Not closable.'
                return True # 成功地处理了异常

这是一个 Closer 在实际使用中的例子,使用一个FTP连接来演示(一个可关闭的socket):

代码语言:javascript
复制
>>> from magicmethods import Closer
>>> from ftplib import FTP
>>> with Closer(FTP('ftp.somesite.com')) as conn:
...         conn.dir()
...
# 为了简单,省略了某些输出
>>> conn.dir()
# 很长的 AttributeError 信息,不能使用一个已关闭的连接
>>> with Closer(int()) as i:
...         i += 
...
Not closable.
>>> i

看到我们的包装器是如何同时优雅地处理正确和不正确的调用了吗?这就是上下文管理器和魔法方法的力量。Python标准库包含一个 contextlib 模块,里面有一个上下文管理器 contextlib.closing() 基本上和我们的包装器完成的是同样的事情(但是没有包含任何当对象没有close()方法时的处理)。

10. 创建描述符对象

描述符是一个类,当使用取值,赋值和删除 时它可以改变其他对象。描述符不是用来单独使用的,它们需要被一个拥有者类所包含。描述符可以用来创建面向对象数据库,以及创建某些属性之间互相依赖的类。描述符在表现具有不同单位的属性,或者需要计算的属性时显得特别有用(例如表现一个坐标系中的点的类,其中的距离原点的距离这种属性)。

要想成为一个描述符,一个类必须具有实现 __get__ , __set____delete__ 三个方法中至少一个。

让我们一起来看一看这些魔法方法:

  • __get__(self, instance, owner) 定义当试图取出描述符的值时的行为。instance 是拥有者类的实例, owner 是拥有者类本身。
  • __set__(self, instance, owner) 定义当描述符的值改变时的行为。instance 是拥有者类的实例, value 是要赋给描述符的值。
  • __delete__(self, instance, owner) 定义当描述符的值被删除时的行为。instance 是拥有者类的实例

现在,来看一个描述符的有效应用:单位转换:

代码语言:javascript
复制
class Meter(object):
    '''米的描述符。'''

    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, owner):
            self.value = float(value)

class Foot(object):
    '''英尺的描述符。'''

    def __get__(self, instance, owner):
            return instance.meter * 3.2808
    def __set__(self, instance, value):
            instance.meter = float(value) / 3.2808

class Distance(object):
    '''用于描述距离的类,包含英尺和米两个描述符。'''
    meter = Meter()
    foot = Foot()

11. 拷贝

有些时候,特别是处理可变对象时,你可能想拷贝一个对象,改变这个对象而不影响原有的对象。这时就需要用到Python的 copy 模块了。然而(幸运的是),Python模块并不具有感知能力, 因此我们不用担心某天基于Linux的机器人崛起。但是我们的确需要告诉Python如何有效率地拷贝对象。

  • __copy__(self) 定义对类的实例使用 copy.copy() 时的行为。copy.copy() 返回一个对象的浅拷贝,这意味着拷贝出的实例是全新的,然而里面的数据全都是引用的。也就是说,对象本身是拷贝的,但是它的数据还是引用的(所以浅拷贝中的数据更改会影响原对象)。
  • __deepcopy__(self, memodict=) 定义对类的实例使用 copy.deepcopy() 时的行为。copy.deepcopy() 返回一个对象的深拷贝,这个对象和它的数据全都被拷贝了一份。memodict 是一个先前拷贝对象的缓存,它优化了拷贝过程,而且可以防止拷贝递归数据结构时产生无限递归。当你想深拷贝一个单独的属性时,在那个属性上调用 copy.deepcopy() ,使用 memodict 作为第一个参数。

这些魔法方法有什么用武之地呢?像往常一样,当你需要比默认行为更加精确的控制时。例如,如果你想拷贝一个对象,其中存储了一个字典作为缓存(可能会很大),拷贝缓存可能是没有意义的。如果这个缓存可以在内存中被不同实例共享,那么它就应该被共享。

12. Pickling

如果你和其他的Python爱好者共事过,很可能你已经听说过Pickling了。Pickling是Python数据结构的序列化过程,当你想存储一个对象稍后再取出读取时,Pickling会显得十分有用。然而它同样也是担忧和混淆的主要来源。

Pickling是如此的重要,以至于它不仅仅有自己的模块( pickle ),还有自己的协议和魔法方法。首先,我们先来简要的介绍一下如何pickle已存在的对象类型(如果你已经知道了,大可跳过这部分内容)。

12.1 小试牛刀

我们一起来pickle吧。假设你有一个字典,你想存储它,稍后再取出来。你可以把它的内容写入一个文件,小心翼翼地确保使用了正确地格式,要把它读取出来,你可以使用 exec() 或处理文件输入。但是这种方法并不可靠:如果你使用纯文本来存储重要数据,数据很容易以多种方式被破坏或者修改,导致你的程序崩溃,更糟糕的情况下,还可能在你的计算机上运行恶意代码。因此,我们要pickle它:

代码语言:javascript
复制
import pickle

data = {'foo': [,,],
                'bar': ('Hello', 'world!'),
                'baz': True}
jar = open('data.pkl', 'wb')
pickle.dump(data, jar) # 将pickle后的数据写入jar文件
jar.close()

过了几个小时,我们想把它取出来,我们只需要反pickle它:

代码语言:javascript
复制
import pickle

pkl_file = open('data.pkl', 'rb') # 与pickle后的数据连接
data = pickle.load(pkl_file) # 把它加载进一个变量
print data
pkl_file.close()

将会发生什么?正如你期待的,它就是我们之前的 data 。

现在,还需要谨慎地说一句:pickle并不完美。Pickle文件很容易因为事故或被故意的破坏掉。Pickling或许比纯文本文件安全一些,但是依然有可能被用来运行恶意代码。而且它还不支持跨Python版本,所以不要指望分发pickle对象之后所有人都能正确地读取。然而不管怎么样,它依然是一个强有力的工具,可以用于缓存和其他类型的持久化工作。

12.2 Pickle你的对象

Pickle不仅仅可以用于内建类型,任何遵守pickle协议的类都可以被pickle。Pickle协议有四个可选方法,可以让类自定义它们的行为(这和C语言扩展略有不同,那不在我们的讨论范围之内)。

  • __getinitargs__(self) 如果你想让你的类在反pickle时调用 __init__ ,你可以定义 __getinitargs__(self) ,它会返回一个参数元组,这个元组会传递给 __init__ 。注意,这个方法只能用于旧式类。
  • __getnewargs__(self) 对新式类来说,你可以通过这个方法改变类在反pickle时传递给 __new__ 的参数。这个方法应该返回一个参数元组。
  • __getstate__(self) 你可以自定义对象被pickle时被存储的状态,而不使用对象的 __dict__ 属性。这个状态在对象被反pickle时会被 __setstate__ 使用。
  • __setstate__(self) 当一个对象被反pickle时,如果定义了 __setstate__ ,对象的状态会传递给这个魔法方法,而不是直接应用到对象的 __dict__ 属性。这个魔法方法和 __getstate__ 相互依存:当这两个方法都被定义时,你可以在Pickle时使用任何方法保存对象的任何状态。
  • __reduce__(self) 当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。reduce 被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 __setstate__ 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选);
  • __reduce_ex__(self) __reduce_ex__ 的存在是为了兼容性。如果它被定义,在pickle时 __reduce_ex__ 会代替 __reduce__ 被调用。__reduce__ 也可以被定义,用于不支持 __reduce_ex__ 的旧版pickle的API调用。

12.3 一个例子

我们的例子是 Slate ,它会记住它的值曾经是什么,以及那些值是什么时候赋给它的。然而 每次被pickle时它都会变成空白,因为当前的值不会被存储:

代码语言:javascript
复制
import time

class Slate:
        '''存储一个字符串和一个变更日志的类
        每次被pickle都会忘记它当前的值'''

        def __init__(self, value):
                self.value = value
                self.last_change = time.asctime()
                self.history = {}

        def change(self, new_value):
                # 改变当前值,将上一个值记录到历史
                self.history[self.last_change] = self.value
                self.value = new_value)
                self.last_change = time.asctime()

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

        def __getstate__(self):
                # 故意不返回self.value或self.last_change
                # 我们想在反pickle时得到一个空白的slate
                return self.history

        def __setstate__(self):
                # 使self.history = slate,last_change
                # 和value为未定义
                self.history = state
                self.value, self.last_change = None, None

总结在最后

这本指南的目标是使所有阅读它的人都能有所收获,无论他们有没有使用Python或者进行面向对象编程的经验。如果你刚刚开始学习Python,你会得到宝贵的基础知识,了解如何写出具有丰富特性的,优雅而且易用的类。如果你是中级的Python程序员,你或许能掌握一些新的概念和技巧,以及一些可以减少代码行数的好办法。如果你是专家级别的Python爱好者,你又重新复习了一遍某些可能已经忘掉的知识,也可能顺便了解了一些新技巧。无论你的水平怎样,我希望这趟遨游Python特殊方法的旅行,真的对你产生了魔法般的效果(实在忍不住不说最后这个双关)。

Love & Share

[ 完 ]

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 咸鱼学Python 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 06. 反射
  • 07. 抽象基类
  • 08. 可调用的对象
  • 09. 上下文管理器
  • 10. 创建描述符对象
  • 11. 拷贝
  • 12. Pickling
    • 12.1 小试牛刀
      • 12.2 Pickle你的对象
        • 12.3 一个例子
        • 总结在最后
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档