python函數式編程之functools、itertools、operator詳解

寫在篇前

  這篇博客主要介紹什麼是函數式編程、在Python中怎麼進行函數式編程。函數式編程(Functional Programming)是一種編程範式,它將計算機運算視爲函數運算,並且避免使用程序狀態以及mutable對象。因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之爲沒有副作用。而允許使用變量的程序設計語言,由於函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。

  函數式編程的一個特點就是,允許把函數本身作爲參數傳入另一個函數,還允許返回一個函數!Python對函數式編程提供部分支持, 包括標準庫itertoolsoperatorfunctools,下面將會一一介紹。由於Python允許使用變量,因此,Python不是純函數式編程語言。本文代碼均放在github 倉庫

itertools

​ 受APL,Haskell和SML的啓發,本模塊實現一系列 iterator ,用於快速創建高效率的迭代器。本模塊中包含的迭代器創建函數可分爲以下三個部分:

無窮迭代器

無窮迭代意味着可以迭代無窮次而不會拋出StopIteration異常,在程序中需要設置一定條件主動停止迭代以避免無限循環。

迭代器 實參 結果 示例
count() start, [step] start, start+step, start+2*step, … count(10) --> 10 11 12 13 14 ...
cycle() p p0, p1, … plast, p0, p1, … cycle('ABCD') --> A B C D A B C D ...
repeat() elem [,n] elem, elem, elem, … 重複無限次或n次 repeat(10, 3) --> 10 10 10
  • 這些函數中最需要注意的是cycle(),它會創建一個迭代器,返回iterable中所有元素並保存一個副本,因此該函數可能需要相當大的輔助空間。其實現大致相當於:

    def cycle(iterable):
        # cycle('ABCD') --> A B C D A B C D A B C D ...
        saved = []
        for element in iterable:
            yield element
            saved.append(element)
        while True:
            for element in saved:
                  yield element
    
  • repeat()函數比較常用方式是給mapzip提供常數:

    >>> list(map(pow, range(10), repeat(2)))
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    

最短停止迭代器

最短停止迭代器,即Iterators terminating on the shortest input sequence,迭代器迭代次數取決於傳入的最短序列(下面的zip_longest()除外)。這一類迭代器函數一共有12個,下面我們挑選幾個具體舉例:

  • accumulate(iterable, func=None)函數接受一個可迭代對象以及一個callable對象爲參數,返回序列累積(如數學累加、累乘)結果或則最大值最小值,總之只要func接受兩個參數進行允許的操作即可,其實現大致等同如下:

    import operator
    def accumulate(iterable, func=operator.add):
        'Return running totals'
        # accumulate([1,2,3,4,5]) --> 1 3 6 10 15
        # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120
        it = iter(iterable)
        try:
            total = next(it)
        except StopIteration:
            return
        yield total
        for element in it:
            total = func(total, element)
            yield total
    

    默認實現一個累加的效果,我們也可以實現一個累乘的效果:

    >>> import operator
    >>> list(accumulate([1, 2, 3, 4, 5], func=operator.mul))
    [1, 2, 6, 24, 120]
    >>> list(accumulate([1, 2, 3, 4, 5], func=lambda x, y: x*y))
    [1, 2, 6, 24, 120]
    
  • dropwhile()takewhile()filterfalse()這三個函數比較相似,均是按一定條件迭代iterable中的元素,下面給出一個例子說明他們的區別:

    # -----------------------dropwhile---------------------------------
    
    print(list(dropwhile(lambda x: x < 3, [1, 2, 3, 4, 5])))  # starting when pred fails
    
    # -----------------------filterfalse------------------------------
    
    print(list(filterfalse(lambda x: x < 3, [1, 2, 3, 4, 5])))  # get elements of seq where pred(elem) is false
    
    # -----------------------takewhile--------------------------------
    
    print(list(takewhile(lambda x: x < 3, [1, 2, 3, 4, 5])))  # seq[0], seq[1], until pred fails
    
    [1, 2, 4]
    [3, 4, 5]
    [3, 4, 5]
    
  • groupby()是對iterable進行自定義分組的一個迭代器創建函數,其一般與sorted()函數配合使用,達到最佳的分組效果,如下面例子對水果進行分類,原諒我才用了比較奇怪的分組方式,我將英文單詞長度一樣的水果分爲一組:

    def key_func(x):
        return len(x)
    
    
    fruits = ['apple', 'banana', 'grape', 'pear', 'chestnut', 'orange']
    fruits = sorted(fruits, key=key_func)  # 可以註釋這句話查看不一樣的效果
    group = groupby(fruits, key=key_func)
    
    for k, v in group:
        print('{0}:{1}'.format(k, list(v)))
    
    4:['pear']
    5:['apple', 'grape']
    6:['banana', 'orange']
    8:['chestnut']
    
  • zip_longest()這是一個比較特殊的函數,與zip()函數相比,前者的迭代次數是取決於更長的輸入序列,並未短序列填充一個固定值:

    for x, y in zip_longest([1, 1, 1, 1], [2, 2, 2], fillvalue=None):  # 於zip對比
        print('(%s, %s)' % (x, y), end='\t')
        
    # output
    (1, 2)	(1, 2)	(1, 2)	(1, None)
    

所有迭代器列表如下,其使用方法其實都是同一套路:

terator Arguments Results Example
accumulate() p [,func] p0, p0+p1, p0+p1+p2, … accumulate([1,2,3,4,5]) --> 1 3 6 10 15
chain() p, q, … p0, p1, … plast, q0, q1, … chain('ABC', 'DEF') --> A B C D E F
chain.from_iterable() iterable p0, p1, … plast, q0, q1, … chain.from_iterable(['ABC', 'DEF']) --> A B C D E F
compress() data, selectors (d[0] if s[0]), (d[1] if s[1]), … compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
dropwhile() pred, seq seq[n], seq[n+1], starting when pred fails dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1
filterfalse() pred, seq elements of seq where pred(elem) is false filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
groupby() iterable[, key] sub-iterators grouped by value of key(v)
islice() seq, [start,] stop [, step] elements from seq[start:stop:step] islice('ABCDEFG', 2, None) --> C D E F G
starmap() func, seq func(*seq[0]), func(*seq[1]), … starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000
takewhile() pred, seq seq[0], seq[1], until pred fails takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
tee() it, n it1, it2, … itn splits one iterator into n
zip_longest() p, q, … (p[0], q[0]), (p[1], q[1]), … zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-

排列組合迭代器

該類函數創建的迭代器和排列組合有關,如product()是進行笛卡爾積運算;permutations()即數學中的排列;combinations()即數學中的組合;combinations_with_replacement()即又放回的組合。

Iterator Arguments Results
product() p, q, … [repeat=1] cartesian product, equivalent to a nested for-loop
permutations() p[, r] r-length tuples, all possible orderings, no repeated elements
combinations() p, r r-length tuples, in sorted order, no repeated elements
combinations_with_replacement() p, r r-length tuples, in sorted order, with repeated elements

下面給出幾個簡單的例子,其中需要說明的是permcomb是scipy中實現排列組合的函數,返回排列組合的組合個數。

from itertools import product, permutations, combinations, combinations_with_replacement
from scipy.special import comb, perm

# -----------------------product-----------------------------
print(list(product([1, 2, 3, 4], [1, 2], repeat=1)))  # *iterables, 他們之間進行笛卡爾積

# -----------------------permutations-------------------------
per = list(permutations([1, 2, 3, 4], r=2))  # 有序
assert len(per) == perm(4, 2)  # A^2_4, 排列組合問題
print(per)

# -----------------------combinations-------------------------

per = list(combinations([1, 2, 3, 4], r=2))  # 無放回抽樣
assert len(per) == comb(4, 2)  # C^2_4
print(per)

# -----------------------combinations-------------------------

per = list(combinations_with_replacement([1, 2, 3, 4], r=2))  # 有放回抽樣
assert len(per) == comb(4, 2, repetition=True)  # 10
print(per)

​ 雖然itertools模塊一下子提供了這麼多cool的迭代器生成函數,但是更多靈活的應用還需要多思考、多查閱文檔。另外,在python中談到迭代器,不得不談到列表生成式、生成器、可迭代對象等,在這篇文章不準備細談。如有需要,請參考我之前的博客Python面向對象、魔法方法最後一個部分。另外,限於篇幅原因,更多例子請參考我放在github倉庫中的代碼,我對每一個迭代器都給出了詳細例子。

operator

基本運算符函數

​ 上面也用到了operator模塊,該模塊提供了一套與Python的內置運算符對應的高效率函數,包括:對象的比較運算、邏輯運算、數學運算以及序列運算。該模塊一共定義了54個(python 3.7.2)運算符函數,可以通過operator.__all__查看,比較常用的一些如下列表所示:

運算 語法 函數
加法 a + b add(a, b)
字符串拼接 seq1 + seq2 concat(seq1, seq2)
包含測試 obj in seq contains(seq, obj)
除法 a / b truediv(a, b)
除法 a // b floordiv(a, b)
按位與 a & b and_(a, b)
按位異或 a ^ b xor(a, b)
按位取反 ~ a invert(a)
按位或 a | b or_(a, b)
取冪 a ** b pow(a, b)
一致 a is b is_(a, b)
一致 a is not b is_not(a, b)
索引賦值 obj[k] = v setitem(obj, k, v)
索引刪除 del obj[k] delitem(obj, k)
索引取值 obj[k] getitem(obj, k)
左移 a << b lshift(a, b)
取模 a % b mod(a, b)
乘法 a * b mul(a, b)
矩陣乘法 a @ b matmul(a, b)
否定(算術) - a neg(a)
否定(邏輯) not a not_(a)
正數 + a pos(a)
右移 a >> b rshift(a, b)
切片賦值 seq[i:j] = values setitem(seq, slice(i, j), values)
切片刪除 del seq[i:j] delitem(seq, slice(i, j))
切片取值 seq[i:j] getitem(seq, slice(i, j))
字符串格式化 s % obj mod(s, obj)
減法 a - b sub(a, b)
真值測試 obj truth(obj)
比較 a < b lt(a, b)
比較 a <= b le(a, b)
相等 a == b eq(a, b)
不等 a != b ne(a, b)
比較 a >= b ge(a, b)
比較 a > b gt(a, b)

這裏重點提一下setitem()delitem()getitem()三個函數,能方便的對序列進行操作,並且其效果是in-place的,下面會繼續講到in-place,請注意關注!

>>> from operator import setitem, delitem, getitem
>>> list_a = [1,2,3,4,5,6]
>>> getitem(list_a, 0)
Out[4]: 1
>>> getitem(list_a, slice(1, 5))
Out[7]: [2, 3, 4, 5]

>>> delitem(list_a, slice(1, 2))
>>> list_a
Out[9]: [1, 3, 4, 5, 6]

>>> setitem(list_a, 1, 2)
>>> list_a
Out[11]: [1, 2, 4, 5, 6]

看到這裏可能你會想,So, 有什麼用呢? 我爲什麼不直接用運算符呢?operator庫提供的函數一來是方便將運算符作爲函數快捷的傳入,二是效率一般也更高:

import operator
from itertools import accumulate
from time import time

start = time()
list(accumulate(range(100000000), func=operator.mul))
print('operator:', time()-start)
start = time()
list(accumulate(range(100000000), func=lambda x, y: x*y))
print('lambda', time()-start)

# Output
operator: 6.269657135009766
lambda 9.311992883682251

operator模塊中運算符函數部分還提供了In-place版本,當調用inplace版本時,計算和賦值會分開進行。對於不可變對象附錄1,如字符串、數字、元祖,值更新操作會進行,但是賦值操作會略過:

>>> a = 'hello'
>>> iadd(a, ' world')
'hello world'
>>> a
'hello'

而對於可變對象附錄1,如列表、字典,inplace method會依次進行計算、賦值操作:

>>> a = [1,2,3]

>>> add(a, [4,5,6])
[1, 2, 3, 4, 5, 6]
>>> a
[1, 2, 3]

>>> iadd(a, [4,5,6])
[1, 2, 3, 4, 5, 6]
>>> a
[1, 2, 3, 4, 5, 6]

屬性查詢

operator塊還定義了用於通用屬性查找的接口。

  • attrgetter(*attrs)

    attrgetter函數常用於mapsorted等接受一個函數作爲參數的函數,用於獲取對象的屬性,該函數返回一個函數g(obj)。其實現其實很簡單,就是利用getattr函數以及閉包附錄2的特性獲取對象的屬性,其實現等同如下代碼:

    def attrgetter(*items):
        if any(not isinstance(item, str) for item in items):
            raise TypeError('attribute name must be a string')
        if len(items) == 1:
            attr = items[0]
            def g(obj):
                return resolve_attr(obj, attr)
        else:
            def g(obj):
                return tuple(resolve_attr(obj, attr) for attr in items)
        return g
    
    def resolve_attr(obj, attr):
        for name in attr.split("."): 
            obj = getattr(obj, name)
        return obj
    

    其效果是,如果指定f = attrgetter('name'),則f(obj)返回obj.name;如果f = attrgetter('name', 'age'),則f(obj)返回(obj.name, obj.age);如果f = attrgetter('name.first', 'name.last'),則f(b)返回(b.name.first, b.name.last)。舉個例子,對學生按年齡進行排序,將attrgetter作爲sorted函數的key函數。

    from operator import attrgetter
    
    
    class Student:
        def __init__(self, name, grade, age):
            self.name = name
            self.grade = grade
            self.age = age
    
        def __str__(self):
            return self.name
    
        __repr__ = __str__
    
    
    students = [
        Student('jane', 96, 12),
        Student('john', 95, 12),
        Student('dave', 98, 10),
    ]
    
    print(sorted(students, key=attrgetter('age')))
    print(sorted(students, key=lambda o: o.age))
    print(sorted(students, key=attrgetter('age', 'grade'), reverse=True))
    
  • itemgetter(*items)

    itemgetter()函數的實現類似於attrgetter(),調用該函數返回一個函數,用於獲取序列數據。當然,這裏說獲取序列數據也許並不嚴謹,應該說可以獲取任何實現了__getitem__魔法方法的類對象元素信息。其實現等同於:

    def itemgetter(*items):
        if len(items) == 1:
            item = items[0]
            def g(obj):
                return obj[item]
        else:
            def g(obj):
                return tuple(obj[item] for item in items)
        return g
    

    比如可用於字符串截取:

    >>> itemgetter(1)('ABCDEFG')
    'B'
    >>> itemgetter(1,3,5)('ABCDEFG')
    ('B', 'D', 'F')
    >>> itemgetter(slice(2,None))('ABCDEFG')
    'CDEFG'
    
  • methodcaller(name[, args...])

    ​ 毫無疑問,methodcaller()的實現方式也是類似的,用於配合接受一個函數作爲參數的函數調用對象的方法,其內部實現等同於如下代碼:

    def methodcaller(name, *args, **kwargs):
        def caller(obj):
            return getattr(obj, name)(*args, **kwargs)
        return caller
    

    ​ 如果指定f = methodcaller('name'), 則 f(b) 返回 b.name()函數的調用結果;如果指定f = methodcaller('name', 'foo', bar=1), 則 f(b) returns b.name('foo', bar=1)的調用結果。

functools

functools模塊設計了一系列應用於高階函數的接口,可以通過functools.__all__查看所有可用函數,包括以下:

>>> import functools
>>> functools.__all__
Out[3]: 
['update_wrapper',
 'wraps',
 'WRAPPER_ASSIGNMENTS',
 'WRAPPER_UPDATES',
 'total_ordering',
 'cmp_to_key',
 'lru_cache',
 'reduce',
 'partial',
 'partialmethod',
 'singledispatch']

reduce被網友笑成爲被放逐到了functools模塊,因爲python語言創始人Guido不喜歡函數式編程,於是把reduce移出了builtin模塊,而map函數則在社區的強烈要求下得以在builtin立足。另外,lambdafilter也是python函數式編程中常用的builtin函數,這四個函數我在python函數詳解曾介紹過,這裏不再贅述;我曾在python裝飾器詳細剖析中簡單剖析過update_wrapperwrapsWRAPPER_ASSIGNMENTSWRAPPER_UPDATES的源碼,這裏也不再贅述。

partial & partialmethod

partial,俗稱偏函數,在python函數詳解中曾介紹過,其作用是爲函數某些參數設置默認值,簡化函數signature,其實現大致等同於下面的代碼,由此我們可以partial object有三個屬性,分別是partial_obj.funcpartial_obj.argspartial_obj.keywords,而並沒有自動創建其他一般函數擁有的屬性,如__name____doc__

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

比如,我們可以使用偏函數創建一個以2爲base的int函數basetwo:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

partialmethod類(python3.7.2)是用描述器實現的,(關於描述器請參考我之前的博客python描述器深度解析),該描述器的行爲類似於partial,只是它被設計爲用作方法定義而不是可直接調用的。

舉例:

>>> class Cell(object):
...     def __init__(self):
...         self._alive = False
...     @property
...     def alive(self):
...         return self._alive
...     def set_state(self, state):
...         self._alive = bool(state)
...     set_alive = partialmethod(set_state, True)
...     set_dead = partialmethod(set_state, False)
...
>>> c = Cell()
>>> c.alive
False
>>> c.set_alive()
>>> c.alive
True

cmp_to_key

​ 將comparison function(比較函數)轉化爲 key function。python中的key function包括sorted(), min(), max(), heapq.nlargest(), itertools.groupby()等等,它們的共同點是至少接受一個序列和一個key function,key function爲序列元素生成一個可用作排序的值;而comparison function接受兩個參數,並比較這兩個參數,根據他們的大小關係返回負值、零或正值。

sorted(iterable, key=cmp_to_key(locale.strcoll))

@total_ordering

​ 在Python面向對象、魔法方法曾說到python有一類對象比較的魔法方法,實現這些方法可以對對象進行比較大小。包括__gt__()__lt__()__eq__()__ne__()__ge__()。根據數學推理,只要我定義了__gt__()__lt__()__ne__()__ge__()中的一個以及定義了__eq__(),其比較大小的所有規則應該是確定的,可是我還是要一一實現剩餘三個方法,好累啊。在python3.2新增的total_ordering裝飾器則是用來簡化該問題,如下官方例子:

@total_ordering
class Student:
    def _is_valid_operand(self, other):
        return (hasattr(other, "lastname") and
                hasattr(other, "firstname"))
    def __eq__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

官方文檔特別提示,這可能帶來執行效率的低下,如果性能分析發現這會是瓶頸時,不妨自己手動實現吧,哈哈!

@lru_cache

​ 學過操作系統的同學對LRU應該不陌生,就是其中一種經典的頁面置換算法,另外還有FIFO、LFU等等。 該函數主要是用來做緩存,把相對耗時的函數結果進行保存,避免傳入相同的參數重複計算。同時,緩存並不會無限增長,不用的緩存會被釋放。

import functools


@functools.lru_cache(maxsize=20, typed=False)
def add(x, y):
    print(f'[Log]-invoke function now, with params x={x}, y={y}')
    return x + y


print(add(5, 6))
print(add(4, 7))
print(add(5, 6))
print(add(5, 6.0))

[Log]-invoke function now, with params x=5, y=6
11
[Log]-invoke function now, with params x=4, y=7
11
11
[Log]-invoke function now, with params x=5, y=6.0
11.0

​ 上面的例子可以看到,兩次調用add(5, 6)實際只發生了一次調用,因爲結果已經緩存,無需再次計算。但是add(5, 6.0)卻再次調用了,因爲我們指定了參數typed=False。也可以看看官方文檔給的斐波那契函數的高效實現,常規遞歸方式會導致很多重複計算,效率低下,緩存已計算可大大提高地櫃效率。

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

@singledispatch

​ 這是PEP443新提出的一個單參數泛型函數裝飾器,只作用於函數的第一個參數,舉例:

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)

​ 要將重載的實現添加到函數中,可以使用泛型函數的register()屬性。它是一個裝飾。對於帶有類型註釋的函數,裝飾器將自動推斷第一個參數的類型:

>>> @fun.register
... def _(arg: int, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> @fun.register
... def _(arg: list, verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

​ 對於不使用類型註釋的代碼,可以將類型參數顯式傳遞給裝飾器本身:

>>> @fun.register(complex)
... def _(arg, verbose=False):
...     if verbose:
...         print("Better than complicated.", end=" ")
...     print(arg.real, arg.imag)
...

​ 爲了能夠register lambda匿名函數和預先存在的函數,可以採用如下方式定義:

>>> def nothing(arg, verbose=False):
...     print("Nothing.")
...
>>> fun.register(type(None), nothing)

​ 調用時,泛型函數根據第一個參數的類型分派到不同的函數,如果傳入了一個沒有註冊的類型,則調用被@singledispatch裝飾的那個函數:

>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615

​ 可以通過下面的方式check,給定類型會調用哪個函數:

>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict)    # note: default implementation
<function fun at 0x103fe0000>

​ 可以通過只讀屬性registry查看已經實現的函數:

>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
          <class 'decimal.Decimal'>, <class 'list'>,
          <class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>

附錄

可變對象 & 不可變對象

​ 一般在函數傳參的時候我們才需要考慮一個對象是可變(mutable)還是不可變(immutable)對象,因爲這往往會決定,我們的函數有沒有副作用(side effects)。我們先來舉個例子,該例中lina指向同一個object,因爲參數傳入參數a是一個可變對象,因此在函數中對它的修改是in-place的,即alin同時被修改了(但是要理解到,實際只發生了一次修改,因爲lina指向同一個object)。

# 當參數是可變對象時
>>> def list_add3(lin):
    lin += [3]
    return lin

>>> a = [1, 2, 3]
>>> b = list_add3(a)
>>> b
[1, 2, 3, 3]
>>> a
[1, 2, 3, 3]

​ 但是,在函數中修改不可變對象時,是在內存中開闢一片新空間保存修改後的結果,即a指向原來的object,tin則指向修改後的object。

>>> def tuple_add3(tin):
    tin += (3,)
    return tin

>>> a = (1, 2, 3)
>>> b = tuple_add3(a)
>>> b
(1, 2, 3, 3)
>>> a
(1, 2, 3)

​ 在python中呢,不可變對象包括integers, floats, complex, strings, bytes, tuples, ranges 和 frozensets;可變對象包括lists, dictionaries, bytearrays 和 sets。

閉包

​ 在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。

​ 我們知道,python函數是第一類對象,它可以作爲參數值傳遞給函數,也可以作爲函數的返回值返回。在上面attrgetter()函數的實現中g就是一個閉包,它包含了外層函數的變量attritems

def attrgetter(*items):
    if any(not isinstance(item, str) for item in items):
        raise TypeError('attribute name must be a string')
    if len(items) == 1:
        attr = items[0]
        def g(obj):
            return resolve_attr(obj, attr)
    else:
        def g(obj):
            return tuple(resolve_attr(obj, attr) for attr in items)
    return g

>>> f = attrgetter('name')
>>> f.__closure__
Out[19]: (<cell at 0x11a131bb8: str object at 0x10d201538>,)
>>> f.__closure__[0]
Out[20]: <cell at 0x11a131bb8: str object at 0x10d201538>
>>> f.__closure__[0].cell_contents
Out[21]: 'name'

關於python函數和第一類對象更多的理解,可以參考我之前的博客python函數詳解python meataclass詳解

reference

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章