光棍节就要到了,要不要给你介绍个 python 对象?

1. 前言

光棍节就要到了,一说介绍对象,我猜你一定想到了派森大叔家的克蕾丝(class)小姐姐和黛夫(def)小哥哥。别想入非非了,严肃点儿!我们今天的的话题,不是介绍男女朋友,而是讲解如何面向对象编程,也就是程序员常说的OOP啦。

不知道前辈们为什么会把 Object Oriented Programming 翻译成面向对象编程,搞得单身程序员经常心猿意马地产生幻觉,以为屏幕上的俊男美女就是自己将来要面对的对象了。说到这里,我觉得还是台湾同行的计算机术语翻译得较为恰当。比如,cache,我们叫“缓存”,人家叫“快取”,音意俱佳。台湾同行把OPP翻译成“物件导向编程”——虽然同样不明觉厉,但至少可以让单身程序员暂时忘记没有“对象”的烦恼。

限盐少许,本博主正式开始为大家介绍对象。

2. 类和对象的概念

学习面向对象编程,首先得搞明白,什么是对象?类是什么?实例化是什么意思?下图表达了我对OOP这几个基本概念的理解(实际上是妥协的结果——我和我的同事们讨论了很久,并翻墙参考了维基百科的说法)。
在这里插入图片描述
类是对我们要处理的客观事物的抽象。类用来描述具有相同的属性和方法的对象的集合,它定义了该集合中每个对象所共有的属性和方法。对象是类在内存的实例,一个类可以实例化为多个对象。类是抽象的,不占用内存,而对象是具体的,占用存储空间。

3. 类的成员

作为Python初学者,大可不必把精力花费在令人费解的概念上,只需要掌握使用类的基本要素就可以了。未来的日子里,你有足够多的时间慢慢体会OOP的博大精深。随着经验的积累,OOP会自然而然地成为你的思维工具。

下面的代码,定义了一个名为A的类。所有的类,都有构造函数和析构函数,此外,还可以包含成员函数和成员变量。我喜欢把成员函数叫做类的方法,把成员变量叫做类的属性。

class A:
    def __init__(self):
        """ 构造函数"""

        self.a = 10 # 定义了一个成员变量a     

    def getA(self):
        """成员函数"""

        print("a=%d" % self.a)

    def __del__():
        """析构函数"""

        print("delete object")

a = A()
a.getA()

当类被实例化为对象时,首先执行构造函数,当对象被销毁时,会自动执行析构函数。一般的,我们会在构造函数中进行初始化工作,在析构函数中进行清理工作。

读到这里,有很多初学者一定会说:我定义类的时候,写过构造函数,但从没有写过析构函数,你为什么说所有的类都有构造函数和析构函数呢?没错,定义类的时候,即便我们不写构造函数和析构函数,这两个方法也照样存在(析构函数稍微有点特殊,我们不能直接看到它——除非是我们自己定义的)。如果我们自己定义了构造函数和析构函数,则将会取代系统自动赋予的这两个函数。下面的例子清晰地说明了其中的奥秘:类A既没有构造函数,也没有析构函数,类B只有析构函数,两个类都可以生成类实例,也都可以销毁,且 del b 时首先调用了自定义的析构函数。

>>> class A:
	pass

>>> a = A()
>>> del a
>>> a
Traceback (most recent call last):
  File "<pyshell#70>", line 1, in <module>
    a
NameError: name 'a' is not defined
>>> class B:
	def __del__(self):
		print('执行析构函数,清理现场')

		
>>> b = B()
>>> del b
执行析构函数,清理现场
>>> b
Traceback (most recent call last):
  File "<pyshell#75>", line 1, in <module>
    b
NameError: name 'b' is not defined

4. 新式类和旧式类

在py2中,类有新式类和旧式类两种。新式类需要继承自虚类object,旧式类则不需要。py2中类的写法有三种:

class A(object):新式类写法
class A():旧式类写法
class A:旧式类写法

在py3中,只有新式类,不再支持旧式类。你如果习惯继承object的写法,也完全没有问题。上面三种写法在py3中都被解释成新式类。新式类和旧式类的主要区别是:

  • 新式类可以继承object的构造函数和析构函数,如果类的构造和析构函数没有特别的工作,可以省略。而旧式类则不能:

    class A():
        def print(self):
            print("I am A")
    
    class B(A):
        def __init__(self):
            A.__init__(self)
    b = B()
    

    此时用py2运行,会出现错误:AttributeError: class A has no attribute '__init__',使用py3不会出现此错误。若改成新式类写法:

    class A(object):
        def print(self):
            print("I am A")
    
    class B(A):
        def __init__(self):
            A.__init__(self)
    b = B()
    

    仍然用py2运行,则都不会出错。

  • 新式类可以使用super:

    class A(object):
        def print(self):
            print("I am A")
    
    class B(A):
        def __init__(self):
            super().__init__()
    b = B()
    
  • 多重继承时,各父类的初始化和函数查找顺序不同:旧式类为深度优先继承,新式类为广度优先继承。

5. 静态变量和实例变量

在构造函数中定义的变量,我们称之为实例变量。实例变量只能在实例化后使用<对象名.变量名>的方式访问。静态变量一般定义在类的开始位置,独立于构造函数之外。静态变量既可以<对象名.变量名>的方式访问,也可以<类名.变量名>的方式访问。通常,类的静态变量一般用于保存类的静态属性,该属性可被类的方法使用,但不应该被类的方法修改。

>>> class A:
	static_x = 10 # 静态变量
	def __init__(self):
		self.instance_y = 5 # 实例变量

>>> a = A()
>>> a.static_x
10
>>> a.instance_y
5
>>> A.static_x
10
>>> A.instance_y
Traceback (most recent call last):
  File "<pyshell#89>", line 1, in <module>
    A.instance_y
AttributeError: type object 'A' has no attribute 'instance_y'

6. 静态函数

与其他语言的静态函数不同,python的静态函数有两种,都是用装饰器实现的:

class A:
    static_x= 10

    def __init__(self):
        self.y = 10

    @staticmethod
    def staticFuc():
        print(A.static_x)

    @classmethod
    def classFuc(cls):
        print(cls.static_x)

A.staticFuc()
A.classFuc()

staticmethod 函数不能使用self参数,因此不成访问任何成员变量,只能通过类名访问类的静态变量。
classmethod 函数也不能使用self参数,因此不成访问任何成员变量,但它有cls参数。cls参数不是对象的引用,而是类的引用,可以通过cls参数访问类的静态变量。

7. 面向对象三要素

面向对象,有三大要素:继承、封装、多态。这里面概念非常多,往往越讲越糊涂。为了不至于误导读者,我尽可能不做解释,只给出例子,请自行揣摩。

(1) 继承

如果派生类只有一个父类,就是单继承。这是最常见的类定义形式。

class Animal:
    def eat(self):
        print('我能吃')

class Brid(Animal):
    def __init__(self):
        Animal.__init__(self)

    def fly(self):
        print('我会飞')

brid = Brid()
brid.eat()
brid.fly()

如果派生类有多个父类,就是多继承。

class Deer:
    def showHorns(self):
        print('我有鹿角')

class Horse:
    def showFace(self):
        print('我有马脸')

class Cow:
    def showHoof(self):
        print('我有牛蹄')

class Donkey:
    def showTail(self):
        print('我有驴尾')

class Milu(Deer, Horse, Cow, Donkey): # 多继承派生出一个四不像类
    def __init__(self):
        Deer.__init__(self)
        Horse.__init__(self)
        Cow.__init__(self)
        Donkey.__init__(self)

milu = Milu()
milu.showHorns()
milu.showFace()
milu.showHoof()
milu.showTail()

不管是单继承还是多继承,都可以在派生类中重写父类的函数——这叫做覆盖。

(2) 封装

所谓封装,就是将类的成员变量、成员函数整合在一起,并对关键的信息进行保护或隐藏。信息保护或隐藏有三个级别:公有、保护、私有。如果你有C++的使用经验,我们先来回顾一下C++的信息隐藏规则:

  1. 公有成员:对类外部的任何代码可见
  2. 保护成员:对类外部的任何代码都不可见,但对派生类可见
  3. 私有成员:对类外部及派生类都不可见

对应这三个级别,Python 是这样定义的:

  1. 以英文字母开头的成员为公有成员
  2. 以一个下划线开头的成员为保护成员
  3. 以两个下划线开关的成员为私有成员

下面我们试试 Python 的信息保护或隐藏规则是否有效。

>>> class A(object):
    def __init__(self, a, b, c):
        self.a = 10      # 公有
        self._b = b      # 保护
        self.__c = c     # 私有

    def getA(self):      # 公有
        return self.a

    def setA(self, a):   # 公有
        self.a = a

    def getB(self):      # 公有
        return self._b

    def _setB(self, b):  # 保护
        self._b = b

    def getC(self):      # 公有
        return self.__c

    def __setC(self, c): # 私有
        self.__c = c

>>> a = A(10, 20, 30)
>>> class B(A):
	pass
>>> b = B(10, 20, 30)

试试访问公有成员:

>>> a.a
10
>>> a.getA()
10
>>> a.setA(5)
>>> a.a
5
>>> b.a
10
>>> b.getA()
10
>>> b.setA(5)
>>> b.a
5

公有成员访问规则与C++相同。先跳过保护成员,看看私有成员:

>>> a.__c
Traceback (most recent call last):
  File "<pyshell#85>", line 1, in <module>
    a.__c
AttributeError: 'A' object has no attribute '__c'
>>> a.getC()
30
>>> a.__setC(5)
Traceback (most recent call last):
  File "<pyshell#87>", line 1, in <module>
    a.__setC(5)
AttributeError: 'A' object has no attribute '__setC'
>>> b.__c
Traceback (most recent call last):
  File "<pyshell#88>", line 1, in <module>
    b.__c
AttributeError: 'B' object has no attribute '__c'
>>> b.getC()
30
>>> b.__setC()
Traceback (most recent call last):
  File "<pyshell#90>", line 1, in <module>
    b.__setC()
AttributeError: 'B' object has no attribute '__setC'

私有成员的访问规则也与C++相同。那我为什么跳过保护成员了?来试试吧:

>>> a._b
20
>>> a._setB(5)
>>> a._b
5

看到这里就已经不对了,应该只有类内部的代码和派生类能使用啊,怎么可以直接用了呢?是的,Python的保护成员访问规则与C++的确实不一样。那 Python 的保护成员是什么样的机制呢?原来,在 Python 的OOP中,保护成员公有成员没有任何区别。保护规则仅适用于 from xxx import * 这一种情况。

testA.py

class A(object):
    pass

class _B(object):
    pass

testB.py

from testA import *

a = A()
b = _B()

执行testB.py时:

Traceback (most recent call last):
  File "testB.py", line 4, in <module>
    b = _B()
NameError: name '_B' is not defined

此时,保护成员_B被保护了。但这种情况仅适用于from xxx import *这一种情况。如果testB.py这样写:

testB.py

from testA import A, _B

a = A()
b = _B()

或者:

import testA

a = testA.A()
b = testA._B()

则是没有任何问题的。

(3) 多态

当父类有多个派生类,且派生类都实现了同一个成员函数,则可以实现多态:

class H2O(object):
    def what(self):
        print("I am H2O")

class Water(H2O):
    def what(self):
        print("I am water")

class Ice(H2O):
    def what(self):
        print("I am ice")

class WaterVapor(H2O):
    def what(self):
        print("I am water vapor");

def what(obj):
    obj.what()

objs = [H2O(), Water(), Ice(), WaterVapor()]

for obj in objs:
    what(obj)

8. 抽象类

抽象类不能被实例化,只能作为父类被其它类继承,且派生类必须实现抽象类中所有的成员函数。抽象类应用场景是什么呢?我曾经做过很多下载数据的脚本插件,不同的数据源使用不同的脚本,所有这些脚本要求必须有名字相同的方法,此时,抽象类就派上用场了。

>>> import abc
>>> class A(object, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def a(self):
        pass

    @abc.abstractmethod
    def b(self):
        pass

>>> class C(A):
	def a(self):
		print("a")
		
>>> c = C()
Traceback (most recent call last):
  File "<pyshell#127>", line 1, in <module>
    c = C()
TypeError: Can't instantiate abstract class C with abstract methods b

9. 单例模式

单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。当你希望在整个系统中,某个类只能出现一个实例时(如软件配置类,无论在软件的什么地方实例化,永远都是那一个对象),单例模式就能派上用场。比如,Python 日志模块中的日志对象,或者异步通讯框架 twisted 里面的反应堆(reactor),都是典型的单例模式——尽管它们不一定是下面这种方法实现的。

python可以使用装饰器的方法使用单例模式:

def Singleton(cls):
    _instance = {}

    def _singleton(*args, **kargs):
        if cls not in _instance:
            _instance[cls] = cls(*args, **kargs)
        return _instance[cls]

    return _singleton

@Singleton
class Config(object):
    pass

cfg1 = Config()
cfg2 = Config()

print(cfg1 is cfg2)

10. 后记

近期有很多朋友通过私信咨询有关Python学习问题。为便于交流,我在CSDN的app上创建了“Python作业辅导”大本营,面向Python初学者,为大家提供咨询服务、辅导Python作业。欢迎有兴趣的同学使用微信扫码加入。

在这里插入图片描述

从博客到公众号,每一篇、每一题、每一句、每一行代码,都坚持原创,绝不复制抄袭,这是我坚守的原则。如果喜欢,请关注我的微信公众号“Python作业辅导员”。

在这里插入图片描述

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天元浪子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值