带你学python基础:面向对象编程

面向对象编程是个啥呢,其实,在传统的语言中,比如 C 语言,是不存在面向对象编程这个概念的,那时候的语言只有面向过程编程,也就是我们写代码从头写到底,最多也就是有函数。所以,这样的代码风格是比较难维护的。

后来,随着编程语言的改进,在很多的语言都有了面向对象的思想,比如 C++、Java、C#等,而 Python也是如此。

一、那什么是面向对象呢?

拿个简单的例子说说,比如我们一个人,有头、身体、腿、手等,这些东西在面向对象的思想中,都可以把他们拆分为一个一个的对象,而不会把人就看做一个对象。

在我们经常玩的游戏中,每一个英雄,每一个兵器,都是一个个的对象,每个事物都是对象。

在面向对象编程中,我们还会谈到另外一个概念:

那么什么是类呢,类是一个抽象的概念,我们知道有动物,动物下面有各种各样不同的动物,狗,老虎等。所以, 是就是动物,也就是不同类型的动物的总称,也是抽象。而 对象就是具体的类别的动物。

类是对象的类型,具有相同属性和行为事物的统称。类是抽象的,在使用的时候通常会找到这个类的一个具体存在。

万物皆对象,对象拥有自己的特征行为

打个比方,我们每个人都可以看做是一个对象,而我们每个人都有我们自己的不同的特征,同时,我们也会产生我们的各种各样的行为。

这个图是不是看了就知道对象的特性了。

相信讲了这么多了,我们应该知道什么是类和对象了。下面我们讲一下,如何定义类。

二、定义类

首先,我们通过一个案例来说说如何定义类,最后再给出定义类的方法。


# 定义类
class pen():
    def __init__(self, str, len):
        self.str = str
        self.len = len  # 实例变量通过init初始化声明

    # 定义类变量
    width = 5

    '''
    获取信息
    '''
    def getStr(self):
        print('str:%s,len:%s' % (self.str, self.len))
        print('width:', pen.width)


pen = pen('初始化', 10)
pen.getStr()

上面定义了一个类,这个类名为 pen,然后,我们在类中定义了它的特征属性 strlen,同时,我们还定义了一个行为(获取信息)。

通过这个例子,我们就可以看出怎么定义类的。

定义类规则


class 类名:
    属性列表
    方法列表

在上面这个例子中,我们发现这里存在两种变量,一种是实例属性,一种是类属性。下面我们就说说这两种变量有什么区别。

  • 类变量:也可以说类属性,类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。如果需要用在函数中使用 类名.类属性访问,如例子中的 width = 5
  • 实例变量:也可以说实例属性,定义在方法中的变量,只作用于当前实例的类, 如例子中的 len

好了,我们知道怎么定义类和定义类变量和实例变量,那么如何访问这些变量呢?

三、访问变量

方法


实例对象.属性

举例
例如,我们需要访问上面的pen的变量,则可以使用下面的方式。


# 访问变量
print(pen.len)
print(pen.width)
print(pen.str)

当然,你可能会想,还有其他方式吗,确实,还有其他方式,Python也提供了类似JavaScript的访问方式。


getattr(obj, name[, default]) #访问对象的属性
hasattr(obj,name) # 检查是否存在一个属性
setattr(obj,name,value) # 设置一个属性。如果属性不存在,会创建一个新属性
delattr(obj, name) # 删除属性

举例


# -*- coding:utf-8 -*-

# 定义类
class pen():
    def __init__(self, str, len):
        self.str = str
        self.len = len  # 实例变量通过init初始化声明

    # 定义类变量
    width = 5

    '''
    获取信息
    '''

    def getStr(self):
        print('str:%s,len:%s' % (self.str, self.len))
        print('width:', pen.width)


pen = pen('初始化', 10)

# 通过内置方法访问属性
print(getattr(pen, 'len'))
print(hasattr(pen, 'len'))

setattr(pen, 'len', 20)
print(pen.len)

delattr(pen, 'len')
print(pen.len)

内置类属性

另外,Python本身还提供了自己内置的类属性,分别有下面这些。


__dict__ : 类的属性(包含一个字典,由类的属性名:值组成) 实例化类名.__dict__
__doc__ :类的文档字符串   (类名.) 实例化类名.__doc__
__name__: 类名,实现方式 类名.__name__
__bases__ : 类的所有父类构成元素(包含了以个由所有父类组成的元组)

举例
我们还是以上面的例子来讲


print(pen.__dict__)  #会将实例对象的属性和值通过字典的形式返回
print(pen.__doc__)

特殊说明

在前面的例子中,我们看到了 initself这两个关键字,下面讲解一下。

__init__():是一个特殊的方法属于类的专有方法,被称为类的构造函数或初始化方法,方法的前面和后面都有两个下划线。

这是为了避免Python默认方法和普通方法发生名称的冲突。每当创建类的实例化对象的时候, __init__()方法都会默认被运行。作用就是初始化已实例化后的对象,这就是构造函数的意思。

在方法定义中,第一个参数 self是必不可少的。类的方法和普通的函数的区别就是self,self并不是Python的关键字,你完全可以用其他单词取代他,只是按照惯例和标准的规定, 推荐使用self

既然是面向对象编程,那么,接下来肯定要说一下面向对象的三大特性了。

四、面向对象的三大特性

封装

封装这个特性,其实在前面就已经接触到了,只是没有明白的说而已。

封装字面上的意思就是把东西包裹起来,那么,在面向对象的编程中,其实封装也就是这个意思,常见的,比如,前面我们说的的类 class,我们把一些对象的属性和行为包裹在一个类里面,这就是 封装的特性

继承

我们都知道,我们有父子关系,很多时候,儿子都会去继承父亲的财产的,好像都是这样的吧,哈哈。

在面向对象的编程中也是这么个意思,但是不叫父亲和儿子,我们把父亲叫做父类,儿子称为子类,我们子类去继承父类的财产,这个就是一个 继承的特性

那我们如何用 Python 来表达这种关系呢,下面,我们用一个例子先讲一下,后面再讲规则。

父亲,儿子和女儿的故事
```

定义类

class Father():
‘’’
定义一个父亲类
‘’’


def __init__(self, money, house):
    self.money = money
    self.house = house

def wealth(self):
    print('父亲给我 %d w, %d 套房子' % (self.money, self.house))

class Son(Father):
‘’’
定义一个儿子类,继承自父亲类
‘’’


def __init__(self, money, house):
    super().__init__(money, house)

class Daughter(Father):
‘’’
定义一个女儿类,继承自父亲类
‘’’


def __init__(self, money, house):
    super().__init__(money, house)

上面定义了一个父亲类,一个儿子类,一个女儿类。

儿子类和女儿类都继承自父亲类,所以,他们就拥有了父亲的财,在编程中也就是拥有了变量和方法。

son = Son(100, 10)
daughter = Daughter(200, 5)

继承自父亲类,所以自己不定义这个wealth方法,也会自动继承这个方法

son.wealth()
daughter.wealth()


![](https://upload-images.jianshu.io/upload_images/5824016-84b9cb813db10b33.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


通过这个例子,定义了一个父亲类,一个儿子类,一个女儿类。

儿子类和女儿类都继承自父亲类,所以,他们就拥有了父亲的财,在编程中也就是拥有了变量和方法。

在上面的例子中发现,我们只要在定义类的时候,在括号()中写上父类的名字,这就是继承了。

其中,我们也要注意,一个 `super()` 方法,它的作用是用来**继承父类的属性。**

所以,下面我们就大概知道继承的规则怎么写了。

**规则**

class DerivedClassName(Base1, Base2, Base3):

.
.
.


**注意**:圆括号中父类的顺序,如果继承的父类中有相同的方法名,而在子类中使用时未指定,python将从左至右查找父类中是否包含方法,在圆括号中有多个类名时,我们称为:**多继承**。也就是说,我们从多个父类继承了。

好了,继承我们就说到这里了。下面我们再说说最后一个面向对象的特性:**多态**!

#### 多态

在谈到多态时,我们就不得不提到另外一个概念了,这个概念就是**重写**。例如,我们的容貌有一些地方是会跟父母很相像的,但是,我们也会有很多我们自己的特点。在面向对象编程里面也是这样的,我们会继承父类的特性,但是,在继承的同时,我们也会发生改变的,这就是**重写**。

那么如何实现重写呢?接着看!

定义类

class Father():
‘’’
定义一个父亲类
‘’’


def __init__(self, money, house):
    self.money = money
    self.house = house

def wealth(self):
    print('父亲给我 %d w, %d 套房子' % (self.money, self.house))

def character(self):
    print('我很高!')

class Son(Father):
‘’’
定义一个儿子类,继承自父亲类
‘’’


def __init__(self, money, house):
    super().__init__(money, house)

def character(self):
    print('我很胖!')

class Daughter(Father):
‘’’
定义一个女儿类,继承自父亲类
‘’’


def __init__(self, money, house):
    super().__init__(money, house)

def character(self):
    print('我很瘦!')

上面定义了一个父亲类,一个儿子类,一个女儿类。

儿子类和女儿类都继承自父亲类,所以,他们就拥有了父亲的财,在编程中也就是拥有了变量和方法。

father = Father(350, 20)
son = Son(100, 10)
daughter = Daughter(200, 5)

继承自父亲类,所以自己不定义这个wealth方法,也会自动继承这个方法

son.wealth()
daughter.wealth()

多态

father.character()
son.character()
daughter.character()


![](https://upload-images.jianshu.io/upload_images/5824016-e67321b942302251.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在继承的那个例子的基础上,我们又在**父亲类**中加了一个 `character` 方法,然后**儿子类和女儿类**在继承这个方法的同时,还对这个 `character` 方法进行了修改。

所以,此时,儿子类和女儿类在调用这两个方法时,显示的内容就不一样了。

也就是说,当子类和父类都存在相同的 `character()`方法时,子类的 `character()` 覆盖了父类的 `character()`,在代码运行时,会调用子类的 `character()`。

这样,我们就获得了继承的另一个好处:**多态**。

多态的**好处**就是,当我们需要传入更多的子类,例如新增 Teenagers、Adult 等时,我们只需要继承 Father 类型就可以了,而 character()方法既可以直不重写(即使用Father的),也可以重写一个特有的。这就是多态的意思。调用方只管调用,不管细节,而当我们新增一种Father的子类时,只要确保新方法编写正确,而不用管原来的代码。这就是著名的“开闭”原则:

- 对扩展开放(Open for extension):允许子类重写方法函数
- 对修改封闭(Closed for modification):不重写,直接继承父类方法函数


ok,面向对象的三大特性就讲到这里。接下来讲讲关于类的其他知识!

### 五、类属性与实例属性

我们在前面的几个例子中,我们发现,在类中我们都定义了一些属性或者说变量。那什么是类属性,什么是实例属性呢?

类属性是类本身的属性,该类的实例都能调用,而实例属性是某个具体的实例特有的属性,不会影响到类,也不会影响到其他实例。

**1.实例属性**

关于实例属性,我们只要记住下面三点就可以了。

- 在`__init__(self,...)`中初始化
- 内部调用时都需要加上self.
- 外部调用时用`对象名.属性名`调用

**2.类属性**
    
-**内部**用`类名.类属性名`调用
- **外部**既可以用`类名.类属性名`,又可以用`对象名.类属性名`来调用,但是,后者是**实例属性的值**。

下面我们看一个简单例子:

定义类

class Father():
‘’’
定义一个父亲类
‘’’
age = 40 # 定义一个类属性


def __init__(self, money, house):
    self.money = money  # 实例属性
    self.house = house

def wealth(self):
    print('父亲给我 %d w, %d 套房子,类属性: %d ,实例属性:%d' % (self.money, self.house, Father.age, self.age))

def character(self):
    print('我很高!')

father = Father(350, 20)

father.house = 4 # 修改实例变量的值,对象名.属性
Father.age = 41 # 修改类变量的值:类名.属性 或 对象名.属性(但后者修改的值是实例属性)
father.age = 42

father.wealth()



![](https://upload-images.jianshu.io/upload_images/5824016-b834ee0a985567a8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

下面,我们再用**类属性实现一个数值自增**

定义类

class Father():
‘’’
定义一个父亲类
‘’’
age = 40 # 定义一个类属性


def __init__(self, money, house):
    self.money = money  # 实例属性
    self.house = house
    Father.age += 1 # 类属性自增

father = Father(350, 20)
print(Father.age)

father2 = Father(350, 20)
print(Father.age)


![](https://upload-images.jianshu.io/upload_images/5824016-43c5373d800fdbe4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

但是,在类中,我们不能用 `self.age` 来自增,这样是不行的,就像在类外不能用`对象名.类变量`来改变值一样。

下面,我们用`self.age` 来自增试试什么效果。

-- coding:utf-8 --

定义类

class Father():
‘’’
定义一个父亲类
‘’’
age = 40 # 定义一个类属性


def __init__(self, money, house):
    self.money = money  # 实例属性
    self.house = house
    self.age += 1 # 类属性自增

father = Father(350, 20)
print(Father.age)

father2 = Father(350, 20)
print(Father.age)



这段代码就将 `Father` 改为 `self`。但输出结果却不改变,如下
![](https://upload-images.jianshu.io/upload_images/5824016-3036069c02a8873d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

接下来,我们再深入的分析一下,看类属性和实例属性到底有什么联系?

定义类

class Father():
‘’’
定义一个父亲类
‘’’
age = 40 # 定义一个类属性


def __init__(self, money, house):
    self.money = money  # 实例属性
    self.house = house

father1 = Father(350, 20)
father2 = Father(350, 20)

father1.age += 1
print(father1.age, father2.age, Father.age)
Father.age += 1
print(father1.age, father2.age, Father.age) # father2.age 因为这个实例属性不存在,所以找类属性为41
Father.age += 1
print(father1.age, father2.age, Father.age)


![](https://upload-images.jianshu.io/upload_images/5824016-ef75a9a35bdd8ec5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

从这个结果我们可以看出,**类属性自增,实例属性是不会跟着自增的,实例属性自增,每个实例属性之间也是独立的,但是,当实例属性不存在时,编译器是会去找类属性的值。**

所以说,在Python中属性的`查找机制`是**自下而上的,即首先在实例属性中查找,如果实例属性不存在,再到类属性中查找**。

### 六、访问权限(来自:https://www.cnblogs.com/Lambda721/p/6130213.html)

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。

但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的`name`、`score`属性:

bart = Student(‘Bart Simpson’, 98)
bart.score
98
bart.score = 59
bart.score
59
```

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线 __,在Python中,实例的变量名如果以 __开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:


class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))

改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问 实例变量.__name实例变量.__score了:


>>> bart = Student('Bart Simpson', 98)
>>> bart.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。

但是如果外部代码要获取name和score怎么办?可以给Student类增加 get_nameget_score这样的方法:


class Student(object):
    ...

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score

如果又要允许外部代码修改score怎么办?可以再给Student类增加 set_score方法:


class Student(object):
    ...

    def set_score(self, score):
        self.__score = score

你也许会问,原先那种直接通过 bart.score = 59也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:


class Student(object):
    ...

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

需要注意的是,在Python中,变量名类似 __xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用 __name____score__这样的变量名。

有些时候,你会看到以一个下划线开头的实例变量名,比如 _name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问 __name是因为Python解释器对外把 __name变量改成了 _Student__name,所以,仍然可以通过 _Student__name来访问 __name变量:


>>> bart._Student__name
'Bart Simpson'

但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把 __name改成不同的变量名。

总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。

最后注意下面的这种错误写法:


>>> bart = Student('Bart Simpson', 98)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'

表面上看,外部代码“成功”地设置了 __name变量,但实际上这个 __name变量和class内部的 __name变量不是一个变量!内部的 __name变量已经被Python解释器自动改成了 _Student__name,而外部代码给 bart新增了一个 __name变量。不信试试:


>>> bart.get_name() # get_name()内部返回self.__name
'Bart Simpson'

例子:


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

    def get_grade(self):
        if self.__score >= 90:
            return 'A'
        elif self.__score >= 60:
            return 'B'
        else:
            return 'C'

bart = Student('Bart Simpson', 59)
print('bart.get_name() =', bart.get_name())
bart.set_score(60)
print('bart.get_score() =', bart.get_score())

print('DO NOT use bart._Student__name:', bart._Student__name)

终于到最后一个知识点了,这篇文章写的真的久!

七、类方法与静态方法

这个知识点就说说,因为没什么好说的。

普通方法我们都知道怎么定义了。

1.普通方法


def fun_name(self,...):
    pass

2.静态方法

  • 通过装饰器 @staticmethod 装饰
  • 不能访问实例属性
  • 参数不能传入 self
  • 与类相关但是不依赖类与实例的方法

3.类方法

  • 通过装饰 @classmethod 修饰
  • 不能访问实例属性
  • 参数必须传入cls
    必须传入cls参数(此类对象和self代表实例对象),并且用此来调用类属性:cls.类属性名。

最后,再总结一下。

  • 静态方法与类方法都可以通过类或者实例来调用,其两个的特点都是不能够调用实例属性。
  • 静态方法不需要接收参数,使用 类名.类属性

下面,再举一个例子看看。


# -*- coding:utf-8 -*-

# 定义类
class Father():
    '''
        定义一个父亲类
    '''
    age = 40  # 定义一个类属性

    def __init__(self, money, house):
        self.money = money  # 实例属性
        self.house = house

    # 创建普通方法
    def getMoney(self):
        # 类属性的使用通过类名.属性名使用 这是规范
        # 私有属性在类里面使用正常使用
        print('我有:%d w' % (self.money))  # 在方法里面使用实例属性

    # 创建一个静态方法
    @staticmethod
    def aa():  # 不需要传递实例
        # 静态方法不能访问实例属性
        # 静态方法只能访问类属性
        print('我:%d 岁' % Father.age)  # 在方法里面使用实例属性

    # 类方法
    @classmethod
    def bb(cls, n):  # class  也不是关键字
        # 类方法不能访问实例属性
        cls.age = n
        print('我:%d 岁' % cls.age)  # 就用cls.类属性


father1 = Father(350, 20)
father2 = Father(350, 20)

# 通过对象来调用静态方
father1.aa()
# 通过对象来调用类方法
father1.bb(18)

# 静态方法和类方法的调用,推荐使用类名的方式去调用
# 通过类名来调用静态方法
Father.aa()
# 通过类名来调用类方法
Father.bb(18)

八、总结

这一节讲了很多,需要好好消化。


   转载规则


《带你学python基础:面向对象编程》欧阳思海 采用 知识共享署名 4.0 国际许可协议,转载请注明署名和出处! 进行许可。
 上一篇
带你学python基础:文件读写,俗称IO操作 带你学python基础:文件读写,俗称IO操作
这一节讲个挺有意思的知识,至少在我以前刚刚接触编程的时候,对于文件操作还是觉得很有意思的事情,这也许是有一种操作文件的激情吧,希望看到这篇文章的读者也会有这样的激情,说明还是很有兴趣的,当然,就算没有,可能是你的兴趣点不在这。 一、文件的打
2019-08-28
下一篇 
带你学python基础:模块和包 带你学python基础:模块和包
一、什么是模块在我们平时的开发过程中,或多或少会用到 Python 的一些内置的功能,或者说,还会用到一些第三方的库,我们用到的这些 Python 的内置的功能,和一些第三方的库,就可以说是一些模块了。 例如,我们在读写文件的时候,我们就会
2019-08-28
  目录