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

天澄發表於2019-03-30

知識點整理基於 Python3.

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

1. Python 魔法方法

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

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)
複製程式碼

輸出結果:

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 方法實現單例(高頻考點):

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 生成器

先來看一個關係圖:

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

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__ 方法。它們之間的關係是:

剖析 Python 面試知識點(一): 魔法方法、閉包/自省、裝飾器/生成器
那什麼是迭代器了?上面例子中有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…

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

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

相關文章