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'}
"""

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

参考原文
魔术方法二
延伸

你可能感兴趣的:(python 魔术方法)