完全掌握Python(五)

本文将详细介绍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,进行修改重构

类似地,方法名前面加 双下划线 __ 也可以阻止外部访问,变成私有方法

说明:

  1. 从技术来说,还是有办法实现访问私有成员的。通过查看对象的 __dict__可以访问到所有成员,如通过查看cloud.__dict__,可以看到私有变量 __tags实际名称为 _TagCloud__tags,这样通过cloud._TagCloud__tags就可以访问到了。
  2. 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中的模块和包的知识。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注