Python 基础学习09:类

自定义对象是 Python 的核心概念。

对象

在面向对象中,对象意味着一系列数据(属性)以及一套访问和操作这些数据的方法。
使用对象的好处:

  • 多态:对不同类型的对象执行相同的操作
  • 封装:对外部隐藏有关对象工作原理的细节
  • 继承:可基于通用类创建出专用类

多态

多态指的是即使不知道变量指向的是哪种对象(字符串、整型、列表、字典)也能够对其执行操作,且操作的行为将随对象所属的类型而异。

多态和方法

与对象属性相关联的函数称为方法。
每当无需知道对象是什么样类型的都能对其执行操作,都是多态在起作用。
内置运算符和大量的内置类的方法都使用了多态,例如:

1 + 2
3
'Hello' + 'World'
'HelloWorld'
'Hello'.count('o')
1
[1,2,3,1].count(1)
2

封装

封装是指向外部隐藏不必要的细节,例如方法或变量。
当调用对象中的方法时不需要操心其它事情,如避免干扰全局变量,通过属性将名称封装在对象中。
属性是归属于对象的变量,就像方法一样。

继承

如果已经有了一个类,并要创建一个与之类似的类,可能是在此基础上增加了几个方法,此时可以继承已有的类,在新创建的类中可以直接调用被继承类中的属性,如变量或方法。

每个对象都属于特定的类,并称为该类的实例。本质上类也是一种对象。

创建自定义类

自定义类的示例:

class Dog:
    def set_name(self, name):
        self.name = name
    def get_name(self, name):
        return self.name
    def run(self):
        print("Dog ", self.name, "running")

class 语句会创建独立的命名空间,用于在其中定义函数。
self 是指向对象本身:

d1 = Dog()
d1.set_name('大黄')
d1.run()
Dog  大黄 running
d2 = Dog()
d2.set_name('大白')
d2.run()
Dog  大白 running

如果没有传递 self 所有的方法都无法访问对象本身(也就是所有要操作的属性所属的对象)。
也可以从外部访问这些属性:

d1.name
'大黄'
d2.name
'大白'

属性、函数和方法

方法和函数的区别表现在是否具有 self 参数上。方法将第一个参数关联到它所属的实例,所需无需提供这个参数,属性也可关联到函数,但是就没有特殊的 self 参数了。

class Demo:
    def method(self):
        print('I have a self')


def method():
    print('I not have self')


instance = Demo()
instance.method()
I have a self
instance.method = method
instance.method()
I not have self

事实上,可以将变量指向一个方法:

instance2 = d1.run
instance2()
Dog  大黄 running

当调用该方法时,它也可以访问 self 参数。

隐藏

Python 没有为私有属性提供直接的支持,意味着能够直接从外部访问对象内部的属性。
要使方法或属性称为私有,只需让名称以两个下划线开头即可:

class Dog:
    def __get_name(self):
        return '大黄'
    def run(self):
        print('Dog', self.__get_name(), 'run')


d1 = Dog()
d1.__get_name()
Traceback (most recent call last):
  File "<pyshell#63>", line 1, in <module>
    d1.__get_name()
AttributeError: 'Dog' object has no attribute '__get_name'. Did you mean: '_Dog__get_name'?
d1.run()
Dog 大黄 run

当从外部直接调用方法时,出现报错,但是内部可以根据名称进行调用。
私有是不可能存在的,其实错误提示中已经告知了方法,使用 _Dog__get_name 进行访问,也就是在方法名称前面加上 _类名 即可:

d1._Dog__get_name()
'大黄'

针对变量的隐藏也是如此:

class Student:
    __name = 'Bob'
    def hi(self):
        print('hi', Student.__name)


s1 = Student()
s1.__name
Traceback (most recent call last):
  File "<pyshell#78>", line 1, in <module>
    s1.__name
AttributeError: 'Student' object has no attribute '__name'
s1._Student__name
'Bob'
s1.hi()
hi Bob

如果不希望名称被修改,又向发出不要从外部修改属性或方法的信号,可以用一个下划线打头,这是一种约定俗成的操作,例如 from module import * 不会导入以一个下划线开头的名称。

类的命名空间

class 语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,类的所有成员都可以访问这个命名空间。
class 中不仅仅存在 def 语句,还可以存在变量等语句,例如:

class Num:
    i = 0
    def init(self):
        Num.i += 1


n1 = Num()
n1.init()
n1.i
1
n2 = Num()
n2.init()
n2.i
2

可以看到每个实例都能访问类中的对象,i 被初始化两次后,值变成了 2,类似于全局变量和局部变量的遮盖问题。
如果直接对属性进行修改,就会遮住类级的变量:

n1.i = 'Bob'
n1.i
'Bob'
n2.i
2

如果是定义成以下方式就可以避免:

class Num:
    def set_i(self, n):
        self.i = n
    def init(self):
        self.i += 1


n1 = Num()
n1.set_i(100)
n1.init()
n1.i
101
n2 = Num()
n2.set_i(1000)
n2.init()
n2.i
1001
n1.i
101

指定超类

子类扩展了超类的定义,要指定超类,需要在 class 语句中的类名称后面加上超类名,并用圆括号括起:

class Filter:
    def init(self):
        self.blocked = []
    def filter(self, sequence):
        return [x for x in sequence if x not in self.blocked]


class SPAMFilter(Filter):
    def init(self):
        self.blocked = ['SPAM']

s = SPAMFilter()
s.init()
s.filter(['SPAM', 'hi',1,'SPAM','123'])
['hi', 1, '123']

可以看到 SPAMFilter 集成了 FilterFilterSPAMFilter 的超类,在 SPAMFilter 中改写了 init 方法,没有修改 filter 方法,但是实例中可以直接调用。

验证集成的关系

使用内置方法 issubclass 可以确定一个类是否是另一个类的子类:

issubclass(SPAMFilter, Filter)
True
issubclass(Filter, SPAMFilter)
False

如果知道子类,想要知道其基类,可以使用类的特殊属性 __bases__

SPAMFilter.__bases__
(<class '__main__.Filter'>,)
Filter.__bases__
(<class 'object'>,)

要确定实例是否是特定对象的实例,可以使用 isinstance 内置方法:

s = SPAMFilter()
isinstance(s, SPAMFilter)
True
f = Filter()
isinstance(f, SPAMFilter)
False
isinstance(f, Filter)
True

也可以使用实例的 __class__ 属性:

f.__class__
<class '__main__.Filter'>
s.__class__
<class '__main__.SPAMFilter'>

多个超类

基类可以有多个:

class Info:
    def set_info(self, name):
        self.name = name


class Print:
    def print_info(self):
        print('Hi, my name is ', self.name)

class PrintInfo(Info, Print):
    pass

pp = PrintInfo()
pp.set_info('imxcai')
pp.print_info()
Hi, my name is  imxcai

除非万不得已,应避免使用多重继承。
同时在 class 语句中,超类的位置很重要,位于前面的类中的方法将覆盖位于后面的类的方法。如果 Info 类中存在 print方法,就会覆盖 Print 中的 print 方法:

class Info:
    def set_info(self, name):
        self.name = name
    def print(self):
        print('hi from Info class')

class Print:
    def print_info(self):
        print('Hi, my name is ', self.name)

class PrintInfo(Info, Print):
    pass

pp = PrintInfo()
pp.set_info('imxcai')
pp.print()
hi from Info class

接口和内省

检查实例是否具有特定的方法可以通过内置方法 hasatter

hasattr(pp, 'print')
True
hasattr(pp, 'delete')
False

也可以通过 callable 来检查是否能够被调用。

抽象基类

Python 通过引入 abc 模块来实现接口的理念。
抽象类是不能实例化的类,其职责是定义子类应实现的一组抽象方法:

from abc import ABC, abstractmethod
class Brid(ABC):
    @abstractmethod
    def fly(self):
        pass


Brid()
Traceback (most recent call last):
  File "<pyshell#221>", line 1, in <module>
    Brid()
TypeError: Can't instantiate abstract class Brid without an implementation for abstract method 'fly'
class BlueBrid(Brid):
    def fly(self):
        print('fly')


BlueBrid()
<__main__.BlueBrid object at 0x7f4d19c78c80>

Brid 类中定义了抽象基类,那么就无法进行实例化,必须使用子类实现了抽象基类才能实例化。

只要实现了 fly 方法,即使不是 Brid 的子类,依然可以通过类型检查:

class Bee:
    def fly(self):
        print('fly..')


b = Bee()
isinstance(b, Brid)
False
Brid.register(Bee)
<class '__main__.Bee'>
isinstance(b, Brid)
True
issubclass(Bee, Brid)
True

使用类中的 register 属性对非子类的类进行注册,这样就能通过类型检查,可以看到子类检查也通过了,但是无法使用超类中的属性。

面向对象设计的一些思考

设计对象时:

  • 将相关东西放在一起
  • 方法应只关心其所属实例的属性
  • 慎用继承
  • 保持简单

确定需要哪些类,以及类中应包含哪些方法,可以按照以下步骤筛选:

  • 将问题描述记录,标记名词、动词和形容词
  • 在名词中找出可能的类
  • 在动词中找出可能的方法
  • 在形容词中找出可能的属性
  • 然后将找出的方法、属性分配给各个类

有了面对对象的模型之后,还需考虑类和对象之间的关系,加以改进。

发表评论

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

滚动至顶部