面试之 Python 基础之面向对象

面向对象三大特性

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

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

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

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

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

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

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

Python 经典类和新式类

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

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

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

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

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

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

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列表。

阅读下面的代码说出运行结果。
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

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

面向对象中super的作用

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

# 用于子类继承基类的方法
class FooParent(object):
    def __init__(self):
        self.parent = 'I\'m the parent.'
        print('Parent')
        print('1111')
 
    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 方法判断。

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’作为第一个参数的名字,就最好用’cls’),不需要实例化就可以使用。
    • 应用场景:当一个方法中只涉及到静态属性的时候可以使用类方法(类方法用来修改类属性)。
      • https://blog.51cto.com/u_15688254/5391548
  • 静态方法:

    • 通过修饰器@staticmethod来进行修饰,静态方法不需要多定义参数,可以通过对象和类来访问,是类中的一个独立的普通函数或者说方法,类或者实例化的对象都可以直接使用它。
    • 应用场景:静态方法主要用于获取一些固定的值,如获取时间、获取一些配置文件,但是不会对其进行频繁的更改,调用时直接 类.静态方法名 就好了。就是整个项目中就可以直接调用静态方法,不需要实例化,本身用类就可以调用。
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 中:

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、使用__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属性共有两种方式,分别是【装饰器】和【类属性】,而【装饰器】方式针对经典类和新式类又有所不同。下面分别展示:

# 类属性方式
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__

__iter__:用于迭代器,之所以列表、字典、元组可以进行for循环,是因为类型内部定义了 __iter__。
__next__: 用于迭代器。
__doc__:表示类的描述信息。

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

__call__:实例对象加( )会执行def __call__:... 方法里边的内容。

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

__module__:表示当前操作的对象在那个模块   obj.__module__
__class__ :表示当前操作的对象的类是什么   obj.__class____doc__:类的描述信息,该描述信息无法被继承
__str__:改变对象的字符串显示 print函数 --->obj.__str__()如果一个类中定义了__str__方法,那么在打印对象时,默认输出该方法的返回值。
__repr__:改变对象的字符串显示交互式解释器 --->obj.__repr__()
__format__:自定制格式化字符串__slots__:一个类变量 用来限制实例可以添加的属性的数量和类型
__dict__:类或对象中的所有成员。
__setitem__,__getitem,__delitem__:用于索引操作,如字典。以上分别表示获取、设置、删除数据。

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()方法返回的对象应该为:

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

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

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

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

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)
print(obj.__value)

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值