本文将详细介绍python中类的使用。
类class
命名法:前面的变量用下划线命名法。类class则建议用Pascal命名法。Pascal命名法是每个单词首字母大写,驼峰形状,连接不用下划线。
创建第一个类:
class Point:
# 创建类
def draw(self):
# python类的方法至少包含一个self参数指向本身
print(“draw”)
point = Point()
# 创建对象
print(type(point))
# 输出对象类型,我们看到是 <class ‘__main__.Point’>
# __main__其实是模块,模块在后面会谈到
print(isinstance(point, Point))
# 确认对象是否是类的实例,本例当然输出 True
构造函数
class Point:
def __init__(self, x, y):
# 构造函数名称约定为 __init__
self.x = x
self.y = y
def draw(self):
print(f”Point({self.x},{self.y})”)
point = Point(1, 2)
# 调用时不用提供self参数,python会自动添加
point.draw()
# 输出 Point(1,2)
类属性和实例属性
实例属性是动态的,是类的某个实例对象特有的;而类属性是全部对象共享的,任何一个对象修改了类属性,所有对象看到的都改变了。
一般情况下仅使用实例属性。
实例属性:
point.z =10
# z 原先并未定义,新建了一个point对象后,直接可以赋值 属性z,就动态添加了
类属性:
class Point:
default_color = “red”
# 添加一个类属性 default_color
def __init__(self, x, y):
…
Point.default_color = “yellow”
# 修改类属性为 “yellow”
…
print(point.default_color)
# point对象查看该属也变成了 “yellow”
类方法和实例方法
与类属性类似,一般只需要定义实例方法就行。但在某些时候,定义类方法也是有必要的。
比如Point类,可能经常需要用到 Point(0,0)这样的初始点。每个实例对像用到时都需要建立一个实例如 point=Point(0,0),我们可以定义类方法避免重复建。
…
@classmethod
def zero(cls):
return cls(0, 0)
# 定义了一个类方法zero,装饰符 @classmethod 表示该方法是类方法
# 参数 cls 指向类定义
当需要用到这个特别Point时,只需调用类方法即可获得
Point.zero()
魔术方法
类中的方法前后都加双下划线的就是魔术方法,比如__init__(),__str__()等等。我们没有明确定义,它们是继承过来的,也不能显式调用,只能由python自动调用。
__str__() 魔术方法:可以显示对象信息,我们可以重写。
…
def __str__(self):
return f”({self.x},{self.y})”
# 重写__str__
point = Point(1, 2)
print(point)
# 显示我们自定义的 “(1,2)”,而不是默认的类型和内存地址信息了
对象的比较
python中对象指向内存地址,没法进行比较。顺序说一下python内置函数id函数可以获取内存地址,如 id(point1)就得到变量point1的内存地址。
python类有魔术方法 __eq__,__gt__ 等,可以实现对象的比较。
…
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# 自定义了对象是否相等的逻辑
def __gt__(self, other):
return self.x > other.x and self.y > other.y
# 自定义了对象是否大于的逻辑
# 值得注意的是,定义了大于的逻辑,python自动搞定了小于的逻辑,
# 我们不必另外定义 __lt__魔术方法就可用
# 下面来测试一下
point1 = Point(1, 2)
point2 = Point(1, 2)
point3 = Point(3, 4)
print(point1 == point2) #True
print(point1 == point3) #False
print(point1 < point3) #True
print(point1 > point3) #False
print(point3 > point1) #True
对象的算术计算
python对象也可用魔术方法实现算术计算。
比如 + 的功能可以重写 __add__ 魔术方法来实现
…
def __add__(self, other):
return Point(self.x+other.x, self.y+other.y)
# 重写魔术方法 __add__
point1 = Point(1, 2)
point2 = Point(3, 4)
point3 = point1+point2
print(point3) # 输出(4,6)
定制数据容器
我们学过的python各种容器,比如列表、集合、字典等,在大部分情况已足够应付开发场景。不过有时也需要创建自定义的容器,利用python类的魔术方法,可以方便地实现。
假设我们要给技术博客的文章打上标签,设计一个类比如命名TagCloud,用于统计博客的各类文章数量。设计完成的TagCloud期待能做如下:
cloud = TagCloud()
len(cloud) # 支持获取标签个数
cloud[“python”] = 10 # 可以手动给某个标签设置文章数量
for tag in cloud:
print(tag)
# 也支持迭代输出各标签
我们如何设计?
根据需求,数据结构我们选择字典,因为字典可以放置类似{“python”:10}这样的信息。TagCloud还要支持len函数获取个数、设置标签的数量以及迭代,就需要重写以下几个魔术方法
- __len__()
- __getitem__()
- __setitem__()
- __iter__()
实现的代码如下:
class TagCloud:
def __init__(self):
self.tags = {}
# 初始化一个空字典
def add(self, tag):
self.tags[tag.lower()] = self.tags.get(tag.lower(), 0)+1
# 加lower()主要是为了防止用户输入不规范的大小写
def __getitem__(self, tag):
return self.tags.get(tag.lower(), 0)
# 重写魔术方法 __getitem__ 是为了能支持 tagcloud[“python”]这样的调用
def __setitem__(self, tag, count):
self.tags[tag.lower()] = count
# __setitem__ 支持 tagcloud[“python”]=10 这样的设置方法
def __len__(self):
return len(self.tags)
# __len__ 支持 len(tagcloud) 这样的调用
def __iter__(self):
return iter(self.tags)
# __iter__ 支持 for tag in tagcloud这样的调用,
# 其中 iter是内置函数
# 实际测试一下
cloud = TagCloud()
len(cloud) # 支持获取标签个数
cloud.add(“python”)
cloud.add(“python”)
cloud.add(“python”)
cloud.add(“Python”)
cloud.add(“javascript”)
cloud.add(“javascript”)
cloud.add(“Javascript”)
print(cloud[“python”]) # 输出4
print(cloud[“Python”]) # 输出4
print(cloud[“javascript”]) # 输出3
print(cloud[“java”]) # 输出0
cloud[“python”] = 10
for tag in cloud:
print(tag, cloud[tag])
# 输出:
# python 10
# javascript 3
私有成员
如果要使用私有成员,需要在变量前加双下划线 __
def __init__(self):
self.__tags = {}
# tags加了双下划数 __,即为私有成员,外部不能访问
# 去掉 __,外部就可访问
小技巧:
vscode中重命名变量可以选中变量再按F2,进行修改重构
类似地,方法名前面加 双下划线 __ 也可以阻止外部访问,变成私有方法。
说明:
- 从技术来说,还是有办法实现访问私有成员的。通过查看对象的 __dict__可以访问到所有成员,如通过查看cloud.__dict__,可以看到私有变量 __tags实际名称为 _TagCloud__tags,这样通过cloud._TagCloud__tags就可以访问到了。
- python不像Java、C#真正实现私有成员,加双划线只是警告:这是私人成员,不要访问。主要是防止意外。
属性
属性在Java、C#中,一般是设一个私有成员,然后再使用getter和setter来控制,python当然也可以这么做,但这不是python的最佳实践,python中有更好的实现方法。
第一种方法:使用内置函数property()
class Product:
def __init__(self, price):
self.__price = price
def get_price(self):
return self.__price
def set_price(self, value):
if value <= 0:
raise ValueError(“price cannot be 0 or less.”)
self.__price = value
price = property(get_price, set_price)
# property内置函数定义属性price,它的获取和赋值方法分别为 get_price和set_price
product = Product(1)
print(product.price)
product.price = -2
# 设置属性price后,就可以直接读取price和设置price了
内置函数设了属性,但没有封装方法,比如外部还是可以看到set_price和get_price等方法,当然我们可以通过加双下划线的方法隐藏,不过代码还是显得比较丑陋。python使用属性的最佳实践是加装饰符。
第二种方法:@property,@price.setter(属性的setter)
class Product:
def __init__(self, price):
self.price = price
# 可以直接用属性,真正的成员是__price
@property # 属性装饰符
def price(self): # 属性方法
return self.__price
@price.setter # 装饰符setter表示设置
def price(self, value): # 同名属性
if value <= 0:
raise ValueError(“price cannot be 0 or less.”)
self.__price = value
如果希望属性只读,只要删除 @price.setter节下的属性方法就可以了
继承
程序开发中有个原则叫DRY:
Don’t Repeat Yourself
也就是说不要重复。有很多途径解决重复,其中继承是其中的一种。
格式:
class Mammal(Animal):
# Mammal继续Animal
Object类
所有类都继承于Object,Object有很多魔术方法,所以所有类都具有魔术方法 __XXX__。
两个有用的内置函数:
- isinstancem = Mammal()print(isinstance(m, Animal))# 由于Mammal继承于Animal,所以是True
- issubclassprint(issubclass(Animal, object))# 所有类都继承于Object,所以是True
方法重写
方法重写是替代或扩展父类的方法。
class Animal:
def __init__(self):
…
class Mammal(Animal):
def __init__(self):
# 重写了父类的 __init__方法
super().__init__()
# 先调用父类的方法,super()用于获取父类的访问权
…
多层次继承
多层继承会增加软件的复杂度,没有必要。
开发中建议类继承层次限制在1-2个,超过了就是自找麻烦。
多重继承
一个类可以继承多个类。
class Employee:
def greet(self):
print(“Employee Greet”)
class Person:
def greet(self):
print(“Person Greet”)
class Manager(Person, Employee):
# 继承两个类
pass
manager = Manager()
manager.greet()
# 两个基类都有greet方法,执行结果就看继承的顺序了
# 增加了程序执行的不确定性
多重继承一般用于小而抽象,功能不重复的地方。
抽象基类
from abc import ABC, abstractclassmethod
class Stream(ABC):
# 继承 ABC 表示是抽象类
@abstractclassmethod
# 这个装饰符表示是抽象方法
def read(self):
pass
抽象类都继承于ABC类。
从抽象类派生的类必须实现抽象方法。
多态性
看个例子。UIControl是个抽象类,有一个抽象方法 draw。但UIControl不负责绘制draw,只是约定了这个方法。真正的draw是由继承UIControl的派生类Textbox、DropDownList等实现。
from abc import ABC, abstractclassmethod
class UIControl(ABC):
@abstractclassmethod
def draw(self):
pass
class TextBox(UIControl):
def draw(self):
print(“Draw a textbox.”)
class DropDownList(UIControl):
def draw(self):
print(“DropDownList”)
def draw(controls):
for control in controls:
control.draw()
ddl = DropDownList()
textbox = TextBox()
draw([ddl, textbox])
鸭子类型 Duck-Typing
我们把前面的代码改一下,不继承抽象类UIControl,具体控件都实现draw()方法,如下。
class TextBox:
def draw(self):
print(“Draw a textbox.”)
class DropDownList:
def draw(self):
print(“DropDownList”)
def draw(controls):
for control in controls:
control.draw()
ddl = DropDownList()
textbox = TextBox()
draw([ddl, textbox])
执行结果与前面是一样的,这就是鸭子类型,只要像鸭子就是鸭子,只要有draw方法就行,而不管类型。
这是因为python是动态语言,所以可以这么玩。
但一般建议还是按照java和C#等语言的方法,定义抽象类抽象方法,子类实现抽象方法这么做。这样比较规范。
扩展内置类型
扩展字符串类str,添加复制功能。
扩展列表类list,跟踪添加操作
如下:
class Text(str):
# 扩展字符串类str
def duplicate(self):
return self+self
class TrackableList(list):
# 扩展list类
def append(self, Object):
print(“Append called.”)
super().append(object)
# 调用测试
text = Text(“Python”)
print(text.duplicate())
# 输出 “PythonPython”
list = TrackableList(range(20))
list.append(’21’)
# 输出 “Append called.”
数据类
仅有数据没有方法的类,按常规做法,也需要定义构造函数定义值。
需要比较大小的话,还要实现 __eq__ 等魔术方法,比较麻烦。
我们可以用 namedtuple来代替。
from collections import namedtuple # 导入命名元组
Point = namedtuple(“Point”, [“x”, “y”])
# namedtuple第一个参数Point指定类型名,第二个指定属性
p1 = Point(x=1, y=2)
p2 = Point(x=1, y=2)
print(p1 == p2)
# 返回True,无需使用__eq__等魔术方法
另外,使用命名元组比普通元组好,可以像类一样通过点 . 访问,比如使用p1.x访问属性x的值,但不能修改,它是只读的。
p1.x = 10
# 出错,不能修改。
# 只有初始化时可以设置值
p1 = Point(x=10, y=2)
小结
本文详细介绍了Python中类的相关用法。
顺便说一下,程序员不能为了面向对象而面向对象,而是要真正理解面向对象解决的是什么,建议大家都认真学习下“四人帮”的设计模式。
下一篇计划介绍python中的模块和包的知识。