强烈推荐超详解Python-魔法函数(高级语法)

Posted ZSYL

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了强烈推荐超详解Python-魔法函数(高级语法)相关的知识,希望对你有一定的参考价值。

1. 前言

1.1 什么是魔法函数?

在Python中,__xx__()的函数叫做魔法方法,以双下划线(__xx__)开始和结束的函数(不可自己定义)为魔法函数。

所谓魔法函数(Magic Methods),是Python的一种高级语法,允许你在类中自定义函数(函数名格式一般为__xx__),并绑定到类的特殊方法中。比如在类A中自定义__str__()函数,则在调用str(A())时,会自动调用__str__()函数,并返回相应的结果。

在我们平时的使用中,可能经常使用__init__函数(构造函数)和__del__函数(析构函数),其实这也是魔法函数的一种。

  • __init__()方法,在创建一个对象时默认被调用,不需要手动调用

  • __init__(self)中的self参数,不需要开发者传递,python解释器会自动把当前的对象引用传递过去。

  • 当使用print输出对象的时候,默认打印对象的内存地址。如果类定义了__str__方法,那么就会打印从在这个方法中 return 的数据。

  • 当删除对象时,python解释器也会默认调用__del__()方法。

  • 调用类实例化的对象的方法时自动调用魔法函数。

  • 在自己定义的类中,可以实现之前的内置函数。

1.2 魔法函数有什么作用?

魔法函数可以为你写的类增加一些额外功能,方便使用者理解。

举个简单的例子,我们定义一个“人”的类People,当中有属性姓名name、年龄age。让你需要利用sorted函数对一个People的数组进行排序,排序规则是按照name和age同时排序,即name不同时比较name,相同时比较age。由于People类本身不具有比较功能,所以需要自定义,你可以这么定义People类:

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

    def __str__(self):
        return self.name + ":" + str(self.age)

    def __lt__(self, other):
        return self.name < other.name if self.name != other.name else self.age < other.age


if __name__=="__main__":

    print("\\t".join([str(item) for item in sorted([People("abc", 18),
        People("abe", 19), People("abe", 12), People("abc", 17)])]))

输出结果:

abc:17	abc:18	abe:12	abe:19

上个例子中的__lt__函数即less than函数,即当比较两个People实例时自动调用。

类似Java中的重写toString()方法,重写Comparable()方法

2. 常见的魔法函数

我们将魔法方法分为:非数学运算和数学运算两大类

3. 非数学运算

3.1 字符串表示

__repr__

__repr__函数和__str__函数:

函数str() 用于将值转化为适于人阅读的形式,而repr() 转化为供解释器读取的形式,某对象没有适于人阅读的解释形式的话,str() 会返回与repr(),所以print展示的都是str的格式。

我们经常会直接输出类的实例化对象,例如:

class CLanguage:
    pass
clangs = CLanguage()
print(clangs)

程序运行结果为:

<__main__.CLanguage object at 0x000001A7275221D0>

通常情况下,直接输出某个实例化对象,本意往往是想了解该对象的基本信息,例如该对象有哪些属性,它们的值各是多少等等。

但默认情况下,我们得到的信息只会是“类名+object at+内存地址”,对我们了解该实例化对象帮助不大。

那么,有没有可能自定义输出实例化对象时的信息呢?

答案是肯定的,通过重写类的 repr() 方法即可。事实上,当我们输出某个实例化对象时,其调用的就是该对象的 repr() 方法,输出的是该方法的返回值。

以本节开头的程序为例,执行 print(clangs) 等同于执行 print(clangs.repr()),程序的输出结果是一样的(输出的内存地址可能不同)。

init(self) 的性质一样,Python 中的每个类都包含 repr() 方法,因为 object 类包含__reper__() 方法,而 Python 中所有的类都直接或间接继承自 object 类。

默认情况下,__repr__() 会返回和调用者有关的 “类名+object at+内存地址”信息。当然,我们还可以通过在类中重写这个方法,从而实现当输出实例化对象时,输出我们想要的信息。

举个例子:

class CLanguage:
    def __init__(self):
        self.name = "C语言中文网"
        self.add = "http://c.biancheng.net"
    def __repr__(self):
        return "CLanguage[name="+ self.name +",add=" + self.add +"]"
clangs = CLanguage()
print(clangs)

程序运行结果为:

CLanguage[name=C语言中文网,add=http://c.biancheng.net]

由此可见,__repr__() 方法是类的实例化对象用来做“自我介绍”的方法,默认情况下,它会返回当前对象的“类名+object at+内存地址”,而如果对该方法进行重写,可以为其制作自定义的自我描述信息。

__str__

直接打印对象的实现方法,__ str__是被print函数调用的。打印一个实例化对象时,打印的其实时一个对象的地址。而通过__str__()函数就可以帮助我们打印对象中具体的属性值,或者你想得到的东西。

在Python中调用print()打印实例化对象时会调用__str__()。如果__str__()中有返回值,就会打印其中的返回值。

class Cat:
    """定义一个猫类"""
 
    def __init__(self, new_name= "汤姆", new_age= 20):
        """在创建完对象之后 会自动调用, 它完成对象的初始化的功能"""
        self.name = new_name
        self.age = new_age  # 它是一个对象中的属性,在对象中存储,即只要这个对象还存在,那么这个变量就可以使用
        # num = 100  # 它是一个局部变量,当这个函数执行完之后,这个变量的空间就没有了,因此其他方法不能使用这个变量
 
    def __str__(self):
        """返回一个对象的描述信息"""
        # print(num)
        return "名字是:%s , 年龄是:%d" % (self.name, self.age)

# 创建了一个对象
tom = Cat("汤姆", 30)
print(tom)
名字是:汤姆 , 年龄是:30

总结:当使用print输出对象的时候,只要自己定义了__str__(self)方法,那么就会打印从在这个方法中return的数据。__str__方法需要返回一个字符串,当做这个对象的描写。

__repr__和__str__区别

看下面的例子就明白了:

class Test(object):
    def __init__(self, value='hello, world!'):
        self.data = value

>>> t = Test()
>>> t
<__main__.Test at 0x7fa91c307190>
>>> print(t)
__main__.Test object at 0x7fa91c307190>

# 看到了么?上面打印类对象并不是很友好,显示的是对象的内存地址
# 下面我们重构下该类的__repr__以及__str__,看看它们俩有啥区别

# 重构__repr__
class TestRepr(Test):
    def __repr__(self):
        return 'TestRepr(%s)' % self.data

>>> tr = TestRepr()
>>> tr               直接终端显示,不print就是面向程序员
TestRepr(hello, world!)
>>> print(tr)         print是面向程序员
TestRepr(hello, world!)

# 重构__repr__方法后,不管直接输出对象还是通过print打印的信息都按我们__repr__方法中定义的格式进行显示了

# 重构__str__
calss TestStr(Test):
    def __str__(self):
        return '[Value: %s]' % self.data

>>> ts = TestStr()
>>> ts
__main__.TestStr at 0x7fa91c314e50>
>>> print(ts)
[Value: hello, world!]

# 你会发现,直接输出对象ts时并没有按我们__str__方法中定义的格式进行输出,而用print输出的信息却改变了

__repr__和__str__这两个方法都是用于显示的,__str__是面向用户的,而__repr__面向程序员。

  • 打印操作会首先尝试__str__和str内置函数(print运行的内部等价形式),它通常应该返回一个友好的显示。
  • __repr__用于所有其他的环境中:用于交互模式下提示回应以及repr函数,如果没有使用__str__,会使用print和str。它通常应该返回一个编码字符串,可以用来重新创建对象,或者给开发者详细的显示。

当我们想所有环境下都统一显示的话,可以重构__repr__方法;当我们想在不同环境下支持不同的显示,例如终端用户显示使用__str__,而程序员在开发期间则使用底层的__repr__来显示,实际上__str__只是覆盖了__repr__以得到更友好的用户显示。

3.2 集合、序列相关

__len__函数、__getitem__函数、__setitem__函数、__delitem__函数和__contains__函数

__ len__函数

在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法。

class Students():
    def __init__(self, *args):
        self.names = args
    def __len__(self):
        return len(self.names)

ss = Students('Bob', 'Alice', 'Tim')
print(len(ss))

输出结果:

3

__getitem__函数

Python的特殊方法__getitem_() 主要作用是可以让对象实现迭代功能。我们通过一个实例来说明。

定义一个Sentence类,通过索引提取单词。

import re
RE_WORD = re.compile(r'\\w+')
class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)  # re.findall函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配
    def __getitem__(self, index):
        return self.words[index]  

测试:

>>> s = Sentence('The time has come')
>>> for word in s:
            print(word)
    
    The
    time
    has
    come
>>> s[0]
    'The'
>>> s[1]
    'time'

通过测试发现,示例 s 可以正常迭代。但是没有定义getitem() 测试则会报错,TypeError: ‘***’ object is not iterable。

序列可以迭代:

我们都知道序列是可以迭代,下面具体说明原因。

解释器需要迭代对象x时, 会自动调用iter(x)方法。

任何Python序列都可迭代的原因是,他们都实现了__getitem__方法。其实,标准的序列也都实现了__iter__方法。

注意:从python3.4 开始,检查对象x能否迭代,最准确的方法是: 调用iter(x)方法,如果不可迭代,在处理TypeError异常。这比使用isinstance(x,abc.Iterable)更准确,因为iter()方法会考虑到遗留的__getitem__()方法,而abc.Iterable类则不考虑。

__ setitem__函数

setitem(self,key,value):该方法应该按一定的方式存储和key相关的value。在设置类实例属性时自动调用的。

# -*- coding:utf-8 -*-
 
class A:
    def __init__(self):
        self['B']='BB'
        self['D']='DD'
        
    def __setitem__(self,name,value):
 
        print "__setitem__:Set %s Value %s" %(name,value)
        
        
if __name__=='__main__':
    X=A()

输出结果为:

__setitem__:Set B Value BB
__setitem__:Set D Value DD

__ delitem__()

__delitem__(self,key):

这个方法在对对象的组成部分使用__del__语句的时候被调用,应删除与key相关联的值。同样,仅当对象可变的时候,才需要实现这个方法。

class Tag:
    def __init__(self):
        self.change={'python':'This is python',
                     'php':'PHP is a good language'}
 
    def __getitem__(self, item):
        print('调用getitem')
        return self.change[item]
 
    def __setitem__(self, key, value):
        print('调用setitem')
        self.change[key]=value
 
    def __delitem__(self, key):
        print('调用delitem')
        del self.change[key]
 
a=Tag()
print(a['php'])
del a['php']
print(a.change)

输出结果:

调用getitem
PHP is a good language
调用delitem
{'python': 'This is python'}

__contains__函数

在Class里添加__contains__(self,x)函数,可判断我们输入的数据是否在Class里.参数x就是我们传入的数据.

如下代码:

class Graph():
    def __init__(self):
        self.items = {'a':1,'b':2,'c':3}
    def __contains__(self,x): # 判断一个定点是否包含在里面
        return x in self.items

a = Graph()
print('a' in a) # 返回True
print('d' in a) # 返回False

>> True
>> False
class Graph():
    def __init__(self):
        self.items = {'a':1,'b':2,'c':3}

    def __str__(self):
        return '打印我干嘛'
    
    def __contains__(self,x): # x参数接受的就是我们手动传递的数据
        if x<10 and x>0:
            return True
        return False

print(9 in Graph())
print(5 in Graph())
print(51 in Graph())

>> True
>> True
>> False

3.3 迭代相关

__iter__函数和__next__函数:

迭代器就是重复地做一些事情,可以简单的理解为循环,在python中实现了__iter__方法的对象是可迭代的,实现了next()方法的对象是迭代器,这样说起来有点拗口,实际上要想让一个迭代器工作,至少要实现__iter__方法和next方法。

很多时候使用迭代器完成的工作使用列表也可以完成,但是如果有很多值列表就会占用太多的内存,而且使用迭代器也让我们的程序更加通用、优雅、pythonic。

如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的next()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

容器(container):

容器是用来储存元素的一种数据结构,容器将所有数据保存在内存中,Python中典型的容器有:list,set,dict,str等等。

class test():
    def __init__(self,data=1):
        self.data = data

    def __iter__(self):
        return self
    def __next__(self):
        if self.data > 5:
            raise StopIteration
        else:
            self.data+=1
            return self.data

for item in test(3):
    print(item)

输出结果:

4
5
6

for … in… 这个语句其实做了两件事。第一件事是获得一个可迭代器,即调用了__iter__()函数。 第二件事是循环的过程,循环调用__next__()函数。

对于test这个类来说,它定义了__iter__和__next__函数,所以是一个可迭代的类,也可以说是一个可迭代的对象(Python中一切皆对象)。

迭代器:

含有__next__()函数的对象都是一个迭代器,所以test也可以说是一个迭代器。如果去掉__itet__()函数,test这个类也不会报错。如下代码所示:

class test():
    def __init__(self,data=1):
        self.data = data

    def __next__(self):
        if self.data > 5:
            raise StopIteration
        else:
            self.data+=1
            return self.data

t = test(3)   
for i in range(3):
    print(t.__next__())

输出结果:

4
5
6

生成器:

生成器是一种特殊的迭代器。当调用fib()函数时,生成器实例化并返回,这时并不会执行任何代码,生成器处于空闲状态,注意这里prev, curr = 0, 1并未执行。然后这个生成器被包含在list()中,list会根据传进来的参数生成一个列表,所以它对fib()对象(一切皆对象,函数也是对象)调用__next__方法。

def fib(end = 1000):
    prev,curr=0,1
    while curr < end:
        yield curr
        prev,curr=curr,curr+prev

print(list(fib()))

输出结果:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

上面只是做了几个演示,这里具体说明一下:

当调用iter函数的时候,生成了一个迭代对象,要求__iter__必须返回一个实现了__next__的对象,我们就可以通过next函数访问这个对象的下一个元素了,并且在你不想继续有迭代的情况下抛出一个StopIteration的异常(for语句会捕获这个异常,并且自动结束for),下面实现了一个自己的类似range函数的功能。

class MyRange(object):
    def __init__(self, end):
        self.start = 0
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.start < self.end:
            ret = self.start
            self.start += 1
            return ret
        else:
            raise StopIteration

from collections.abc import *

a = MyRange(5)
print(isinstance(a, Iterable))
print(isinstance(a, Iterator))

for i in a:
    print(i)

输出结果是:

True
True
0
1
2
3
4

接下来我们使用next函数模拟一次:

class MyRange(object):
    def __init__(self, end):
        self.start = 0
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.start < self.end:
            ret = self.start
            self.start += 1
            return ret
        else:
            raise StopIteration

a = MyRange(5)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a)) # 其实到这里已经完成了,我们在运行一次查看异常

可以看见一个很明显的好处是,每次产生的数据,是产生一个用一个,什么意思呢,比如我要遍历[0, 1, 2, 3…]一直到10亿,如果使用列表的方式,那么是会全部载入内存的,但是如果使用迭代器,可以看见,当用到了(也就是在调用了next)才会产生对应的数字,这样就可以节约内存了,这是一种懒惰的加载方式。

总结

  • 可以使用collection.abs里面的Iterator和Iterable配合isinstance函数来判断一个对象是否是可迭代的,是否是迭代器对象。
  • iter实际是映射到了__iter__函数。
  • 只要实现了__iter__的对象就是可迭代对象(Iterable),正常情况下,应该返回一个实现了__next__的对象(虽然这个要求不强制),如果自己实现了__next__,当然也可以返回自己。
  • 同时实现了__iter__和__next__的是迭代器(Iterator),当然也是一个可迭代对象了,其中__next__应该在迭代完成后,抛出一个StopIteration异常。
  • for语句会自动处理这个StopIteration异常以便结束for循环。

3.4 可调用

__call__函数:

该方法的功能类似于在类中重载 () 运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式使用。作用:为了将类的实例对象变为可调用对象。

class CLanguage:
    # 定义__call__方法
    def __call__(self,name,add):
        print("调用__call__()方法",name,add)

clangs = CLanguage()
clangs("C语言中文网","http://c.biancheng.net")

程序执行结果为:

调用__call__()方法 C语言中文网 http://c.biancheng.net

函数本身可以被调用:

def func():
    pass

print(callable(func)强烈推荐一款Python可视化神器!强烈必备!

Python高级语法-私有属性-魔法属性(4.7.2)

Python面向对象-高级用法

python进阶之内置函数和语法糖触发魔法方法

python的魔法方法详解(带双下划线的方法)

python魔法方法详解