Python基础-9 类

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,因为 boolint 的子类。 但是,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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容