1.2.13 面向对象编程

优质
小牛编辑
132浏览
2023-12-01

面向对象编程

到现在为止,在我们编写的所有程序中,我们围绕着函数,也就是处理数据的语句块来设计我们的程序,这叫做面向过程的编程方式,还有一种组织你的程序的方式,是将数据和函数组合起来打包到称为对象的东西里面,这叫做面向对象编程技术。大多数情况下,你可以使用面向过程的编程方式,但当你编写大型程序或者有一些适用于这种方式更好的问题时,你可以使用面向对象的编程技术。

类和对象是面向对象编程的两个主要方面,一个创建一个新的类型,在这里对象是类的一个实例。一个比喻,你可以有int型变量,换句话说,存储整数的变量是int类的一个实例(对象)。

静态语言的程序员应该注意

注意,整数甚至被看作(int类的)对象。这不像在C++和(1.5版本以前的)Java语言中整数是原始的原生数据类型

关于类的更多细节,请看help(int)。

C#和Java程序员将发现这和装箱和拆封的概念相似。

对象可以使用属于对象的普通变量存储数据。属于一个对象或类的对象被称为字段。对象也可以通过使用属于类的函数有函数性。这样的函数被称为类的方法,这个术语是很重要的,因为它帮助我们区分函数和变量哪些是独立的,那些是属于一个类或对象的。总体而言,这些字段和方法可以被称为类的属性

字段有两种类型,它们可以属于类的每个实例/对象,或属于类本身。它们被分别称为实例变量类变量

要创建一个类使用class的关键字,类的字段和方法在一个缩进块中列出。

self

类的方法与普通的函数只有一个特别的不同点--他们必须有一个额外的第一个名字、必须被添加到参数列表的开始处,但你调用该方法时,不用给此参数的值,Python将提供它。这个特别的变量指向对象本身,按照惯例,它的名字是self

虽然,你可以给这个参数任何名字,强烈推荐你使用名称self --任何其他的名字肯定是不清楚的。使用标准的名字,有许多优势--你的程序的任何读者将立即认出它,如果你使用self,甚至专门的ide(集成开发环境)也可以帮助你。

C++/Java/C#程序员要注意

在Python中,self相当于C++中的指针this、Java和C#中的this引用。

你一定很想知道Python怎样给self赋值,为什么你不需要给它一个值。一个例子会使这个清楚。假设,你有一个称为MyClass的类和这个类的实例称为myobject。当你调用这个对象的方法myobject.method(arg1, arg2)时,Python将自动转换成MyClass.method(myobject, arg1, arg2)--这是关于self的所有特殊之处。

这也意味着,如果你有一个不带任何参数的方法,那么你还得有一个参数——self

** 类

最简单的类可能是如下面的示例所示(另存为simplestclass.py).

class Person:
    pass # 一个空块

p = Person()
print(p)

输出:

$ python3 simplestclass.py
<__main__.Person object at 0x019F85F0>

它是如何工作的:

我们使用的class语句和类的名称创建一个新的类,接下来是形成类的主体语句的一个缩进块。在这里,我们使用pass语句表示这是一个空的块。

接下来,我们使用类名后跟一对圆括号创建这个类的一个对象/实例(在接下来的部分,我们将学习更多关于实例化的知识)。为了验证,我们通过简单地打印它确认变量的类型。它告诉我们,在__main__模块中有一个Person类的实例。

注意,你的对象存储在计算机内存的地址也被打印了。因为Python找到任何地址就存储对象,因而,在你的计算机上地址会有所不同。

对象的方法

我们已经讨论了类/对象除了有额外的self变量外,还可以有方法,就像函数。现在,我们将看到一个例子(另存为”的方法py”)。

例子(保存为 oop_method.py):

class Person:
    def say_hi(self):
        print('嗨,你好吗?')

p = Person()
p.say_hi()
# 上面这两行也可写成Person().say_hi()

输出:

$ python oop_method.py
嗨,你好吗?

它是如何工作的:

在这里我们看到self在起作用。注意, say_hi方法不包含任何参数,但在函数定义中仍有 self

__init__ 方法

在Python中有许多特别重要的方法名称,现在,我们看看__init__方法的重要性。

类的一个对象一被初始化, __init__方法就运行。这个方法对你的对象做任何初始化都是有用的。

例子 (保存为 oop_init.py):

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

    def say_hi(self):
        print('嗨,我的名字是', self.name)

p = Person('Swaroop')
p.say_hi()
# 以上两行也可以写成 Person('Swaroop').sayHi()

输出:

$ python class_init.py
嗨,我的名字是 Swaroop

它是如何工作的:

在这里,我们定义一个带参数name(和通常的 self)的__init__方法。在这里,我们只是创建一个新的称作name的字段。注意,尽管它们都叫 name,但它们是两个不同的变量。因为self.name中的点符号意味着"self"对象的一部分有个叫"name" 的东西,而另一个name是一个局部变量,因此没有问题。因为我们明确地表明我们所指的是哪个的名字,没有混乱。

最重要的是。请注意。我们没有显式地调用 __init__ 方法,而是当创建类的一个实例时,通过在类名称后的括号内传递参数,这是该方法的特殊意义。

现在,我们可以在我们的方法中使用self.name字段了,在say_hi方法中已经做了演示。

类和对象的变量

我们已经讨论了类与对象的部分功能(即方法),现在让我们了解一下数据部分。数据部分,即字段,只不过是被绑定到对象和类的命名空间名字的普通变量。这意味着,这些名字只有在类和对象的环境内有效。这就是为什么他们被叫做命名空间的原因。

有两种类型的字段--类变量和对象变量,它们的分类取决于类和对象分别属于哪种变量。

类变量是共享的——他们可以被该类的所有实例访问。类变量只是一个拷贝,当任何一个对象改变一个类变量时,所有的其它实例都将改变。

对象变量是类的每个对象或实例所特有的。既然这样,每个对象都有自己的字段拷贝,也就是说,在不同的实例中,它们不共享,同名的字段没有任何联系。一个例子能使你容易理解(保存为oop_objvar.py):

class Robot:
    """表示人一机器人,有一个名字。"""

    # 一个类变量,数机器人的数量
    population = 0

    def __init__(self, name):
        """初始化数据。"""
        self.name = name
        print("(初始化 {})".format(self.name))

        # 当创建一个人时,机器人人口加1
        Robot.population += 1

    def __del__(self):
        """我将要死了。"""
        print("{0} 正在被毁!".format(self.name))

        Robot.population -= 1

        if Robot.population == 0:
            print("{}是最后一个。".format(self.name))
        else:
            print("还有{:d}机器人在工作。".format(Robot.population))

    def say_hi(self):
        """机器人问候。

        是的,它们能做作那个。"""
        print("你好,我的主人叫我".format(self.name))

    @classmethod
    def how_many(cls):
        """打印当前人口。"""
        print("我们有{:d}个机器人。".format(cls.population))

droid1 = Robot('R2-D2')
droid1.say_hi()
Robot.how_many()

droid2 = Robot('C-3PO')
droid2.say_hi()
Robot.how_many()

print("\n机器人在这能做一些工作。\n")

print("机器人已经完成了它们的工作,因此,让我们销毁它们。")
droid1.die()
droid2.die()

Robot.how_many()

输出:

$ python objvar.py
(初始化 R2-D2)
你好,我的主人叫我
我们有1个机器人。
(初始化 C-3PO)
你好,我的主人叫我
我们有2个机器人。

机器人在这能做一些工作。

机器人已经完成了它们的工作,因此,让我们销毁它们。
R2-D2 正在被毁!
还有1机器人在工作。
C-3PO 正在被毁!
C-3PO是最后一个。
我们有0个机器人。

它是如何工作的:

这是一个很长的例子,但有助于展示类和对象变量的特性。在这里,population 属于Robot类,因此是一个类变量。name变量属于对象(使用self分配),因此是一个对象变量。

因此,我们提到population类变量使用Robot.population而不是self.population。我们在那个对象的中提到对象变量name使用self.name符号。记住对象和类变量的简单区别。还请注意,一个对象变量与一个类变量名字相同时,类变量将被隐藏!

除了使用Robot.population,我们还可以使用self.__class__.population访问类变量,因为每一个对象都可以通过self.__class__属性访问他的类。

how_many实际上是一个属于类而不是对象的方法,这意味着我们可以将其定义成 classmethodstaticmethod中的任何一个,这取决于我们是否需要知道是哪个类。因为,我们不需要这样的信息,我们主张staticmethod

我们使用修饰符将how_many方法标识为类方法。

我们可以把修饰符想象成为一个包装函数的快捷方式,所以使用@classmethod修饰符和下面的调用是一样的:

how_many = classmethod(how_many)

我们注意到__init__方法使用一个name变量初始化Robot实例。在这个方法中,因为还有一个机器人被添加,我们为population计数加1。还发现,self.name的值是针对每一个对象的,这表明对象变量的特性。

记住,你必须只有使用self引用同一对象的变量和方法,这就是所谓的属性引用

在这个程序中,我们也看到了类和方法的文档字符串的用法。在运行时我们可能通过使用Robot.__doc__访问类的文档字符串,使用 Robot.say_hi.__doc__ 访问方法的为文档字符串。

die方法中,我们简单的将Robot.population计数减1。

所有的类成员是公共的,一个例外是:如果你使用的数据成员的名字使用了双下划线前缀__privatevar, Python使用命名修饰来有效地使它成为一个私有变量。

因此,下面的惯例是,只在对象和类中使用的任何变量,首先应该以一个下划线开始,其他所有的名字都是公共的,且可以被用于其他的类/对象使用。记住,这只是一个惯例和不是被Python强制执行的(除了双下划线前缀)。

C++/Java/C#程序员要注意 在Python中,所有类成员(包括数据成员)是公共有和所有的方法是虚拟。

继承

面向对象编程的一个好处是代码的重用,一种方式是通过继承机制实现,继承可以被想像为实现类之间的一种类型和子类型的关系。

假设您想编写一个大学里教师和学生记录的程序,他们有一些共同的特性,如姓名、年龄和地址。他们也有特定的特性,如老师的工资、课程和树叶和学生的学费、分数。

您可以为每个类型创建两个独立的类,并且处理它们,但要添加一个新的共同特征意味着要在这两种独立的类中都要添加,很快就会变得难以处理。

一个更好的方法是创建一个共同的类称为SchoolMember,然后从这个类继承老师类和学生类,也就是说它们成为这个类的子类,可以对这些子类添加特定的特征。

这种方式有很多优点,如果我们在SchoolMember中添加/更改任何功能,在子类中会自动反映出来。例如,您可以为学生和老师添加一个新的身份证字段,可能通过直接把它们添加到SchoolMember类中来实现。然而,子类中的变化不影响其他子类。另一个优点是,如果你引用SchoolMember类的一个老师或学生对象,在某些情况下如计算学校成员的数量时会很有用。这就是所谓的多态性,如果父类是预期的,子类在任何情况下可以被取代,即对象可以当做父类的一个实例。

还观察到,我们重用父类的代码,在不同的类中我们不需要重复,而在使用独立的类的情况下我们不得不重复。

在这种情况下,SchoolMember类被称为基类超类TeacherStudent类被称为派生类子类

现在,我们将看到作为程序的这个例子(保存为 oop_subclass.py):

class SchoolMember:
    '''代表任何学校成员。'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print("(初始化学校成员: {})".format(self.name))

    def tell(self):
        '''告诉我细节。'''
        print("Name:'{}' Age:'{}'".format(self.name, self.age), end=" ")

class Teacher(SchoolMember):
    '''代表老师。'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print("(初始化老师: {})".format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print("Salary: '{0:d}'".format(self.salary))

class Student(SchoolMember):
    '''代表学生。'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print("(初始化学生: {})".format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print("Marks: '{:d}'".format(self.marks))

t = Teacher("Mrs. Shrividya", 40, 30000)
s = Student("Swaroop", 25, 75)

# 打印一个空行
print() 

members = [t, s]
for member in members:
    # 为Teachers和Students工作
    member.tell()

输出:

$ python inherit.py
(初始化学校成员: Mrs. Shrividya)
(初始化老师: Mrs. Shrividya)
(初始化学校成员: Swaroop)
(初始化学生: Swaroop)

Name:"Mrs. Shrividya" Age:"40" Salary: "30000" Name:"Swaroop" Age:"25" Marks: "75"~

它是如何工作的:

使用继承,在类定义中,在类的名称后,我们在元组中指定基类名称,接下来,我们观察到使用self变量,显式地调用基类的__init__方法,这样我们可以初始化对象的基类部分。这是非常重要的,记住——Python不会自动调用基类的构造函数,您自己必须显式地调用它。

我们还观察到,我们可以在类名前加前缀调用基类的方法,然后和其它参数一道传递给 self变量值。

注意,当我们使用SchoolMember类的tell方法时,我们可以把TeacherStudent的实例作为SchoolMember的实例。

同时,观察到子类的tell方法的调用,不是SchoolMember类的tell方法。要理解这一点的一种方法是,Python 总是在实际的类型中开始寻找方法,如本例。如果它不能找到方法,它开始按在类定义中元组中指定的顺序一个接一个地查找属于它的基类的方法。

术语提示--如果在继承元组中不止列出一个类,那么它被称为多重继承

tell()方法中,end参数是用于来将换行变为在 print()调用结束后以空格开始。

小结

我们已经探讨了类和对象的各个方面以及与之关联的各种术语。我们也看到了面向对象编程的好处和缺陷。Python是高度面向对象,从长远看仔细理解这些概念仔细将对你很有帮助。

接下,我们将学习如何处理输入/输出和如何在Python中访问文件。


继续阅读输入/输出