学习面向对象,如果一开始就从理论说起,可能会非常难以理解。所以,我尝试从语法层面说起,这样可能更好理解一些。
什么是类
在说面向对象之前,需要先从类( Class ) 说起,因为是先有类,再有对象的。我们可以创建一个类:
class Person:
name = 'Bob'
age = 14
def go():
print('go')
比如 name 、 age 都是类的属性,也可以理解成类当中的变量(叫做属性)。类当中还有一个函数(这样的说法不准确,类当中的函数叫做方法) ,叫做 go 。
通过上面这个例子,我们可以来给类从语法层面下一个定义了:
所谓的类,一种数据类型,包含变量和方法。
说类也是一种数据类型,就像列表一样。我们来看看如何声明一个列表:
pers = ['bob', 'june']
你看,列表是一组数据的集合。那么对象呢?和列表不一样的是,它是变量(属性)和函数(方法)的集合。我们访问列表中的元素:
print(pers[0]) # bob
我们访问对象中的属性:
print(Person.name)
所以,从语法层面而言,不要把类看得很特殊,其实和列表、字典之类的数据类型没什么区别。 所以,我们说类本质上只是一种数据类型罢了。 如果是面试,说的好听点: 从数据结构的角度来看, 类是对数据和行为的封装 。
什么是对象
对象是从哪里来的?不是来源同桌,也不是来源于婚恋网站哦。在编程中,对象的来源只有一种,通过类生成一个对象(也叫做实例化)。所以,从语法上来说,对象是对类的拷贝。
这实例化的概念,其实和现实世界不一样。因为现实世界中,我们是先有人,然后根据人的共同特征行为抽象出一个人类的概念。而在编程中,是先有类,然后根据类复制出一个具体的人。所以,这一点是和现实世界相反的。
然后实例化的语法如下:
class Person:
name = ''
bob = Person()
单纯是类其实没什么意义,只有将类编程具体的人才是又意义。因为这个世界说到底,人类这个词还是由具体的某个人发明的。
什么是面向对象编程
从语法层面来说,Python 中的一切元素都是对象。其他的类型也是对象的一种子类型,举个例子,字符串也是一种对象:
message = 'String'
print(message.upper()) # STRING
从上面这个例子中,我们可以发现字符串也有行为(方法)。所以,什么是面向对象编程? 从语法层面来说,把编程语言中所有的实体(变量、类型、方法、表达式等)都作为对象, 然后我们使用各种对象进行编程。我们将这种做法称之为面向对象编程 。
什么是构造方法
一个类当中,只有两种东西,属性和方法,没有其他。很明显构造方法也是类的一个方法。而且构造方法也是一个特殊的方法。为什么说 构造方法是特殊的方法,因为这个方法由特殊的语法作用: 一般用来初始化对象 。
当我们通过一个类复制出一个对象的时候(实例化), Python 就会去主动运行这个构造方法。我们自己定义的方法,必须在程序中手动的调用,而构造方法会在实例化的时候,由 Python 语言本身(解释器) 去执行它,而且构造方法的方法名称,必须是 __init__ 。这就是它的特殊之处,用代码来演示:
class Person:
name=""
def __init__(self, name):
print(name)
def say():
print("say")
Person("Bob")
执行这段程序,只会输出 Bob ,但是在程序中,我们并没有手动的去调用 __init__ 这个方法,这说明 Python 在 Person('Bob') 实例化的时候,会自动执行 __init__ 方法。正如这个构造方法的方法名字(init)一样, 我们通常会利用它会在实例化的时候自动执行这个特性,去初始化这个类的属性 。
实际上,我们练习一下现实生活,每个孩子生下(实例化)的时候,就有双手双脚双眼双嘴(属性)。实际上,正是为了模拟现实中的这种情况,所以会有一个构造方法,来做这件事情。
切记: 编程是用来模拟现实的,在模拟现实的基础上,解决一些现实问题, 所以在学习编程的过程中,要时常地联系现实生活。
类变量和实例变量
我们首先来定义一个类:
class Duck:
# 定义在这个位置的称之为类变量
age = 2
def __init_(self, name):
# 定义在这个位置,称之为实例变量
self.name = name
关于类变量和实例变量,Python 语言和其他语言是很不一样的。Python 是根据定义的位置加以区分的,而其他语言可能会有特定的的关键字加以区分。下面以 PHP 为例:
class Duck {
static $age = 2; ## 类变量
$name = ''; ## 实例变量
}
所以,关于类变量和实例变量,不能参照其他语言的经验。
当我们去打印一个对象的变量的时候,会按照如下顺序去查找:
- 先查找实例变量
- 如果实例变量没有找到,会查找类变量
- 如果类变量没有找到会查找继承的变量
类或实例中地方法,在大多数时候是为了改变类的数据(也称之为类的状态)。所以,如何在类的方法中访问类变量或者实例变量呢?
class DuckDuckGo:
url = ''
def __init__(self, method = 'POST'):
# 访问实例变量
print(self.method)
# 访问类变量
print(DuckDuckGo.url)
print(self.__class__.url)
实例方法、类方法和静态方法
实例方法
什么是实例方法, 就是我们在类方法中,将第一个参数定义为 self (也可以是其他的单词,比如说 this , 但 Python 推荐我们使用 self这个单词)。
Python 会在我们调用这个方法时候,给个名为 self 的变量赋值,这个值就是当前调用这个方法的对象的实例。
如果有其他的编程语言的经验,其他语言中会使用 this 这个关键字来表示当前实例,Python 也一样只是需要在方法声明的第一个传参显式地定义为 self 。
class DuckDuckGo:
def search(self):
pass
类方法
类方法的定义如下, 使用 @classmethod 注解来声明一个方法是类方法:
class DuckDuckGo:
url = 'https://www.duckduckgo.com'
@classmethod
def settings(cls):
print(cls.url)
和实例方法要在传参的列表中加入 self 一样,类方法也要在传参列表中添加一个名为 cls 的传参。这个名字也是约定俗成的,虽然也可以使用其他的名字。
调用类方法如下:
DuckDuckGo.settings()
类方法和实例方法的区别
那么类方法和实例方法有什么区别呢?**类方法关联的是类本身,而实例方法关联的是实例本身。**这句话是不是好理解?我举一个例子: 当我们和别人谈论中午吃什么的时候,指的是我自己中午吃什么。而当国家领导人和欧盟主席谈论国际粮草供应的时候,讨论的是人类吃什么,而不是他们两个人中午吃什么。
怎么什么时候用对象方法,什么时候用实例方法呢?当你想要改变实例变量的时候,用实例方法。当你想要改变类变量的时候,使用类方法。
注意: 在 Python 中,我们可以在实例方法中,改变类变量,也可以使用实例对象来调用类方法。但是并不建议这么做,虽然语法上允许这么做,但是违反了类和对象实例的语义。
静态方法
静态方法的定义语法如下:
class DuckDuckGo:
url = 'https://www.duckduck.com'
@staticmethod
def ping():
print(DuckDuckGo.url)
那么静态方法和类方法以及实例方法的区别?
最大的不同在于,静态方法并没有向类方法和实例方法一样要传入一个类似于 self 和 cls 这样的传参。另外,静态方法,可以同时通过类或者实例对象取调用。
DuckDuckGo.ping()
ddk = DuckDuckGo()
ddk.ping()
类方法和静态方法都是可以使用类变量,但是不能调用对象方法。
在其他的语言中,静态方法和类方法是一样的。但是在 Python 中,将其区分开来了。**并不建议在实际的编程中使用静态方法,因为它和类以及对象的关联是很弱的。**基本上,静态方法都可以使用类方法代替。
属性和方法的可见性
在面向对象的设计中,属性和方法是可以设置可见性的(你可以理解成,我不想让别人看到我的心)。在 Python中,可见性分为 Public(公开), Private(私有) 两种。
区别与其他的语言,并没有类似于 Public 或者 Private 这样的关键字的修饰,而是使用双下划线的方式区分是否为公开或私有的属性方法:
class DuckDuckGo:
__url = 'https://www.duckduckgo.com'
def __dns():
pass
如果你调用私有的 __dns 方法,Python 会给出错误的提示:
>>> DuckDuckGo.__dns()
Traceback (most recent call last):
File "test.py", line 9, in <module>
DuckDuckGo.__dns()
AttributeError: type object 'DuckDuckGo' has no attribute '__dns'
如果你调用私有的变量,同样也会报错:
>>> print(DuckDuckGo.__url)
Traceback (most recent call last):
File "test.py", line 9, in <module>
print(DuckDuckGo.__url)
AttributeError: type object 'DuckDuckGo' has no attribute '__url'
我们可以通过打印对象的 __dict__ 来查看对象的所有的成员变量:
>>> print(DuckDuckGo.__dict__)
...省略...'_DuckDuckGo__url': 'https://www.duckduckgo.com', '_DuckDuckGo__dns': <function DuckDuckGo.__dns at 0x7f27771ae0d0>
从上面的输出可以看到,Python 内部处理私有的属性和方法,是将我们的属性和方法重命名为 _类名+变量/方法名 , 例如 _DuckDuckGo__url 和 _DuckDuckGo__dns 。
所以,其实没有什么是不能访问的,私有的也是可以访问的。之所以我们访问的时候报错,是因为 Python 将我们的私有变量重命名了。如果要访问,也是访问重命名之后的变量或方法:
>>> print(DuckDuckGo._DuckDuckGo__url)
https://www.duckduckgo.com
所以,严格意义上来讲,Python 并没有对成员变量或方法的可见性设置一种可靠的机制来禁止其他实体的对其进行访问。
**继承 **
什么是继承
其实我们在之前地例子中,我们已经使用过继承了。我们自己创建了一个类 Person , 在这个类中有一个特殊地方法,我们叫它为构造方法 __init__ 。那么这个名为构造方法地方法是从哪里来地呢?是从继承得到的。那么它是继承谁的呢?继承一个名为 object 的对象的。这个对象是 Python 语言本身提供的对象,所有其他的对象都默认继承这个对象。
class Person(object):
pass
class Person:
pass
上面两种写法是一样,默认情况下,我们创建的类就是继承 object 类的。object 类当中声明了一些类似于 _ init_ 这样的方法,这些方法都会在特定的时候,由 Python 语言本身去调用。
继承这个概念,也是来源于生活的。我一生下来,就继承了我爸爸妈妈的部分基因 ,这也是编程中的继承在语法层面的含义 。
我们再来举一个例子:
class Person(object):
def say(self):
print('say')
class Father(Person):
def teach_kids():
print('teach')
bob = Father()
bob.say() # say
这个程序运行之后,输出了 say 。可是我们在 Father 这个类中,并没有声明一个 say 的方法,却可以调用,这是为什么呢?因为 Father 这个类继承了 Person 这个类,而 Person 这个类声明了 say 这个方法,被 Father 类继承。 所以,我们说继承从语法层面来说,是为了实现类的属性或方法的复用 。
然后,这个 Father 类自己还有一个名为 teach_kids 的方法,意思是教育孩子。Father 不光是继承了 Person 类的 say 方法,而且还延申扩展出了教育孩子的行为能力。 所以,继承还能扩展类的能力,这也是符合现实情况的,我们有句话叫做青出于蓝而胜于蓝,就是这样的道理 。
什么是重写
上面的例子中,Person 拥有 say 方法,而 Father 自动继承了 Person 的 say 方法。那么如果 Person 有 say 方法, Father 自己也有 say 方法呢?
class Person(object):
def say(self):
print('Person say')
class Father(Person):
def say(self):
print('Father say')
my_father = Father()
my_father.say() # 这里输出是 Person say 还是 Father say 呢?
当然,输出的是 Father say 啦。 因为如果父类(Person) 和子类(Father) 都有相同一个方法,那么子类中的方法会重写父类中的方法 。
这样例子,在现实生活中,也比比皆是。举个例子: 我父亲的足球踢得很好,而我并没有继承我父亲的这个基因,虽然也会踢足球,但是提的不好。所以,没有道理,我一定要和我父亲一样能踢好足球。说到底,我虽然继承了我父亲的一些基因,但是我终究是我自己,以我自己的实际情况为准,而不是父亲的复制品。
站在巨人(super)的肩膀上
“如果我看得比别人更远,那是因为我站在巨人的肩膀上。”这是牛顿说的。有时候,我们利用继承可以站在巨人的肩膀上,利用巨人的能力使得自己做的更好。我们用代码来说明:
class SuperMan(object):
def see(self):
print('I see the darkness')
class My(SuperMan):
def see(self):
super().see()
print('I saw the light')
my = My()
my.see()
运行这个程序,输出结果如下:
I see the darkness
I saw the light
SuperMan 拥有一个方法,叫做 see,这个方法输出一句话:I see the darkness(我看透了黑暗)。My 继承了 SuperMan 并且重写了这个 see 的方法,在我这个 see 的方法中,我可以使用 super().see() 这样的语法调用我继承的父类,也就是 SuperMan 的方法,并且输出一句话: I saw the light(我看到了光明)。
这就是我说的,站在巨人的肩膀上,不光看透了黑暗,还看到了光明。
重写和super的语法意义
首先重写和 supper 的特性都是基于继承的。
重写的意义在于,如果子类和父类都具有相同的一个方法,那么子类的方法会重写父类,换句话说, 子类调用自己的方法的时候,不会去执行父类中的同名方法 。
而 super 呢?它的语法意义在于,它可以让子类自己选择是否继承并使用父类的方法,换句话说, super 这个关键字使得子类可以使用父类的能力,并且做更多的事情 。
多重继承
在现实的生活中,多重继承是在正常不过的行为。比如我爸妈生了我,我继承了我爸妈各自的部分基因。
但是,现在大多数实现了面向对象思想的编程语言都放弃实现多重继承的特性,比如 Java、PHP 都不支持多重继承。
Python 是支持多重继承的,但大多数的人都认为要避免使用多重继承,因为这比较麻烦。举个例子,你同时继承了你妈妈的个性和你爸爸的个性,而你自己有没有衍生出自己的个性,那么你以谁的个性为准?是爸爸的,还是妈妈的?
所以,在实际的编程中,也无需使用多重继承,至于其语法,不了解也没有关系。
总结
继承是为了代码的复用,而重写是为了复用的同时不影响子类自己的发展,而 super 则是允许子类在自己选择是否保留父类的能力,还能展现自己的个性 。
魔法方法(Special Method)
魔法方法,这个名字有两个词语组成,分别是魔法和方法。
有的人会称之为魔法函数,这是不准确的。因为函数是定义在类之外的,而魔法方法只能是定义在类当中的。在 Python 的官方文档中,写的是 Special Method (其日文是 特殊メソッド),不是 Special Function, 所以应该翻译成魔法方法,而不是魔法函数。
什么是魔法?魔法就是一些看上去很神奇的操作或事情,换句话说就是普通函数无法实现的能力。在对象中定义的函数称之为方法,而魔法方法一定是定义在对象中的。
其实魔法方法,还具有一个特性: 不需要我们在代码中主动的调用,而是在某些特定时刻由 Python 解释器调用的方法 。上面的例子中的 __init__ 即使如此。
class Person(Object):
def __init__(self, name):
self.name = name
然后当我们实例化这个对象的时候,这个 __init__ 就会自动执行,为 self.name 赋值。
然后再举一个魔法方法的例子:
class Person(Object):
def __init__(self, name):
self.name = name
def __repl(self, name):
return "My name is " + self.name
当我们打印这个类的实例化对象的时候,就会自动执行 __repl__ 魔法方法,并输出 My name is ......:
p = Person('admin')
print(p) ## My name is admin
国内是翻译成魔法方法,但是其日文是 特殊メソッド , 翻译成中文是 特殊方法 ,而翻译成魔法方法不是非常好理解。说是特殊方法比较好理解,特殊在哪里呢?
- 必须出现在类当中,方法名以双下划线开头,以双下划线结尾
- 无需手动调用,由 Python 解释器在特定的时机去调用
- 实现特定的功能,比如因为
__init__在对象被实例化的时候被执行,所以非常适合做一些初始化属性的事情
Python 中由很多类似的魔法方法,无需一次性记住,但是需要知道其原理以及应用。在其他的弱类型语言,比如 PHP 中,也有类似的实现。这样的实现是为了增加代码的灵活性。