猪行天下之Python基础——8.1 类与对象

1,301 阅读15分钟

内容简述:

  • 1、面相对象的理解
  • 2、类与对象
  • 3、继承
  • 4、组合
  • 5、对象相关的内置函数

1、面相对象的理解

考虑到部分读者可能没有接触过面向对象编程,所以先介绍下面向对象的一些特征,形成一个面向对象概念的基本认知,有助于后面具体的学习Python的面向对象编程。


① 对象引入

按照普通人的认知,对象就是我们日常生活中谈论到的男女对象。见过这样的问题:

“我没有对象是不是就没办法学习面向对象编程了?”

答案肯定不是,编程界有这样一句名言,“万物皆对象”,意思:把任何事物都看做一个对象。

所有的事物都具有两个特点:「有什么」和「能做什么」,举个例子:

老鹰有翅膀,能够飞翔。在面向对象看来,「老鹰」就是一个「对象」,「翅膀」是「属性」,「用来描述对象」,而「能飞翔」是「方法」,这就是「对象的功能」。


② 类的引入

自然界除了老鹰外,还有很多有翅膀的动物,比如燕子,天鹅等。它们都具有上面两个相似的特点,我们可以把这种「具有相同或相似性质的对象」进行抽象(抽取共性)从而形成一个类,比如这里的鸟类,可以写出这样的伪代码:

鸟类{
    属性:翅膀 = (不同的颜色,不同的形状)
    方法:飞翔(能否)
}

然后有个名字叫「类的实例」,类的一个个具体实现,其实就是「对象」,比如这里实例化不同的鸟的伪代码:

老鹰 = 鸟类(翅膀 = 长而宽阔,飞翔(能))
燕子 = 鸟类(翅膀 = 俊俏轻快,飞翔(能))
鸭子 = 鸟类(翅膀 = 较短,飞翔(否))

③ 类设计的要求和思想

  • 要求:高内聚,低耦合,从而提高对象的可复用性。
  • 思想:以对象为中心,先开发类,得到对象,再通过对象间的相互通信实现功能。

2、类与对象

对类与对象的概念有个大概的认知后,我们来开始学习Python中类与对象相关的语法。

① 定义类和实例

Python中使用class关键字定义类,我们来定义一个鸟类:

class Bird:
    """
    鸟类
    """

    kind = '动物'
name = "鸟"

    def __init__(self, name, wings):
        self.name = name
        self.wings = wings

    def can_fly(self, can):
        print("%s有 %s 的翅膀,%s飞翔" % (self.name, self.wings, '能' if can else '不能'))

if __name__ == '__main__':
    eagle = Bird("老鹰""长而宽阔")
    duck = Bird("鸭子""较短")
    eagle.can_fly(True)
    duck.can_fly(False)

运行结果如下

老鹰有 长而宽阔 的翅膀,能飞翔
鸭子有 较短 的翅膀,不能飞翔

② __init__初始化函数

在实例化对象的时候会自动调用,给该对象属性的值初始化,要注意: 不推荐使用类名.__init__()这种方式去调用初始化函数,以避免引起不必要的问题。


new构造函数

关于这个函数就涉及到Python经典类与新式类的概念了,在Python 2.x中,默认经典类除非显式的继承object类才是新式类。而在Python 3.x默认所有类都是新式类不用显式继承object类
 
新式类相比经典类增加了很多的内置属性,比如可以通过__class__获得自身类型等,还有这里的__new__()函数。而这个函数的调用时机在init__函数之前,作用是:可以调用其他类的构造方法或者直接返回别的对象来作为本类的实例
 
__new__(cls, *args,**kw),第一个参数表示要实例化的类,该参数在实例化的时候由Python解释器自动提供。另外要注意,__new()
函数,必须要有返回值,如果该函数没有成功返回cls类型的对象,是不会调用__init__()来对对象进行初始化!!!

初学者容易把这两个弄混,还是列出来帮助区分:

  • __init__():用于初始化新实例控制初始化过程,可添加一些属性,做些额外操作,发生在类实例被创建完后,它是对象级别的函数
  • __new__():用于控制生成新实例的过程,它是类级别的函数

④ 类属性与实例属性

类属性类的属性,当定义了一个类属性后,这个变量虽然归类所有但是类和实例都可以访问到类属性是类和实例的共有数据。当类属性和实例属性同名时,访问顺序是:实例属性 -> 类属性如果两个都不存在则会报错。比如下面这样的代码:

print("类访问类变量:%s" % Bird.kind)
print("类访问变类量:%s" % Bird.name)
print("实例访问类变量:%s" % eagle.kind)
print("实例访问同名变量:%s" % duck.name)

运行结果如下

类访问类变量:动物
类访问变类量:鸟
实例访问类变量:动物
实例访问同名变量:鸭子

实例属性则是与实例绑定的属性,可以通过 实例名.属性名 的方式调用,代表了实例的数据部分


⑤ 类函数,成员函数与静态函数

「类函数」用于访问类属性,使用@classmethod装饰器来修饰,第一个参数是cls,类本身用于调用类属性但是不能访问实例属性。类方法可以通过类直接调用,或通过实例直接调用。但无论哪种调用方式,最左侧传入的参数一定是类本身!!!代码示例如下:

class A:
    @classmethod
    def fun_a(cls):
        print(type(cls), cls)

if __name__ == '__main__':
    A.fun_a()
    a = A()
    a.fun_a()

运行结果如下

<class 'type'> <class '__main__.A'>
<class 'type'> <class '__main__.A'>

「成员函数和类实例绑定」类实例化后才能调用,它的第一个参数表示实例本身,一般用self表示成员函数可以直接操作对象内部的数据。如果使用类直接调用成员函数需要显式地将实例作为参数传入。代码示例如下:

class B:
    def fun_b(self):
        print("Call fun_b()")

if __name__ == '__main__':
    b = B()
    b.fun_b()
    B.fun_b(b)  # 类调用成员函数需将实例传入

运行结果如下

Call fun_b()
Call fun_b()

静态函数」,在定义上面的fun_b函数的时候,智能提示里就有一个Make method static的选项,对于这种不需要self参数的函数(无需实例参与)都可以定义成静态函数调用过程中无需将类实例化。使用@staticmethod装饰器声明,通过 类名.函数名实例.函数名进行调用,代码示例如下:

class C:
    @staticmethod
    def fun_c():
        print("Call fun_c()")

if __name__ == '__main__':
    C.fun_c()
    c = C()
    c.fun_c()

运行结果如下

Call fun_c()
Call fun_c()

⑥ 访问控制

所谓的访问控制,就是「类的属性和方法是公有还是私有」,如果属性和方法只能在类内部访问,而不能被实例访问的话,我们就称这个属性或方法为私有的
 
Python和其他编程语言不同,没有类似于public和private这样的访问权限修饰符,而是采用一种「名字改编技术」。默认公有,而私有的属性名和方法名会加上两下划线,比如下面的__skill,当然这只是伪私有,改成了_类名私有属性/方法名,比如下面调用people._Person__skill,是可以访问到私有成员的:

class People:
    sex = 1  # 类属性
    __skill = "敲代码"  # 私有类属性,只能类内部访问,外部无法访问

    def speak(self):
        print("我是一个人,技能是:%s" % self.__skill, end='\t')

people = People()
people.speak()
people.sex = -1
print("性别:" + ("男" if people.sex == 1 else "女"))
print("访问私有属性:%s" % people._People__skill)

运行结果如下

我是一个人,技能是:敲代码   性别:女
访问私有属性:敲代码

虽然可以这样访问到私有成员,但是不建议这样做!

另外还有一种「单下划线开头的变量名或方法名」,同样是私有成员,不过类和实例都能访问,也会被子类继承。如果你不想属性或方法被子类继承就还是用双下划线吧!还有一种「开头结尾都是双下划线的属性或函数」是类的特殊成员,有特殊用途,比如上面的__init__初始化方法;最后如果你「定义的变量和某个保留关键字冲突」的话,可以使用单下划线作为后缀,比如:in_ = 1。


⑦ 动态绑定

Python中可以「动态地为类或对象绑定属性或函数」。类动态绑定属性与函数,对该类的所有实例有效。代码示例如下:

class A:
    def __init__(self, id_):
        self.id_ = id_

# 定义一个用于动态绑定的函数
def set_name(self, name):
    print("调用了动态绑定的函数")
    self.name = name

if __name__ == '__main__':
    # 动态绑定一个属性
    A.kind = "人类"

    # 动态绑定一个函数
    A.set_name = set_name
    a = A(1)

    # 类访问动态绑定的属性
    print(A.kind)

    # 实例访问动态绑定的属性
    print(a.kind)

    # 类访问动态绑定的函数
    A.set_name(a,'123')

    # 实例访问动态绑定的函数
    a.set_name('321')

运行结果如下

人类
人类
调用了动态绑定的函数
调用了动态绑定的函数

实例动态绑定属性与函数,只对当前对象有效,对其他实例无效,需要用到一个MethodType类,代码示例如下:

from types import MethodType

class B:
    def __init__(self, id_):
        self.id_ = id_

# 定义一个用于动态绑定的函数
def set_name(self, name):
    print("调用了动态绑定的函数")
    self.name = name

if __name__ == '__main__':
    b_1 = B('1')

    # 动态为实例1绑定一个属性
    b_1.kind = "人类"

    # 动态为实例1绑定一个函数
    b_1.set_name = MethodType(set_name, b_1)

    # 实例1设置动态绑定的属性与函数
    print(b_1.kind)
    b_1.set_name('123')

    # 另一个类实例调用动态绑定的属性
    b_2 = B('2')
    print(b_2.kind)

运行结果如下

人类
Traceback (most recent call last):
调用了动态绑定的函数
  File "/Users/jay/Project/Python/Book/Chapter 9/9_7.py", line 30in <module>
    print(b_2.kind)
AttributeError: 'B' object has no attribute 'kind'

3、继承

面向对象的最大优点是代码重用,而实现代码重用的重要的方法就是通过Python的继承机制。这个继承理解为我们日常说的遗产继承,儿子继承父亲的遗产。类比成编程里对应子类和父类,子类继承父类所有的属性与函数,可以进行重写者进行扩展以实现更多的功能。

Python中关于继承的规则如下

  • 继承写法class 子类(父类)
  • 子类可以继承父类的所有属性与方法
  • 子类定义与父类同名的属性与方法会自动覆盖
  • 重写时如果想调用父类的同名方法可以使用super().方法名调用
  • 父类的私有属性、方法不能继承,即__(双下划线)开头的属性名和方法名`
  • 子类可以调用super().init()的方式初始化父类

① 单继承

所谓的单继承就是只继承一个父类,代码示例如下:

class Bird:
    def __init__(self, name):
        self.name = name
    def can_fly(self, can):
        self.can = can
    def __str__(self):
        return self.name + ("能够" if self.can == True else "不能够") + "飞翔。"

class Duck(Bird):
    # 子类扩展父类中的方法
    def set_color(self, color):
        self.color = color

    # 重写父类中里的方法
    def __str__(self):
        return self.color + "的" + self.name + ("能够" if self.can == True else "不能够") + "飞翔," + "会游泳。"

if __name__ == '__main__':
    duck = Duck("小鸭子")
    duck.can_fly(False)
    duck.set_color("黄色")
    print(duck)

运行结果如下

黄色的小鸭子不能够飞翔,会游泳。

另外要注意,父类是无法调用子类方法的,比如下面的代码:

bird = Bird('鸟')
bird.can_swin()

运行后直接报错

Traceback (most recent call last):
  File "/Users/jay/Project/Python/Book/Chapter 9/9_8.py", line 33in <module>
    bird.can_swin()
AttributeError: 'Bird' object has no attribute 'can_swin'

② 多继承

多继承就是「同时继承多个父类的属性与方法」,多个父类间用逗号隔开,另外要注意如果父类们中有相同的方法,调用的顺序是:谁在前面先调用那个父类中的方法,比如有class Person(Name, Sex,Age),三个父类里都有一个show的方法,那么子类调用的是Name里的show()!

你可以通过内置属性__mro__查看对象搜索方法时的先后顺序。另外,如果不是非得用多继承不可的话,应该尽量避免使用它,有时会出现一些不可遇见的BUG。多继承的代码示例如下:

class A:
    def show_A(self):
        print('父类A')

class B:
    def show_B(self):
        print('父类B')

# 定义一个子类,继承A和B类
class C(A, B):
    def show_C(self):
        print('子类C')

if __name__ == '__main__':
    c = C()
    c.show_A()
    c.show_B()
    c.show_C()

运行结果如下

父类A
父类B
子类C

4、组合

多继承的一个替代方案就是通过组合的方式,「把需要用到的类丢到组合类中实例化」,代码示例如下:

class Book:
    def __init__(self, num):
        self.num = num

class Phone:
    def __init__(self, num):
        self.num = num
class Wallet:
    def __init__(self, num):
        self.num = num

class Bag:
    def __init__(self, x, y, z):
        self.book = Book(x)
        self.phone = Phone(y)
        self.wallet = Wallet(z)
    def show_bag(self):
        print("您的背包里有:【书本】* %d 【手机】* %d 【钱包】* %d" %
(self.book.num, self.phone.num, self.wallet.num))

if __name__ == '__main__':
    bag = Bag(321)
    bag.show_bag()

运行结果如下

您的背包里有:【书本】* 3 【手机】* 2 【钱包】* 1

5、对象相关的内置函数

Python中还为我们提供了一些与对象相关的内置函数,如下表所示:

函数 作用
issubclass(class, classinfo) 如果第一个参数是第二个参数的子类,返回True,
否则返回False
isinstance(object, classinfo) 如果第一个参数是第二个参数的实例对象,返回True,
否则返回False
hasattr(object, name) 测试一个对象中是否有指定的属性,属性名要用引号
括着!
getattr(object, name, [,default]) 返回对象的指定属性值,不存在返回default值,
没设会报ArttributeError异常
setattr(object, name, value) 设置对象中指定属性的值,属性不存在会新建并赋值
delattr(object, name) 删除对象中的指定属性的值,不存在会报ArttributeError异常
property(fget,fset,fdel,doc) 返回一个可以设置属性的属性

如果本文对你有所帮助,欢迎
留言,点赞,转发
素质三连,谢谢😘~