剖析 Python 面試知識點(一): 魔法方法、閉包/自省、裝飾器/生成器

知識點整理基於 Python3.python

1. Python 魔法方法

在Python中用雙下劃線__包裹起來的方法被成爲魔法方法,能夠用來給類提供算術、邏輯運算等功能,讓這些類可以像原生的對象同樣用更標準、簡潔的方式進行這些操做。 下面介紹經常被問到的幾個魔法方法。git

1.1 __init__

__init__方法作的事情是在對象建立好以後初始化變量。不少人覺得__init__是構造方法,其實否則,真正建立實例的是__new__方法,下面會講它,先來看看__init__方法。面試

class Person(object):
    def __init__(self, name, age):
        print("in __init__")
        self.name = name
        self.age = age 

p = Person("TianCheng", 27) 
print("p:", p)
複製代碼

輸出:編程

in __init__
p: <__main__.Person object at 0x105a689e8>
複製代碼

明白__init__負責初始化工做,日常也是咱們常常用到的。。設計模式

1.2 __new__

構造方法: __new__(cls, […]) __new__是Python中對象實例化時所調用的第一個函數,在__init__以前被調用。__new__將class做爲他的第一個參數, 並返回一個這個class的 instance。而__init__是將 instance 做爲參數,並對這個 instance 進行初始化操做。每一個實例建立時都會調用__new__函數。下面來看一個例子:緩存

class Person(object):
    def __new__(cls, *args, **kwargs):
        print("in __new__")
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print("in __init__")
        self._name = name
        self._age = age

p = Person("TianCheng", 27)
print("p:", p)
複製代碼

輸出結果:bash

in __new__
in __init__
p: <__main__.Person object at 0x106ed9c18>
複製代碼

能夠看到先執行 new 方法建立對象,而後 init 進行初始化。假設將__new__方法中不返還該對象,會有什麼結果了?數據結構

class Person(object):
    def __new__(cls, *args, **kwargs):
        print("in __new__")
        instance = super().__new__(cls)
        #return instance

    def __init__(self, name, age):
        print("in __init__")
        self._name = name
        self._age = age

p = Person("TianCheng", 27)
print("p:", p)

# 輸出:
in __new__
p: None
複製代碼

發現若是 new 沒有返回實例化對象,init 就無法初始化了。閉包

如何使用 new 方法實現單例(高頻考點):app

class SingleTon(object):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            cls._instance = cls.__new__(cls, *args, **kwargs)
        return cls._instance

s1 = SingleTon()
s2 = SingleTon()
print(s1)
print(s2)
複製代碼

輸出結果:

<__main__.SingleTon object at 0x1031cfcf8>
<__main__.SingleTon object at 0x1031cfcf8>
複製代碼

s1, s2 內存地址一致,實現單例效果。

1.3 __call__

__call__ 方法,先須要明白什麼是可調用對象,平時自定義的函數、內置函數和類都屬於可調用對象,但凡是能夠把一對括號()應用到某個對象身上均可稱之爲可調用對象,判斷對象是否爲可調用對象能夠用函數 callable。舉例以下:

class A(object):
    def __init__(self):
        print("__init__ ")
        super(A, self).__init__()

    def __new__(cls):
        print("__new__ ")
        return super(A, cls).__new__(cls)

    def __call__(self):  # 能夠定義任意參數
        print('__call__ ')

a = A()
a()
print(callable(a))  # True
複製代碼

輸出:

__new__
__init__
__call__
True
複製代碼

執行 a() 纔會打印出 __call__。 a是一個實例化對象,也是一個可調用對象。

1.4 __del__

__del__ 析構函數,當刪除一個對象時,則會執行此方法,對象在內存中銷燬時,自動會調用此方法。舉例:

class People:
    def __init__(self,name,age):
        self.name=name
        self.age=age

    def __del__(self): # 在對象被刪除的條件下,自動執行
        print('__del__')

obj=People("Tiancheng", 27)
#del obj #obj.__del__() #先刪除的狀況下,直接執行__del__
複製代碼

輸出結果:

__del__
複製代碼

2. 閉包 和 自省

2.1 閉包

2.1.1 什麼閉包

簡單的說,若是在一個內部函數裏,對在外部做用域(但不是在全局做用域)的變量進行引用,那麼內部函數就被認爲是閉包(closure)。來看一個簡單的例子:

>>>def addx(x):
>>>    def adder(y): return x + y
>>>    return adder
>>> c =  addx(8)
>>> type(c)
<type 'function'>
>>> c.__name__
'adder'
>>> c(10)
18
複製代碼

其中 adder(y) 函數就是閉包。

2.1.2 實現一個閉包並能夠修改外部變量

def foo():
    a = 1
    def bar():
        a = a + 1
        return a
    return bar
c = foo()
print(c())
複製代碼

有上面一個小例子,目的是每次執行一次,a 自增1,執行後是否正確了?顯示會報下面錯誤。

local variable 'a' referenced before assignment
複製代碼

緣由是bar()函數中會把a做爲局部變量,而bar中沒有對a進行聲明。 若是面試官問你,在 Python2 和 Python3 中如何修改 a 的值了。 Python3 中只需引入 nonlocal 關鍵字便可:

def foo():
    a = 1
    def bar():
        nonlocal a
        a = a + 1
        return a
    return bar
c = foo()
print(c()) # 2
複製代碼

而在 Python2 中沒有 nonlocal 關鍵字,該如何實現了:

def foo():
    a = [1]
    def bar():
        a[0] = a[0] + 1
        return a[0]
    return bar
c = foo()
print(c()) # 2
複製代碼

需藉助可變變量實現,好比dict和list對象。

閉包的一個經常使用場景就是 裝飾器。 後面會講到。

2.2 自省(反射)

自省,也能夠說是反射,自省在計算機編程中一般指這種能力:檢查某些事物以肯定它是什麼、它知道什麼以及它能作什麼。 與其相關的主要方法:

  • hasattr(object, name) 檢查對象是否具體 name 屬性。返回 bool.
  • getattr(object, name, default) 獲取對象的name屬性。
  • setattr(object, name, default) 給對象設置name屬性
  • delattr(object, name) 給對象刪除name屬性
  • dir([object]) 獲取對象大部分的屬性
  • isinstance(name, object) 檢查name是否是object對象
  • type(object) 查看對象的類型
  • callable(object) 判斷對象是不是可調用對象
>>> class A:
...   a = 1
...
>>> hasattr(A, 'a')
True
>>> getattr(A, 'a')
1
>>> setattr(A, 'b', 1)
>>> getattr(A, 'b')
1
>>> delattr(A, 'b')
>>> hasattr(A, 'b')
False
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> isinstance(1, int)
True
>>> type(A)
<class 'type'>
>>> type(1)
<class 'int'>
>>> callable(A)
True
複製代碼

3. 裝飾器 和 迭代器

3.1 裝飾器

裝飾器本質上是一個 Python 函數或類,它可讓其餘函數或類在不須要作任何代碼修改的前提下增長額外功能(設計模式中的裝飾器模式),裝飾器的返回值也是一個函數/類對象。它常常用於有切面需求的場景,好比:插入日誌、性能測試、事務處理、緩存、權限校驗等場景。

3.1.1 簡單裝飾器

先來看一個以前閉包的例子:

def my_logging(func):

    def wrapper():
        print("{} is running.".format(func.__name__))
        return func()  # 把 foo 當作參數傳遞進來時,執行func()就至關於執行foo()
    return wrapper

def foo():
    print("this is foo function.")

foo = my_logging(foo)  # 由於裝飾器 my_logging(foo) 返回的時函數對象 wrapper,這條語句至關於 foo = wrapper
foo() # 執行foo至關於執行wrapper
複製代碼

但在Python裏有@語法糖,則能夠直接這樣作:

def my_logging(func):

    def wrapper():
        print("{} is running.".format(func.__name__))
        return func()
    return wrapper

@my_logging
def foo():
    print("this is foo function.")

foo()
複製代碼

上面兩者都會有以下打印結果:

foo is running.
this is foo function.
複製代碼

my_logging 就是一個裝飾器,它一個普通的函數,它把執行真正業務邏輯的函數 func 包裹在其中,看起來像 foo 被 my_logging 裝飾了同樣 my_logging 返回的也是一個函數,這個函數的名字叫 wrapper。在這個例子中,函數進入和退出時 ,被稱爲一個橫切面,這種編程方式被稱爲面向切面的編程(AOP)。

若是 foo 帶有參數,如何將參數帶到 wrapper 中了?

def my_logging(func):

    def wrapper(*args, **kwargs):
        print("{} is running.".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@my_logging
def foo(x, y):
    print("this is foo function.")
    return x + y

print(foo(1, 2))
複製代碼

能夠經過 *args, **kwargs 接收參數,而後帶入func中執行,上面執行結果爲:

foo is running.
this is foo function.
3
複製代碼

3.1.2 帶參數的裝飾器

裝飾器的語法容許咱們在調用時,提供其它參數,好比@decorator(a)。這樣就大大增長了靈活性,好比在日誌告警場景中,能夠根據不一樣的告警定告警等級: info/warn等。

def my_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "info":
                print("{} is running. level: ".format(func.__name__), level)
            elif level == "warn":
                print("{} is running. level: ".format(func.__name__), level)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@my_logging(level="info")
def foo(name="foo"):
    print("{} is running".format(name))

@my_logging(level="warn")
def bar(name="bar"):
    print("{} is running".format(name))

foo()
bar()
複製代碼

結果輸出:

foo is running. level:  info
foo is running
bar is running. level:  warn
bar is running
複製代碼

上面的 my_logging 是容許帶參數的裝飾器。它其實是對原有裝飾器的一個函數封裝,並返回一個裝飾器。咱們能夠將它理解爲一個含有參數的閉包。當使用@my_logging(level="info")調用的時候,Python 可以發現這一層的封裝,並把參數傳遞到裝飾器的環境中。 @my_logging(level="info")等價於@decorator

3.1.3 類裝飾器

裝飾器不只能夠是函數,還能夠是類,相比函數裝飾器,類裝飾器具備靈活度大、高內聚、封裝性等優勢。使用類裝飾器主要依靠類的__call__方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。

class MyLogging(object):

    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print("class decorator starting.")
        a = self._func(*args, **kwargs)
        print("class decorator end.")
        return a

@MyLogging
def foo(x, y):
    print("foo is running")
    return x + y

print(foo(1, 2))
複製代碼

輸出結果:

class decorator starting.
foo is running
class decorator end.
3
複製代碼

3.1.4 functools.wraps

Python 中還有一個裝飾器的修飾函數 functools.wraps,先來看看它的做用是什麼?先來看看有一個問題存在,由於原函數被裝飾函數裝飾後,發生了一下變化:

def my_logging(func):

    def wrapper(*args, **kwargs):
        print("{} is running.".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@my_logging
def foo(x, y):
    """ add function """
    print("this is foo function.")
    return x + y

print(foo(1, 2))
print("func name:", foo.__name__)
print("doc:", foo.__doc__)
複製代碼

打印結果:

foo is running.
this is foo function.
3
func name: wrapper
doc: None
複製代碼

問題出來了,func name 應該打印出 foo 纔對,並且 doc 也不爲None。由此發現原函數被裝飾函數裝飾以後,元信息發生了改變,這明顯不是咱們想要的,Python裏能夠經過functools.wraps來解決,保持原函數元信息。

from functools import wraps

def my_logging(func):

 @wraps(func)
    def wrapper(*args, **kwargs):
        print("{} is running.".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@my_logging
def foo(x, y):
    """ add function """
    print("this is foo function.")
    return x + y

print(foo(1, 2))
print("func name:", foo.__name__)
print("doc:", foo.__doc__)
複製代碼

輸出結果:

foo is running.
this is foo function.
3
func name: foo
doc:
    add function
複製代碼

3.1.5 多個裝飾器的執行順序

@a
@b
@c
def f ():
    pass
複製代碼

執行順序爲 f = a(b(c(f)))

3.2 迭代器 VS 生成器

先來看一個關係圖:

3.2.1 container(容器)

container 能夠理解爲把多個元素組織在一塊兒的數據結構,container中的元素能夠逐個地迭代獲取,能夠用in, not in關鍵字判斷元素是否包含在容器中。在Python中,常見的container對象有:

list, deque, ....
set, frozensets, ....
dict, defaultdict, OrderedDict, Counter, ....
tuple, namedtuple, …
str
複製代碼

舉例:

>>> assert 1 in [1, 2, 3]      # lists
>>> assert 4 not in [1, 2, 3]
>>> assert 1 in {1, 2, 3}      # sets
>>> assert 4 not in {1, 2, 3}
>>> assert 1 in (1, 2, 3)      # tuples
>>> assert 4 not in (1, 2, 3)
複製代碼

3.2.2 可迭代對象(iterables) vs 迭代器(iterator)

大部分的container都是可迭代對象,好比 list or set 都是可迭代對象,能夠說只要是能夠返回一個迭代器的均可以稱做可迭代對象。下面看一個例子:

>>> x = [1, 2, 3]
>>> y = iter(x)
>>> next(y)
1
>>> next(y)
2
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>
複製代碼

可見, x 是可迭代對象,這裏也叫container。y 則是迭代器,且實現了__iter____next__ 方法。它們之間的關係是:

那什麼是迭代器了?上面例子中有2個方法 iter and next。可見經過iter方法後就是迭代器。 它是一個帶狀態的對象,調用next方法的時候返回容器中的下一個值,能夠說任何實現了__iter__和next方法的對象都是迭代器,__iter__返回迭代器自身,next返回容器中的下一個值,若是容器中沒有更多元素了,則拋異常。 迭代器就像一個懶加載的工廠,等到有人須要的時候纔給它生成值返回,沒調用的時候就處於休眠狀態等待下一次調用。

3.2.3 生成器(generator)

生成器必定是迭代器,是一種特殊的迭代器,特殊在於它不須要再像上面的__iter__()和next方法了,只須要一個yiled關鍵字。下面來看一個例子: 用生成器實現斐波拉契:

# content of test.py
def fib(n):
    prev, curr = 0, 1
    while n > 0:
        yield curr
        prev, curr = curr, curr + prev
        n -= 1
複製代碼

到終端執行fib函數:

>>> from test import fib
>>> y = fib(10)
>>> next(y)
1
>>> type(y)
<class 'generator'>
>>> next(y)
1
>>> next(y)
2
複製代碼

fib就是一個普通的python函數,它特殊的地方在於函數體中沒有return關鍵字,函數的返回值是一個生成器對象(經過 yield 關鍵字)。當執行f=fib()返回的是一個生成器對象,此時函數體中的代碼並不會執行,只有顯示或隱示地調用next的時候纔會真正執行裏面的代碼。 假設有千萬個對象,須要順序調取,若是一次性加載到內存,對內存是極大的壓力,有生成器以後,能夠須要的時候去生成一個,不須要的則也不會佔用內存。

日常可能還會遇到一些生成器表達式,好比:

>>> a = (x*x for x in range(10))
>>> a
<generator object <genexpr> at 0x102d79a20>
>>> next(a)
0
>>> next(a)
1
>>> a.close()
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
複製代碼

這些小技巧也是很是有用的。close能夠關閉生成器。生成器中還有一個send方法,其中send(None)與next是等價的。

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> generator = double_inputs()
>>> generator.send(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> generator.send(None)
>>> generator.send(10)
20
>>> next(generator)
>>> generator.send(20)
40
複製代碼

從上面的例子中能夠看出,生成器能夠接收參數,經過send(value)方法,且第一次不能直接send(value),須要send(None)或者next()執行以後。也就是說調用send傳入非None值前,生成器必須處於掛起狀態,不然將拋出異常。

3.2.4 迭代器和生成器的區別

可能你看完上面的,有點好奇到底他們兩者有什麼區別了?

  • 迭代器是一個更抽象的概念,任何對象,若是它有next方法(next python3,python2 是 __next__方法)和__iter__方法,則能夠稱做迭代器。

  • 每一個生成器都是一個迭代器,可是反過來不行。一般生成器是經過調用一個或多個yield表達式構成的函數s生成的。同時知足迭代器的定義。

  • 生成器能作到迭代器能作的全部事,並且由於自動建立了iter()和 next()方法,生成器顯得特別簡潔,並且生成器也是高效的。

『剖析Python面試知識點』完整內容請查看 : gitbook.cn/gitchat/act…

更多精彩文章請關注公衆號: 『天澄技術雜談』

相關文章
相關標籤/搜索