Python 做面向对象编程所必需的基础知识
MultiMCU EDU 2023-06-02

背景介绍:
Python 支持三种形式的编程,分别是:“命令式”、“函数式”和“面向对象式”。
很多优秀的开源 Python 项目里都用到了“面向对象式”编程,本文 Sugar 就来说一说用 Python 做面向对象编程所必需的基础知识。

我们之前已经看过两种编程范例:命令式(使用语句,循环和函数作为子例程)和函数(使用纯函数,高阶函数和递归)。
另一个非常流行的范例是面向对象编程(OOP)。

使用创建对象,这实际上是 OOP 的焦点。
类描述对象是什么,就像是对象的蓝图、描述或定义。
可以使用相同的类来创建多个不同的对象。

使用关键字 class缩进来写类,例如:

   
		
class Cat: def __init__ (self, color, legs): self.color = color self.legs = legs felix = Cat("ginger", 4)rover = Cat("dog-coloured", 4)stumpy = Cat("brown", 3)

这个代码定义了一个名为 Cat 的类,它有两个属性:colorlegs
然后,使用这个类创建了 3 个单独的对象。

__init__

__init__ 方法是类里最重要的方法。
当用一个类名像使用函数一样创建对象时会调用这个函数。

类内所有的方法的第一个参数都必须是self(尽管并未明确传递),Python 会将 self 参数添加到列表中;调用方法时不需要包含它。在方法定义中,self 指的是调用该方法的对象实例。

类的实例具有属性,这些属性是与它们相关联的数据。
在上面的示例中,Cat 实例具有属性“颜色”和“腿”。可以通过在实例后面放置一个和属性名来访问它们。因此,在 __init__  方法中,self.attribute创建和设置实例属性,并用于给属生赋初始值
例:

   
		
class Cat: def __init__(self, color, legs): self.color = color self.legs = legs felix = Cat("ginger", 4)print(felix.color)

输出:

   
		
>>>ginger>>>

在上面的例子中,__init__方法接受两个参数并将它们分配给对象属性。该 __init__ 方法被称为类的构造函数

【填空题】在空白处填入关键字来创建一个类和这个类的构造函数,构造函数有一个形式参数,并且用这个形式参数给“name”属性赋值,最后用这个类创建一个对象。

   
		
______ Student: def ______(self, name); self______ = name test = Student("Bob"_

【答案】class , __init__ , .name , )

方法

可以通过自定义方法来给类加入功能。
记住:所有的方法必须self作为第一个形参。
跟访问属性的语法一样,类的对象依然用方式使用自定义方法。
例:

   
		
class Dog: def __init__(self, name, color): self.name = name self.color = color def bark(self): print("Woof!") fido = Dog("Fido", "brown")print(fido.name)fido.bark()

输出:

   
		
>>>FidoWoof!>>>

可以在类体内分配变量作为类的属性,类内分配的变量可以通过类的对象访问,也可以直接用类名访问
例:

   
		
class Dog: legs = 4 def __init__(self, name, color): self.name = name self.color = color fido = Dog("Fido", "brown")print(fido.legs)print(Dog.legs)

输出:

   
		
>>>44>>>

类内分配的变量被类的所有对象所共享。

【填空题】创建一个类并在类内创建一个 sayHi() 方法。

   
		
class Student_ def __init__(self, name): self.name = name ______sayHi(______): print("Hi from "+ _____.name) s1 = Student("Amy")s1.sayHi()

【答案】: , def , self , self

继承

继承为类之间共享功能提供了一个途径。
试想一下有下面几个类:猫、狗和兔子等。尽管它们有不同点(比如狗类定义了“汪汪”方法),仍然可以找到它们之间共同的地方(比如都有颜色和名字)。
这些共同点可以让它们都继承于“动物”类来实现。
让一个类继承于另一个类的方法是:在子类定义时加个括号并把父类放在括号里。
例:

   
		
class Animal:  def __init__(self, name, color): self.name = name self.color = colorclass Cat(Animal): def purr(self): print("Purr...")class Dog(Animal): def bark(self): print("Woof!") fido = Dog("Fido", "brown")print(fido.color)fido.bark()

输出:

   
		
>>>brownWoof!>>>

下面哪个选项正确的定义了继承于 Egg 类的 Spam 类

  • [ ] class Egg(Spam):

  • [ ] class (Spam)Egg:

  • [x] class Spam(Egg):

多态

从别的类继承来的类叫做子类
被子类继承的类叫做“父类”。
当子类内有与父类相同的属性或方法时,相同的部分被覆写。
例:

   
		
class Wolf:  def __init__(self, name, color): self.name = name self.color = color def bark(self): print("Grr...")class Dog(Wolf): def bark(self): print("Woof") husky = Dog("Max", "grey")husky.bark()

输出:

   
		
>>>Woof>>>

在这个例子里,Wolf 是父类,Dog 是子类。

【问】下面代码的输出结果是什么?

   
		
class A: def method(self): print(1)class B(A): def method(self): print(2) B().method()

【答】2

继承可传递

继承可以是间接的。一个类可以继承于另一个类,而另一个类可以是第三个类的子类。
例:

   
		
class A: def method(self): print("A method")class B(A): def another_method(self): print("B method")class C(B): def third_method(self): print("C method") c = C()c.method()c.another_method()c.third_method()

输出:

   
		
>>>A methodB methodC method>>>

注意:不能环形继承。

【问】下面的程序输出什么?

   
		
class A: def a(self): print(1)class B(A): def a(self): print(2)class C(B): def c(self): print(3) c = C()c.a()

【答】 2

子类内调用父类函数

super() 函数的使用。
例:

   
		
class A: def spam(self): print(1)class B(A): def spam(self): print(2) super().spam() B().spam()

输出:

   
		
>>>21 >>>

super().spam() 调用了父类的 spam() 函数。

魔法方法

魔法方法的命名由两个下划线开头和两个下划线结尾,是特殊的方法。
目前为止我们接触过唯一的一个魔法方法是 init,但除此之外还有别外的一些魔法方法。
这些魔法方法用于创建不能由普通方法表示的特殊方法。

一、运算符重载

运算符重载是魔法方法使用的普遍例子。
运算符重载的目的是让类的对象能够使用 + 、 这样的符号进行相应的操作。
下面的例子里的魔法方法是 *add

例:

   
		
class Vector2D: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Vector2D(self.x + other.x, self.y + other.y) first = Vector2D(5, 7)second = Vector2D(3, 9)result = first + secondprint(result.x)print(result.y)

输出:

   
		
>>>816    >>>

上面的例子返回了一个新的对象并将之赋给了 result。

常用的运算符重载如下表:

函数名 运算
__sub__ -
__mul__ *
__truediv__ /
__floordiv__ //
__mod__ %
__pow__ **
__and__ &
__xor__ ^
__or__ \

通常情况下表达式x + y译成x.__add__(y)
然而,如果 x 尚未实现__add__,并且 x 和 y 是不同类型的,那么将译成y.__radd__(x)。上面表格里的所有方法都有相应的 r 方法。
例:

   
		
class SpecialString: def __init__(self, cont): self.cont = cont  def __truediv__(self, other): line = "=" * len(other.cont) return "\n".join([self.cont, line, other.cont]) spam = SpecialString("spam")hello = SpecialString("Hello world!")print(spam / hello)

输出:

   
		
>>>spam============Hello world!>>>

【问】如果没有定义任何魔法方法,那么 A()^B() 将会被译成什么样子?

  • [ ] A().__xor__(B())

  • [x] B().__rxor__(A())

  • [ ] B().xor(A())

二、比较运算符

函数名 比较运算
__lt__ <
__le__ <=
__eq__ ==
__ne__ !=
__gt__ >
__ge__ >=

如果没有定义 __ne__,则返回 __eq__ 的取反。
例:

   
		
class SpecialString: def __init__(self, cont): self.cont = cont def __gt__(self, other): for index in range(len(other.cont)+1): result = other.cont[:index] + ">" + self.cont result += ">" + other.cont[index:] print(result) spam = SpecialString("spam")eggs = SpecialString("eggs")spam > eggs

输出:

   
		
>>>>spam>eggse>spam>ggseg>spam>gsegg>spam>seggs>spam>>>>

三、容器相关魔法方法

有几种能使类像容器一样使用的魔法方法。
__len__ 相当于 len()
__getitem__ 用来获取索引位置的值
__setitem__ 用来分配索引位置的值
__delitem__ 用来删除索引位置的值
__iter__  用来迭代对象(比如在循环中使用)
__contains__ 相当于 in

还有很多其余的魔法方法这里没有提及,比如 __call__ 用来像函数一样调用对象,另外还有 __int____str__ 等用于将对象转换为内置类型。
例:

   
		
import random class VagueList: def __init__(self, cont): self.cont = cont  def __getitem__(self, index): return self.cont[index + random.randint(-1, 1)]  def __len__(self): return random.randint(0, len(self.cont)*2) vague_list = VagueList(["A", "B", "C", "D", "E"])print(len(vague_list))print(len(vague_list))print(vague_list[2])print(vague_list[2])

输出:(因为有 random 参与,所以输出不唯一)

   
		
>>>67DC>>>

【问】x[y]=z 译成什么样子?

  • [ ] y.__getitem__(x,z)

  • [ ] x.setitem(z,y)

  • [x] x.__setitem__(y,z)

对象生命周期

对象的生成周期由:创建操作销毁三个部分组成。

对象生命周期的第一阶段是对象所属类的定义

下一阶段是当调用__init__时进行的实例化,此时会分配内存给对像实例。在此之前会调用类的__new__方法。通常只有在特殊情况下才将其覆盖。

经历过以上过程后,对象就可以被使用了。

在对象建立后,可以通代码在对象上调用函数以及访问对象的属性与对象进行交互。

最后,在对象结束使用的时候就可以把对象销毁。

对象的销毁

当对象被销毁后,给该对象分配的内存就随之被释放,之后被释放的内存可以用于做其他的事情。

对象的销毁发生在对象的引用计数归零时。引用计数是指与对象相关的变量或其他元素正在被使用的数量。如果没有使用中的元素(即引用数为零)就表示该对象与程序运行无关,所以这个对象就可以被安全地销毁。

在某些特殊的情况下,两个甚至更多对象只能相互引用,因此也能够被删除。

del 用于删除对象。del的魔法方法是__del__。

当对象不再被使用时,对象的删除过程称为“垃圾收集”。总的来说,对象的引用计数增加发生在被分配了新的名字、放入了一个容器(列表、元组、词典)的时候。对象引用计数的减少发生在使用 del 删除对象、对象被重新分配、对象引用超出范围的情况下。当对象的引用计数归零时,Python 会自动地销毁它。例如:

   
		
a = 42 # 创建对象b = a # 引用计数增加c = [a] # 引用计数增加del a # 引用计数减少b = 100 # 引用计数减少c[0] = -1 # 引用计数减少

数据隐藏

面向对象编程的一个重要部分是封装,就是把相关的变量和函数打包在一个类的实例(对象)当中以便使用。这就应当隐藏类的实现细节,把干净标准的接口提供给想要使用该类的用户。

在其他的编程语言中通常用private私有成员来完成数据隐藏,私有成员可以阻止外部访问类中的方法和属性。

Python 的哲学略有不同。Python 并不严格阻止对类内任何部分的访问,也就是说无法强制方法或属性完全对外隐藏。

弱私有方法和属性

在 Python 中私有的方法或属性以单个下划线开头,表示它们是私有的,不应该由外部访问。但“不应该”、“不提倡”并不表示“不能”。唯一的影响是:这些以单个下划线开头的方法或属性不能通过from module_name import *的方式导入。

   
		
class Queue: def __init__(self, contents): self._hiddenlist = list(contents)  def push(self, value): self._hiddenlist.insert(0, value)  def pop(self): return self._hiddenlist.pop(-1)  def __repr__(self): return "Queue({})".format(self._hiddenlist) queue = Queue([1, 2, 3])print(queue)queue.push(0)print(queue)queue.pop()print(queue)print(queue._hiddenlist)

输出:

   
		
>>>Queue([1, 2, 3])Queue([0, 1, 2, 3])Queue([0, 1, 2])[0, 1, 2]>>>

强私有方法和属性

在 Python 中私有的方法或属性以双下划线开头,强私有方法或属性无法从类外直接访问。这样做的目的并不是为了完全的数据隐藏,而是为了防止子类中有同名的方法或者属性。

_类名是用类外访问强私有方法和属性的间接办法,例如:

   
		
class Spam: __egg = 7 def print_egg(self): print(self.__egg) s = Spam()s.print_egg()print(s._Spam__egg)print(s.__egg)

输出:

   
		
>>>77AttributeError: 'Spam' object has no attribute '__egg'>>>

问:如何从b类外访问__a?
答:_b__a

类方法

类方法用类名调用,传给类的cls参数(对比理解:常规方法传给self参数)。类方法用@classmethod标记,如下:

   
		
class Rectangle: def __init__(self, width, height): self.width = width self.height = height  def calculate_area(self): return self.width * self.height  @classmethod def new_square(cls, side_length): return cls(side_length, side_length) square = Rectangle.new_square(5)print(square.calculate_area())

输出:

   
		
>>>25>>>

静态方法

静态方法与普通方法的差别是:不接受其他参数(指self参数)。静态方法用@staticmethod标记,如下:

   
		
class Pizza: def __init__(self, toppings): self.toppings = toppings  @staticmethod def validate_topping(topping): if topping == "pineapple": raise ValueError("No pineapples!") else: return True ingredients = ["cheese", "onions", "spam"]if all(Pizza.validate_topping(i) for i in ingredients): pizza = Pizza(ingredients) 

当 ingredients 里有 pineapple 时就抛出异常。

注意:上例中的i有点先使用后定义的感觉。对于 Sugar 习惯了 C/C++ 的人来讲一开始觉得很不适应,但确定这就是常人的表达习惯。Sugar 觉得“严谨”是后天训练的,而并不是所有人都乐于后天“严谨”的训练,这是为什么 Python 会更广泛地被接受的原因之一吧。

只读属性

通过@property给方法加只读属性,如下:

   
		
class Pizza: def __init__(self, toppings): self.toppings = toppings  @property def pineapple_allowed(self): return False pizza = Pizza(["cheese", "tomato"])print(pizza.pineapple_allowed)pizza.pineapple_allowed = True

输出:

   
		
>>>False AttributeError: can't set attribute>>>

setter 与 getter

看下面代码了解setter的作用,getter类似。当有具体需求的使候能想到就可以了,不做多余的说明。

注意:pineapple_allowed()方法的多态:

   
		
class Pizza: def __init__(self, toppings): self.toppings = toppings self._pineapple_allowed = False  @property def pineapple_allowed(self): return self._pineapple_allowed  @pineapple_allowed.setter def pineapple_allowed(self, value): if value: password = input("Enter the password: ") if password == "Sw0rdf1sh!": self._pineapple_allowed = value else: raise ValueError("Alert! Intruder!") pizza = Pizza(["cheese", "tomato"])print(pizza.pineapple_allowed)pizza.pineapple_allowed = Trueprint(pizza.pineapple_allowed)

运行结果:

   
		
>>> FalseEnter the password: Sw0rdf1sh!True>>> 

PS

一、基本概念











二、控制结构











三、函数与模块






四、异常





五、文件

应用篇

关注作者

欢迎扫码关注我的公众号MultiMCU EDU。

提示:在公众号“关于我”页面可加作者微信好友。

喜欢本文求点赞,有打赏我会更有动力。

本文源自微信公众号:MultiMCU EDU,不代表用户或本站观点,如有侵权,请联系nick.zong@aspencore.com 删除!

声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
  • 相关技术文库
  • 硬件
  • 原理图
  • 信号完整性
  • EMI
下载排行榜
更多
评测报告
更多
广告