logo

Python Class & OOP

王哲峰 / 2023-01-09


目录

Python OOP

为何使用类?

程序就是“用一些东西来做事”, 简而言之, 类就是一种定义新种类的东西的方式, 它反映了在程序领域中的真实对象

类是 Python 的程序组成单元, 就像函数和模块一样: 类是封装逻辑和数据的另一种方式. 实际上类也定义了新的命名空间, 在很大程序上就像模块. 但是, 类有三个重要的独到之处, 使其在建立对象时更为有用:

类属性继承搜索

Python 中大多数 OOP 的故事, 都可以简化成这个表达式:

在 Python 对象模型中, 类和通过类产生的实例是两种不同的对象类型:

实例从它的类继承属性, 而类是从搜索树中所有比它更上层的类中继承属性:

读取属性只是简单地搜索“树”而已, 称这种搜索程序为继承, 因为树中位置较低的对象继承了树中位置较高的对象拥有的属性. 从下至上进行搜索时, 连接至树中的对象就是树中所有上层对象所定义的所有属性的集合体, 直到树的最顶端.

类和实例

类方法调用

每当我们调用附属于类的函数时, 总会隐含着这个类的实例. 这个隐含的主体或环境就是称之为面向对象模型的一部分原因: 当运算执行时, 总是有个主体对象.

编写类树

类树:

class C2: ...
class C3: ...
class C1(C2, C3): ...

I1 = C1()
I2 = C1()

属性:

方法:

构造函数:

OOP 是为了代码重用

类产生多个实例对象

类对象、实例对象:

类对象提供默认行为

实例对象是具体元素

类通过继承进行定制

运算符重载

运算符重载只是意味着在类方法中拦截内置的操作, 当类的实例出现内置操作中, Python 自动调用方法, 并且方法的返回值变成了相应操作的结果.

运算符重载就是让用类写成的对象, 可以截获并响应用在内置类型上的运算: 加法、切片、打印和点号运算.

构造函数和表达式

示例 1:

# number.py
class Number:

    def __init__(self, start):
        self.data = start
    
    def __sub__(self, other):
        return Number(self.data - other)

>>> from number import Number
>>> X = Number(5)
>>> Y = X - 2
>>> Y.data

示例 2: 构造函数参数使用方法

class Person_v1(object):

    def __init__(self, name, gender, **kw):
        self.name = name
        self.gender = gender
        for key, value in kw.items():
            setattr(self, key, value)


class Person_v2(object):

    def __init__(self, name, gender, **kw):
        self.name = name
        self.gender = gender
        self.__dict__.update(kw)

p1 = Person_v1("wangzf", "male", age = 18, course = "Python")
p2 = Person_v2("wangzf", "male", age = 18, course = "Python")

print(p1.age)
print(p1.course)

print(p2.age)
print(p2.course)

常见的运算符重载方法

在类中, 对内置对象所能做的事, 几乎都有相应的特殊名称的重载方法:

所有重载方法的名称前后都有两个下划线字符, 以便把同类中定义的变量名区别开来. 特殊方法名称和表达式或运算的映射关系, 是由 Python 语言预先定义好的(在标准语言手册中有说明).

运算符重载方法也都是可选的, 如果没有编写或继承一个方法, 类直接不支持这些运算, 并且试图使用它们会引发一个异常.

索引和分片

class Indexer:
    
    def __getitem__(self, index):
        return index ** 2

>>> X = Indexer()
>>> X[2]
>>> for i in range(5):
>>>     print(X[i], end = " ")

索引迭代

迭代器对象

尽管 getitem 技术有效, 但它真的只是迭代的一种退而求其次的方法. 如今, Python 中的所有的迭代环境都会先尝试 iter 方法, 再尝试 getitem. 也就是说, 它们宁愿使用迭代协议, 然后才是重复对对象进行索引运算. 只有在对象不支持迭代协议 的时候, 才会尝试索引运算. 一般来讲, 你也应该优先使用 iter, 它能够比 getitem 更好地支持一般的迭代环境

从技术角度来讲, 迭代环境是通过调用内置的 iter 去尝试寻找 iter 方法来实现的, 而这种方法 应该返回一个迭代器对象. 如果已经提供了, Python 就会重复调用这个迭代器对象的 next 方法, 直到发生 StopIteration 异常. 如果没有找到这类 iter 方法, Python 会改用 getitem 机制, 就像之前说的那样 通过偏移量重复索引, 直到引发 IndexError 异常(对于手动迭代来说, 一个 next 内置函数也可以很方便地使用: next(I) 与 I.next() 是相同的).

用户定义的迭代器

iter 机制中, 类就是通过实现迭代器协议来实现用户定义的迭代器的.

# iters.py file

class Squares:

    def __init__(self, start, stop):
        self.value = start - 1
        self.stop = stop

    def __iter__(self):
        return self 
    
    def __next__(self):
        if self.value == self.stop:
            raise StopIteration
        self.value += 1
        return self.value ** 2

>>> from iters import Squares
>>> for i in Squares(1, 5):
>>>     print(i, end = " ")

>>> X = Squares(1, 5) # iterate manually: what loops do
>>> I = iter(X)       # iter calls __iter__
>>> next(I)           # next calls __next__
>>> next(I)
>>> next(I)

有多个迭代器的对象

成员关系

属性引用

返回字符串表达形式

右侧加法和原处加法

Call 表达式

比较

布尔测试

类可能也定义了赋予其实例布尔特性的方法. 在布尔环境中, Python 首先尝试 __bool__ 来获取一个直接的布尔值, 然后, 如果没有该方法, 就尝试 __len__ 类根据对象的长度确定一个真值. 通常首先使用对象状态或其他信息来生成 一个布尔结果.

示例 1:

# class 1
class Truth:

    def __bool__(self):
        return True

>>> X = Truth()
>>> if X: 
>>>     print("yes!")

# class 2
class Truth:

    def __bool__(self):
        return False

>>> X = Truth()
>>> bool(X)

示例 2:


class Truth:
    def __len__(self):
        return 0

>>> X = Truth()
>>> if not X:
>>>    print("no!")

示例 3: 如果两个方法都有, Python 喜欢 bool 胜过 len, 因为它更具体

class Truth:

    def __bool__(self):
        return True
    
    def __len__(self):
        return 0

>>> X = Truth()
>>> if X:
>>>     print("yes!")

示例 4: 如果没有定义真的方法, 对象毫无疑义地看作真

class Truth:
    pass

>>> X = Truth()
>>> bool(X)

对象析构函数

每当实例产生时, 就会调用 __init__ 构造函数. 每当实例空间被回收时(在垃圾收集时), 它的对立面 __del__, 也就是 析构函数(destructor method), 就会自动执行.

示例:

class Life:

    def __init__(self, name = "unknown"):
        print("Hello", name)
        self.name = name
    
    def __del__(self):
        print("Goodbye", self.name)
    
brian = Life("Brian")
brian = "loretta"

类与字典的关系

类产生的基本继承模型其实非常简单: 所涉及的就是在连续的对象树中搜索属性, 实际上, 建立的类中可以什么东西都没有(空的命名空间对象).

class rec:
    pass

基于字典的记录的示例:

rec = {}
rec["name"] = "mel"
rec["age"] = 45
rec["job"] = "trainer/writer"
print(rec["name"])

基于类的记录的示例:

class rec:
    pass

rec.name = "mel"
rec.age = 45
rec.job = "trainer/writer"
print(rec["name"])

实例都有一个不同的属性字典:

class rec:
    pass

pers1 = rec()
pers1.name = "rel"
pers1.job = "trainer"
pers1.age = 40

pers2 = rec()
pers2.name = "vls"
pers2.job = "developer"

print(pers1.name)
print(pers2.name)

完整的类实现记录及其处理:

class Person:
    def __init__(self, name, job):
        self.name = name
        self.job = job
    
    def info(self):
        return (self.name, self.job)

rec1 = Person("mel", "trainer")
rec2 = Person("vls", "developer")

print(rec1.job)
print(rec2.info())

实例

在这里, 我们将编写两个类:

在这个过程中, 将创建两个类的实例并测试它们的功能. 完成实例之后, 将给出实用类的一个漂亮的例子, 把实例存储到一个 shelve 的面上对象数据库中, 使它们持久化. 通过这种方式, 可以把这些代码用作模板, 从而发展为完全用 Python 编写的一个完备的个人数据库.

最后, 这里创建的类在代码量上相对较小, 但是他们将演示 Python 的 OOP 模型的所有主要思想. 不管其语法细节如何, Python 的类系统实际上很大程度上就是在一堆对象中查找属性, 并为函数给定一个特殊的第一个参数.

步骤 1: 创建实例

# person.py
class Person:
    pass

编写构造函数

# Add record field initialization
class Person:

    def __init__(self, name, job, pay):
        self.name = name
        self.job = job
        self.pay = pay

参数名 namejobpay 出现了两次:

class Person:

    def __init__(self, name, job = None, pay = 0):
        self.name = name
        self.job = job
        self.pay = pay

以两种方法使用代码

# person.py

# class
class Person:

    def __init__(self, name, job = None, pay = 0):
        self.name = name
        self.job = job
        self.pay = pay

# 进行中测试代码, __name__ 检查模块
if __name__ == "__main__":
    # self-test code
    bob = Person("Bob Smith")
    sue = Person("Sue Jones", job = "dev", pay = 100000)
    print(bob.name, bob.pay)
    print(sue.name, sue.pay)

步骤 2: 添加行为方法

# person.py
    
# class
class Person:

    def __init__(self, name, job = None, pay = 0):
        self.name = name
        self.job = job
        self.pay = pay


if __name__ == "__main__":
    bob = Person("Bob Smith")
    sue = Person(name = "Sue Jones", job = "dev", pay = 100000)
    print(bob.name, bob.pay)
    print(sue.name, sue.pay)
    print(bob.name.split()[-1])
    sue.pay *= 1.10
    print(sue.pay)

编写方法

# person.py
    
# class
class Person:

    def __init__(self, name, job = None, pay = 0):
        self.name = name
        self.job = job
        self.pay = pay
    
    def lastName(self):
        return self.name.split()[-1]
    
    def giveRaise(self, percent):
        self.pay = int(self.pay * (1 + percent))


if __name__ == "__main__":
    bob = Person("Bob Smith")
    sue = Person(name = "Sue Jones", job = "dev", pay = 100000)
    print(bob.name, bob.pay)
    print(sue.name, sue.pay)
    print(bob.lastName(), sue.lastName())
    sue.giveRaise(.10)
    print(sue.pay)

步骤 3: 运算符重载

运算符重载, 在一个类中编写这样的方法, 当方法在类的实例上运行的时候, 方法截获并处理内置的操作.

常见运算符重载方法:

提供打印显示

__str__ 方法的原理是, 每次一个实例转换为其打印字符的串的时候, __str__ 都会自动运行. 由于这就是打印一个对象所会做的事情, 所以直接的效果就是, 打印一个对象会显示对象的 __str__ 方法所返回的内容, 要么自己定义一个方法, 要么从一个超类继承一个该方法.

# person.py
    
# class
class Person:

    def __init__(self, name, job = None, pay = 0):
        self.name = name
        self.job = job
        self.pay = pay
    
    def lastName(self):
        return self.name.split()[-1]
    
    def giveRaise(self, percent):
        self.pay = int(self.pay * (1 + percent))
    
    def __str__(self):
        return "[Person: %s, %s]" % (self.name, self.pay)


if __name__ == "__main__":
    bob = Person("Bob Smith")
    sue = Person(name = "Sue Jones", job = "dev", pay = 100000)
    print(bob)
    print(sue)
    print(bob.lastName(), sue.lastName())
    sue.giveRaise(.10)
    print(sue)

步骤 4: 通过子类定制行为

要展示 OOP 的真正的能力, 我们需要定义一个超类/子类关系, 以允许我们扩展软件并替代一些继承的行为. 毕竟, 这是 OOP 背后的 主要思想; 基于已经完成的工作的定制来促进一种编码模式, 可以显著地缩减开发时间.

编写子类

扩展方法

多态的作用

继承、定制和扩展

OOP: 大思路

步骤 5: 定制构造函数

调用重定义的超类构造函数, 在 Python 中是一种很常见的编码模式.

在构造的时候, Python 自身使用继承来查找并调用唯一的一个 __init__ 方法, 也就是类树中最低的一个. 如果需要在构造的时候运行更高的 __init__ 方法, 必须通过超类的名称调用它们.

这种方法的积极之处在于, 你可以明确指出哪个参数传递给超类的构造函数, 并且可以选择根本就不调用它: 不调用超类的构造函数允许你整个替代其逻辑, 而不是扩展它.

# person.py

# class
class Person:

    def __init__(self, name, job = None, pay = 0):
        self.name = name
        self.job = job
        self.pay = pay
    
    def lastName(self):
        return self.name.split()[-1]
    
    def giveRaise(self, percent):
        self.pay = int(self.pay * (1 + percent))
    
    def __str__(self):
        return "[Person: %s, %s]" % (self.name, self.pay)

class Manager(Person):
    def __init__(self, name, pay):
        Person().__init__(self, name, "mgr", pay)
    
    def giveRaise(self, name, "mgr", pay):
        Person.giveRaise(self, percent + bonus)



if __name__ == "__main__":
    bob = Person("Bob Smith")
    sue = Person(name = "Sue Jones", job = "dev", pay = 100000)
    print(bob)
    print(sue)
    print(bob.lastName(), sue.lastName())
    sue.giveRaise(.10)
    print(sue)
    tom = Manager("Tom Jones", 50000)
    tom.giveRaise(.10)
    print(tom.lastName())
    print(tom)

OOP 比我们认为的要简单

在完整的形式中, 不管类的大小如何, 它捕获了 Python 的 OOP 机制中几乎所有重要的概念:

这些概念中的大多数都只是基于3个简单的思路:

通过这种方法, 我们可以使自己的代码在未来易于修改, 通过驾驭类的倾向以构造代码减少冗余.

大体上, 这就是 Python 中的 OOP 的全部.

组合类的其他方式

步骤 6: 使用内省工具

步骤 7: 把对象存储在数据库中

Python 对象持久化: 让对象在创建它们的程序退出后依然存在

Pickle 和 Shelve

对象持久化通过 3 个标准库模块来实现, 这三个模块在 Python 都可用:

在 shelve 数据库中存储对象

交互地探索 shelve

更新 shelve 中的对象

类的设计

class 语句

class 语句的一般形式

class Class_name(superclass, ...):
    data = value
    def method(self, ...):
        self.member = value

在 class 语句内, 任何赋值语句都会产生类属性, 而且还有特殊名称方法重载运算符

示例

示例 1

class ShareData:
    spam = 42

x = ShareData()
y = ShareData()
print(x.spam)
print(y.spam)
print(ShareData.spam)

# 通过类修改修改了实例和类的数据
ShareData.spam = 99
print(x.spam, y.spam, ShareData.spam)

# 通过实例修改只能修改实例本身的数据
x.spam = 88
print(x.spam, y.spam, ShareData.spam)

示例 2

class MixedNames:
    data = "spam"

    def __init__(self, value):
        self.data = value
    
    def display(self):
        print(self.data, MixedNames.data)

x = MixedNames(1)
y = MixedNames(2)
x.display()
y.display()

方法

方法

instance.method(args, ...)

class.method(instance, args, ...)

self 参数

示例

class NextClass:
    def printer(self, text):
        self.message = text
        print(self.message)

x = NextClass()
x.printer("instance call")
x.message

调用超类构造函数

由于所有属性 init 方法是由继承进行查找的, 在构造时, Python 会找出并且只调用一个 init. 如果要保证子类的构造函数也会执行超类构造时的逻辑, 一般都必须通过类明确地调用超类的 init 方法.

这种通过类调用方法的模式, 是扩展继承方法行为(而不是完全取代)的一般基础.

class Super:
    def __init__(self, x):
        ...default code...
    
class Sub(Super):
    def __init__(self, x, y):
        Super.__init__(self, x):
            ..custom code...

I = Sub(1, 2)

这是代码有可能直接调用运算符重载方法的环境之一. 如果真的想运行超类的构造方法, 自然只能用这种方式进行调用: 没有这样的调用, 子类会完全取代超类的构造函数.

其他方法调用的可能性

继承

属性树的构造

继承方法的专有性

继承树搜索模式变成了将系统专有化的最好方式, 因为继承会先在子类寻找变量名, 然后才查找超类, 子类就可以对超类的属性重新定义来取代默认的行为. 实际上, 可以把整个系统做成类的层次, 再新增 外部的子类来对其进行扩展, 而不是在原处修改已经存在的逻辑.

重新定义继承变量名的概念引出了各种专有化技术:

示例:

class Super:

    def method(self):
        print("in Super.method")

class Sub(Super):
    
    def method(self):                   # override method
        print("starting Sub.method")    # add actions
        Super.method(self)              # run default action
        print("ending Sub.method")

>>> x = Super()
>>> x.method()

# in Super.method

>>> x = Sub()
>>> x.method()
# starting Sub.method
# in Super.method
# ending Sub.method

直接调用超类方法是重点.

类接口技术

扩展只是一种与超类接口的方式

clas Super(object):

    def method(self):
        print("in Super.method")
    
    def delegate(self):
        self.action()
    
class Inheritor(Super):
    pass

class Replacer(Super):

    def method(self):
        print("in Replacer.method")

class Extender(Super):

    def method(self):
        print("starting Extender.method")
        Super.method(self)
        print("ending Extender.method")

class Provider(Super):
    def action(self):
        print("in Provider.action")
    
if __name__ == "__main__":
    for klass in (Inheritor, Replacer, Extender):
        print("\n" + klass.__name__ + "...")
        klass().method()
        print("\nProvider...")
        x = Provider()
        x.delegate()

抽象超类

抽象超类: 类的部分行为默认是由其子类所提供的, 如果预期的方法没有在子类中定义, 当继承搜索失败时, Python 会引发未定义变量名的异常.

Python3 抽象超类: 在一个 class 头部使用一个关键字参数, 以及特殊的 @ 装饰器语法.

类的编写者偶尔会使用 assert 语句, 使这种子类需求更加明显, 或者引发内置的异常 NotImplementedError:

class Super:
    
    def delegate(self):
        self.action()
    
    def action(self):
        assert False, "action must be defined!"

X = Super()
X.delegate()
class Super:
    
    def delegate(self):
        self.action()
    
    def action(self):
        raise NotImplementedError("action must be defined!")

对于子类的实例, 将得到异常, 除非子类提供了期待的方法来替代超类中的默认方法:

    class Super:
            
        def delegate(self):
            self.action()
        
        def action(self):
            raise NotImplementedError("action must be defined!")

    class Sub(Super):
        pass
    
    X = Sub()
    X.delegate()
class Super:
        
    def delegate(self):
        self.action()
    
    def action(self):
        raise NotImplementedError("action must be defined!")

class Sub(Super):
    def action(self):
        print("spam")

X = Sub()
X.delegate()

Python3 抽象超类

from abc import ABCMeta, abstractmethod

class Super(metaclass = ABCMeta):

    @abstractmethod
    def method(self, ...):
        pass
from abc import ABCMeta, abstractmethod

# -------------------------
# 不能产生一个实例, 除非在类树的较低层级定义了该方法
# -------------------------
class Super(metaclass = ABCMeta):

    def delegate(self):
        self.action()
    
    @abstractmethod
    def action(self):
        pass
    
X = Super()
# -------------------------
# class 2
# -------------------------
class Super(metaclass = ABCMeta):

    def delegate(self):
        self.action()
    
    @abstractmethod
    def action(self):
        pass

class Sub(Super):
    pass

X = Sub()
# -------------------------
# class 3
# -------------------------
class Super(metaclass = ABCMeta):

    def delegate(self):
        self.action()
    
    @abstractmethod
    def action(self):
        pass

class Sub(Super):
    def action(self):
        print("spam")

X = Sub()
X.delegate()

命名空间: 完整的内容总结

这里将用于解析变量名的所有规则进行总结, 首先要记住的是, 点号和无点号的变量名会用不同的方式处理, 而有些作用域是用于对对象命名空间做初始设定的:

简单变量名: 如果赋值就不是全局变量

无点号的简单变量名遵循函数的 LEGB 作用域法则, 具体如下:

属性名称: 对象命名空间

赋值将变量名分类

命名空间字典

命名空间链接

类的文档字符串

示例:

# docstr.py file 

"""I am: docstr.__doc__"""

def func(args):
    """I am: docstr.func.__doc__"""
    pass

class spam:
    """I am: spam.__doc__ or docstr.spam.__doc__"""
    def method(self, arg):
        """I am: spam.method.__doc__ or self.method.__doc__"""
        pass
import docstr

docstr.__doc__
docstr.func.__doc__
docstr.spam.__doc_
docstr.spam.method.__doc__

help(docstr)

类的设计

OOP 的设计问题, 就是如何使用类来对有用的对象进行建模!

Python 和 OOP

Python 的 OOP 实现可以概括为三个概念:

OOP 和 继承: “是一个”关系

OOP 和组合: “有一个”关系

OOP 和委托: “包装”对象

类的高级主题

与设计相关的其他话题

其他实例

class FirstClass:
    
    def setdata(self, value):
        self.data = value
    
    def display(self):
        print(self.data)

x = FirstClass()
y = FirstClass()

x.setdata("King Arthur")
y.setdata(3.14159)

x.display()
y.display()

x.data = "New value"
x.display()

x.anothername = "spam"

# ------------------------------

class SecondClass(FirstClass):

    def display(self):
        print("Current value = %s" % self.data)

z = SecondClass()
z.setdata(42)
z.display()

x.display()

# ------------------------------

class ThirdClass(SecondClass):

    def __init__(self, value):
        self.data = value
    
    def __add__(self, other):
        return ThirdClass(self.data + other)

    def __str__(self):
        return '[ThirdClass: %s]' % self.datas
    
    def mul(self, other):
        self.data = other
    
a = ThirdClass('abc')
a.display()
print(a)

b = a + "xyz"
b.display()
print(b)

a.mul(3)
print(a)