第十章 方法、属性和迭代器

 

在python语言中,存在一些特殊的方法,这些方法往往在命名上与普通方法不同。例如,一些方法会在名字前后各加两个下划线,这种有特殊含义的命名方式,千万不要随意使用。这些方法会在特殊的情况下被调用。

10.1 构造方法

构造方法非常重要,是创建对象的过程中被调用的第一个方法,通常用于初始化对象中需要的资源,如初始化一些变量。

class Person:
	# Person 类的构造方法
    def __init__(self,name = "Bill"):
        print("构造方法已经被调用")
        self.name = name
    def getName(self):
        return self.name
    def setName(self,name):
        self.name= name

# 实例化的时候,自动调用了init构造方法,进行初始化
person = Person()     				
print(person.getName())
# 实例化的时候,指定了name值,构造方法会被调用
person1 = Person(name = "Mike")		
print(person1.getName())
person1.setName(name = "John")
print(person1.getName())

10.1.2 重写普通方法和构造方法

之前学习过继承,当B类继承A类时,B类就会拥有A类的所有成员变量和方法。如果B类中的方法名与A类中的方法名相同,那么B类中同名方法就会重写A类中同名方法。如果在B类中定义了同名构造方法,同样也会重写A类中的构造方法,也就是说,创建B类对象,实际上是调用B类中的构造方法,而不是A类中的构造方法。

# 父类
class Bird:
	# 构造方法
    def __init__(self):
        self.hungry= True
    def eat(self):
        if self.hungry:
            print("已经吃了虫子!")
            self.hungry = False
        else:
            print("已经吃过饭了,不饿了!")

# 实例化父类对象,并调用构造方法、普通方法
b = Bird()
b.eat()
b.eat()

# 子类
class SongBird(Bird):
    def __init__(self):
        self.sound = '向天再借五百年'
    def sing(self):
        print(self.sound)
    def eat(self,thing):
        print(thing)

# 实例化子类对象        
sb = SongBird()
sb.sing()
# 调用继承的父类方法时,子类的init构造方法覆盖,hungry变量没有初始化,抛出异常
sb.eat()

另外要注意的是,在Python语言中,重写方法只看方法名,并不看参数。只要方法名相同,就会覆盖父类的同名方法。

10.1.3 使用super函数

在子类中如果重写了超类(多层的父类关系)方法,通常需要在子类方法中调用超类的同名方法,也就是说,重写了超类的方法,实际上应该是一种增量的重写方式,子类方法会在超类同名方法的基础上做一些其他工作。

如果在子类中访问超类中的方法,需要使用super函数。该函数返回的对象代表超类对象,所以访问super函数返回的对象中的资源都属于超类。super函数可以不带任何参数,也可以带两个参数,第1个参数表示当前类的类型,第2个参数需要传入self。

# 父类
class Animal:
    def __init__(self):
        print("Animal init")

# 子类
class Bird(Animal):
    def __init__(self, hungry):
		# 引用了上层类的构造方法
        super().__init__()
        self.hungry= hungry
    def eat(self):
        if self.hungry:
            print("已经吃了虫子!")
            self.hungry = False
        else:
            print("已经吃过饭了,不饿了!")

# 实例化父类
b = Bird(False)
b.eat()
b.eat()

# 孙子类
class SongBird(Bird):
    def __init__(self,hungry):
		# 引用了上层类的构造方法
        super(SongBird,self).__init__(hungry)
        self.sound = '向天再借五百年'
        
    def sing(self):
        print(self.sound)

# 可调用超类中的普通方法、构造方法        
sb = SongBird(True)
sb.sing()
sb.eat()

10.2 特殊成员方法

尽管构造方法__init__对于一个类非常重要,但还有一些其他的特殊方法也同样重要。

10.2.1 自定义序列

使用4个特殊方法定义自己的序列类

  • __len__(self):返回序列中元素的个数。使用len函数获取序列对象的长度时会调用该方法。
  • __getitem__(self,key):返回与所给键对应的值。__getitem__方法的第2个参数表示键key,在使用sequence[key]获取值时会调用该方法。
  • __setitem__(self,key,value):设置key对应的值。__setitem__方法的第2个参数表示键key,第3个参数不表示值value。当使用 sequence[key]=value 设置序列中键对应的值时调用该方法。
  • __delitem__(self,key):从序列中删除键为key的key-value对。当使用del关键字删除序列中键为key的key-value对时调用该方法。
class FactorialDict:
    def __init__(self):
		# 创建字典对象
        self.numDict = {}
	# 用于计算阶乘的方法
    def factorial(self,n):
        if n == 0 or n == 1:
            return 1
        else:
            return n * self.factorial(n -1)
	# 从字典中获取key对应的value时调用该方法
    def __getitem__(self,key):
        print('__getitem__方法被调用,key={}'.format(key))
        if key in self.numDict:
            return self.factorial(self.numDict[key])
        else:
            return 0
	# 设置key对应的value时调用该方法
    def __setitem__(self,key, value):
        print('__setitem__方法被调用,key={}'.format(key))
        self.numDict[key] = int(value)
	# 使用del删除key对应的key-value对时调用
    def __delitem__(self,key):
        print('__delitem__方法被调用,key={}'.format(key))
        del self.numDict[key]
	# 使用len函数获取字典中key-value对个数时调用
    def __len__(self):
        print('__len__方法被调用')
        return len(self.numDict)

# 自动实例化,自动调用相应的构造方法,非常方便
d = FactorialDict()
d['4!'] = 4
d['7!'] = 7
d['12!'] = '12'

print('4!', '=', d['4!'])
print('7!', '=',d['7!'])
print('12!', '=',d['12!'])
print('len','=',len(d))
del d['7!']
print('7!', '=',d['7!'])
print('len','=',len(d))

10.2.2 从内建列表、字符串和字典继承

到目前为止,已经学习了与序列/映射相关的4个特殊方法,在实现自定义的序列/映射时,需要实现这4个方法,不过每次都要实现所有的4个方法太麻烦了,为此,python提供了几个内建类(list、dict 和 str),分别实现列表、字典和字符串的默认操作。只需要从这三个类继承,并实现必需的方法即可。

# --------------------继承list类--------------------
class CounterList(list):
	# list 的构造方法必须指向一个可变参数,用于初始化列表
    def __init__(self,*args):
        super().__init__(*args)
        self.counter = 0
	# 从列表中获取值时,计数器加1
    def __getitem__(self,index):
        self.counter += 1
		# 调用超类的 __getitem__ 方法获取指定的值,当前方法只负责计数器加1
        return super(CounterList, self).__getitem__(index)

# 创建一个CounterList对象,并初始化列表
c = CounterList(range(10))
print(c)
# 反转列表c
c.reverse()
print(c)
# 删除c中的一组值
del c[2:7]
print(c)
print(c.counter)
# 将c列表中的两个值相加
print(c[1] + c[2])
print(c.counter)

# --------------------定义一个dict继承类--------------------
class CounterDict(dict):
	# dict 的构造方法必须指向一个可变参数,用于初始化字典
    def __init__(self,*args):
        super().__init__(*args)
        self.counter = 0
	# 从列表中获取值时,计数器加1
    def __getitem__(self,key):
        self.counter += 1
        return super(CounterDict, self).__getitem__(key)

# 创建一个CounterDict对象,并初始化字典
d = CounterDict({'name':'Bill'})
print(d['name'])
# get方法并不会调用 __getitem__ 方法,所以计数器并不会加1,运行结果:None
print(d.get('age'))    
# 为字典添加新的key-value对
d['age'] = 30
print(d['age'])
print(d.counter)

# --------------------定义一个从str继承的类--------------------
class MultiString(str):
	# 该方法会在 __init__ 方法之前调用,用于验证字符串构造方法的参数
	# 该方法的参数要与 __init__ 方法的参数保持一致
    def __new__(cls, *args, sep = ' '):
        s = ''
		# 将可变参数中所有的值连接成一个大字符串,中间用end指定的分隔符分隔
        for arg in args:
            s += arg + sep
		# 最后需要去掉字符串结尾的分隔符,所以先算出最后的分隔符的开始索引
        index = -len(sep)
        if index == 0:
            index = len(s)
		# 返回当前的MultiString对象
        return str.__new__(cls, s[:index])
    def __init__(self, *args, sep = ' '):
        pass

# 拼接字符串
cs1 = MultiString('a', 'b', 'c')
cs2 = MultiString('a', 'b', 'c', sep=',')
cs3 = MultiString('a', 'b', 'c', sep='')
print('[' + cs1 + ']')
print('[' + cs2 + ']')
print('[' + cs3 + ']')

10.3 属性

通常会将类的成员变量称为属性。在创建类实例后,可以通过类实例访问这些属性,也就是读写属性的值。不过直接在类中定义成员变量,尽管可以读写属性的值,但无法对读写的过程进行监视。例如,在读取属性值时无法对属性值进行二次加工,在写属性值时也无法效验属性值是否有效。

10.3.1 传统的属性

在python语言中,如果要为类增加属性,需要在构造方法 __init__中通过self添加,如果要读写属性的值,需要创建类的实例,然后通过类的实例读写属性的值。

class Rectangle:
	# 添加了两个属性left、top
    def __init__(self):
        self.left = 0
        self.top = 0
	# 同时设置left和top属性的值,position参数值应该是元组或列表类型
    def setPosition(self,position):
        self.left,self.top = position
	# 同时获取left和top属性的值,返回的值时元组类型
    def getPosition(self):
        return self.left,self.top

r = Rectangle()
r.left = 10
r.top = 20
print('left','=',r.left)
print('top','=',r.top)
# 通过 setPosition 方法设置left和top属性的值
r.setPosition([30,50])
# 通过 getPosition 方法设置left和top属性的值
print('position', '=', r.getPosition())

尽管通过get…和set…方法可以解决监控属性的问题,但在监控属性值的变化时,就暴露内部的实现机制有些不妥,而且如果属性访问者由于某些原因要直接使用属性,而不是调用get…和set…方法,那么所有的get…和set…方法的代码都需要修改,这样的工作量非常大,而且容易出错。

10.3.2 property 函数

在使用对象的属性时,按一般理解直接使用 obj.propertyName 即可。同时希望可以监控 propertyName 的读写操作。

property函数 可以与三个方法绑定,该函数会创建一个属性,并通过返回值返回这个属性。第1个参数需要指定用于监控读属性值的方法,第2个参数需要指定用于监控写属性值的方法,第3个参数需要指定删除该属性时调用的方法。

class Rectangle:
    def __init__(self):
        self.left = 0
        self.top = 0
    # setPosition方法可以是其他的名字,写
    def setPosition(self,position):
        print('setPosition方法被调用')
        self.left,self.top = position
    # getPosition方法可以是其他的名字,读        
    def getPosition(self):
        print('getPosition方法被调用')
        return self.left,self.top
	# deletePosition方法可以是其他的名字,删
    def deletePosition(self):
        print('position属性已经被删除')
        self.left = 0
        self.top = 0
	# 监控读、写、删
    position = property(getPosition, setPosition,deletePosition)

# 传统读写属性方式
r = Rectangle()
r.left = 10
r.top = 20
print('left','=',r.left)
print('top','=',r.top)

# 调用property方法监控属性,并调用读属性getPosition方法
print('position', '=', r.position)
# 调用property方法监控属性,并调用写属性setPosition方法
r.position = 100,200
# 调用property方法监控属性,并调用读属性getPosition方法
print('position', '=', r.position)
# 调用property方法监控属性,并调用删属性deletePosition方法
del r.position
print(r.position)
r.position = 30,40
print('r.position','=',r.position)

知识点:

  • 通过property函数设置的语属性绑定的方法的名称没有任何限制。但是用于监控读属性和删属性操作的方法只能有一个self参数,还需要一个参数值,用于接收设置属性的值。
  • 删除对象的属性只是调用了通过property函数绑定的回调方法,并没有真正的删除对象的属性。本例中只是重新初始化了left和top属性的值。

10.3.3 监控对象中所有的属性

尽管使用property函数可以将三个方法与一个属性绑定,在读写属性值和删除属性时会调用相应的方法进行处理,但是如果需要监控的属性很多,则这样做就意味着在类中需要定义大量的get…和set…方法。所以说,property函数只是解决了外部调用这些属性的问题,并没有解决内部问题。

介绍三个特殊成员方法,当任何一个属性进行读写和删除操作时,都会调用它们中的一个方法进行处理。

  • __getattr__(self,name):用于监控所有属性的读操作,其中name表示监控的属性名。
  • __setattr__(self,name,value):用于监控所有属性的写操作,其中name表示监控的属性名,value表示设置的属性值。
  • __delattr__(self,name):用于监控所有属性的删除操作,其中name表示监控的属性名。
class Rectangle:
    def __init__(self):
        self.width = 0
        self.height = 0
        self.left = 0
        self.top = 0

	# 对属性进行写操作时调用该方法
    def __setattr__(self,name,value):        
        print("{}被设置,新值为{}".format(name,value))
        if name == 'size':
            self.width, self.height = value
        elif name == 'position':
            self.left, self.top = value
        else:
			# __dict__ 是内部维护的一个特殊成员变量,用于保存成员变量的值,所以这条语句必须加上
            self.__dict__[name] = value  

	# 对属性进行读操作时调用该方法
    def __getattr__(self,name):
        print("{}被获取".format(name))
        if name == 'size':
            return self.width,self.height
        elif name == 'position':
            return self.left, self.top 

	# 对属性进行删操作时调用该方法
    def __delattr__(self,name):
        if name == 'size':
            self.width,self.height = 0, 0
        elif name == 'position':
            self.left, self.top = 0,0

r = Rectangle()
# 写属性监控
r.size = 300,500
r.position = 100,400
# 读属性监控
print('size', '=', r.size)
print('position', '=', r.position)
# 删属性监控
del r.size, r.position
print(r.size)
print(r.position)

注意:使用了一个 __dict__ 成员变量,这是系统内置的成员变量,用于保存对象中所有属性的值。如果不在类中定义 set…方法,则系统默认在设置属性值时将这些值都保存在 __dict__变量中,但是,如果定义了set…方法,则需要将这些属性值保存到 __dict__字典中。可以利用这个特性来控制某个属性可设置的值的范围。

class MyClass:
     def __setattr__(self,name,value):        
        if name == 'value':
			# 要求设置的属性值为正数,否则不设置
            if value > 0:
                self.__dict__[name] = value
            else:
                print('{}属性的值必须大于0'.format(name))
        else:
           self.__dict__[name] = value

c = MyClass()
c.value = 20
print('c.value','=',c.value)
c.value = -43
print('c.value','=',c.value)

10.4 静态方法和类方法

python类包含三种方法:实例方法、静态方法和类方法。实例方法前面已经学习了很多,想要调用实例方法,必须要实例化类,然后才可以调用。也就是说,调用实例化方法需要类的实例(对象)。而静态方法在调用时根本不需要类的实例(静态方法不需要self参数)。

类方法的调用与静态方法完全一样,所不同的是,类方法与实例方法的定义方式相同,都需要一个self参数,只不过这个self参数的含义不同。对于实例方法来说,这个self参数就代表当前类的实例,可以通过self访问对象中的方法和属性而类方法的self参数表示类的元数据,也就是类的本身,不能通过self参数访问对象中的方法和属性,只能通过这个self参数访问类的静态方法和静态属性。

定义静态方法需要使用 @staticmethod 装饰器,定义类方法需要使用 @classmethod 装饰器。

class MyClass:
    name = "Bill"
    
	# 构造方法
	def __init__(self):
        print("MyClass的构造方法被调用")
        self.value = 20
    
	# 定义静态方法,不需要实例化,可以通过类方法的self调用
	@staticmethod
    def run():
        print('*', MyClass.name, '*')
        print("MyClass的静态方法run被调用")
    
	# 定义类方法,可以通过类方法的self调用静态方法、静态属性
	@classmethod
    # 这里self是元数据,不能访问对象中的构造方法、实例方法和属性
    def do(self):
        print(self)
        print('[', self.name, ']')
        print('调用静态方法run')
        self.run()
        # 如果是类方法,就无法访问self中的成员变量了
        #print(self.value)
        print("成员方法do被调用")
    
	# 实例方法
	def do1(self):
        print(self.value)
        print('<',self.name, '>')
        print(self)

# 通过类直接调用静态方法,不需要实例化        
MyClass.run()
# 通过类的实例调用类方法,需要实例化
c = MyClass()
c.do()
# 也可以过类直接调用类方法
MyClass.do()
# 通过类实例访问类的静态变量
print('MyClass2.name','=',MyClass.name)
# 通过类的实例访问实例方法
c.do1()

知识点

  • 通过实例定义的变量只能被实例方法访问。
  • 而直接在类中定义的静态变量,既可以被实例方法访问,也可以被静态方法和类方法访问。
  • 实例方法不能被静态方法和类方法访问,但静态方法和类方法可以被实例方法访问。
  • 类方法可以访问静态方法或静态变量

10.5 迭代器

迭代就是循环的意思,也就是对一个集合中的元素进行循环,从而得到每一个元素。对于自定义的类,也可以让其支持迭代。

10.5.1 自定义可迭代的类

为什么不使用列表呢?

列表可以获取列表的长度,然后使用变量 i 对列表索引进行循环,也可以获取集合的所有元素,且容易理解。好理解,好操作,但这是要付出代价的。列表之所以可以利用索引快速定位其中的任何一个元素,是因为列表是一下子将所有的数据都装载在内存中,而且是一块连续的内存空间。当数据量比较小时,实现较容易。当数据量很大时,会非常消耗内存资源。而迭代不同,迭代是读取多少元素,就将多少元素装载到内存中,不读取就不装载,但迭代器是从前向后顺序读取内容的。

定义一个迭代器类,用于无限迭代的直角三角形。

class RightTriangle:
    def __init__(self):
        self.n = 1
    def __next__(self):        
        result = '*' * (2 * self.n - 1)
        self.n += 1
        return result
    # 迭代器:该方法必须返回一个迭代器
    def __iter__(self):
        return self

rt = RightTriangle()
for e in rt:
    if len(e) > 10:
        break
    print(e)
*
***
*****
*******
*********

10.5.2 将迭代器转换为列表

尽管迭代器很好用,但仍然不具备某些功能,例如,通过索引获取某个元素,进行分片操作。这些操作都是列表的专利,所以在很多时候,需要将迭代器转换为列表。但有很多迭代器都是无限迭代的,因此,在将迭代器转换为列表时,需要给迭代器能够迭代元素限定一个范围,否则内存就会溢出。想要迭代器停止迭代,只需要抛出 StopIteration异常 即可。通过list函数可以直接将迭代器转换为列表。

# 将迭代器转换为列表
class Fibonacci:
    def __init__(self):
        self.a = 0
        self.b = 1
    def __next__(self):
        result = self.a
        self.a, self.b = self.b, self.a + self.b
        # 停止迭代,抛出异常即可
		if result > 100: raise StopIteration
        return result
    def __iter__(self):
        return self

# 将迭代器转换为列表:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
fibs1 = Fibonacci()
print(list(fibs1))

# 通过遍历得到:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
fibs2 = Fibonacci()
for fib in fibs2:
    print(fib, end = ' ')

注意:尽管在迭代的过程中抛出了StopIteration异常,但这个异常是由系统处理的,并不会在程序中输出异常信息。

10.6 生成器

如果说迭代器是以类为基础的单值产生器,那么生成器就是以函数为基础的单值产生器。也就是说,迭代器和生成器都只能一个值一个值地生产。每迭代一次,只能得到一个值。所不同的是,迭代器需要在类中定义 __next__ 和 __iter__方法,在使用时需要创建迭代器的实例。而生成器是通过函数展现的,可以直接调用,所以从某种意义上来说,生成器在使用上更简洁。

10.6.1 创建生成器

要定义一个生成器,首先要定义一个函数,在该函数中对某个集合或迭代器进行迭代,然后使用yield语句产生当前要生成的值,这时候函数会被冻结,直到调用生成器的代码继续迭代下一个值,生成器才会继续执行。

nestedList = [[1,2,3],[4,3,2],[1,2,4,5,7]]
# 定义生成器函数
def enumList(nestedList):
	# 对列表进行二维迭代
    for subList in nestedList:
        for element in subList:
			# 获取当前迭代值,冻结函数(暂时不在往下执行迭代)
            yield element

# 利用循环,迭代生成列表
for num in enumList(nestedList):
    print(num, end=' ')
print()

# 直接使用列表list函数获取生成器中迭代的列表,非常简洁
numList = list(enumList(nestedList))
print(numList)
print(numList[1:4]

10.6.2 递归生成器

如果想要对三维、四维甚至更多维度的列表进行一维化处理,可以采用递归的方式进行处理。处理的方式是先对多维度迭代,然后判断每个列表元素是否还是列表。如果仍然是列表,则继续对这个列表进行迭代。如果是一个普通的值,则使用yield语句返回生成的值。

# 递归生成器,对多维度列表进行一维化处理
def enumList(nestedList):
    try:
        for subList in nestedList:
			# 递归调用函数,函数参数仍然为列表
            for element in enumList(subList):
				# 直到抛出异常时,说明参数已经是普通元素了
                yield element
    except TypeError:
        # 返回普通元素值,这个异常也是递归的终止条件
		yield nestedList
        
nestedList = [4,[1,2,[3,5,6]],[4,3,[1,2,[4,5]],2],[1,2,4,5,7]]
for num in enumList(nestedList):
    print(num, end=' ')

注意:如果多维度的元素值是字符串类型,那也会进行迭代,原因是字符串可以看作字符的列表。如果要将字符串作为一个整体输出,在进行迭代之前,先要判断当前元素是不是字符串类型(字符串可以使用"+"进行拼接,否则try时抛出异常)。

# 多维降一维,字符串整体返回
def enumList(nestedList):
    try:
        try: nestedList + ''
        except TypeError:
            # 非字符串,什么都不做,执行下边for循环,程序不停止----是列表
			pass
        else:
			# 是字符串,抛出异常给上层try
            raise TypeError
        
		# 递归迭代列表,直到非列表即普通元素时,抛出异常给上层try
		for subList in nestedList:
           for element in enumList(subList):
                yield element
    except TypeError:
        # 非字符串或非列表时,输出值
		yield nestedList
		
# 循环调用生成器
nestedList = ['a',['b',['c'],20,123,[['hello world']]]]
for num in enumList(nestedList):
    print(num, end=' ')
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值