python 魔术方法

python 魔术方法

前言

在做python开发的过程中,我们大家都会遇到在class(类)中使用双下划线的方法,这些都是我们经常所说的"魔法"方法.这些方法可以对类添加特殊的功能,使用恰当可以很大的提升我们在开发过程中的便捷性,方便的进行扩展.

概览

目前我们常见的魔法方法大致可分为以下几类:

  • 构造与初始化
  • 类的表示
  • 访问控制
  • 比较操作
  • 容器类操作
  • 可调用对象
  • Pickling序列化

我们这次主要介绍这几类常用魔法方法:

1.构造与初始化

__init__
构造方法是我们使用频率最高的魔法方法了,几乎在我们定义类的时候,都会去定义构造方法,它的主要作用就是在初始化一个对象时,定义这个对象的初始值。

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age


p1 = Person('Jack', 25)
p2 = Person('shuke', 20)

__new__

  1. 事实上,当我们理解了new方法后,我们还可以利用它来做一些其他有趣的事情,比如实现 设计模式中的 单例模式(singleton)
  2. 依照Python官方文档的说法,new方法主要是当你继承一些不可变的class时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。还有就是实现自定义的metaclass
  3. 这个方法我们一般很少定义,不过我们在一些开源框架中偶尔会遇到定义这个方法的类。实际上,这才是"真正的构造方法",它会在对象实例化时第一个被调用,然后再调用init,它们的区别主要如下:
  • new的第一个参数是cls,而init的第一个参数是self
  • new返回值是一个实例,而init没有任何返回值,只做初始化操作
  • new由于是返回一个实例对象,所以它可以给所有实例进行统一的初始化操作
  1. 由于new优先于init调用,且返回一个实例,所以我们可以利用这种特性,每次返回同一个实例来实现一个单例类:

__new__的作用:

class PositiveInteger(int):

  def __init__(self, value):

    super(PositiveInteger, self).__init__(self, abs(value))

i = PositiveInteger(-3)

print(i)

但运行后会发现,结果根本不是我们想的那样,我们仍然得到了-3。这是因为对于int这种不可变的对象,我们只有重载它的new方法才能起到自定义的作用。
修改后的代码如下:

class PositiveInteger(int):

  def __new__(cls, value):

    return super(PositiveInteger, cls).__new__(cls, abs(value))

i = PositiveInteger(-3)

print(i)

通过重载new方法,我们实现了需要的功能.

class g(float):
    """千克转克"""

    def __new__(cls, kg):
        return float.__new__(cls, kg * 2)


# 50千克转为克
a = g(50)
print(a)  # 100.0
print(a + 100)  # 200.0 由于继承了float,所以可以直接运算,非常方便!

new来实现单例
因为类每一次实例化后产生的过程都是通过new来控制的,所以通过重载new方法,我们 可以很简单的实现单例模式。

# 写法一
class Singleton(object):
    def __new__(cls):
        # 关键在于这,每一次实例化的时候,我们都只会返回这同一个instance对象

        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)

        return cls.instance

obj1 = Singleton()
obj2 = Singleton()
obj1.attr1 = 'value1'
print(obj1.attr1, obj2.attr1)
print(obj1 is obj2)

"""
>>>
value1 value1
True
"""
可以看到obj1和obj2是同一个实例。

# 写法二
class Singleton(object):
    """单例"""
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance


class MySingleton(Singleton):
    pass


a = MySingleton()
b = MySingleton()
print(a is b)
"""
>>> 
True
"""

2. del析构方法

这个方法代表析构方法,也就是在对象被垃圾回收时被调用。但是请注意,执行del x不一定会执行此方法。

由于Python是通过引用计数来进行垃圾回收的,也就是说,如果这个实例还是有被引用到,即使执行del销毁这个对象,但其引用计数还是大于0,所以不会触发执行del
例子:
此时我们没有对实例进行任何操作时,del在程序退出后被调用。

class Person(object):
    def __del__(self):
        print('__del__')

a = Person()
print('exit')

"""
exit
__del__
"""

由于此实例没有被其他对象所引用,当我们手动销毁这个实例时,del被调用后程序正常退出。

class Person(object):
    def __del__(self):
        print('__del__')


a = Person()
b = a  # b引用a
del a  # 手动销毁,不触发__del__
print('exit')

"""
exit
__del__
"""

此时实例有被其他对象引用,尽管我们手动销毁这个实例,但依然不会触发del方法,而是在程序正常退出后被调用执行。
为了保险起见,当我们在对文件、socket进行操作时,要想安全地关闭和销毁这些对象,最好是在try异常块后的finally中进行关闭和释放操作!

3. 类的表示

str/repr
这两个魔法方法一般会放到一起进行讲解,它们的主要差别为:

str强调可读性,而repr强调准确性/标准性
str的目标人群是用户,而repr的目标人群是机器,它的结果是可以被执行的
%s调用str方法,而%r调用repr方法
来看几个例子,了解内置类实现这2个方法的效果:

>>> a = 'hello'
>>> str(a)
'hello'
>>> '%s' % a   # 调用__str__
'hello'
>>>
>>> repr(a)     # 对象a的标准表示,也就是a是如何创建的
"'hello'"
>>> '%r' % a    # 调用__repr__ 
"'hello'"
>>>
>>>
>>> import datetime
>>> b = datetime.datetime.now()
>>> str(b)
'2018-05-03 19:08:45.921879'
>>> print(b)     # 等同于print str(b)
2018-05-03 19:08:45.921879
>>>
>>>
>>> repr(b)     # 展示对象b的标准创建方式(如何创建的 
'datetime.datetime(2018, 5, 3, 19, 8, 45, 921879)'
>>> b
datetime.datetime(2018, 5, 3, 19, 8, 45, 921879)
>>>
>>> c = eval(repr(b))
>>> c
datetime.datetime(2018, 5, 3, 19, 8, 45, 921879)

从上面的例子可以看出这两个方法的主要区别,在实际中我们定义类时,一般这样定义即可:

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        # 格式化,友好对用户展示
        return 'name: %s, age: %s' % (self.name, self.age)

    def __repr__(self):
        # 标准化展示
        return "Person('%s', %s)" % (self.name, self.age)


person = Person('zhangsan', 20)
print(str(person))      # name: zhangsan, age: 20
print('%s' % person)    # name: zhangsan, age: 20
print(repr(person))     # Person('zhangsan', 20)
print('%r' % person)    # Person('zhangsan', 20)

"""
name: zhangsan, age: 20
name: zhangsan, age: 20
Person('zhangsan', 20)
Person('zhangsan', 20)
"""

这里值得注意的是,如果只定义了strrepr其中一个,那会是什么结果?

如果只定义了str_,那么repr(person)输出<main.Person object at 0x10783b400>
如果只定义了repr,那么str(person)与repr(person)结果是相同的
也就是说,repr在表示类时,是一级的,如果只定义它,那么str = repr
str展示类时是次级的,用户可自定义类的展示格式,如果没有定义repr,那么repr(person)将会展示缺省的定义。

4. 对象判断

hash/eq
hash方法返回一个整数,用来表示该对象的唯一标识,配合eq方法判断两个对象是否相等(==):

class Person(object):
    def __init__(self, uid):
        self.uid = uid

    def __repr__(self):
        return 'Person(%s)' % self.uid

    def __hash__(self):
        return self.uid

    def __eq__(self, other):
        return self.uid == other.uid


p1 = Person(1)
p2 = Person(1)
print(p1 == p2)
p3 = Person(2)
print(set([p1, p2, p3]))  # 根据唯一标识去重输出 set([Person(1), Person(2)])

"""
True
{Person(1), Person(2)}
"""

如果我们需要判断两个对象是否相等,只要我们重写hasheq方法就可以完成此功能。此外使用set存放这些对象时,会根据这两个方法进行去重操作。

5. 对象布尔判断

bool
当调用bool(obj)时,会调用bool方法,返回True/False。

class Person(object):
    def __init__(self, uid):
        self.uid = uid

    def __bool__(self):
        return self.uid > 10

p1 = Person(1)
p2 = Person(15)
print(bool(p1))  # False
print(bool(p2))  # True

"""
False
True
"""

⚠️: 在Python3中,nonzero被重命名bool

6. 访问控制

访问控制相关的魔法方法,主要涉及以下几个:

setattr:通过.设置属性或setattr(key, value)
getattr:访问不存在的属性
delattr:删除某个属性
getattribute:访问任意属性或方法
来看一个完整的例子:


class Person(object):
    def __setattr__(self, key, value):
        """属性赋值"""
        if key not in ('name', 'age'):
            return
        if key == 'age' and value < 0:
            raise ValueError()
        super(Person, self).__setattr__(key, value)

    def __getattr__(self, key):
        """访问某个不存在的属性"""
        return 'unknown'

    def __delattr__(self, key):
        """删除某个属性"""
        if key == 'name':
            raise AttributeError()
        super(Person, self).__delattr__(key)

    def __getattribute__(self, key):
        """所有属性/方法调用都经过这里"""
        if key == 'money':
            return 100
        if key == 'hello':
            return self.say
        return super(Person, self).__getattribute__(key)

    def say(self):
        return 'hello'

p1 = Person()
p1.name = 'zhangsan'  # 调用__setattr__
p1.age = 20  # 调用__setattr__
print(p1.name)  # zhangsan
print(p1.age)  # 20
setattr(p1, 'name', 'lisi')  # 调用__setattr__
setattr(p1, 'age', 30)  # 调用__setattr__
print(p1.name)  # lisi
print(p1.age)  # 30

p1.gender = 'male'  # __setattr__中忽略对gender赋值
print(p1.gender)  # gender不存在,调用__getattr__返回:unknown
print(p1.money)  # money不存在,在__getattribute__中返回100
print(p1.say())  # hello
print(p1.hello())  # hello,调用__getattribute__,间接调用say方法

del p1.name  # __delattr__中引发AttributeError
p2 = Person()
p2.age = -1  # __setattr__中引发ValueError
  1. setattr
    通过此方法,对象可在在对属性进行赋值时进行控制,所有的属性赋值都会经过它。
    一般常用于对某些属性赋值的检查校验逻辑,例如age不能小于0,否则认为是非法数据等等。
  2. getattr
    很多同学以为此方法是和setattr完全对立的,其实不然!
    这个方法只有在访问某个不存在的属性时才会被调用,看上面的例子,由于gender属性在赋值时,忽略了此字段的赋值操作,所以此属性是没有被成功赋值给对象的。当访问这个属性时,getattr被调用,返回unknown。
  3. del
    删除对象的某个属性时,此方法被调用。一般常用于某个属性必须存在,否则无法进行后续的逻辑操作,会重写此方法,对删除属性逻辑进行检查和校验。
  4. getattribute
    这个方法我们很少用到,它与getattr很容易混淆。它与前者的区别在于:
    getattr访问某个不存在的属性被调用,getattribute访问任意属性被调用
    getattr只针对属性访问,getattribute不仅针对所有属性访问,还包括方法调用

7. Python的类下面的item系列

xxxitem:使用 [''] 的方式操作属性时被调用
setitem:每当属性被赋值的时候都会调用该方法,因此不能再该方法内赋值 self.name = value 会死循环
getitem:当访问不存在的属性时会调用该方法
delitem:当删除属性时调用该方法

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# __author__ = "shuke"
# Date: 2018/5/2

class A(object):
    def __init__(self):
        self['B'] = "BB"
        self['D'] = "DD"
        del self['B']

    def __setitem__(self, name, value):
        '''''
        @summary: 每当属性被赋值的时候都会调用该方法,因此不能再该方法内赋值 self.name = value 会死循环
        '''
        print("__setitem__:Set %s Value %s" % (name, value))
        self.__dict__[name] = value

    def __getitem__(self, name):
        '''''
        @summary: 当访问不存在的属性时会调用该方法
        '''
        print("__getitem__:No attribute named '%s'" % name)
        return 123

    def __delitem__(self, name):
        '''''
        @summary: 当删除属性时调用该方法
        '''
        print("__delitem__:Delect attribute '%s'" % name)
        del self.__dict__[name]
        print(self.__dict__)


if __name__ == "__main__":
    X = A()
    X['bb'] = "BB"
    print(X.__dict__)

"""
>>> 
__setitem__:Set B Value BB
__setitem__:Set D Value DD
__delitem__:Delect attribute 'B'
{'D': 'DD'}
__setitem__:Set bb Value BB
{'D': 'DD', 'bb': 'BB'}
"""

越是强大的魔法方法,责任越大,如果你不能正确使用它,最好还是不用为好,否则在出现问题时很难排查!

参考原文
魔术方法二
延伸

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,218评论 4 16
  • 今天看到一则新闻“15岁男孩跳楼自杀 父亲在孩子头七祭日跳楼身亡”。 从标题上,就能感受到深深的悲伤,一位中年男子...
    oyhfairy阅读 596评论 5 7
  • 污黑的泥垢, 混雜著雨水的潮濕, 在對你說第101句情話時, 囂張地揮發到似火驕陽, 灼熱了你我漸逝的溫熱。 醜陋...
    涯无晨阅读 243评论 0 0
  • 一年一度的文化盛事,“上海书展”又开始啦! 这是我每年来海上的理由之一! 周末的会场依旧世博景象。那家伙,那场面,...
    红墨水阅读 231评论 0 1