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 代码:
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__
方法,我们将以字符串方式拥有一个漂亮的对象版本,它也可以用于再次构造对象。
让我们对上面的代码进行更改:
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)
如果我们保存并运行这段代码,这就是我们得到的:
__main__.Dog object at 0x0000026BD792CF08> __main__.Dog object at 0x0000026BD792CFC8> False
等等,如果它们具有相同的名字和相同的品种,它们怎么可能不是两只平等的狗呢?让我们使用之前制作的图表将其可视化。
首先,当我们执行时print(max)
,Python 会看到没有自定义的__repr__
方法定义,它会使用该__repr__
方法的默认实现。max
和两个对象pax
是两个不同的对象。是的,它们具有相同的名称和相同的品种,但它们是 class 的不同实例Dog
。事实上,它们指向不同的内存位置,正如我们从输出的前两行中看到的那样。这个事实对于理解对象和类之间的区别至关重要。
如果我们现在执行第一个代码示例,我们可以看到实现自定义__repr__
方法时输出的差异:
Dog(name=Max, breed=Golden Retriever) Dog(name=Pax, breed=Labrador)
定义新方法
假设我们想要获取max
对象的名称。由于在这种情况下name
属性是公共的,我们可以简单地通过使用访问属性来获取它max.name
。但是如果我们想返回对象的昵称怎么办?
那么,在那种情况下,我们创建一个在类内部调用的方法get_nickname()
。然后,在类的定义之外,我们只需调用方法max.get_nickname()
:
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())
如果我们运行这个片段,我们会得到以下输出:
>python snippet.py Max Max, the Golden Retriever
访问修饰符:公共、受保护和私有
现在让我们考虑访问修饰符。在 OOP 语言中,访问修饰符是用于设置类、方法或属性的可访问性的关键字。在 C++ 和 Java 中情况不同,访问修饰符是由语言定义的显式关键字。在 Python 中,没有这样的东西。Python 中的访问修饰符是一种约定,而不是对访问控制的保证。
让我们通过代码示例看一下这意味着什么:
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
。这在实践中意味着什么?让我们尝试在类定义下添加以下代码:
account = BankAccount("ABXX", "01/01/2022") print(account.number) print(account._openingDate) print(account.__deposit)
如果我们尝试执行此代码,我们将得到如下内容:
> 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()
方法。让我们看一个例子:
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方法。因此,我们不是直接从类外部访问它们,而是实现方法来做到这一点。
我们很容易猜到,执行这段代码会得到以下输出:
> python snippet.py 0 True 100
继承
不要重复自己。面向对象编程鼓励 DRY 原则,而继承是用于执行 DRY 原则的策略之一。在本节中,我们将了解继承在 Python 中的工作原理。请注意,我们将使用术语parent class和child class。其他别名可能包括父类的基类和子类的派生类。由于继承定义了类的层次结构,因此区分父类和所有子类非常方便。
好的,让我们从一个例子开始。假设我们要模拟一个教室。一个教室是由一位教授和一些学生组成的。他们有什么共同点?他们都有什么关系?好吧,他们当然都是人类。因此,它们共享一定数量的特征。这里为了简单起见,我们将一个类定义Person
为具有两个私有属性,name 和 surname。此类还包含get()
和set()
方法。
下图显示了一个父类和两个子类。
正如我们所见,在Student
和Professor
类中我们都为Person
该类定义了所有方法和属性,因为它们是从Person
. 此外,还有其他以粗体突出显示的特定于子类的属性和方法。
下面是这个例子的代码:
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
我们可以这样做:
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
上:
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
。
让我们看看关于多态性的其他一些有趣的事情:
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__
方法中的内容,因为我们稍后会回来讨论它。
你能猜出这段代码的输出吗?那么,输出如下:
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
对象。但是请记住,我们这里有两个不同的对象,它们是从两个不同的类创建的。
如果我们想检查一个对象是否是某个类的实例,我们可以使用以下函数:
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 语言中一样,我们可以用不同的方式调用相同的方法——例如,使用不同数量的参数。当我们想要设计默认行为但不想阻止用户自定义它时,这可能很有用。
让我们看一个例子:
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
。
那么预期的行为是什么?这是预期的输出:
> 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()
了父类的方法。
为了在实践中展示这个想法,我们可以稍微修改一下我们在本文开头介绍的代码片段:
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
,同时仍然允许子级根据自己的需要对其进行修改。
如果我们运行上面的代码,这是我们得到的输出:
> 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 中使用它们。