Python:类的特殊方法

在Python类中有些方法名、属性名的前后都添加了双下划线,这种方法、属性通常都属于Python的特殊方法和特殊属性,开发者可以通过重写这些这些方法或直接调用这些方法来实现特殊的功能。最常见的特殊方法是前面介绍的构造方法:__init__,开发者可以通过重写类中的__init__方法来实现自己的初始化逻辑。

提示:Python是一门尽量简单的语言,它不像某些语言(如Java)需要让类的实现接口,并实现接口中的方法。Python采用的是一种“约定”的机制,Python按照约定,以特殊名字的方法、属性来提供特殊的功能。

Python类中的特殊方法、特殊属性有些需要开发者重写,有些可以直接调用,掌握这些常的特殊方法、特殊属性也是非常重要的。

8.1 常见的特殊方法

8.1.1 重写__repr__方法

class Item:
    def __init__(self,name,price):
        self.name=name
        self.price=price
#创建一个Item对象,将之赋值给im变量
im=Item("鼠标",9.8)
#打印im所引用的Item对象
print(im)#<__main__.Item object at 0x000001F27C253F50>

上面程序创建了一个Item对象,然后使用print()方法输出Item对象。

按道理来说,print()函数只能在控制台打印字符串,而Item示例是内存中的一个对象,怎么能直接转换为字符串输出呢?事实上,当使用该方法输出Item对象时,实际上输出的是Item对象的__repr__()方法的返回值。也就是说,下面两行代码的效果完全一样。

print(im)#<__main__.Item object at 0x00000121C1950080>
print(im.__repr__())#<__main__.Item object at 0x00000121C1950080>

__repr__()是Python类中的一个特殊方法,由于object类已提供了该方法,而所有的Python类都是object类的子类,因此所有的Python对象都具有__repr__()方法。

因此,当程序需要将任何对象与字符串进行连接时,都可先调用__repr__()方法将对象转换成字符串,然后将两个字符串连接在一起。

im_str=im.__repr__()+""

__repr__()是一个非常特殊的方法,它是一个“自我描述”的方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息。用来告诉外界该对象具有的状态信息。

object类提供的__repr__()方法总是返回该对象实现类的“类名+object at+内存地址”值,这个返回值并不能真正实现“自我描述”的功能。因此,如果用户需要自定义类能实现“自我描述”的功能,就必须重写__repr__()方法。

class Apple:
    #实现构造器
    def __init__(self,color,weight):
        self.color = color
        self.weight =weight
    #重写__repr__()方法,用于实现Apple对象的“自我描述”
    def __repr__(self):
        return "Apple[color="+self.color+", weight="+str(self.weight)+"]"
a=Apple("红色",5.68)
#打印Apple对象
print(a)#Apple[color=红色, weight=5.68]

从上面的打印结果可以看出,通过重写Apple类的__repr__()方法,就可以让系统打印Apple对象时打印出该对象的“自我描述”信息。

大部分时候,重写__repr__()方法总是返回该对象的所有令人感兴趣的信息所组成的字符串,通常可返回如下格式的字符串:

类名[field1=值1,field2=值2,...]

8.1.2 析构方法:__del__

与__init__()方法对应的是__del__()方法,__init__()方法用于初始化Python对象,而__del__()则用于销毁Python对象——子啊任何Python对象将要被系统回收之时,系统都会调用该对象的__del__()方法。

当程序不再需要一个Python对象时,系统必须把该对象所占用的内存空间释放出来,这个过程称垃圾回收(GC),Python会自动回收所有对象所占用的内存空间,因此开发者无须关心对象垃圾回收的过程。

提示:Python采用自动引用计数(ARC)方式来回收对象所占用的空间,当程序中有一个变量引用该Python对象时,Python会自动保证该对象引用计数为1;当程序中有两个变量引用该变量对象时,Python会自动保证该对象引用计数为2……依次类推,如果一个对象的引用计数变成了0,则说明程序中不再有变量引用该对象,表明程序不再需要该对象,因此Python就会回收该对象。

大部分时候,Python的ARC都能准确、高效的回收系统中的每个对象。但如果系统中出现循环引用的情况,比如对象a持有一个变量引用对象b,而对象b又持有一个实例变量引用对象a,此时两个对象的引用计数都是1,而实际上程序已经不再有变量引用它们,系统应该回收它们,此时Python的垃圾回收器就可能没那么快,要等专门的循环垃圾回收器来检测并回收这种引用循环。

当一个对象被垃圾回收时,Python就会自动调用该对象的__del__()方法。

需要说明的时,不要以为对一个变量执行del操作,该变量所引用的对象就会被回收——只是当对象的引用计数变成0时,该对象才会被回收。因此,如果一个对象有多个变量引用它,那么del其中一个变量是不会回收该对象的。

class Item:
    def __init__(self, name, price):
        self.name=name
        self.price=price
    #定义析构函数
    def __del__(self):
        print('del删除对象')
#创建一个Item对象,将之赋值给im变量
im=Item("鼠标",29.8)
x=im
#打印im所引用的Item对象
del im
print("----------------")
"""
----------------
del删除对象
"""

上面程序先创建一个Item对象,并将该对象赋值给im变量,x=im代码将im赋值给变量x,这样程序中有两个变量引用Item对象,接下来程序执行del im代码删除im对象——此时由于还要变量引用该Item对象,因此程序并不会回收Item对象。

将上面x=Item代码删除,再次运行上面的程序,将会看到如下的输出结果:

"""
del删除对象
----------------
"""

当程序执行del im之后,此时程序中不再有任何变量引用该Item对象,因此系统会立即回收该对象,则无须等带程序结束之前。

注意:最后需要说明的是,如果父类通过了__del__()方法,则系统重写__del__()方法时,必须显示调用父类的__del__()方法,这样才能保证合理回收父类实例的部分属性。

8.1.3 __dir__方法

对象的__dir__()方法用于列出该对象内部的所有属性(包括方法)名,该方法将会返回包含所有属性(方法)名的序列。

当程序对某个对象执行dir(object)函数时,实际上就是将该对象的__dir__()方法返回值进行排序,然后包装成列表。

class Item:
    def __init__(self,name,price):
        self.name = name
        self.price = price
    def info():
        pass
#创建一个Item对象,将之赋值给im变量
im=Item("鼠标",29.8)
print(im.__dir__())#返回所有属性(包括方法)组成的列表
print(dir(im))#返回所有属性(包括方法)排序之后的列表
"""
['name', 'price', '__module__', '__init__', 'info', '__dict__', '__weakref__', '__doc__', '__new__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'info', 'name', 'price']
"""

运行上面程序,可以看到程序不仅会输出我们为对象定义的name、price、info方法啊三个属性和方法,而且还有大量系统内置的属性和方法,如刚刚介绍的__repr__和__del__方法。

8.1.4 __dict__属性

__dict__属性用于查看对象内部存储的所有属性名和属性值组成的字典,通常程序直接使用该属性即可。程序使用__dict__属性即可查看对象的所有内部状态,也可通过字典语法来访问或修改指定属性的值。

class Item:
    def __init__(self,name,price):
        self.name = name
        self.price = price
im=Item("鼠标",28.9)
print(im.__dict__)#{'name': '鼠标', 'price': 28.9}
#通过__dict__访问name属性
print(im.__dict__['name'])#鼠标
#通过__dict__访问price属性
print(im.__dict__['price'])#28.9
im.__dict__['name']='键盘'
im.__dict__['price']='32.8'
print(im.name)#键盘
print(im.price)#32.8

8.1.5__getattr__、__setattr__等

当程序操作(包括访问、设置、删除)对象的属性时,Python系统同样会执行该对象指定的方法。这些方法共涉及如下几个:

(1)__getattribute__(self,name):当程序访问对象的name属性时被自动调用。

(2)__getattr__(self,name):当程序访问对象的name属性且该属性不存在时被自动调用。

(3)__setattr__(self,name,value):当程序对对象的name属性赋值时被自动调用。

(4)__delattr__(self,name):当程序删除对象的name属性时被自动调用。

通过重写上面的方法,可以Python类“合成“属性——当属性不存在时,程序会委托给上面的__getattr__、setattr__、__delattr__方法来实现,因此程序可通过重写这些方法来“合成”属性。

class Rectangle:

    def __init__(self,width,height):

        self.width=width

        self.height=height

    def __setattr__(self,name,value):

        print('——————设置%s属性——————'%name)

        if name=="size":

            self.__dict__[name]=value

        else:

            self.__dict__[name]=value

    def __getattr__(self,name):

        print('——————读取%s属性——————'%name)

        if name=='size':

            return self.width,self.height

        else:

            raise AttributeError

    def __delattr__(self,name):

        print('——————删除%s属性——————'%name)

        if name=='size':

            self.__dict__['width']=0

            self.__dict__['height']=0

rect=Rectangle(3,4)

print(rect.size)

rect.size=6,8

print(rect.width)

del rect.size

print(rect.size)

"""

——————设置width属性——————

——————设置height属性——————

——————读取size属性——————

(3, 4)

——————设置size属性——————

——————设置width属性——————

——————设置height属性——————

6

——————删除size属性——————

——————读取size属性——————

(0, 0)

"""

上面程序实现了__setattr__()和__getattr__()方法,并在实现这两个方法时对size属性进行了判断,如果程序正在获取size属性,__getattr__()方法将返回self.width和self.height组成的元组,如果获取其他属性则直接引发AttributeError异常;如果程序正在设置size属性,则转换为对self.width、self.height属性的赋值,则通过对象的__dict__属性进行赋值。

关于上面这两个方法要进行一些说明:

  1. 对于__getattr__()方法:它只处理程序访问指定属性且该属性不存在的情形。比如程序访问width或height属性,Rectangle对象本身包含该属性,因此该方法不会被触发。所有重写该方法只需处理我们需要“合成”的属性(比如size),假如程序试图访问其他不存在的属性,当然直接引发AttributeError异常即可。
  2. 对于__setattr__()方法,只要程序试图对指定属性时总会触发该方法,因此无论程序时对width、height属性赋值,还是对size属性赋值,该方法都会被触发。所以重写该方法既要处理对size属性赋值的情形,也要处理对width、height属性赋值的情形。尤其时处理对width、height属性赋值的时候,千万不要在__setattr__()方法中再次对width、height赋值,因为对象这两个属性赋值会在次触发__setattr__()方法,这样会让程序陷入死循环。

如果程序需要在读取、设置属性之前进行某种拦截(比如检查数据是否合法之类的),也可通过重写__setattr__()或__getattribute__方法来实现。

class User:
    def __init__(self,name,age):
        self.name = name
        self.age = age
    #重写__setattr__()方法对设置的属性值进行检查
    def __setattr__(self,name,value):
        #如果正在设置name属性
        if name=='name':
            if 2<len(value)<=8 or len(value)>8:
                self.__dict__['name']=value
            else:
                raise ValueError('name的长度必须在2~8之间')
        elif name=='age':
            if 10<value<60:
                self.__dict__['age']:value
            else:
                raise ValueError('age值必须在10~60之间')
u=User('fkit',24)
print(u.name)
print(u.age)
#u.name='fk'#引发异常
u.age=2#引发异常

8.2 与反射相关的属性和方法

如果程序在运行过程中要动态判断是否包含某个属性(包括方法),甚至要动态设置某个属性值,则可通过Python的反射支持来实现。

8.2.1动态操作属性

在动态检查对象是否包含某些属性(包括方法)相关的函数有如下几个:

  1. hasattr(obj,name):检查obj对象是否包含命名为name的属性或方法。
  2. getattr(object,name[,default]):获取object对象中名为name属性或方法。
  3. setattr(object,name,value,/):将obj对象的name属性设为value。
class Comment:
    def __init__(self,detail,view_times):
        self.detail = detail
        self.view_times = view_times
    def info():
        print("一条简单的评论,内容是%s"%self.detail)
c=Comment("1234",20)
#判断是否包含指定的属性和方法
print(hasattr(c,'detail'))#True
print(hasattr(c,'view_times'))#True
print(hasattr(c,'info'))#True
#获取指定属性的属性值
print(getattr(c,'detail'))#1234
print(getattr(c,'view_times'))#20
#由于info是方法,故下面代码会提示:name 'info' is not defined
#print(getattr(c,info,'默认值'))
#为指定属性设置属性值
setattr(c,'detail','天气不错')
setattr(c,'view_times',32)
#输出重新设置后的属性值
print(c.detail)#天气不错
print(c.view_times)#32

上面程序定义了一个Comment 类,接下来程序创建了Comment类的实例。程序后面部分示范了hasattr()、getattr()、setattr()三个函数。

从上面最后两行输出来看,程序使用setattr()函数可以改变Python对象的属性值;如果使用该函数对Python对象设置的属性不存在,那么就表现为添加属性——反正Python是动态语言。

#设置不存在的属性,即为对象添加属性
setattr(c,'test','新增的测试属性')
print(c.test)#新增的测试属性

实际上setattr()函数还可以对方法进行设置,在使用setattr()函数重新设置对象的方法时,新设置的方法是未绑定方法。

def bar():
    print('一个简单的bar方法')
#将c的info方法设为bar函数
setattr(c,'info',bar)
c.info()#一个简单的bar方法

上面程序先定义了一个bar()函数,在该函数中不能定义self参数(否则需要程序员显式为参数传入参数值,系统不会自动为该参数绑定参数值)。接下来程序调用setattr()函数将Comment对象的info()方法设置为bar()函数,然后程序调用Comment对象的info()方法,其实就是调用程序中的bar()函数。

运行上面代码,可以看到运行c.info()方法时实际执行的是前面定义的bar()函数。

不仅如此,程序完全可通过setattr()函数将info()方法设置成普通值,这样将会把info变成一个属性,而不是方法。

#将c的info设置为字符串'fkit'

setattr(c,'info','fkit')

c.info()#TypeError: 'str' object is not callable

8.2.2__call__属性

上面程序可用hasattr()函数判断指定属性(或方法)是否存在,但到底是属性还是方法,则需要进一步判断它是否可调用。程序可通过判断该属性(或方法)是否包含__call__属性来确定它是否可调用。

class User:
    def __init__(self,name,passwd):
        self.name = name
        self.passwd =passwd
    def vaildLogin(self):
        print("验证%s的登录"%self.name)
u=User('crazyit','leegang')
#判断u.name是否包含__call__方法,即判断它是否可调用
print(hasattr(u.name,'__call__'))#False
#判断u.passwd是否含__call__方法,即判断它是否可调用
print(hasattr(u.passwd,'__call__'))#False
#判断u.validLogin是否包含__call__方法,即判断它是否可调用
print(hasattr(u.vaildLogin,'__call__'))#True

上面程序分别判断User对象的name、passwd、vaildLogin是否包含__call__方法,如果包含该方法,则表明它是可调用的;否则就说明它是不可调用的。

从上面程序的输出结果不难看到,对于name、passwd两个属性,由于它们都是不可调用的,因此程序在判断它们是否包含__call__方法时输出False;对于vaildLogin方法,由于它是可调用的,因此程序在判断它是否包含__call__方法输出True。

实际上,一个函数(甚至对象)之所以能执行,关键就在于__call__()方法。实际上x(arg1,arg2,…),只是x.__call__(arg1,arg2,…)的快捷写法,因此我们甚至可以为自定义类添加__call__方法,从而使得该类的实例也可变成可调用的。

#定义Role类
class Role:
    def __init__(self,anme):
        self.anme = anme
    #定义__call__方法
    def __call__(self):
        print("执行Role对象")
r=Role('管理员')
#直接调用Role对象,就是调用该对象的__call__方法
r()#执行Role对象

上面程序中最后一行代码使用调用函数的语法来调用对象,这看上去似乎是错误的,但由于该Role类提供了__call__方法,因此调用对象的本质就是执行对象的__call__方法。

对于程序中的函数,同样即可使用函数的语法来调用它,也可把函数当成对象,调用它的__call__方法。

def foo():
    print('--foo函数')
#下面示范了通过两种方式来调用foo()函数
foo()
foo.__call__()
"""
--foo函数
--foo函数
"""

运行上面代码,可以看到foo()和foo.__call__()效果完全相同。

8.3 与序列相关的特殊方法

Python的序列可包含多个元素,开发者只要实现复合序列要求的特殊方法,就可实现自己的序列。

8.3.1 序列相关方法

序列最重要的特征就是可包含多个元素,因此和序列有关的特殊方法有如下几个:

(1)__len(self):该方法的返回值决定序列中元素的个数。

(2)__getitem__(self,key):该方法获取指定索引对应的元素。该方法的key应该是整数值或slice对象,否则该方法会引发KeyError异常。

(3)__contains__(self,item):该方法判断序列是否包含指定元素。

(4)__setitem__(self,key,value):该方法设置指定索引对应的元素。该方法的key应该是整数值或slice对象,否则该方法会引发KeyError异常。

(5)__delitem__(self,key):该方法删除指定索引对应的元素。

如果程序实现不可变序列(程序只能获取序列中元素,不能修改),只要实现上面前3个方法就行:如果程序要实现可变序列(程序既能获取序列中的元素,也可修改),则需要实现上面5个方法。

def check_key(key):
    '''
    该函数将会负责检查序列的索引,该索引必须是整数值,否则引发TypeError异常
    且程序要求索引必须为非负整数值,否则引发IndexError异常
    '''
    if not  isinstance(key,int):raise TypeError('索引值必须是整数')
    if key<0:raise IndexError('索引值必须为非负整数')
    if key>=26**3:raise IndexError('索引值不能超过%d'%26**3)
class StringSeq:
    def __init__(self):
        #用于存储被修改的数据
        self.__changed={}
        #用于存储已删除元素的索引
        self.__deleted=[]
    def __len__(self):
        return 26**3
    def __getitem__(self,key):
        '''
        根据索引获取序列中元素
        '''
        check_key(key)
        #如果在self.__changed中找到修改后的数据
        if key in self.__changed:
            return self.__changed[key]
        #如果key在self.__deleted中,说明该元素已被删除
        if key in self.__deleted:
            return None
        #否则根据计算规则返回序列元素
        three=key//(26**2)
        two=(key-three*26*26)//26
        one=key%26
        return chr(65+three)+chr(65+two)+chr(65+one)
    def __setitem__(self,key,value):
        '''
        根据索引修改序列中元素
        '''
        check_key(key)
        #将修改的元素以key-value对的形式保存在__changed中
        self.__changed[key]=value
    def __delitem__(self,key):
        '''
        根据索引删除序列中元素
        '''
        check_key(key)
        #如果__deleted列表中没有包含被删除的key,则添加被删除的key
        if key not in self.__deleted:self.__deleted.append(key)
        #如果__changed中包含被删除的key,则删除它
        if key in self.__changed:del self.__changed[key]
#创建序列
sq=StringSeq()
#获取序列的长度,实际上就是返回__len__()方法返回值
print(len(sq))#17576
print(sq[26*26])#BAA
#打印修改之前的sq[1]
print(sq[1])#AAB
#修改sq[1]元素
sq[1]='fkit'
#打印西修改之后的sq[1]
print(sq[1])#fkit
#删除sq[1]
del sq[1]
print(sq[1])#None
#再次对sq[1]赋值
sq[1]='crazyit'
print(sq[1])#crazyit

8.3.2 实现迭代器

前面介绍了使用for循环遍历列表、元组和字典等,这些对象都是可迭代的,因此它们都属于迭代器。

如果开发者需要实现迭代器,只要实现如下两个方法:

(1)__iter(self):该方法返回与一个迭代器(iterator),迭代器必须包含一个__next__()方法,该方法返回迭代器的下一个元素。

(2)__reversed__(self):该方法主要为内建的reversed()反转函数提供支持,当程序调用reversed()函数对指定迭代器执行反转时,实际上是由该方法实现的。

从上面介绍不难0看出,如果程序不需要让迭代器反转迭代,其实只需要实现第一个方法即可。

#定义一个代表斐波那契数列的迭代器
class Fibs:
    def __init__(self,len):
        self.first=0
        self.sec=1
        self.__len=len
    #定义迭代器所需的__next__方法
    def __next__(self):
        #如果__len__属性为0,结束迭代
        if self.__len==0:
            raise StopIteration
        #完成数列计算
        self.first,self.sec=self.sec,self.first+self.sec
        #数列长度建1
        self.__len-=1
        return self.first
    #定义__iter__方法,该方法返回迭代器
    def __iter__(self):
        return self
#创建Fibs对象
fibs=Fibs(10)
#获取迭代器的下一个元素
print(next(fibs))
#使用for循环遍历迭代器
for el in fibs:
    print(el,end=' ')

上面程序定义一个Fibs类,该类实现了__iter__()方法,该方法返回self,因此他要求该类必须提供__next__()方法,该方法会返回数列的下一个值。程序使用__len属性控制数列的剩余长度,当__len为0时,程序停止遍历。

上面程序创建了一个长度为10 的数列,程序开始使用内置的next()函数来获取迭代器的下一个元素,该next()函数其实就是通过迭代器的__next()__方法来实现的。

程序接下来使用for循环来遍历数列。运行该程序,将看到如下结果:

"""
1 2 3 5 8 13 21 34 55
"""

此外,程序可使用内置的iter()函数将列表、元组等转换成迭代器。

#将列表转换为迭代器
my_iter=iter([2,'fkit',4])
#依次获取迭代器的下一个元素
print(my_iter.__next__())#2
print(my_iter.__next__())#fkit

8.3.3 扩展列表、元组和字典

前面介绍的方法可实现自定义序列、自定义迭代器。实际上前面介绍的列表、元组等本身都实现了这些序列方法、迭代器方法,因此它们既是序列,也是迭代器。

很多时候,如果程序明确需要一个特殊的列表、元组或字典类,由两个选择:

  1. 自己实现序列、迭代器等各种方法,自己来实现这个特殊的类。
  2. 扩展系统已有的列表、元组或字典。

很明显,第一种方式有点繁琐,因为这意味着开发者要把所有方法都自己实现一遍;第二种方法就简单多了,只要继承系统已有的列表、元组或字典类,然后重写或新增方法即可。

下面程序将会示范开发一个新的字典类,这个字典类可根据value来获取key。由于字典中value是可以重复的,因此该方法会返回指定value对应的全部key组成的列表。

#定义它ValueDict类,继承dict类
class ValueDict(dict):
    #定义构造函数
    def __init__(self, *args, **kwargsds):
        #调用父类的构造函数
        super().__init__(*args, **kwargsds)
    #新增getkeys(self,val)
    def getkeys(self,val):
        result=[]
        for key,value in self.items():
            if value ==val:result.append(key)
        return result
my_dict=ValueDict(语文=92,数学=89,英语=92)
#获取92对应的所有key
print(my_dict.getkeys(92))#['语文', '英语']
my_dict['编程']=92
print(my_dict.getkeys(92))#['语文', '英语', '编程']

8.4生成器

生成器和迭代器的功能非常相似,他也提供__next__()方法,这意味着程序同样可调用内置的next()函数来获取生成器的下一个值,也可使用for循环来遍历生成器。

生成器和迭代器的区别在于:迭代器通常是先定义一个迭代器类,然后创建实例来创建迭代器;而生成器则先定义一个包含yield语句的函数,然后通过调用该函数来创建生成器。

生成器是一种非常优秀的语法,Python使用生成器可以让程序变得很优雅。

8.4.1创建生成器

创建生成器需要两步操作:

  1. 定义一个包含yield语句的函数。
  2. 调用(1)创建的函数得到生成器。
def test(val,step):
    print("-------函数开始执行-------")
    cur=0
    #遍历0~val
    for i in range(val):
        #cur添加i*step
        cur+=i*step
        yield cur

上面函数与前面介绍的普通函数的最大区别在于yield cur这行,如果将这行代码改为print(cur),那么这个函数就显得比较普通了——该函数只是简单地遍历区间,并将循环计数器乘以step后添加到cur变量上,该数列中两个值的差值会逐步递增。

如果将上面的yield cur语句改为print(cur,end=’’),执行test(10,2)函数将会看到如下的输出结果:

0 2 6 12 20 30 42 56 72 90

yield cur语句的作用有两点:

  1. 每次返回一个值,有点类似return语句。
  2. 冻结执行,程序每次执行到yield语句时就会被暂停。

在程序被yield语句冻结之后,当程序再次调用next()函数获取生成器的下一个值时,程序才会继续向下执行。

需要指出的是,调用包含yield语句的函数并不会立即执行,他只是返回一个生成器。只有当程序通过next()函数调用生成器或遍历生成器时,函数才会真正执行。

#执行函数,返回生成器
t=test(10,2)
print("=========================")
#获取生成器的第一个值
print(next(t))#0,生成器被“冻结”在yield处
print(next(t))#2,生成器被再次“冻结”在yield处
"""
=========================
-------函数开始执行-------
0
2
"""

当程序调用next(t)时,生成器会返回yield cur语句返回的值(第一次返回0),程序被冻结在yield语句处,因此可以看到上面生成器第一次输出的值为0.

当程序第二次调用next(t)时,程序的“冻结”被解除,继续向下执行,这一次循环计数器i变成1,在执行cur+=i*step之后,cur变成了2,生成器再次返回yield cur语句返回的值(这一次返回2),程序再次被冻结在该yield处,因此可以看到上面生成器第二次输出的值为2.

程序也可使用for循环来遍历生成器,相当于不断地使用next()函数获取生成器的下一个值。

for ele in t:
    print(ele,end=' ')
"""
=========================
-------函数开始执行-------
0 2 6 12 20 30 42 56 72 90
"""

此外程序可使用list()函数将生成器能生成的所有值转换成列表,也可使用tupele()函数将生成器能生成的所有值转换成元组。

#再次创建生成器
t=test(10,1)
#将生成器转换成列表
print(list(t))
#再次创建生成器
t=test(10,3)
#将生成器转换成元组
print(tuple(t))
"""
-------函数开始执行-------
[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
-------函数开始执行-------
(0, 3, 9, 18, 30, 45, 63, 84, 108, 135)
"""

Python主要提供了以下两种方式创建生成器:

  1. 使用for循环的生成器推导
  2. 调用带yield语句的生成器函数

生成器是Python的一个特色功能,在其他语言中往往没有对应的机制,因此很多Python开发者对生成器机制不甚了解。但实际上生成器是一个非常优秀的机制,以看实际开发的经验来看,生成器至少有以下几个优势:

  1. 当使用生成器来生成多个数据时,程序是按需获取数据的,他不会一开始就把所有数据都生成出来,而是每次调用next()获取下一个数据,生成器才会执行一次,因此可以减少代码的执行次数。比如前面介绍的示例,程序不会一开始就把生成器函数中的循环都执行完成,而是每次调用next()时才执行一次循环体。
  2. 当函数需要返回多个数据时,如果不使用生成器,程序员需要使用列表或元组来收集函数返回的多个值,当函数要返回的数据量较大的时候,这些列表、元组会带俩一定的内存开销,如果使用生成器就不存在这个问题,生成器可以按需、逐个返回数据。
  3. 使用生成器的代码更加简洁。

8.4.2 生成器的方法

当生成器运行起来之后,开发者还可以为生成器提供值,提供这种方式让生成器与“外部程序”动态地交换数据。

为实现生成器与“外部程序”动态地交换数据,需要借助生成器的send()方法,该方法的功能与前面示例中所使用的next()函数的功能非常相似,它们都用于获取生成器所生成的下一个值,并将生成器冻结在yield语句处;但send()方法可以接收一个参数,该参数值会被发送给生成器函数。

在生成器函数内部,程序可通过yield表达式来获取send()方法所发送的值——这意味此时程序应该使用一个变量来接收yield语句的值。如果程序依然使用next()哈桑农户来获取生成器所生成的下一个值,那么yield语句返回None。

归纳起来就是两句话:

  1. 外部程序通过send()方法发送数据。
  2. 生成器函数使用yield语句接收数据。

另外,需要说明的是,只有等到生成器被冻结之后,外部程序才能使用send()方法向生成器发送数据。获取生成器第一次所生成的值,应该使用next()函数;如果程序非要使用send()方法获取生成器第一次生成的值,也不能向生成器发送数据,只能为该方法传入None参数。

def square_gen(val):
    i=0
    out_val=None
    while True:
        #使用yield语句生成值,使用out_val接收send()方法发送的参数值
        out_val=(yield out_val**2)if out_val is not None else (yield i**2)
        #如果程序使用send()方法获取下一个值,out_val会获取send()方法的参数值
        if out_val is not None:print("====%d"%out_val)
        i+=1
sg=square_gen(5)
#第一次调用send()方法获取值,只能传入None作为参数
print(sg.send(None))
print(next(sg))
print('----------------------')
#调用send()方法获取生成器的下一个值,参数9会被发送给生成器
print(sg.send(9))
#再次调用send()方法获取生成器的下一个值
print(next(sg))
"""
0
1
----------------------
====9
81
9
"""

提示:通过上面的执行过程不难看出,生成器根本不能获取第一次调用send()方法发送的参数值,这就是Python要求生成器第一次调用send()方法时只能发送None参数的原因。

此外,生成器还提供了如下两种常用方法:

  1. close():该方法用于停止生成器
  2. throw():该方法用于在生成器内部(yield语句内)引发一个异常。
sg.throw(ValueError)
"""
Traceback (most recent call last):
  File "", line 19, in <module>
    sg.throw(ValueError)
  File "", line 6, in square_gen
    out_val=(yield out_val**2)if out_val is not None else (yield i**2)
                                                           ^^^^^^^^^^
ValueError
"""

从上面输出结果可以看到,在程序调用生成的throw()方法引发异常之后,程序就会在yield语句中引发异常。

将上面的sg.throw(ValueError)代码注释掉,为程序增加两行代码示范stop()方法的用法,在程序调用stop()方法关闭生成器之后,程序就不能再去获取生成器的下一个值,否则就会引发异常。

#关闭生成器
sg.close()
print(next(sg))#StopIteration
"""
Traceback (most recent call last):
  File "", line 21, in <module>
    print(next(sg))#StopIteration
          ^^^^^^^^
StopIteration
"""

8.5运算符重载特殊方法

Python允许自定义类提供特殊方法,这样就可以让自定义类的对象也支持各种运算符的运算。

8.5.1 与数值运算符相关的特殊方法

根据前面介绍,与数值运算相关的运算符包括算术运算符、位运算符等,其实这些运算符都是由对应的方法提供支持的。开发人员可以为自定义类提供如下方法:

  1. object.__add__(self,other):加法运算,为“+”运算符提供支持。
  2. object.__sub__(self,other):减法运算,为“-”运算符提供支持。
  3. object.__mul__(self,other):乘法运算,为“*”运算符提供支持。
  4. object.__matmul__(self,other):矩阵乘法,为“@”运算符提供支持。

提示:对Python的矩阵运算需要先为Python安装numpy模块,提供命令行窗口输入pip3 install numpy命令来安装numpy模块;然后再导入怒骂朋友模块,可以提供在程序中添加from numpy import *或import numpy as np语句来导入numpy模块。

  1. object.__truediv__(self,other):除法运算,为“/”运算符提供支持。
  2. object.__floordiv__(self,other):整除运算,为“//”运算符提供支持。
  3. object.__mod__(self,other):求余运算,为“%”运算符提供支持。
  4. object.__divmod__(self,other):求余运算,为“divmod”运算符提供支持。
  5. object.__pow__(self,other[,modulo]):乘方运算,为“**”运算符提供支持。
  6. object.__lshift__(self,other):右移运算,为“<<”运算符提供支持。
  7. object.__rshift__(self,other):右移运算,为“>>”运算符提供支持。
  8. object.__and__(self,other):按位与运算,为“&”运算符提供支持。
  9. object.__xor__(self,other):按位异或运算,为“^”运算符提供支持。
  10. object.__or__(self,other):按位或运算,为“|”运算符提供支持。

一旦为自定义类提供了上面这些方法,程序就可以直接俄用运算符来操作该类的实例。比如程序执行x+y,相当于调用x.__add__(self,y),因此只要x所属的类提供__add__(self,other)方法即可:如果自定义类没有提供对应的方法,程序会返回NotImplemented。

class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height =height
    #定义setSize()函数
    def setSize(self,size):
        self.width ,self.height = size
    #定义getsize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__add__方法,该对象可执行"+"运算
    def __add__(self,other):
        #要求参加“+”运算的另一个操作数必须时Rectangle
        if not isinstance(other,Rectangle):
            raise TypeError('+运算要求目标时Rectangle')
        return Rectangle(self.width+other.width,self.height+other.height)
    def __repr__(self):
        return 'Rectangle(width=%g,height=%g)' %(self.width,self.height)
r1=Rectangle(4,5)
r2=Rectangle(3,4)
#对两个Rectangle执行加法运算
r=r1+r2
print(r)#Rectangle(width=7,height=9)

当程序执行x+y运算时,Python首先会尝试使用x的__add__方法进行计算;如果x没有提供__add__方法,Python还会尝试调用y的__radd__方法进行计算。这意味着上面介绍的各种数值运算相关方法,还有一个带r的版本:

  1. object.__radd__(self,other):当y提供该方法时,可执行x+y。
  2. object.__rsub__(self,other):当y提供该方法时,可执行x-y。
  3. object.__rmul__(self,other):当y提供该方法时,可执行x*y。
  4. object.__rmatmul__(self,other):当y提供该方法时,可执行x@y。
  5. object.__rtruediv__(self,other):当y提供该方法时,可执行x/y。
  6. object.__rfloordiv__(self,other):当y提供该方法时,可执行x//y。
  7. object.__rmod__(self,other):当y提供该方法时,可执行x%y。
  8. object.__rdivmod__(self,other):当y提供该方法时,可执行x divmod y。
  9. object.__rpow__(self,other[,modulo]):当y提供该方法时,可执行x**y。
  10. object.__rlshift__(self,other):当y提供该方法时,可执行x<<y。
  11. object.__rrshift__(self,other):当y提供该方法时,可执行x>>y。。
  12. object.__rand__(self,other):当y提供该方法时,可执行x&y。
  13. object.__rxor__(self,other):当y提供该方法时,可执行x^y。
  14. object.__ror__(self,other):当y提供该方法时,可执行x|y。

简单来说,如果自定义类提供了上面列出的__rxxx__()方法,那么该自定义类的对象就可以出现在对应的运算符的右边。

class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height
    #定义setSize()函数
    def setSize(self,size):
        self.width,self.height = size
    #定义getSize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__radd__方法,该对象可出现在“+"的右边
    def __radd__(self,other):
        #要求参与"+"运行的另一个操作数必须是数值
        if not (isinstance(other,int) or isinstance(other,float)):
            raise TypeError("+运算要求目标是数值")
        return Rectangle(self.width+other,self.height+other)
    def __repr__(self):
        return "Rectangle(width=%g,height=%g)" % (self.width,self.height)
r1=Rectangle(4,5)
#r1有__radd__方法,因此它可以出现在“+”运算符的右边
r=3+r1
print(r)#Rectangle(width=7,height=8)

此外,Python还提供各种扩展后的赋值运算符,这些扩展后的赋值运算夫也是由特殊方法来提供支持的:

  1. object.__iadd__(self,other):为“+=”运算符提供支持。
  2. object.__isub__(self,other):为“-=”运算符提供支持。
  3. object.__imul__(self,other):为“*=”运算符提供支持。
  4. object.__imatnul__(self,other):为“@=”运算符提供支持。
  5. object.__itruediv__(self,other):为“/=”运算符提供支持。
  6. object.__ifloordiv__(self,other):为“//=”运算符提供支持。
  7. object.__imod__(self,other):为“%=”运算符提供支持。
  8. object.__ipow__(self,other):为“**=”运算符提供支持。
  9. object.__ilshift__(self,other):为“<<=”运算符提供支持。
  10. object.__irshift__(self,other):为“>>=”运算符提供支持。
  11. object.__iand__(self,other):为“&=”运算符提供支持。
  12. object.__ixor__(self,other):为“^=”运算符提供支持。
  13. object.__ior__(self,other):为“|=”运算符提供支持。
class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height
    #定义setSize()函数
    def setSize(self,size):
        self.width,self.height = size
    #定义getSize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__iadd__方法,该对象可出现在“+"的右边
    def __iadd__(self,other):
        #要求参与"+="运行的另一个操作数必须是数值
        if not (isinstance(other,int) or isinstance(other,float)):
            raise TypeError("+=运算要求目标是数值")
        return Rectangle(self.width+other,self.height+other)
    def __repr__(self):
        return "Rectangle(width=%g,height=%g)" % (self.width,self.height)
r=Rectangle(4,5)
#r由__iadd__方法,因此它支持“+=”运算
r+=2
print(r)Rectangle(width=6,height=7)

8.5.2 与比较运算符相关的特殊方法

Python提供的>、<、>=、<=、==和!=等运算符同样是由特殊方法提供支持的。因此,如果程序为定义类提供了这些特殊方法,那么程序就可使用这些比较运算符来比较大小了。

下面是与比较运算符相关的特殊方法:

  1. object.__It__(self,other):为“<”运算符提供支持。
  2. object.__le__(self,other):为“<=”运算符提供支持。
  3. object.__eq__(self,other):为“==”运算符提供支持。
  4. object.__ne__(self,other):为“!=”运算符提供支持。
  5. object.__gt__(self,other):为“>”运算符提供支持。
  6. object.__ge__(self,other):为“>=”运算符提供支持。

虽然Python为每一个比较运算符都提供了方法,但实际上往往并不需要实现那么多的特殊方法,对于同一个类的实例比较大小而言,通常只要实现其中三个方法即可。因为在实现__gt__()方法之后,程序即可使用“>”和“<”两个运算符;在实现__eq__()方法之后,程序即可使用“==”和“!=”两个运算符:在实现__gc__()方法之后,程序即可使用“>=”和“<=”两个运算符。

class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height
    #定义setSize()函数
    def setSize(self,size):
        self.width,self.height = size
    #定义getSize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__gt__方法,该对象属性可支持">"和"<"比较
    def __gt__(self,other):
        #要求参与">"比较的另一个操作数必须是Rectangle
        if not isinstance(other,Rectangle):
            raise TypeError('>比较要求必须是Rectangle')
        return True if self.width*self.height>other.width*other.height else False
    #定义__eq__方法,该对象支持"=="和"!="比较
    def __eq__(self,other):
        if not isinstance(other,Rectangle):
            raise TypeError('==比较要求目标必须是Rectangle')
        return True if self.width*self.height==other.width*other.height else False
        # 定义__ge__方法,该对象支持">="和"<="比较
    def __ge__(self, other):
        if not isinstance(other, Rectangle):
            raise TypeError('>=比较要求目标必须是Rectangle')
        return True if self.width * self.height >= other.width * other.height else False
    def __repr__(self):
        return "Rectangle(width=%g,height=%g)" % (self.width,self.height)
r1=Rectangle(4,5)
r2=Rectangle(3,4)
print(r1>r2)#True
print(r1>=r2)#True
print(r1<r2)#False
print(r1<=r2)#False
print(r1==r2)#False
print(r1!=r2)#True
print("---------------")
r3=Rectangle(2,6)
print(r2>=r3)#True
print(r2>r3)#False
print(r2<=r3)#True
print(r2<r3)#False
print(r2==r3)#True
print(r2!=r3)#False

8.5.3 单目运算符相关的特殊方法

Python还提供了+(单目求正)、-(单目求负)、~(单目取反)等运算符,这些运算符也有对应的特殊方法。

  1. object.__neg__(self):为单目求负(-)运算符提供支持。
  2. object.__pos__(self):为单目求正(+)运算符提供支持。
  3. object.__invert__(self):为单目取反(~)运算符提供支持。
class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height
    #定义setSize()函数
    def setSize(self,size):
        self.width,self.height = size
    #定义getSize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__neg__方法,该对象可执行求负运算(-)
    def __neg__(self):
        self.width,self.height = self.height,self.width
    def __repr__(self):
        return "Rectangle(width=%g,height=%g)" % (self.width,self.height)
r=Rectangle(4,5)
#对Rectangle执行求负运算
-r
print(r)#Rectangle(width=5,height=4)

8.5.4 与类型转换相关的特殊方法

Python还提供了str()、int()、float()、complex()等函数(其实是这些类的构造器)将其他类型的对象转换成字符串、整型、浮点数和复数,这些转换同样也是由特殊方法在底层提供支持。下面是这些方法:

  1. object.__str__(self):对应于调用内置的str()函数将该对象转换成字符串。

提示:对象的__str__()和__repr__()方法的功能有些类似,它们都用于将对象转换成字符串。区别在于:__repr__代表的是“自我描述“的方法,当程序调用print()函数输出对象时,Python会自动调用该对象的__repr__()方法,而__str__()方法则只有在显式调用str()函数时才会起作用。

  1. object.__bytes__(self):对应于调用内置的bytes()函数将该对象转换成字节内容,该方法应该返回bytes对象。
  2. object.__complex__(self):对应于调用内置的complex()函数将该对象转换成复数,该方法应该返回complex对象。
  3. object.__int__(self):对应于调用内置的int()函数将该对象转换成整数,该方法应该返回int对象。
  4. object.__float__(self):对应于调用内置的float()函数将该对象转换成浮点数,该方法应该返回float对象。
class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height
    #定义setSize()函数
    def setSize(self,size):
        self.width,self.height = size
    #定义getSize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__int__方法,程序可调用int()函数将该对象转换成整数
    def __int__(self):
        return int(self.width*self.height)
r=Rectangle(4,5)
print(int(r))#20

8.5.5 与常见的内建函数相关的特殊方法

Python还提供了一些常见的内建函数,当使用这些内建函数处理对象时,实际上也是由以下特殊方法来提供支持的:

  1. object.__format__(self,format_spec):对应于调用内置的format()函数将对象转换成格式化字符串。
  2. object.__hash__(self):对应于调用内置的hash()函数将对象转换成该对象的hash码。
  3. object.__abs__(self):对应于调用内置的abs()函数将对象转换成绝对值。
  4. object.__round__(self[,digits]):对应于调用内置的round()函数执行四舍五入取整。
  5. object.__trunc__(self):对应于调用内置的trunc()函数执行截断取整。

注意:如果某个自定义类没有提供__int__(self)方法,而是提供了__trunc__(self)方法,那么程序在调用内置的int()函数将其转换成整数时,底层将由__trunc__(self)方法提供支持。

(6)object.__floor__(self):对应于调用内置的floor()函数执行向下取整。

(7)object.__ceil__(self):对应于调用内置的ceil()函数执行向上取整。

class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height
    #定义setSize()函数
    def setSize(self,size):
        self.width,self.height = size
    #定义getSize()函数
    def getSize(self):
        return self.width,self.height
    #使用property定义属性
    size=property(getSize,setSize)
    #定义__round__方法,程序可调用round()函数将该对象执行四舍五入取整
    def __round__(self, ndigits=0):
        self.width,self.height=round(self.width,ndigits),round(self.height,ndigits)
        return self
    def __repr__(self):
        return "Rectangle(width=%g,height=%g)" % (self.width,self.height)
r=Rectangle(4.13,5.56)
#对Rectangle对象执行四舍五入取整
result=round(r,1)
print(r)#Rectangle(width=4.1,height=5.6)
print(result)#Rectangle(width=4.1,height=5.6)

  • 35
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

提刀立码,调参炼丹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值