01-Python 面向对象

面向对象三大特性

面向对象是一种编程思想,是以类的眼光来看待事物的一种方式。

  • 封装:将共同的属性和方法封装到同一个类下面。

    • 第一层面:创建类和对象会分别创建二者的名称空间,我们只能用 类名. 或者 obj. 的方式去访问里面的名字,这本身就是一种封装。
    • 第二层面:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
  • 继承:将多个类的共同属性和方法封装到一个父类下面,然后在用这些类来继承这个类的属性和方法。

  • 多态:Python 天生是支持多态的。指的是基类的同一个方法在不同的派生类中有着不同的功能。

    Java 中的多态,分为编译时多态和运行时多态。

    • 编译时多态:主要是通过方法的重载(overload)来实现,函数重载允许在同一个类中定义多个同名函数,但参数类型或个数不同。Java 会根据方法参数列表的不同来区分不同的方法,在编译时就能确定该执行重载方法中的哪一个。这是静态的多态,也称为静态多态性、静态绑定、前绑定。
      但也有一种特殊的方法重写的情况,属于编译时多态。在方法重写时,当对象的引用指向的是当前对象自己所属类的对象时,也是编译时多态,因为在编译阶段就能确定执行的方法到底属于哪个对象。
    • 运行时多态:主要是通过方法的重写(override)来实现,让子类继承父类并重写父类中已有的或抽象的方法。这是动态的多态,也称为”后绑定“,这是我们通常所说的多态性。

Python 面向对象中的继承有什么特点

继承的实现方式主要有 2 类:实现继承、接口继承。

  • 实现继承是指使用基类的属性和方法而无需额外编码的能力。
  • 接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力(子类重构爹类方法)。

Python 经典类和新式类

python 有两种类:经典类和新式类。

  • python3:都是新式类,默认继承 object。
    class Animal(object): 等于 class Animal:

  • python2:经典类和新式类并存。
    class Animal:经典类
    class Animal(object):新式类

继承分为单继承和多继承,Python 是支持多继承的。

如果没有指定基类,Python3 的类会默认继承 object 类,object 是所有 Python 类的基类,它提供了一些常见方法(如 __str__ )的实现。

对象可以调用自己本类和父类的所有方法和属性,先调用自己的,自己没有才调父类的。谁(对象)调用方法,方法中的 self 就指向谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Foo:  
def __init__(self):
self.func()

def func(self):
print('Foo.func')

class Son(Foo):
def func(self):
print('Son.func')

s = Son() # Son.func
# ========================================================
class A:
def get(self):
self.say()

def say(self):
print('AAAAA')

class B(A):
def say(self):
print('BBBBB')

b = B()
b.get() # 输出结果为:BBBBB

面向对象深度优先和广度优先是什么

Python 的类可以继承多个类,Python 的类如果继承了多个类,那么其寻找方法的方式有两种:

  • 当类是经典类时,多继承情况下,会按照深度优先方式查找。
  • 当类是新式类时,多继承情况下,会按照广度优先方式查找。

简单点说就是:经典类是纵向查找,新式类是横向查找。

什么是面向对象的 MRO

MRO 就是方法解析顺序。

  1. 在没有多重继承的情况下,对象执行一个方法,如果对象没有对应的方法,那么向上(父类)搜索的顺序是非常清晰的。如果向上追溯到 object 类(所有类的父类)都没有找到对应的方法,那么将会引发 AttributeError 异常。

  2. 有多重继承尤其是出现菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得依赖 MRO。

    • Python3 中的类以及 Python2 中的新式类使用C3算法来确定 MRO,它是一种类似于广度优先搜索的方法。
    • Python2 中的旧式类(经典类)使用深度优先搜索来确定 MRO。

可以使用类的 mro() 方法或 __mro__ 属性来获得类的 MRO 列表。

阅读下面的代码说出运行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A:
def who(self):
print('A', end='')

class B(A):
def who(self):
super(B, self).who()
print('B', end='')

class C(A):
def who(self):
super(C, self).who()
print('C', end='')

class D(B, C):
def who(self):
super(D, self).who()
print('D', end='')

item = D()
item.who() # ACBD

上面 D 类中的 super(D, self).who() 表示以 D 类为起点,向上搜索 self(D类对象)的 who 方法,D 类对象的 MRO 列表是 D --> B --> C --> A --> object

面向对象中 super 的作用

在使用 super 函数时,可以通过 super(类型, 对象) 来指定对 哪个对象哪个类 为起点向上搜索父类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 用于子类继承基类的方法
class FooParent(object):
def __init__(self):
self.parent = 'I\'m the parent.'
print('Parent')

def bar(self, message):
print("%s from Parent" % message)


class FooChild(FooParent):
def __init__(self):
# super(FooChild,self) 首先找到 FooChild 的父类(就是类 FooParent),然后把类 FooChild 的对象转换为类 FooParent 的对象
super(FooChild, self).__init__()
print('Child')

# def bar(self, message):
# # super(FooChild, self).bar(message)
# print('Child bar fuction')
# print(self.parent)


if __name__ == '__main__':
fooChild = FooChild()
fooChild.bar('HelloWorld')

如何判断是函数还是方法

  • 看他的调用者是谁:

    • 如果调用者是类,就需要传入一个参数 self 的值,这时他就是一个函数。
    • 如果调用者是对象,就不需要给 self 传入参数值,这时他就是一个方法。
  • 使用 isinstance 方法判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from types import FunctionType, MethodType


class Foo(object):
def __init__(self):
self.name = 'lcg'

def func(self):
print(self.name)


obj = Foo()
print(obj.func) # <bound method Foo.func of <__main__.Foo object at 0x000001ABC0F15F98>>
print(Foo.func) # <function Foo.func at 0x000001ABC1F45BF8>

print(isinstance(obj.func, FunctionType)) # False
print(isinstance(obj.func, MethodType)) # True

print(isinstance(Foo.func, FunctionType)) # True
print(isinstance(Foo.func, MethodType)) # False

静态方法(staticmethod)和类方法(classmethod)区别和应用场景 ★★★★★

  • 类方法:

    • 类对象所拥有的方法,用修饰器 @classmethod 来标识其为类方法,对于类方法,第一个参数必须是类对象,一般以 cls 作为第一个参数(当然可以用其他名称的变量作为其第一个参数,但是大部分人都习惯以 cls 作为第一个参数的名字),不需要实例化就可以使用。
    • 特点:不需要实例化即可使用,而且可以访问和修改类级别的属性。
    • 应用场景:
      1. 类级别的操作:有时你需要执行一些操作,这些操作是在类级别上进行的,而不是在实例级别。例如,你可能想要重置类的某个状态或者更新类的静态属性。
      2. 配置管理:类方法可以用来管理类的配置或设置,这些配置对于所有实例都是通用的。
      3. 工厂方法:类方法可以用作工厂方法,根据不同的参数创建不同的实例。在 Python 中,每个类只能有一个构造函数(__init__)。如果你需要根据不同的参数集创建多个构造函数,可以使用静态方法来模拟不同的构造函数。
        特别说明,静态方法也可以实现上面功能,当静态方法每次都写上类名,就不方便使用。
  • 静态方法:

    • 通过修饰器 @staticmethod 来进行修饰,静态方法不需要多定义参数,可以通过对象和类来访问,是类中的一个独立的普通函数或者说方法,类或者实例化的对象都可以直接使用它。
    • 特点:不用实例化就能用,而且又属于一个类。静态方法主要用于将函数“附加”到类,而不需要该函数与类的实例或类本身有任何特定的关联。与当前类强关联,但不希望与外界函数相混淆;
    • 应用场景:
      1. 静态方法主要用于获取一些固定的值,如获取时间、获取一些配置文件,但是不会对其进行频繁的更改,调用时直接 类.静态方法名 就好了。就是整个项目中就可以直接调用静态方法,不需要实例化,本身用类就可以调用。
      2. 工具函数:当你在类中定义了一些通用的函数,这些函数不依赖于类的实例属性,也不需要访问实例方法时,可以将它们定义为静态方法。例如,一些数学计算(sin、cos、tan)或者数据处理工具。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Num:
# 普通方法:能用 Num 调用而不能用 实例化对象 调用
def one():
print('1')

# 实例方法:能用 实例化对象 调用而不能用 Num 调用
def two(self):
print('2')

# 静态方法:能用Num和实例化对象调用
@staticmethod
def three():
print('3')

# 类方法:第一个参数cls长什么样不重要,都是指 Num 类本身,调用时将Num类作为对象隐式地传入方法
@classmethod
def go(cls):
cls.three()

Num.one() # 1
#Num.two() # TypeError: two() missing 1 required positional argument: 'self'
Num.three() # 3
Num.go() # 3

i=Num()
#i.one() # TypeError: one() takes 0 positional arguments but 1 was given
i.two() # 2
i.three() # 3
i.go() # 3

metaclass 作用以及应用场景

metaclass 用来指定类是由谁创建的。如下面创建 Foo 类的示例时调用 MyType()()

类的 metaclass 默认是 type。我们也可以指定类的 metaclass 值。在 Python3 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyType(type):
def __call__(self, *args, **kwargs):
return 'MyType'


class Foo(object, metaclass=MyType):
def __init__(self):
return 'init'

def __new__(cls, *args, **kwargs):
return cls.__init__(cls)

def __call__(self, *args, **kwargs):
return 'call'


obj = Foo()
print(obj) # MyType

用尽量多的方法实现单例模式

单例模式是一种常用的软件设计模式。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。

如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 1、使用__new__方法
class Singleton(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
orig = super(Singleton, cls) # 其实就是object
cls._instance = orig.__new__(cls, *args, **kwargs)
return cls._instance

class MyClass(Singleton):
a = 1


# 2、共享属性
# 创建实例时把所有实例的__dict__指向同一个字典,这样它们具有相同的属性和方法.
class Borg(object):
_state = {}
def __new__(cls, *args, **kw):
ob = super(Borg, cls).__new__(cls, *args, **kw)
ob.__dict__ = cls._state
return ob
class MyClass2(Borg):
a = 1


# 3、装饰器版本
def singleton(cls, *args, **kw):
instances = {}
def getinstance():
if cls not in instances:
instances[cls] = cls(*args, **kw)
return instances[cls]
return getinstance

@singleton
class MyClass:
...


# 4、import方法:作为python的模块是天然的单例模式
# mysingleton.py
class My_Singleton(object):
def foo(self):
pass
my_singleton = My_Singleton()

# to use
# from mysingleton import my_singleton
my_singleton.foo()

单例模式应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。
再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。

property

property() 函数的作用是在新式类中返回属性值。可以对应于某个方法,希望能够像调用属性一样来调用方法,此时可以将一个方法加上 property。

定义 property 属性共有两种方式,分别是【装饰器】和【类属性】,而【装饰器】方式针对经典类和新式类又有所不同。下面分别展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 类属性方式
class C(object):
def __init__(self):
self._x = None

def getx(self):
return self._x

def setx(self, value):
self._x = value

def delx(self):
del self._x

x = property(getx, setx, delx, "I'm the 'x' property.")


# 如果 c 是 C 的实例化, c.x 将触发 getter, c.x = value 将触发 setter, del c.x 将触发 deleter。
c = C()
c.x = 1
print(c.x) # 1


# 装饰器方式
# property 属性的定义和调用要注意:定义时,在实例方法的基础上添加 @property 装饰器,仅有一个 self 参数调用时,无需括号。
class C(object):
def __init__(self):
self._x = None

@property
def x(self):
"""I'm the 'x' property."""
return self._x

@x.setter
def x(self, value):
self._x = value

@x.deleter
def x(self):
del self._x

Python 中为什么没有函数重载

C++、Java、C# 等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。

这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解 Python 是动态类型语言,是否知道 Python 中函数的可变参数、关键字参数这些概念。

首先 Python 是解释型语言,函数重载现象通常出现在编译型语言中。其次 Python 是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者 Python 中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。

魔法方法(魔法方法|双下划线方法) ★★★★★

列举面向对象中带双下划线的特殊方法,如:__new____init__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
__iter__:用于迭代器,之所以列表、字典、元组可以进行for循环,是因为类型内部定义了 __iter__
__next__:用于迭代器。

__new__:生成实例
__init__:生成实例的属性,构造方法,通过类创建对象时,自动触发执行。
__del__:析构方法,当对象在内存中被释放时,自动触发执行。如当 del obj 或者应用程序运行完毕时,执行该方法里边的内容。

__call__:实例对象加( )会执行 def __call__() 方法。

__enter____exit__:使用 with 语句,对象的 __enter__() 被触发,有返回值则赋值给 as 声明的变量;with 中代码块执行完毕时执行 __exit__() 里边的内容。

__str__:改变对象的字符串显示。print(obj) ---> obj.__str__()。如果一个类中定义了__str__方法,那么在打印对象时,默认输出该方法的返回值。
__repr__:改变对象的字符串显示。交互式解释器 --->obj.__repr__()
__format__:自定制格式化字符串

__doc__:表示类的描述信息,该描述信息无法被继。
__module__:表示当前操作的对象在那个模块
__class__:表示当前操作的对象的类是什么

__slots__:一个类变量 用来限制实例可以添加的属性的数量和类型
__dict__:显示类或对象中的所有成员。
__setitem__,__getitem,__delitem__:用于索引操作,如字典。以上分别表示获取、设置、删除数据。
__hash__
__setattr__,__delattr__
__lt__, __le__, __eq__, __ne__, __gt__, __ge__:比较的时候会用到

class Foo:
def __init__(self,name):
self.name=name

def __getitem__(self, item):
print(self.__dict__[item])

def __setitem__(self, key, value):
self.__dict__[key]=value

def __delitem__(self, key):
print('del obj[key]时,我执行')
self.__dict__.pop(key)

def __delattr__(self, item):
print('del obj.key时,我执行')
self.__dict__.pop(item)

f1=Foo('sb')
f1['age']=18
f1['age1']=19
del f1.age1
del f1['age']
f1['name']='alex'
print(f1.__dict__)
__get__():调用一个属性时,触发
__set__():为一个属性赋值时,触发
__delete__():采用del删除属性时,触发

__init__()__new__() 方法有什么区别

Python 中调用构造器创建对象属于两阶段构造过程:

  • 首先执行 __new__(cls, *args, **kwargs) 方法获得对象所需的内存空间,第一个参数 cls 是当前正在实例化的类。

    如果要得到当前类的实例,应当在当前类中的 new() 方法语句中调用当前类的父类的 new() 方法。如果当前类是直接继承自 object,那当前类的 new() 方法返回的对象应该为:

    1
    2
    3
    def __new__(cls, *args, **kwargs):  
    ...
    return object.__new__(cls)
    • 如果新式类中没有重写 new() 方法,Python 默认是调用该类的直接父类的 new() 方法来构造该类的实例,如果该类的直接父类也没有重写 new(),那么将一直追溯至 object 的 new() 方法,因为 object 是所有新式类的基类。
    • 如果新式类中重写了 new() 方法,那么你可以自由选择任意一个的其他的新式类(必定要是新式类,只有新式类必定都有 new(),因为所有新式类都是 object 的后代,而经典类则没有 new() 方法)的 new() 方法来制造实例,包括这个新式类的所有前代类和后代类,只要它们不会造成递归死循环。
  • 再通过 __init__() 执行对内存空间数据的填充(对象属性的初始化)。

__new__() 方法的返回值是创建好的 Python 对象(的引用),而 __init__() 方法的第一个参数就是这个对象(的引用),所以在 __init__() 中可以完成对对象的初始化操作。

注意:__new__ 是类方法,它的第一个参数是类,__init__ 是对象方法,它的第一个参数是对象。

运行下面的代码是否会报错,如果报错请说明哪里有什么样的错,如果不报错请说出代码的执行结果。

1
2
3
4
5
6
7
8
9
10
11
12
class A: 
def __init__(self, value):
self.__value = value

@property
def value(self):
return self.__value

obj = A(1)
obj.__value = 2
print(obj.value) # 1
print(obj.__value) # 2

点评:这道题有两个考察点,一个考察点是对 ___ 开头的对象属性访问权限以及 @property 装饰器的了解,另外一个考察的点是对动态语言的理解,不需要过多的解释。

扩展:如果不希望代码运行时动态的给对象添加新属性,可以在定义类时使用 __slots__ 魔法。例如,我们可以在上面的 A 中添加一行 __slots__ = ('__value', ),再次运行上面的代码,将会在原来的第 10 行处产生 AttributeError 错误。


01-Python 面向对象
https://flepeng.github.io/interview-20-开发语言类-21-Python-01-Python-面向对象/
作者
Lepeng
发布于
2020年8月8日
许可协议