带你学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()

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

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

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

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

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

规则

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

注意:圆括号中父类的顺序,如果继承的父类中有相同的方法名,而在子类中使用时未指定,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()

在继承的那个例子的基础上,我们又在父亲类中加了一个 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()

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

# 定义类
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)

但是,在类中,我们不能用 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。但输出结果却不改变,如下

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

# 定义类
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)

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

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

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

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

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

>>> 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)

八、总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,507评论 6 13
  • 定义类并创建实例 在Python中,类通过 class 关键字定义。以 Person 为例,定义一个Person类...
    绩重KF阅读 3,872评论 0 13
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,640评论 2 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 农历正月初五,还有十天就开学了! 其实也开始顾虑起开学来,这个假期,孩子还是挺充实的,不知不觉的能自己读课外书了!...
    Givemeone阅读 263评论 0 0