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 中的约定是为所有需要读取或修改的属性创建getset方法。因此,我们不是直接从类外部访问它们,而是实现方法来做到这一点。

我们很容易猜到,执行这段代码会得到以下输出:

> python snippet.py
0
True
100

继承

不要重复自己。面向对象编程鼓励 DRY 原则,而继承是用于执行 DRY 原则的策略之一。在本节中,我们将了解继承在 Python 中的工作原理。请注意,我们将使用术语parent classchild class。其他别名可能包括父类的基类和子类的派生类。由于继承定义了类的层次结构,因此区分父类和所有子类非常方便。

好的,让我们从一个例子开始。假设我们要模拟一个教室。一个教室是由一位教授和一些学生组成的。他们有什么共同点?他们都有什么关系?好吧,他们当然都是人类。因此,它们共享一定数量的特征。这里为了简单起见,我们将一个类定义Person为具有两个私有属性,name 和 surname。此类还包含get()set()方法。

下图显示了一个父类和两个子类。

显示一个父类和两个子类的图像图

正如我们所见,在StudentProfessor类中我们都为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 Studentand Professoralso 中定义我们上面定义的所有东西。Python 允许我们让类StudentProfessor类从类(父类)继承一堆特性。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

多态性

上面介绍的例子展示了一个强大的想法。对象可以从其层次结构中的其他对象继承行为和数据。StudentProfessor类都是该类的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_ferrariCarself.__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 中使用它们。

Python的面向对象编程介绍 推荐阅读

Spring Boot教程10 – Spring Boot应用程序属性

如何使用Git Merge

优惠券