Python基础面试中常常问道的问题

今天来积攒一波经验。

python语法及其他基础部分(保存后续增加)

1、可变与不可变类型:
2、深拷贝和浅拷贝的实现方式、区别:
3、deepcopy如果你来设计,如何实现:
4、new()与init()的区别:
5、你知道的几种设计模式:
6、编码和解码你了解过 么
7、列表推到list comprehension和生成器的优劣
8、什么是装饰器,如果想在函数之后进行装饰,应该怎么做
9、手写个使用装饰器实现单例模式:
10、使用装饰器的单例模式和使用其他方法的单例模式,在后续的使用中有什么区别?
11、手写 正则邮箱地址:
12、介绍下垃圾回收、引用计数\分代回收\孤立引用环:
13、多进程和多线程的区别、CPU密集型适合用什么?
14、进程通信的方式有几种?
15、介绍一下协成,为何比线程还快?
16、range和xrange的区别
17、将IP字符串“172.0.0.1”转换为32位二进制的函数。

以上问题下面进行总结一下。

1、可变与不可变类型
python中的对象分为:可变对象和不可变对象.

可变对象:列表(list)、字典(dict )
不可变对象:int、string、float、tuple

可变对象和不可变对象之间的区别:可变数据类型中,即便对数据进行更改,数据的id也不会发生变化,而不可变数据类型中,只要对数据的值进行更改,则数据的id就发生变化。
下面来举个例子说明问题:

对于可变对象 list

>>> listtest1=[]
>>> id(listtest1)
42033680
>>> listtest2=listtest1   # listtest2和listtest1 指向同一个内存地址 42033680
>>> id(listtest1)
42033680
>>> listtest1.append(5)  # 修改listtest1的值
>>> listtest1            # listtest1的内容发生了变化
[5]
>>> id(listtest1)        # 但是listtest1的内存地址并没有发生变化,这是因为list对象是可变类型,所以修改list对象的值并不会开辟一片内存来保存这个值,而是在原对象的基础上进行修改,所以listtest1的id并未发生变化。同理,listtest2也一样。
42033680
>>> listtest2
[5]
>>> id(listtest2)
42033680

对于可变对象 dict

>>> dicttest1={}
>>> id(dicttest1)
42047360
>>> dicttest1["name"]="PanPanZhao"
>>> dicttest1
{'name': 'PanPanZhao'}
>>> id(dicttest1) # 内存地址并未发生变化
42047360

对于不可变对象int :

>>> x=y=5
>>> x
5
>>> y
5
>>> id(x)
33971488
>>> id(y)
33971488
>>> x=x+1 # x的值和内存地址发生了变化,
>>> x  
6
>>> id(x)
33971476
>>> y  # y的值和内存地址并未发生变化
5
>>> id(y)
33971488

总结一下:对于不可变类型int,无论创建多少个不可变对象,只要它们的值相同,就指向同一个内存地址,同样的情况还适用于较短的字符串。
但是对于其他不可变类型就不一样了,对于float对象而言,创建多个float对象,即使它们的值相同,它们所指向的内存地址是不同的。
原因:python对int和短字符串进行了缓存,无论声明多少个值相同的变量,实际上都指向同个内存地址。

1.1 可变与不可变类型的有什么作用
答:python函数的参数传递:在python中规定函数的参数传递为引用传递,也就是说传递给函数的参数是这个变量所指向的内存地址。但是在C中,参数传递可以有值传递和引用传递,当需要修改外面参数的值的时候就才用引用传递(在参数前面加一个*,表示传递参数所指的内存地址),但不需要修改外部参数值的时候就采用值传递

那么在python中如何实现和值传递和引用传递相似的功能呢?———可变类型和不可变类型就发挥作用了。当你需要实现函数参数引用传递的功能的时候,你就将类型为list ,dict的变量传递给相应的函数。当你需要实现函数值传递功能的时候,你就可以将类型为int,float,string,tuple类型的变量传递给函数当做参数。

听说python只允许引用传递是为方便内存管理,因为python使用的内存回收机制是计数器回收,就是每块内存上有一个计数器,表示当前有多少个对象指向该内存。每当一个变量不再使用时,就让该计数器-1,有新对象指向该内存时就让计数器+1,当计时器为0时,就可以收回这块内存了。当然我觉得它肯定不止用了计数器吧,应该还有其他的技术,比如分代回收什么的。不再讨论之列,就这样了

以上是对第一个问题的理解。参考网址

2、深拷贝和浅拷贝的实现方式以及区别
我们从三个方面来讲解这个问题。
2.1 Python中直接赋值
python中的直接赋值相当于传递对象的引用而已,原对象改变,被赋值的对象也会改变。下面给大家举个列子。

>>> print [id(i) for i in love]
[35128144, 34888764, 41771536]
>>> lover=love   # 赋值方式:这里love和lover对象指向同一个内存地址 
>>> print id(lover)
34550848
>>> print [id(i) for i in lover]
[35128144, 34888764, 41771536]
>>> love[0]="You"  # 修改love的第一个元素,那么lover的第一个元素也会做相应的修改
>>> love
['You', 24, ['am', 'loving', 'you']]
>>> print id(love)
34550848
>>> print [id(i) for i in love]
[41760936, 34888764, 41771536]
>>> print lover    # 看这里的lover也做了相应的修改,只不过对原始对象中的不可变元素做修改后,既改变了这个对象的值,也改变了这个对象的地址。
['You', 24, ['am', 'loving', 'you']]
>>> love[1]=25
>>> love
['You', 25, ['am', 'loving', 'you']]
>>> print id(love)
34550848
>>> print [id(i) for i in love]
[41760936, 34888752, 41771536]
>>> print id(lover)
34550848
>>> print [id(i) for i in lover]
[41760936, 34888752, 41771536]
love[2].append("too")
>>> love
['You', 25, ['am', 'loving', 'you', 'too']]
>>> lover        # 看这里的lover也做了相应的修改,只不过对原始对象中的可变元素做修改后,只是改变了这个对象的值,不会改变这个对象的地址。
['You', 25, ['am', 'loving', 'you', 'too']]
>>> print [id(i) for i in love]
[41760936, 34888752, 41771536]
>>> print [id(i) for i in lover]
[41760936, 34888752, 41771536]
>>> 

这里给大家放一张网上的图片,以加深理解。
赋值操作的示意图
2.2 Python中浅拷贝
copy浅拷贝,没有拷贝子对象(这里应该指的是可变对象),只是将新对象给了一个新的首地址,里面的数据和地址都和原始数据相同,所以原始数据改变,子对象会改变,给大家放一张图就明白了。
python中的浅拷贝示意图
用代码表示出来就是:

>>> import copy
>>> love=["I",24,["am","loving","you"]]
>>> lover=copy.copy(love)     # 这里是浅拷贝的方法
>>> id(love)
35140672
>>> id(lover)  # 浅拷贝后love和lover这两个对象的内存地址是不同的
41229848
>>> love[0]="You"
>>> id(love)
35140672
>>> id(lover)
41229848
>>> [id(i) for i in love]
[42547536, 35478588, 42558488]      # 在love中改变不可变对象会既会改变不可变对象的值,也会不可变对象的地址
>>> [id(i) for i in lover]
[35717968, 35478588, 42558488]     # 对比可以发现,love和lover中第一个元素的地址发生了变化
>>> love[2].append("too")  
>>> love
['You', 24, ['am', 'loving', 'you', 'too']]
>>> lover
['I', 24, ['am', 'loving', 'you', 'too']]
>>> [id(i) for i in love]       # 在love中改变可变对象的值,只会改变可变对象的值,其地址是不会发生改变的。
[42547536, 35478588, 42558488]
>>> [id(i) for i in lover]
[35717968, 35478588, 42558488]

2.3 Python中深拷贝

python中的深拷贝,包含对原始对象的子对象的深拷贝,所以,对原拷贝对象的改变不会对深拷贝后对象没有任何影响。下面放一张图供大家理解。

python中深拷贝的示意图
这里举个例子加深理解:

>>> import copy
>>> love=["I",24,["am","loving","you"]]
>>> lover=copy.deepcopy(love)
>>> id(love)
41050760
>>> id(lover)
41050880     # 二者的首地址不同
>>> [id(x) for x in love]
[6226768, 5987388, 41049184]   
>>> [id(x) for x in lover]
[6226768, 5987388, 41051680]   # 二者的可变子对象的地址也不相同
>>> love[1]=18
>>> love
['I', 18, ['am', 'loving', 'you']]   # 对love中不可变对象的修改不影响lover中对应的子对象
>>> lover
['I', 24, ['am', 'loving', 'you']]
>>> [id(x) for x in love]
[6226768, 5987460, 41049184]
>>> [id(x) for x in lover]
[6226768, 5987388, 41051680]
>>> love[2].append("too")
>>> love
['I', 18, ['am', 'loving', 'you', 'too']]   # 对love中可变对象的修改不会影响到lover对象中对应的子对象
>>> [id(x) for x in love]
[6226768, 5987460, 41049184]
>>> [id(x) for x in lover]
[6226768, 5987388, 41051680]
>>> lover
['I', 24, ['am', 'loving', 'you']]   综上可以发现,深拷贝的两个对象,各自改变后,互不影响。

总结:

1、赋值:简单地拷贝对象的引用,两个对象的id相同。
2、浅拷贝:创建一个新的组合对象,这个新对象与原对象共享内存中的子对象。
3、深拷贝:创建一个新的组合对象,同时递归地拷贝所有子对象,新的组合对象与原对象没有任何关联。虽然实际上会共享不可变的子对象,但不影响它们的相互独立性。

浅拷贝和深拷贝的不同仅仅是对组合对象来说,所谓的组合对象就是包含了其它对象的对象,如列表,类实例。而对于数字、字符串以及其它“原子”类型,没有拷贝一说,产生的都是原对象的引用。
以上学习的参考网址:python中的赋值、深拷贝和浅拷贝
python变量和对象的关系,以及赋值、浅拷贝、深拷贝的关系

3、python中的deepcopy如果你来设计,如何实现

4、__new__()与__init__()的区别
new方法是类创建实例的方法, 创建对象时调用,返回当前对象的一个实例
init()方法是类实例创建之后调用,用于对当前对象的一些初始化,没有返回值。
new()方法和init()方法所接收的参数是一样的。
需要注意的几点:
1、init()函数并不相当于C++或者C#中的构造函数,因为在执行init()函数的时候,实例已经构造出来了
2、子类可以不重写init()方法,实例化子类时可以自动调用超类中已定义的init()方法。但是如果重写了init(),实例化子类时将不会再 隐式的去调用超类中已定义的init()代码。
3、如果重写了init(),为了能使用或扩展超类中的行为,最好显式的调用超类的init()方法

下面举个例子来说明一下:

class smallApple(object):
    def __init__(self,name):
        print "__init__() is called"
        self.name=name

    def __new__(cls,name="smallApple"):
        print "__new__() is called"
        return super(smallApple,cls).__new__(cls,name)
运行输出:
>>> s=smallApple("It's yours")
__new__() is called
__init__() is called
>>> 

1.s=smallApple(“It’s yours”)
2.首先执行使用name参数来执行smallApplen类的new方法,这个new方法会 返回smallApple类的一个实例(通常情况下是使用 super(smallApple, cls).new(cls, … …) 这样的方式),
3.然后利用这个实例来调用类的init方法,上一步里面new产生的实例也就是 init里面的的 self
所以,initnew 最主要的区别在于:
1.init 通常用于初始化一个新实例,控制这个初始化的过程,比如添加一些属性, 做一些额外的操作,发生在类实例被创建完以后。它是实例级别的方法。
2.new 通常用于控制生成一个新实例的过程。它是类级别的方法

因为类每一次实例化后产生的过程都是通过new来控制的,所以通过重载new方法,我们 可以很简单的实现单例模。那我们利用new()方法来实现单例模式的一种:

# -*- coding: cp936 -*-
class smallApple(object):
    def __init__(self,name="It's yours"):
        print "__init__() is called"
        self.name=name

    def __new__(cls,name="smallApple"):
        print "__new__() is called"
        # make sure the instance is unique
        if not hasattr(cls,"instance"):
              cls.instance= super(smallApple,cls).__new__(cls,name)
        return cls.instance
# 测试
>>> obj1=smallApple("i am a")
__new__() is called
__init__() is called
>>> obj2=smallApple("i am a")
__new__() is called
__init__() is called
>>> obj1 is obj2
True
>>> obj1.age=22
>>> obj2.age
22
>>> smallApple.instance
<__main__.smallApple object at 0x027D3BF0>
>>> hex(id(obj1))
'0x27d3bf0'
>>> hex(id(obj2))
'0x27d3bf0'
>>> 

其实,大多数时候是不需要写new()函数的,那么什么情况下需要写new()函数呢?

当继承不可变类或者metaclass的时候,需要用到new()方法。
下面来举个例子来说明,当实现int类型恒为正数的类 ,是因为对于int这种 不可变的对象,我们只有重载它的new方法才能起到自定义的作用。

class PositiveInt(int):
     def __new__(cls,value):
         return super(PositiveInt,cls).__new__(cls,abs(value))
# 输出
>>> PositiveInt(-16)
16

5、你知道的几种设计模式
一、单例模式
二、简单工厂模式
三、抽象工厂模式
四、建造者模式
下面依次举例这四种设计模式:
1、单例模式
单例模式(Singleton Pattern)是一种常见的设计模式,该模式的目的是确保一个类只有一个实例.
在Python中可以用多种方式来实现单例模式:

  1. 使用python的模块
  2. 使用python的new()方法
  3. 使用python的装饰器(decorator)
  4. 使用python的元类(metaclass)

1.1 使用python的模块
python的模块其实就是天然的单例模式,因为python的模块在第一次加载的时,执行模块代码,生成.pyc文件,当再次需要导入这个模块的时候,python会再次加载第一次生成的.pyc文件,而不会执行模块的代码。所以,我们只需要将相关的函数和数据放在一个模块中,就可以实现单例模式了,下面是具体的做法。

# 下面是类的定义
class Singleton(object):
    def Unique(self):
        print "I am the Unique Methold"

MySingleton=Singleton()
# 下面是导入模块后的执行结果
>>> from pattern import MySingleton
>>> MySingleton.Unique()
I am the Unique Methold

以上是导入模块成功的方式,下面给出一个错误的方式。

# 以下是类定义
class Singleton(object):
    def Unique(): # 这里类定义有误,参数没有加self
        print "I am the Unique Methold"

MySingleton=Singleton()
# 以下是模块的导入的代码
>>> from pattern import MySingleton
>>> MySingleton.Unique()   

Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    MySingleton.Unique()
TypeError: Unique() takes no arguments (1 given)   # 因为类定义中的代码有误,所以模块导入的时候会出现错误,那我们把pattern模块中的类定义代码修改过来后,再次import时

>>> from pattern import MySingleton
>>> MySingleton.Unique()

Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    MySingleton.Unique()
TypeError: Unique() takes no arguments (1 given)
>>> 
# 还是不可以,这说明,不管import多少次模块,只要shell不关闭,import的模块都是第一次import时生成的.pyc文件

1.2 使用python的new()方法
为了使类只出现一个实例,那么我们可以控制实例的创建过程来达到这个目的、下面看代码:

 # l类的定义
class SingletonTwo(object):

    def __new__(cls,name="you"):
        if not hasattr(cls,"instance"):
            cls.instance=super(SingletonTwo,cls).__new__(cls,name)
        return cls.instance

    def __init__(self,name="you"):
        print "get name already"
        self.name=name

# 模块测试
>>> obj1=SingletonTwo("11111")
get name already
>>> obj1.name
'11111'
>>> obj2=SingletonTwo("222222")
get name already
>>> obj2.name
'222222'
>>> obj1==obj2
True
>>> hex(id(obj1))
'0x290f9d0'
>>> hex(id(obj2))
'0x290f9d0'
>>> obj1.name="gwijgoir"
>>> obj2.name
'gwijgoir'
 # 这样保证了这个类只有一个实例,从代码里面可以看到实例obj1和obj2的地址相同,但是它们的name在最开始却不是相同的,但是修改obj1的那么后,obj2的name也会变跟obj1一样,所以这个地方我会再探究一下、、、、、、

1.3 python中的装饰器(decorater)来实现单例模式
未完待续、、、、、、

1.4 python中的元类(metaclass)来实现单例模式
未完待续、、、、、

猜你喜欢

转载自blog.csdn.net/xiongchengluo1129/article/details/79113591