背景介绍:
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__ 方法是类里最重要的方法。
当用一个类名像使用函数一样创建对象时会调用这个函数。
类内所有的方法的第一个参数都必须是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()
输出:
>>>
Fido
Woof!
>>>
可以在类体内分配变量作为类的属性,类内分配的变量可以通过类的对象访问,也可以直接用类名访问。
例:
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)
输出:
>>>
4
4
>>>
类内分配的变量被类的所有对象所共享。
【填空题】创建一个类并在类内创建一个 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()
输出:
>>>
brown
Woof!
>>>
下面哪个选项正确的定义了继承于 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 method
B method
C 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()
输出:
>>>
2
1
>>>
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 + second
print(result.x)
print(result.y)
输出:
>>>
8
16
>>>
上面的例子返回了一个新的对象
并将之赋给了 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
e>spam>ggs
eg>spam>gs
egg>spam>s
eggs>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 参与,所以输出不唯一)
>>>
6
7
D
C
>>>
【问】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)
输出:
>>>
7
7
AttributeError: '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
类似。当有具体需求的使候能想到就可以了,不做多余的说明。
注意:
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 = True
print(pizza.pineapple_allowed)
运行结果:
>>>
False
Enter the password: Sw0rdf1sh!
True
>>>
一、基本概念
01、Python 是什么
02、第一个 Python 程序
03、计算
04、浮点数
05、幂、商、余
06、字符串
07、简单输入输出
08、字符串操作
09、类型转换
10、变量
11、就地操作
二、控制结构
01、布尔值与比较
02、if 语句
03、else 语句
04、布尔逻辑
05、运算符优先级
06、while 循环
07、列表
08、列表操作
09、列表功能函数
10、range 函数
11、for 循环
三、函数与模块
01、代码重用
02、函数
03、函数参数
04、函数返回值
05、注释
06、函数的特别用法
四、异常
01、异常
02、异常处理
03、finally
04、抛出异常
05、断言
五、文件
01、打开文件
02、读文件
03、写文件
04、文件操作的好习惯
应用篇
01、MicroPython 实时体温获取
提示:在公众号“关于我”页面可加作者微信好友。
喜欢本文求点赞,有打赏我会更有动力。