Python中常用魔术方法

阅读别人编写的Python代码时,经常会在他们定义的类中看到以“__”为开头和结尾的方法,经过进一步学习后才知道类中实现的这类方法被称为“魔术方法”。“魔术方法”在一些情况下会被自动的调用,通过一些简单的定义就可以实现比较神奇的功能。如果你希望根据自己的需求去实现具有“特殊”功能的类,那么就需要对这些方法进行重写。下面内容将对一些常用的“魔术方法”进行介绍。

1、__init__方法

__init__方法功能有点类似于其它面向对象语言中的构造函数,在类实例化对象时它会被自动的调用。一般情况下,在定义的类中都会实现__init__方法,在__init__方法内根据创建类对象时传入参数对类对象的属性进行初始化操作。

class student(object):
    def __init__(self, _name, _age):
        self.name = _name
        self.age  = _age
        
    def get_info(self):
        print('%s is %d years old' %(self.name, self.age))

测试结果:

>>> stu = student("Michael", 12)
>>> stu.get_info()
Michael is 12 years old

2、__new__方法

前面的__init__方法承担了类实例化对象后对类对象属性必要的初始化操作,而__new__方法则是在__init__方法之前被调用创建类对象。与__init__方法不同的是__new__方法必须返回一个值,返回所创建对象的实例。

__new__方法定义形式:

def __new__(cls, *args, **kw):
    pass

__new__方法的首个参数是cls,因此,它是属于一个类方法,这也是我们可以通过object.__new__来调用它的原因。

不过,一般情况下,在定义的类中很少去重写__new__方法,但在实现单例设计模式时,可通过重写__new__方法实现。

class single(object):
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super(single, cls).__new__(cls, *args, **kwargs)
        return cls._instance
        
    def __init(self):
        pass

上述single类,通过重写了__new__方法,实现了单例设计模式。_instance是single类的属性,用于描述是否已经创建了实例对象。在创建一个single对象时,首先根据类属性_instance判断是否是第一次创建对象,如果是调用父类的__new__方法创建对象,如果不是,就返回之前创建的对象。

测试结果:

>>> obj1 = single()
>>> obj2 = single()
>>> obj1
<__main__.single object at 0x0000000003D44F08>
>>> obj2
<__main__.single object at 0x0000000003D44F08>

3、__call__方法

该方法的功能类似于在类中重载()运算符,使得类实例对象可以像调用普通函数那样,以“类对象名()”形式使用,调用的结果即调用到__call__方法。

class call_cls(object):
    def __call__(self):
        print('__call__ function...')

测试结果:

>>> obj = call_cls()
>>> obj()
__call__ function...

4、__str__方法

如果类中实现了__str__方法,当使用print输出对象时,那么就会打印出从这个方法中返回的数据

class cat(object):
    def __init__(self, _name, _hobby):
        self.name  = _name
        self.hobby = _hobby
        
    def __str__(self):
        return ('%s hobby is %s' %(self.name, self.hobby))

测试结果:

>>> Tom = Cat('Tom', 'eat fish')
>>> print(Tom)
Tom hobby is eat fish

5、__iter__和__next__方法

如果一个类想像list、tuple那样,可以用于for...in循环,那么它就要在内部实现一个__iter__方法,该方法返回一个迭代对象。然后Python的for循环不断的调用该迭代对象的__next__方法来获取循环的下一个值,直到遇到StopIteration异常时退出循环。

现在我们来写一个可用于for...in循环的类,这个类用来计算斐波那契数列

先来看第一种写法:

class Fib_Calc(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值        
        
class Fib(object):
    def __iter__(self):
        return Fib_Calc()

Fib类中实现__iter__方法,该方法中要返回一个可迭代对象。该对象是Fib_Calc类的实例化,所以,Fib_Calc中还应包含__next__方法

Fib_Calc类中的__next__方法根据斐波那契数列特点,计算一次结果

测试结果:

>>> fib = Fib()
>>> for i in fib:
    print(i)

    
1
1
2
3
5
8
13
21
34
55
89

再来看第二种写法:

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __iter__(self):
        return self # 实例本身就是迭代对象,故返回自己

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值

我们可以不像第一种写法那样,直接将__iter__和__next__方法在同一个类中实现。一个类包含__next__方法,这个类就是一个可迭代类,在__iter__方法中只需要返回这个对象自身,那么返回的结果就是一个可迭代对象。

这里同时实现__iter__和__next__方法的对象,也就是我们所说的“迭代器”。

6、__getitem__方法

上面使用__iter__和__next__方法,实现了类能像list和tuple一样使用for...in循环,那么类是否能像list那样按下标取出元素呢?

当然是可以的,如果类要表现得像list那样按照下标取出元素,需要在类中实现__getitem__方法。仍然以斐波拉契数列为例进行演示

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a    

现在,就可以按下标访问数列的任意一项了:

>>> fib = Fib()
>>> fib[2]
2
>>> fib[4]
5

已经实现了下标取数据,但是要像list一样使用切片功能,还需要在__getitem__方法中加入判断。

class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, int): # n是索引
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
        if isinstance(n, slice): # n是切片
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L

__getitem__()中检测到以切片方式访问时,获取切片开始和结束位置,计算开始至结束位置范围对应的斐波拉契数组值,并将其放入到一个列表中,待循环结束后返回这个列表。

测试结果:

>>> fib = Fib()
>>> fib[4]
5
>>> fib[:5]
[1, 1, 2, 3, 5]

7、__getattr__方法

一般情况下,如果我们访问到一个对象中不存在的成员属性时,会发生异常

def student(object):
    def __init__(self, _name):
        self.name = _name
>>> stu = student('Li Ming')
>>> print(stu.name)
Li Ming
>>> print(stu.age)
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in <module>
    print(stu.age)
AttributeError: 'student' object has no attribute 'age'

很明显,这是因为类中没有age这个属性引起的。除了在类中添加age属性外,Python中还提供了另外一种机制,在类实现__getattr__方法,动态返回一个属性。

>>> stu = student('Li Ming')
>>> print(stu.age)
100

小结

这里仅介绍的最常用的几个“魔术方法”,通过例子演示,我们可以看到Python编程时使用“魔术方法”去定制自己类的优势所在。Python中还有很多“魔术方法”可供我们去使用,后续接触到时再对本文的内容进行补充!

posted on 2020-12-28 21:13  quinoa  阅读(582)  评论(0编辑  收藏  举报