01-Python 面向对象
面向对象三大特性
面向对象是一种编程思想,是以类的眼光来看待事物的一种方式。
封装:将共同的属性和方法封装到同一个类下面。
- 第一层面:创建类和对象会分别创建二者的名称空间,我们只能用
类名.
或者obj.
的方式去访问里面的名字,这本身就是一种封装。 - 第二层面:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
- 第一层面:创建类和对象会分别创建二者的名称空间,我们只能用
继承:将多个类的共同属性和方法封装到一个父类下面,然后在用这些类来继承这个类的属性和方法。
多态:Python 天生是支持多态的。指的是基类的同一个方法在不同的派生类中有着不同的功能。
Java 中的多态,分为编译时多态和运行时多态。
- 编译时多态:主要是通过方法的重载(overload)来实现,函数重载允许在同一个类中定义多个同名函数,但参数类型或个数不同。Java 会根据方法参数列表的不同来区分不同的方法,在编译时就能确定该执行重载方法中的哪一个。这是静态的多态,也称为静态多态性、静态绑定、前绑定。
但也有一种特殊的方法重写的情况,属于编译时多态。在方法重写时,当对象的引用指向的是当前对象自己所属类的对象时,也是编译时多态,因为在编译阶段就能确定执行的方法到底属于哪个对象。 - 运行时多态:主要是通过方法的重写(override)来实现,让子类继承父类并重写父类中已有的或抽象的方法。这是动态的多态,也称为”后绑定“,这是我们通常所说的多态性。
- 编译时多态:主要是通过方法的重载(overload)来实现,函数重载允许在同一个类中定义多个同名函数,但参数类型或个数不同。Java 会根据方法参数列表的不同来区分不同的方法,在编译时就能确定该执行重载方法中的哪一个。这是静态的多态,也称为静态多态性、静态绑定、前绑定。
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 |
|
面向对象深度优先和广度优先是什么
Python 的类可以继承多个类,Python 的类如果继承了多个类,那么其寻找方法的方式有两种:
- 当类是经典类时,多继承情况下,会按照深度优先方式查找。
- 当类是新式类时,多继承情况下,会按照广度优先方式查找。
简单点说就是:经典类是纵向查找,新式类是横向查找。
什么是面向对象的 MRO
MRO 就是方法解析顺序。
在没有多重继承的情况下,对象执行一个方法,如果对象没有对应的方法,那么向上(父类)搜索的顺序是非常清晰的。如果向上追溯到
object
类(所有类的父类)都没有找到对应的方法,那么将会引发AttributeError
异常。有多重继承尤其是出现菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得依赖 MRO。
- Python3 中的类以及 Python2 中的新式类使用C3算法来确定 MRO,它是一种类似于广度优先搜索的方法。
- Python2 中的旧式类(经典类)使用深度优先搜索来确定 MRO。
可以使用类的 mro()
方法或 __mro__
属性来获得类的 MRO 列表。
阅读下面的代码说出运行结果。
1 |
|
上面 D
类中的 super(D, self).who()
表示以 D 类为起点,向上搜索 self
(D类对象)的 who
方法,D
类对象的 MRO 列表是 D --> B --> C --> A --> object
。
面向对象中 super 的作用
在使用 super
函数时,可以通过 super(类型, 对象)
来指定对 哪个对象 以 哪个类 为起点向上搜索父类方法。
1 |
|
如何判断是函数还是方法
看他的调用者是谁:
- 如果调用者是类,就需要传入一个参数 self 的值,这时他就是一个函数。
- 如果调用者是对象,就不需要给 self 传入参数值,这时他就是一个方法。
使用
isinstance
方法判断。
1 |
|
静态方法(staticmethod)和类方法(classmethod)区别和应用场景 ★★★★★
类方法:
- 类对象所拥有的方法,用修饰器
@classmethod
来标识其为类方法,对于类方法,第一个参数必须是类对象,一般以cls
作为第一个参数(当然可以用其他名称的变量作为其第一个参数,但是大部分人都习惯以cls
作为第一个参数的名字),不需要实例化就可以使用。 - 特点:不需要实例化即可使用,而且可以访问和修改类级别的属性。
- 应用场景:
- 类级别的操作:有时你需要执行一些操作,这些操作是在类级别上进行的,而不是在实例级别。例如,你可能想要重置类的某个状态或者更新类的静态属性。
- 配置管理:类方法可以用来管理类的配置或设置,这些配置对于所有实例都是通用的。
- 工厂方法:类方法可以用作工厂方法,根据不同的参数创建不同的实例。在 Python 中,每个类只能有一个构造函数(
__init__
)。如果你需要根据不同的参数集创建多个构造函数,可以使用静态方法来模拟不同的构造函数。
特别说明,静态方法也可以实现上面功能,当静态方法每次都写上类名,就不方便使用。
- 类对象所拥有的方法,用修饰器
静态方法:
- 通过修饰器
@staticmethod
来进行修饰,静态方法不需要多定义参数,可以通过对象和类来访问,是类中的一个独立的普通函数或者说方法,类或者实例化的对象都可以直接使用它。 - 特点:不用实例化就能用,而且又属于一个类。静态方法主要用于将函数“附加”到类,而不需要该函数与类的实例或类本身有任何特定的关联。与当前类强关联,但不希望与外界函数相混淆;
- 应用场景:
- 静态方法主要用于获取一些固定的值,如获取时间、获取一些配置文件,但是不会对其进行频繁的更改,调用时直接
类.静态方法名
就好了。就是整个项目中就可以直接调用静态方法,不需要实例化,本身用类就可以调用。 - 工具函数:当你在类中定义了一些通用的函数,这些函数不依赖于类的实例属性,也不需要访问实例方法时,可以将它们定义为静态方法。例如,一些数学计算(sin、cos、tan)或者数据处理工具。
- 静态方法主要用于获取一些固定的值,如获取时间、获取一些配置文件,但是不会对其进行频繁的更改,调用时直接
- 通过修饰器
1 |
|
metaclass 作用以及应用场景
metaclass 用来指定类是由谁创建的。如下面创建 Foo 类的示例时调用 MyType()()
类的 metaclass 默认是 type。我们也可以指定类的 metaclass 值。在 Python3 中:
1 |
|
用尽量多的方法实现单例模式
单例模式是一种常用的软件设计模式。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
1 |
|
单例模式应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。
再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。
property
property()
函数的作用是在新式类中返回属性值。可以对应于某个方法,希望能够像调用属性一样来调用方法,此时可以将一个方法加上 property。
定义 property 属性共有两种方式,分别是【装饰器】和【类属性】,而【装饰器】方式针对经典类和新式类又有所不同。下面分别展示:
1 |
|
Python 中为什么没有函数重载
C++、Java、C# 等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。
这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解 Python 是动态类型语言,是否知道 Python 中函数的可变参数、关键字参数这些概念。
首先 Python 是解释型语言,函数重载现象通常出现在编译型语言中。其次 Python 是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者 Python 中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。
魔法方法(魔法方法|双下划线方法) ★★★★★
列举面向对象中带双下划线的特殊方法,如:__new__
、__init__
1 |
|
__init__()
和 __new__()
方法有什么区别
Python 中调用构造器创建对象属于两阶段构造过程:
首先执行
__new__(cls, *args, **kwargs)
方法获得对象所需的内存空间,第一个参数 cls 是当前正在实例化的类。如果要得到当前类的实例,应当在当前类中的
new()
方法语句中调用当前类的父类的new()
方法。如果当前类是直接继承自 object,那当前类的new()
方法返回的对象应该为:1
2
3def __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 |
|
点评:这道题有两个考察点,一个考察点是对 _
和 __
开头的对象属性访问权限以及 @property
装饰器的了解,另外一个考察的点是对动态语言的理解,不需要过多的解释。
扩展:如果不希望代码运行时动态的给对象添加新属性,可以在定义类时使用 __slots__
魔法。例如,我们可以在上面的 A
中添加一行 __slots__ = ('__value', )
,再次运行上面的代码,将会在原来的第 10 行处产生 AttributeError
错误。