背景介绍:
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 的类,它有两个属性:color 和 legs。
然后,使用这个类创建了 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>eggs spam>ggs spam>gs spam>s 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 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 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 删除!