赞
踩
第九章 魔法方法、特性和迭代器
9.1 如果你使用的不是python3
在Python 2.2中,Python对象的工作方式有了很大的变化。这种变化带来了多个方面的影响。这些影响对Python编程新手来说大都不重要,但有一点需要注意:即便你使用的是较新的Python 2版本,有些功能(如特性和函数super)也不适用于旧式类。要让你的类是新式的,要么在模块开头包含赋值语句__metaclass__ = type(这以前提到过),要么直接或间接地继承内置类object或其他新式类。
在Python 3中没有旧式类,因此无需显式地继承object或将__metaclass__设置为type。所有的类都将隐式地继承object。如果没有指定超类,将直接继承它,否则将间接地继承它。
9.2 构造函数
构造函数是几乎所有程序中都用的典型的魔法方法:__init__(是否记得,在定义My_Exception时用过)。
构造函数不同于普通方法的地方在于,将在对象创建后自动调用它们。
9.2.1 重写普通方法和特殊的构造函数
每个类都有一个或多个超类,并从它们那里继承行为。对类B的实例调用方法(或访问其属性)时,如果找不到该方法(或属性),将在其超类A中查找。
'''
- class A:
- def hello(self):
- print("你好,这是A类的方法。")
-
- class B(A):
- pass
-
- a=A()
- b=B()
- a.hello()
- b.hello()
'''
- 你好,这是A类的方法。
- 你好,这是A类的方法。
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
如果子类中也定义了一个和父类同名的方法,那么子类对象调用这个方法时,它不再调用父类方法而调用子类方法。
'''
- class D(A):
- def hello(self):
- print("你好,这是D类的方法。")
-
- d=D()
- d.hello()
'''
- 你好,这是A类的方法。
- 你好,这是A类的方法。
- 你好,这是D类的方法。
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
这种在子类中重新定义同名方法的方法就叫方法重写。
重写是继承机制的一个重要方面,对构造函数来说尤其重要。构造函数用于初始化新建对象的状态,而对大多数子类来说,除超类的初始化代码外,还需要有自己的初始化代码。虽然所有方法的重写机制都相同,但与重写普通方法相比,重写构造函数时更有可能遇到一个特别的问题:重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。
'''
- class C:
- def __init__(self):
- self.a=10
- print("我是C类")
- def ser(self):
- print("我是C类的ser方法",self.a)
-
- class E(C):
- def __init__(self):
- print("我是E类")
-
- c=C()
- e=E()
- c.ser()
- #e.ser()
'''
- 我是C类
- 我是E类
- 我是C类的ser方法 10
- Traceback (most recent call last):
- File "xx.py", line29, in <module>
- e.ser()
- File "xx.py", line 20, in ser
- print("我是C类的ser方法",self.a)
- AttributeError: 'E' object has no attribute 'a'
-
-
- ------------------
- (program exited with code: 1)
-
- 请按任意键继续. . .
为何会这样呢?因为在子类E中重写了构造函数,但新的构造函数没有包含任何初始化属性父类a的代码。要消除这种错误,子类E的构造函数必须调用其父类C的构造函数,以确保基本的初始化得以执行。为此,有两种方法:调用未关联父超类构造函数,或者使用函数super。
9.2.2 调用未关联的构造函数
(这主要应用于Python2.x版本,因此略去)
9.2.3使用函数super
'''
- class F(C):
- def __init__(self):
- super().__init__()
- print("我是F类")
-
- c=C()
- f=F()
- c.ser()
- f.ser()
'''
- 我是C类
- 我是F类
- 我是C类的ser方法 10
- 我是C类的ser方法 10
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
9.3 元素访问
虽然__init__无疑是你目前遇到的最重要的特殊方法,但还有不少其他的特殊方法,让你能够完成很多很酷的任务。
9.3.1 基本的序列和映射协议
序列和映射基本上是元素(item)的集合,要实现它们的基本行为(协议),不可变对象需要实现2个方法,而可变对象需要实现4个。
i、__len__(self):这个方法应返回集合包含的项数,对序列来说为元素个数,对映射来说为键-值对数。如果__len__返回零(且没有实现覆盖这种行为的__nonzero__),对象在布尔上下文中将被视为假(就像空的列表、元组、字符串和字典一样)。
ii、__getitem__(self, key):这个方法应返回与指定键相关联的值。对序列来说,键应该是0~n -1的整数(也可以是负数,这将在后面说明),其中n为序列的长度。对映射来说,键可以是任何类型。
iii、__setitem__(self, key, value):这个方法应以与键相关联的方式存储值,以便以后能够使用__getitem__来获取。当然,仅当对象可变时才需要实现这个方法。
iv、__delitem__(self, key):这个方法在对对象的组成部分使用__del__语句时被调用,应删除与key相关联的值。同样,仅当对象可变(且允许其项被删除)时,才需要实现这个方法。
对于这些方法,还有一些额外的要求。
i、对于序列,如果键为负整数,应从末尾往前数。换而言之,x[-n]应与x[len(x)-n]等效。
ii、如果键的类型不合适(如对序列使用字符串键),可能引发TypeError异常。
iii、对于序列,如果索引的类型是正确的,但不在允许的范围内,应引发IndexError异常。
'''
- d={"a":10,"b":20,"c":30,"d":20,"e":10,}
- len1=d.__len__()
- getitem1=d.__getitem__("c")
- print(len1,"\t",getitem1)
- d.__setitem__("f",90)
- print(d.__len__())
- print(d.__len__(),"\t",d.__getitem__("f"))
- d.__delitem__("f")
- print(d.__len__(),"\t",d.__getitem__("c"))
'''
- 5 30
- 6
- 6 90
- 5 30
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
以上程序如果最后一句改为print(d.__len__(),"\t",d.__getitem__("c"))将会报错。
9.3.2 从list、dict和str派生
可以建立一些继承了list、dict和str的新类,以满足不同的需要。形如:
'''
- class NewList(list):
- def __init__(self,*args):
- super().__init__(*args)
- #定义新方法或重写list的方法
- pass
'''
9.4 其他魔法方法9.5 特性
封装是面向对象编程的一个重要的特性,但是经过封装的类,是不能直接访问私有属性的,为了达到访问类中私有属性的目的,在类中含必须创建一些方法,利用这些方法达到间接访问这些私有属性的目的。由这种方法访问的属性称为特性(property)。
9.5.1 函数property
property() 函数的作用是在类中返回属性值。将 property 函数用作装饰器可以很方便的创建只读属性。
格式为class property([fget[, fset[, fdel[, doc]]]])其中:fget为获取属性值的函数;fset为设置属性值的函数;fdel为删除属性值函数;doc为属性描述信息。调用函数property时,还可不指定参数、指定一个参数、指定三个参数或指定四个参数。如果没有指定任何参数,创建的特性将既不可读也不可写。如果只指定一个参数(获取方法) ,创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法(这个方法不接受任何参数)。第四个参数也是可选的,指定一个文档字符串。这些参数分别名为fget、fset、fdel和doc。如果你要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参数来实现。
'''
- class Rect:
- def __init__(self):
- self.__width=0
- self.__leng=0
- def get_size(self):
- if self.__width and self.__leng:
- return "矩形的宽{},矩形的长{}".format (self.__width,self.__leng)
- else:
- return 0
-
- def set_size(self,size):
- self.__width,self.__leng=size
- def del_size(self):
- del self.__width
- del self.__leng
-
-
- size=property(get_size,set_size,del_size,"隐藏了set、get、del方法")
-
-
- r=Rect()
- r.size=10,5
- print(r.size)
- r.size=150,100
- print(r.size)
'''
- 矩形的宽10,矩形的长5
- 矩形的宽150,矩形的长100
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
这样不但隐藏了类的私有属性而且把类的set、get及del方法也隐藏了。
9.5.2 静态方法和类方法
静态方法和类方法是这样创建的:将它们分别包装在staticmethod和classmethod类的对象中。静态方法的定义中没有参数self,可直接通过类来调用。类方法的定义中包含类似于self的参数,通常被命名为cls。对于类方法,也可通过对象直接调用,但参数cls将自动关联到类。
'''
- class MyClass():
- __a=0
- b=1
- @staticmethod #静态方法装饰器(以后说)
- def smeth(): #定义静态方法
- print("这是静态方法")
-
- @classmethod #类方法装饰器(以后说)
- def cmeth(cls): #定义类方法
- print("这是类方法")
-
- def __init__(self):
-
- print("这是MyClass类")
-
- def run(self):
- print("这是run方法")
-
- MyClass.smeth() #静态方法和类方法不需要实例,可直接调用。
- MyClass.cmeth()
- my_class=MyClass()
- my_class.run() #实例方法不需实例化才能调用
'''
- 这是静态方法
- 这是类方法
- 这是MyClass类
- 这是run方法
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
9.5.3 __getattr、__setattr__等方法
这些方法大都是拦截对对象属性访问的。
i、__getattribute__(self, name)在属性被访问时自动调用(只适用于新式类)。
ii、__getattr__(self, name)在属性被访问而对象没有这样的属性时自动调用。
iii、__setattr__(self, name, value)试图给属性赋值时自动调用。
iv、__delattr__(self, name)试图删除属性时自动调用。
相比函数property,这些魔法方法使用起来要棘手些(从某种程度上说,效率也更低),但它们很有用,因为你可在这些方法中编写处理多个特性的代码。然而,在可能的情况下,还是使用函数property吧。
9.6 迭代器
将更详细地介绍迭代器。对于魔法方法,这里只介绍__iter__,它是迭代器协议的基础。
9.6.1 迭代器协议
迭代(iterate)意味着重复多次,就像循环那样。本书前面只使用for循环迭代过序列和字典,但实际上也可迭代其他对象:实现了方法__iter__的对象
方法__iter__返回一个迭代器,它是包含方法__next__的对象,而调用这个方法时可不提供任何参数。当你调用方法__next__时,迭代器应返回其下一个值。如果迭代器没有可供返回的值,应引发StopIteration异常。你还可使用内置的便利函数next,在这种情况下,next(it)与it.__next__()等效。
使用迭代器不是像for in那样一次获取所有的值,而是根据需要一次可获取一个或几个值,因此它节省空间.使用迭代器更通用、更简单、更优雅。
还有一些序列不能用列表的方式显示,这时迭代器就是更好的工具。下面来看一个不能使用列表的示例,因为如果使用列表,这个列表的长度必须是无穷大的!这个“列表”为斐波那契数列。
为更好的理解书中的例子,先了解一下下面的程序:
'''
- a=0
- b=1
- a,b=b,a+b
-
'''
等价于:a=b,b=a+b,即把后一个数传给前一个数(b赋值给a),
而后一个数得到与前一个数的和(即把a+b赋给b)。
'''
- print(a)
- print(b)
- for i in range(5):
- a,b=b,a+b
- print("_"*80)
- print(a)
- print(b)
'''
- 1
- 1
- _______________________________________________________________________________
- 1
- 2
- _______________________________________________________________________________
-
- 2
- 3
- _______________________________________________________________________________
- 3
- 5
- _______________________________________________________________________________
-
- 5
- 8
- _______________________________________________________________________________
- 8
- 13
- ------------------
- (program exited with code: 0)
- 请按任意键继续. . .
斐波那契数列:
'''
- class Fibonacci:
- def __init__(self):
- self.a=0
- self.b=1
- def __next__(self): #创建斐波那契数列
- self.a,self.b=self.b,self.a+self.b
- return self.a #返回迭代器集合
- def __iter__(self): #定义集合可迭代
- return self #返回实例化对象
-
- fib=Fibonacci()
- for f in fib:
- if f>1000:
- print(f)
- break
'''
- 1597
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
9.6.2从迭代器创建序列
除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可将它们转换为序列。
'''
- class Test:
- value = 0
- sums=0
- def __next__(self):
- self.value+=1
- self.sums=1+(1/self.value) #无限序列(如果以此为基础输出常数e的近似值怎么办?)
- if self.value>10:
- raise StopIteration #设置异常终止程序(StopIteration异常是在循环对象穷尽所有元素时的报错)
- return self.sums
- def __iter__(self):
- return self
-
- ti = Test()
- print(list(ti))
'''
- [2.0, 1.5, 1.3333333333333333, 1.25, 1.2, 1.1666666666666667, 1.1428571428571428
- , 1.125, 1.1111111111111112, 1.1]
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
9.7 生成器
生成器是一种使用普通函数语法定义的迭代器。
9.7.1 创建生成器
生成器(generator)即是一个含有yield的函数。反过来说包含yield语句的函数都被称为生成器。
生成器和一般函数不同,它不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。
生成器的另一个特点就是节省空间,它可以一边循环一边计算,可以在循环的过程中不断推算出后续的元素。
要创建一个generator,有很多种方法。
第一种方法很简单,只有把一个列表生成式的[]中括号改为()小括号,就创建一个generator
'''
- a=[x for x in range(10)]
- print(type(a))
- b=(x for x in range(10))
- print(type(b))
'''
- <class 'list'>
- <class 'generator'>
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
列表和生成器的输出方式不同:
'''
- print(a)
- print("*"*80)
- print(b)
- print("*"*80)
- print(next(b)) #它必须用next函数输出,每次打印一个数字
- print(next(b)) #第二次接着打印第二个数字
'''
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- ***********************************************************************
-
- <generator object <genexpr> at 0x000000000213CCF0>
- ***********************************************************************
-
- 0
- 1
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
也可以用for in循环:
'''
- for i in b:
- print(i)
'''
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
创建一个generator后,一般不会调用next(),而是通过for循环来迭代,并且不需要关心StopIteration的错误。
第二个创建生成器的方法就是定义含有yield关键字的“函数”。
'''
- def fib(num):
- i,x,y=0,0,1
- while i<num:
- yield y
- x,y=y,x+y #斐波那契数列
- i+=1
- return
-
- for j in fib(10):
- print(j,end="\t")
'''
- 1 1 2 3 5 8 13 21 34 55
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
9.7.2 递归式生成器
'''
- def gen(nums):
- try:
- try:nums+'' #筛选字符串
- except TypeError:pass
- else:
- raise TypeError
- for n in nums:
- for i in gen(n): #递归
- yield i
- except TypeError:
- yield nums
-
- nums=[22,33,44,[44,67,[88,23,19]]]
- lis=list(gen(nums))
- print(lis)
'''
- [22, 33, 44, 44, 67, 88, 23, 19]
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
以上可以看出,在同为可迭代的情况下,生成器和函数仅有yield和return的区别。
9.7.4 通用生成器
生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话说,这两个实体通常被视为一个,通称为生成器。
'''
- def generator():
- print("一")
- yield 10
- print("二")
- yield 18
- print("三")
- yield 22
- print("四")
- yield 60
- g=generator()
- print(next(g))
- print(next(g))
'''
- 一
- 10
- 二
- 18
-
-
- ------------------
- (program exited with code: 0)
-
- 请按任意键继续. . .
用这种方式可以生成任意的数据序列,且能达到节约空间的目的。
9.7.4 生成器(generator}的方法
在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。这个通信渠道包含如下两个端点。
i、外部世界:外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要发送的“消息”,可以是任何对象)。
ii、生成器:在挂起的生成器内部,yield可能用作表达式而不是语句。换而言之,当生成器重新运行时,yield返回一个值——通过send从外部世界发送的值。如果使用的是next,yield将返回None。仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。要在此之前向生成器提供信息,可使用生成器的函数的参数。
send函数官方文档是这样说的,“恢复执行,并将一个值发送到生成器函数。参数为当前输出的结果。send()方法返回下一个值,如果到达生成器末尾,则引发StopIteration。如果是刚启动生成器就用send函数,那么就用None作为其参数调用send函数,因为不知道当前输出的结果。”——其实在不知道当前输出的结果时也要用None作为参数。
'''
- print(g.send(18))
- print(g.send(None))
-
- '''
- 三
- 22
- 四
- 60
- ------------------
- (program exited with code: 0)
- 请按任意键继续. . .
生成器还包含另外两个方法。
方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个traceback对象。
方法close:用于停止生成器,调用时无需提供任何参数。
9.7.5 模拟生成器
(主要针对较老的Python版本,略)
9.8 八皇后问题
9.8.1 生成器的回溯
在有些应用程序中,你不能马上得到答案。你必须尝试多次,且在每个递归层级中都如此。打个现实生活中的比方吧,假设你要去参加一个很重要的会议。你不知道会议在哪里召开,但前面有两扇门,而会议室就在其中一扇门的后面。你选择进入左边那扇门后,又看到两扇门。你再次选择进入左边那扇门,但发现走错了。因此你往回走,并进入右边那扇门,但发现也走错了。因此你继续往回走到起点,现在可以尝试进入右边那扇门。这就是回溯.对于逐步得到结果的复杂递归算法,非常适合使用生成器来实现。
伪代码:
- for 第一级的可能路径:
- for 第一级的可能路径:
- for 第一级的可能路径:
- .
- .
- .
- for 第一级的可能路径:
- 能解决吗?
9.8.2 问题
.
.
.
(待续)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。