Python的面向对象编程介绍,在本文中,我们将深入探讨 Python 中的面向对象编程 (OOP)。我们不会深入探讨 OOP 的理论方面。这里的主要目标是演示我们如何在 Python 中使用面向对象的范例。据Statista称,Python是开发人员中第四大最常用的编程语言。这是为什么?好吧,有人说这是因为 Python 的简化语法;其他人说这是因为 Python 的多功能性。不管是什么原因,如果我们想研究一门流行的编程语言,Python应该是我们的选择之一。
面向对象的基础
让我们从面向对象编程的温和总结开始。面向对象编程是一种编程范式——一组为必须如何做事设定标准的思想。
OOP 背后的思想是使用对象对系统进行建模。对象是我们感兴趣的系统的组成部分,它通常具有特定的目的和行为。每个对象都包含方法和数据。方法是对数据执行操作的过程。方法可能需要一些参数作为参数。
Java、C++、C#、Go 和 Swift 都是面向对象编程语言的例子。当然,所有这些语言中 OOP 原则的实现都是不同的。每种语言都有其语法,在本文中,我们将了解 Python 如何实现面向对象的范例。
要从总体上了解有关 OOP 的更多信息,值得阅读来自 MDN 的这篇文章,或关于为什么 OOP 如此广泛的有趣讨论。
类和对象
OOP 的第一个重要概念是对象的定义。假设您有两只狗,分别叫做 Max 和 Pax。他们有什么共同点?他们是狗,他们代表狗的想法。即使它们的品种或颜色不同,它们仍然是狗。在此示例中,我们可以将 Max 和 Pax 建模为对象,或者换句话说,建模为狗的实例。
但是等等,什么是狗?我如何模拟狗的想法?使用类。
正如我们在上图中看到的,类是定义数据和行为的模板。然后,从类提供的模板开始,我们创建对象。对象是类的实例。
让我们看一下这段 Python 代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Dog(): def __init__(self, name, breed): self.name = name self.breed = breed def __repr__(self): return f"Dog(name={self.name}, breed={self.breed})" max = Dog("Max", "Golden Retriever") pax = Dog("Pax", "Labrador") print(max) print(pax) |
在第 1 行,我们使用名称声明了一个新类Dog
。然后我们遇到一个叫做__init__
. 每个 Python 类都有这个,因为它是默认构造函数。该方法用于初始化对象的状态,因此它为新创建的对象的变量赋值。作为构造函数的参数,我们有name
、 thebreed
和一个名为 的特殊关键字self
。这是该方法的第一个参数并非巧合。
在类代码内部,self
关键字代表类的当前实例。这意味着每次我们想要访问属于该类的一个实例(max
或者pax
是两个不同的实例)的某个方法或变量时,我们必须使用self
关键字。如果现在还不完全清楚,请不要担心;它将在下一节中介绍。
查看__init__
方法的第一行 — self.name = name
。换句话说,这对 Python 解释器说:“好的,我们正在创建的这个对象将有一个名称 ( self.name
),并且这个名称在name
参数中”。同样的事情也发生在breed
论证上。好的,所以我们可以在这里停下来。这是用于定义类的基本蓝图。在跳转到执行此代码段之前,让我们看一下在__init__
.
第二种方法称为__repr__
. 在 Python 中,该__repr__
方法将类对象表示为字符串。通常,如果我们不显式定义它,Python 会以自己的方式实现它,现在我们将看到不同之处。默认情况下,如果我们没有显式定义一个__repr__
方法,当调用函数print()
or时str()
,Python 会返回对象的内存指针。不太适合人类阅读。相反,如果我们定义一个自定义__repr__
方法,我们将以字符串方式拥有一个漂亮的对象版本,它也可以用于再次构造对象。
让我们对上面的代码进行更改:
1 2 3 4 5 6 7 8 9 10 11 |
class Dog: def __init__(self, name, breed): self.name = name self.breed = breed max = Dog("Max", "Golden Retriever") pax = Dog("Max", "Golden Retriever") # Default (internal) implementation of __repr__ print(max) print(pax) print(max == pax) |
如果我们保存并运行这段代码,这就是我们得到的:
1 2 3 |
__main__.Dog object at 0x0000026BD792CF08> __main__.Dog object at 0x0000026BD792CFC8> False |
等等,如果它们具有相同的名字和相同的品种,它们怎么可能不是两只平等的狗呢?让我们使用之前制作的图表将其可视化。
首先,当我们执行时print(max)
,Python 会看到没有自定义的__repr__
方法定义,它会使用该__repr__
方法的默认实现。max
和两个对象pax
是两个不同的对象。是的,它们具有相同的名称和相同的品种,但它们是 class 的不同实例Dog
。事实上,它们指向不同的内存位置,正如我们从输出的前两行中看到的那样。这个事实对于理解对象和类之间的区别至关重要。
如果我们现在执行第一个代码示例,我们可以看到实现自定义__repr__
方法时输出的差异:
1 2 |
Dog(name=Max, breed=Golden Retriever) Dog(name=Pax, breed=Labrador) |
定义新方法
假设我们想要获取max
对象的名称。由于在这种情况下name
属性是公共的,我们可以简单地通过使用访问属性来获取它max.name
。但是如果我们想返回对象的昵称怎么办?
那么,在那种情况下,我们创建一个在类内部调用的方法get_nickname()
。然后,在类的定义之外,我们只需调用方法max.get_nickname()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Dog: def __init__(self, name, breed): self.name = name self.breed = breed def get_nickname(self): return f"{self.name}, the {self.breed}" def __repr__(self): return f"Dog(name={self.name}, breed={self.breed})" max = Dog("Max", "Golden Retriever") pax = Dog("Pax", "Labrador") print(max.name) print(max.get_nickname()) |
如果我们运行这个片段,我们会得到以下输出:
1 2 3 |
>python snippet.py Max Max, the Golden Retriever |
访问修饰符:公共、受保护和私有
现在让我们考虑访问修饰符。在 OOP 语言中,访问修饰符是用于设置类、方法或属性的可访问性的关键字。在 C++ 和 Java 中情况不同,访问修饰符是由语言定义的显式关键字。在 Python 中,没有这样的东西。Python 中的访问修饰符是一种约定,而不是对访问控制的保证。
让我们通过代码示例看一下这意味着什么:
1 2 3 4 5 6 7 8 |
class BankAccount: def __init__(self, number, openingDate): # public access self.number = number # protected access self._openingDate = openingDate # private access self.__deposit = 0 |
在这段代码中,我们创建了一个名为BankAccount
. 任何新BankAccount
对象都必须具有三个属性:一个数字、一个开放日期和一个设置为 0 的初始存款。请注意前面的单下划线 ( _
)openingDate
和前面的双下划线 ( __
) deposit
。
伟大的!按照Python的约定,单下划线作为protected
成员的前缀,双下划线作为成员的前缀private
。这在实践中意味着什么?让我们尝试在类定义下添加以下代码:
1 2 3 4 |
account = BankAccount("ABXX", "01/01/2022") print(account.number) print(account._openingDate) print(account.__deposit) |
如果我们尝试执行此代码,我们将得到如下内容:
1 2 3 4 5 6 7 |
> python snippet.py ABXX 01/01/2022 Traceback (most recent call last): File "snippet.py", line 14, in <module> print(account.__deposit) AttributeError: 'BankAccount' object has no attribute '__deposit' |
我们可以打印帐户number
,因为它是公共属性。我们可以打印openingDate
,即使根据惯例不建议这样做。我们无法打印deposit
.
在 deposit 属性的情况下,读取或修改其值的正确方法应该是通过get()
和set()
方法。让我们看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class BankAccount: def __init__(self, number, openingDate): self.number = number self._openingDate = openingDate self.__deposit = 0 def getDeposit(self): return self.__deposit def setDeposit(self, deposit): self.__deposit = deposit return True account = BankAccount("ABXX", "01/01/2022") print(account.getDeposit()) print(account.setDeposit(100)) print(account.getDeposit()) |
在上面的代码中,我们定义了两个新方法。第一个叫getDeposit
,第二个叫setDeposit
。顾名思义,它们用于获取或设置存款。OOP 中的约定是为所有需要读取或修改的属性创建get和set方法。因此,我们不是直接从类外部访问它们,而是实现方法来做到这一点。
我们很容易猜到,执行这段代码会得到以下输出:
1 2 3 4 |
> python snippet.py 0 True 100 |
继承
不要重复自己。面向对象编程鼓励 DRY 原则,而继承是用于执行 DRY 原则的策略之一。在本节中,我们将了解继承在 Python 中的工作原理。请注意,我们将使用术语parent class和child class。其他别名可能包括父类的基类和子类的派生类。由于继承定义了类的层次结构,因此区分父类和所有子类非常方便。
好的,让我们从一个例子开始。假设我们要模拟一个教室。一个教室是由一位教授和一些学生组成的。他们有什么共同点?他们都有什么关系?好吧,他们当然都是人类。因此,它们共享一定数量的特征。这里为了简单起见,我们将一个类定义Person
为具有两个私有属性,name 和 surname。此类还包含get()
和set()
方法。
下图显示了一个父类和两个子类。
正如我们所见,在Student
和Professor
类中我们都为Person
该类定义了所有方法和属性,因为它们是从Person
. 此外,还有其他以粗体突出显示的特定于子类的属性和方法。
下面是这个例子的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Person: def __init__(self, name, surname): self.__name = name self.__surname = surname def getName(self): return self.__name def getSurname(self): return self.__surname def setName(self, newName): self.__name = newName def setSurname(self, newSurname): self.__surname = newSurname |
然后,我们有两个实体要建模, theStudent
和 the Professor
。没有必要在Person
类 for Student
and Professor
also 中定义我们上面定义的所有东西。Python 允许我们让类Student
和Professor
类从类(父类)继承一堆特性。Person
我们可以这样做:
1 2 3 4 5 6 7 8 9 10 |
class Student(Person): def __init__(self, name, surname, grade): super().__init__(name, surname) self.__grade = grade def getGrade(self): return self.__grade def setGrade(self, newGrade): self.__grade = newGrade |
在第一行中,我们使用通常的class Student()
语法定义了一个类,但在我们放置的括号内Person
。这告诉 Python 解释器这是一个名为的新类Student
,它从名为 的父类继承了属性和方法Person
。为了稍微区分这个类,有一个额外的属性叫做grade
. 此属性表示学生就读的年级。
同样的事情发生在Professor
上:
1 2 3 4 5 6 7 8 9 10 |
class Professor(Person): def __init__(self, name, surname, teachings): super().__init__(name,surname) self.__teachings = teachings def getTeachings(self): return self.__teachings def setTeachings(self, newTeachings): self.__teachings = newTeachings |
有一个我们以前从未见过的新元素。在上面代码片段的第 3 行,有一个奇怪的函数叫做super().__init__(name,surname)
.
Python 中的super()
函数用于让子级访问父类的成员。在这种情况下,我们调用__init__
类的方法Person
。
多态性
上面介绍的例子展示了一个强大的想法。对象可以从其层次结构中的其他对象继承行为和数据。Student
和Professor
类都是该类的子类Person
。多态的思想,顾名思义,就是让对象有多种形状。多态性是 OOP 语言中使用的一种模式,其中类在共享相同接口的同时具有不同的功能。
说到上面的例子,如果我们说一个Person
对象可以有很多形状,我们的意思是它可以是一个Student
, aProfessor
或者我们创建的任何类作为 的子类Person
。
让我们看看关于多态性的其他一些有趣的事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Vehicle: def __init__(self, brand, color): self.brand = brand self.color = color def __repr__(self): return f"{self.__class__.__name__}(brand={self.brand}, color={self.color})" class Car(Vehicle): pass tractor = Vehicle("John Deere", "green") red_ferrari = Car("Ferrari", "red") print(tractor) print(red_ferrari) |
那么,让我们来看看吧。我们定义一个类Vehicle
。然后,我们创建另一个类Car
作为 的子类Vehicle
。这里没有什么新鲜事。tractor
为了测试这段代码,我们创建了两个不同的对象,并将它们存储在两个名为和的独立变量中red_ferrari
。注意这里类Car
里面什么都没有。它只是被定义为一个不同的类,但到目前为止它的行为与其父类没有什么不同。暂时不要理会__repr__
方法中的内容,因为我们稍后会回来讨论它。
你能猜出这段代码的输出吗?那么,输出如下:
1 2 |
Vehicle(brand=John Deere, color=green) Car(brand=Ferrari, color=red) |
注意这里发生的魔法。该__repr__
方法在类内部定义Vehicle
。的任何实例都Car
将采用它,因为Car
是 的子类Vehicle
。但Car
没有定义__repr__
. 它和它的父母一样。
所以这里的问题是为什么行为不同。为什么印刷品显示两种不同的东西?
原因是,在运行时,Python 解释器识别出类red_ferrari
是Car
. self.__class__.__name__
将给出对象类的名称,在本例中是self
对象。但是请记住,我们这里有两个不同的对象,它们是从两个不同的类创建的。
如果我们想检查一个对象是否是某个类的实例,我们可以使用以下函数:
1 2 |
print(isinstance(tractor, Vehicle)) # Yes, tractor is a Vehicle object! print(isinstance(tractor, Car)) # No, tractor is only a Vehicle object. Not a Car object. |
在第一行,我们问以下问题:是tractor
类的实例Vehicle
吗?
在第二行,我们反问:是tractor
类的一个实例Car
吗?
方法重载
在 Python 中,就像在任何其他 OOP 语言中一样,我们可以用不同的方式调用相同的方法——例如,使用不同数量的参数。当我们想要设计默认行为但不想阻止用户自定义它时,这可能很有用。
让我们看一个例子:
1 2 3 4 5 6 7 8 9 10 |
class Overloading: def sayHello(self, i=1): for times in range(i): print("Nice to meet you!") a = Overloading() print("Running a.sayHello():") a.sayHello() print("Running a.sayHello(5):") a.sayHello(5) |
在这里,我们定义了一个名为sayHello
. 此方法只有一个参数,即i
. 默认情况下,i
值为 1。在上面的代码中,当我们a.sayHello
第一次调用而不传递任何参数时,i
将采用其默认值。第二次,我们改为传递 5 作为参数。这意味着i=5
。
那么预期的行为是什么?这是预期的输出:
1 2 3 4 5 6 7 8 9 |
> python snippet.py Running a.sayHello(): Nice to meet you! Running a.sayHello(5): Nice to meet you! Nice to meet you! Nice to meet you! Nice to meet you! Nice to meet you! |
第一次调用a.sayHello()
将只打印"Nice to meet you!"
一次消息。第二次调用a.sayHello()
将打印"Nice to meet you!"
五次。
方法覆盖
当我们在父类和子类中定义了一个具有相同名称的方法时,就会发生方法覆盖。在这种情况下,我们说孩子正在做方法覆盖。
基本上,它可以如下所示进行演示。下图显示了覆盖方法的子类。
中的sayHello()
方法Student
重写sayHello()
了父类的方法。
为了在实践中展示这个想法,我们可以稍微修改一下我们在本文开头介绍的代码片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Person: def __init__(self, name, surname): self.name = name self.surname = surname def sayHello(self): return ("Hello, my name is {} and I am a person".format(self.name)) class Student(Person): def __init__(self, name, surname, grade): super().__init__(name,surname) self.grade = grade def sayHello(self): return ("Hello, my name is {} and I am a student".format(self.name)) a = Person("john", "doe") b = Student("joseph", "doe", "8th") print(a.sayHello()) print(b.sayHello()) |
在这个例子中,我们有方法sayHello()
,它在两个类中都定义了。不过,的Student
实现方式sayHello()
有所不同,因为学生用另一种方式打招呼。这种方法很灵活,因为父级不仅公开了一个接口,而且公开了 的默认行为的一种形式sayHello
,同时仍然允许子级根据自己的需要对其进行修改。
如果我们运行上面的代码,这是我们得到的输出:
1 2 3 |
> python snippet.py Hello, my name is john and I am a person Hello, my name is joseph and I am a student |
结论
到目前为止,Python 中 OOP 的基础知识应该已经很清楚了。在本文中,我们了解了如何创建类以及如何实例化它们。我们解决了如何创建具有不同可见性标准的属性和方法。我们还发现了 OOP 语言的基本属性,如继承和多态性,以及最重要的是如何在 Python 中使用它们。