9. 类
9.1 面向对象
在本章开始之前,我们需要先了解面向对象(OOP,Object Oriented Programming)的概念。如果学习过其他面向对象的编程语言,那么可以跳过这部分。
面向对象是和面向过程相对的。以佩奇去吃饭为例:
面向过程是这样的:
去食堂(...) 买饭(...) 吃饭(...)
面向对象是这样的:
佩奇 = 猪() 佩奇.移动(食堂) 面条 =佩奇.购买(面条) 佩奇.吃(面条)
可以看到,面向对象的一个突出特点就是操作对象来实现功能。
类是一类事物的抽象,描述了一类事物应该有的属性和功能。
我们可以把类看成是属性和功能(函数)的结合。例如,狗具有年龄、体重、名字等属性,有发出叫声、吃饭、奔跑等功能。
而现实中的事物往往是具体的,例如一条名字为"旺旺",年龄3年,体重10kg的小狗。这种具体的东西我们叫做类的实例。
面向对象有一些特定的术语,如类、方法、数据成员等,Python3 面向对象 | 菜鸟教程
(runoob.com)。可以搜索面向对象等关键字找到这些概念。本文不去讲解这些术语,尽量通过案例讲解类的作用。
9.2 定义与使用类 class
最简单的定义类的语法:
class 类名: 一些函数、语句
通常,我们使用初始化函数定义类的属性,并定义一些功能函数实现类的功能。
初始化函数是一个名称为__init__()
特殊方法,可以有参数且第一个参数总是self
(约定俗成的,并不是语法规定)。如果设置了初始化方法,调用类名(参数)
时就会自动调用该方法。
self
参数代表实例,通过实例调用方法自动传入实例对象作为self参数。带有self参数的方法通常称为实例方法。
class Dog: def __init__(self, name="无名", age=0, weight=0): self.name = name self.age = age self.weight = weight def bark(self): print("汪汪汪") def growup(self): self.age += 1
定义类之后,使用类一般是创建实例对象,通过实例对象进行操作。
ww = Dog("旺旺",0,1) # 使用类名(参数),创建实例对象 print(ww.age) # 使用 实例对象.属性, 访问属性 ww.growup() # 使用实例对象.方法,调用方法。 # ww作为self参数,传入growup(self)方法 ww.bark() print(ww.age)
9.3 继承
9.3.1 单继承
类可以继承自其它类,被继承的叫做基类(或父类),继承者叫做派生类(或子类)。通过继承子类可以拥有父类的方法和属性。例如,上面的Dog
类是一种动物,那么就可以通过继承Animal
类获得Animal
的属性,重量,年龄等;拥有动物的方法,长大等。
这样做的好处在类少的时候不那么明显,当类多了之后,例如我们继续创建猫类,鸟类,鱼类…之后,我们通过继承就可以减少很多重复代码。
继承的语法:
class 派生类(基类): 一些语句
class Animal: def __init__(self,name="无名", age=0, weight=0): self.name = name self.age = age self.weight = weight def growup(self): self.age += 1 class Dog(Animal): def __init__(self, name="无名", age=0, weight=0): super().__init__(name,age,weight)#使用super().调用父类方法 def bark(self): print("汪汪汪") ww = Dog("旺旺",0,1) print(ww.age) ww.bark() ww.growup() # growup方法 继承自Animal print(ww.age) print("汪汪汪")
注释:Python 3 可以使用直接使用 super().xxx
代替 super(Class, self).xxx
:
派生类的执行过程:
派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。
派生类的实例化没有任何特殊之处: DerivedClassName() 会创建该类的一个新实例。 方法引用将按以下方式解析:搜索相应的类属性,如有必要将按基类继承链逐步向下查找,如果产生了一个函数对象则方法引用就生效。
派生类可能会重写其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,所以调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。
在派生类中的重载方法实际上可能想要扩展而非简单地替换同名的基类方法。 有一种方式可以简单地直接调用基类方法:即调用 BaseClassName.methodname(self, arguments)。 有时这对客户端来说也是有用的。 (请注意仅当此基类可在全局作用域中以 BaseClassName 的名称被访问时方可使用此方式。)
Python有两个内置函数可被用于继承机制:
- 使用
isinstance()
来检查一个实例的类型:isinstance(obj, int)
仅会在obj.__class__
为int
或某个派生自int
的类时为True
。 - 使用
issubclass()
来检查类的继承关系:issubclass(bool, int)
为True
,因为bool
是int
的子类。 但是,issubclass(float, int)
为False
,因为float
不是int
的子类。
9.3.2 多继承
Python 也支持多重继承。但是用的很少,而且有可能造成名称混乱,不推荐。
带有多个基类的类定义语句如下所示:
class 派生类(基类1, 基类2, 基类3): 一些语句 #需要用 基类1.方法 来调用基类方法
例如:
class A: def __init__(self): self.aname = 'a' class B: def __init__(self): self.bname = 'b' class C(A, B): def __init__(self): #super().__init__() # 如果用super().方法()来调用父类方法,将按照顺序向上找到第一个符合条件的父类 A.__init__(self) B.__init__(self) cc = C() print(cc.aname, cc.bname)
对于多数应用来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的,当层次结构中存在重叠时不会在同一个类中搜索两次。 因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果在那里未找到,再到 Base2 中搜索,依此类推。
真实情况比这个更复杂一些;方法解析顺序会动态改变以支持对 super() 的协同调用。 这种方式在某些其他多重继承型语言中被称为后续方法调用,它比单继承型语言中的 super 调用更强大。
动态改变顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个父类可通过多条路径被最底层类所访问)。 例如,所有类都是继承自 object,因此任何多重继承的情况都提供了一条以上的路径可以通向 object。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化, 保留每个类所指定的从左至右的顺序,只调用每个父类一次,并且保持单调(即一个类可以被子类化而不影响其父类的优先顺序)。 总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 要了解更多细节,请参阅 The Python 2.3 Method Resolution Order | Python.org。
9.4 类变量与实例变量
实例变量属于实例,每个实例单独拥有,
类变量属于类, 类的所有实例共享。
如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例属性。
class Dog: kind = 'canine' # 类变量,所有实例共享 def __init__(self, name): self.name = name # 实例变量,每个实例单独有自己的
>>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.kind # 所有狗共享 'canine' >>> e.kind # 所有狗共享 'canine' >>> d.name # d独有 'Fido' >>> e.name # e独有 'Buddy'
9.5 私有变量
python中没有类似java或C++那样用private
限定的、只能从内部访问的私有变量。
但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。
名称改写:Python通过 名称改写对私有变量提供有限支持。 任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为去除了前缀下划线的当前类名称。
名称改写只是修改了名字。
名称改写有助于让子类重载方法而不破坏类内方法调用。例如:
class Mapping: def __init__(self, iterable): self.items_list = [] self.__update(iterable) def update(self, iterable): for item in iterable: self.items_list.append(item) __update = update # private copy of original update() method class MappingSubclass(Mapping): def update(self, keys, values): # provides new signature for update() # but does not break __init__() for item in zip(keys, values): self.items_list.append(item)
即使在 MappingSubclass 引入了一个 __update 标识符的情况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update 而在 MappingSubclass 类中被替换为 _MappingSubclass__update。
请注意传递给 exec() 或 eval() 的代码不会将发起调用类的类名视作当前类;这类似于 global 语句的效果,因此这种效果仅限于同时经过字节码编译的代码。 同样的限制也适用于 getattr(), setattr() 和 delattr(),以及对于 __dict__ 的直接引用。
9.6 使用空类模拟C的结构体
有时会需要使用类似于 C 的“struct”这样的数据类型,将一些命名数据项捆绑在一起。 这种情况适合定义一个空类:
class Employee: pass john = Employee() # Create an empty employee record # Fill the fields of the record john.name = 'John Doe' john.dept = 'computer lab' john.salary = 1000
9.7 迭代器
大多数容器都可以使用for
语句进行迭代,如列表、元组、字典、字符串等
for element in [1, 2, 3]: print(element) for element in (1, 2, 3): print(element) for key in {'one':1, 'two':2}: print(key) for char in "123": print(char) for line in open("myfile.txt"): print(line, end='')
在幕后,for
语句会在容器对象上调用 iter()
。 该函数返回一个定义了 __next__()
方法的迭代器对象,__next__()
方法将逐一访问容器中的元素。 当元素用尽时,__next__()
将引发 StopIteration
异常来通知终止 for
循环。
你可以使用 next()
内置函数来调用 __next__()
方法;这个例子显示了它的运作方式:
>>> s = 'abc' >>> it = iter(s) # 返回迭代器对象 >>> it <str_iterator object at 0x10c90e650> >>> next(it) #使用next() 等价于 调用 it的__next__()方法 'a' >>> next(it) 'b' >>> next(it) 'c' >>> next(it) # 元素用尽将引发 StopIteration异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> next(it) StopIteration
因此,只要给类加上__iter__
方法返回迭代对象, 加上__next__
方法返回元素,就可以将自定义的类变为迭代器,然后就可以对其使用for
循环了。
class Reverse: """Iterator for looping over a sequence backwards.""" def __init__(self, data): self.data = data self.index = len(data) def __iter__(self): return self def __next__(self): if self.index == 0: raise StopIteration self.index = self.index - 1 return self.data[self.index]
>>> rev = Reverse('spam') >>> iter(rev) <__main__.Reverse object at 0x00A1DB50> >>> for char in rev: ... print(char) ... m a p s
9.8 生成器
9.8.1 生成器
生成器 是一个用于创建迭代器的简单而强大的工具,看起来是带yield
的函数,但是实际上创建了迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
一个创建生成器的示例如下:
def reverse(data): for index in range(len(data)-1, -1, -1): yield data[index]
>>> for char in reverse('golf'): # 使用时就像迭代器 ... print(char) ... f l o g
可以用生成器来完成的操作同样可以用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__() 和 __next__() 方法。
另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。
除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。
9.8.2 生成器表达式
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
示例:
>>> sum(i*i for i in range(10)) # sum of squares 285 >>> xvec = [10, 20, 30] >>> yvec = [7, 5, 3] >>> sum(x*y for x,y in zip(xvec, yvec)) # dot product 260 >>> unique_words = set(word for line in page for word in line.split()) >>> valedictorian = max((student.gpa, student.name) for student in graduates) >>> data = 'golf' >>> list(data[i] for i in range(len(data)-1, -1, -1)) ['f', 'l', 'o', 'g']
9.8.3 yield 作为协程
yield
另外一个小众的使用场景,是变相实现协程的效果,即在同一个线程内,实现不同任务交替执行:
def mytask1(): print('task1 开始执行') ''' task code ''' yield def mytask2(): print('task2 开始执行') ''' task code ''' yield gene1=mytask1() gene2=mytask2() # 实现mytask1 mytask2 交替执行 for i in range(100): next(gene1) next(gene2)