Python筆記

字符串格式化調用方法 —— format

通過創建字符串模板,利用format函數,替代相應的值。

可以通過絕對位置、相對位置以及關鍵字進行替代。例如

# 字符串格式化調用方法
template = '{0}, {1} and {2}' # By position
print(template.format('spam', 'ham', 'eggs'))
#spam, ham and eggs

template = '{motto}, {pork} and {food}' # By keyword
print(template.format(motto='spam', pork='ham', food='eggs'))
#spam, ham and eggs

template = '{}, {} and {}' # By relative position
print(template.format('spam', 'ham', 'eggs')) # New in 3.1 and 2.7
#'spam, ham and eggs'

print('{motto}, {0} and {food}'.format(42, motto=3.14, food=[1, 2]))
# 3.14, 42 and [1, 2]

可以看到,food是一個列表,它會根據__str__()進行替代。

格式化調用的高級用途

格式化字符串指定對象屬性(點表示)和字典鍵。例如

print('My {map[kind]} runs {sys.platform}'.format(sys=sys, map={'kind': 'laptop'}))
#My laptop runs win32

形式化結構

{fieldname|conversionflag:formatspec}

  • fieldname是指定參數的一個數字或關鍵字
  • conversionflag可以是r、s,或者a,分別是該值上對repr、str或ascii內置函數的一次調用
  • formatspce指定了如何表示該值,包括字段寬度、對齊方式、補零、小數點精度等細節,可以表示成
    [fill|align][sign][#][0][width][.precision][typecode]
    例如align可能是<,>或=,分別表示左對齊、右對齊或居中對齊。

例如

print('{0.platform:>10} = {1[kind]:<10}'.format(sys, dict(kind='laptop')))
#     win32 = laptop

print('{0:f}, {1:.2f}, {2:06.2f}'.format(3.14159, 3.14159, 3.14159))
#3.141590, 3.14, 003.14

if/else三元表達式

例子

A = 't' if 'spam' else 'f'
print(A)
#t

迭代器

可迭代對象,基本上,就是序列觀念的通用化。

Python中所謂的迭代協議:有__next__方法的對象會前進到下一個結果,而在一系列結果的末尾時,則會引發StopIteration。在Python中,任何這類對象都認為是可迭代的。任何這類對象也能以for循環或其他迭代工具遍歷,因為所有迭代工具內部工作起來都是在每次迭代中調用__next__,并且捕捉StopIteration異常來確定合適離開。

L = [1,2,3]
I = iter(L)
print(I.__next__())
#1
print(I.__next__())
#2
print(next(I))
#3
print(next(I))
#Traceback (most recent call last):
#StopIteration

自動迭代和手動迭代

L = [1, 2, 3]
for X in L: # Automatic iteration
    print(X ** 2, end=' ') # Obtains iter, calls __next__, catches exceptions
#1 4 9

I = iter(L) # Manual iteration: what for loops usually do
while True:
    try: # try statement catches exceptions
        X = next(I) # Or call I.__next__ in 3.X
    except StopIteration:
        break
    print(X ** 2, end=' ')
#1 4 9

多個迭代器和單個迭代器

多個迭代器,例如range,它不是自己的迭代器(手動迭代時,需要使用iter產生一個迭代器)。它支持在其結果上的多個迭代器,這些迭代器會記住它們各自的位置。與之相反,例如zip,它不支持多個迭代器。

R = range(3) # range allows multiple iterators
next(R)
#TypeError: 'range' object is not an iterator

I1 = iter(R)
print(next(I1))
#0
print(next(I1))
#1
I2 = iter(R)
print(next(I2))
#0

Z = zip((1, 2, 3), (10, 11, 12))
I1 = iter(Z)
I2 = iter(Z) # Two iterators on one zip
print(next(I1))
#(1, 10)
print(next(I1))
#(2, 11)
print(next(I2)) # (3.X) I2 is at same spot as I1!
#(3, 12)

變量域

image.png

內置作用域僅僅是一個名為builtins的內置模塊,但是必須要導入(即import builtins)之后才能使用內置作用域。

import builtins
print(dir(builtins))

閉合(closure)或工廠函數

一個能夠記住嵌套作用域的變量值的函數,盡管那個作用域已經不存在了。

其實,閉包指延伸了作用域的函數,其中包含函數定義體中引用、但是不在定義體中定義的非全局變量。函數是不是匿名的沒有關系,關鍵是它能訪問定義體之外定義的非全局變量。

def maker(N):
    def action(X): # Make and return action
        return X ** N # action retains N from enclosing scope
    return action

f = maker(2)
print(f)
#<function maker.<locals>.action at 0x0000019C3FCE6620>

print(f(3))
#9

這是函數式編程常用的方式。

在大多數情況下,給內層的lambda函數通過默認參數傳遞值沒什么必要。例如

def func1():
    x = 4
    action = (lambda n: x ** n) # x remembered from enclosing def
    return action

def func2():
    x = 4
    action = (lambda n, x=x: x ** n) # Pass x in manually
    return action

f1 = func1()
f2 = func2()
print(f1(2))
#16
print(f2(2))
#16

但是,如果嵌套在一個循環中,并且嵌套的函數引用了一個上層作用域的變量,該變量被循環所改變,所有在這個循環中產生的函數將會有相同的值——在最后一次循環中完成時被引用變量的值。

def makeActions():
    acts = []
    for i in range(5): # Tries to remember each i
        acts.append(lambda x: i ** x) # But all remember same last i!
    return acts

acts = makeActions()
print(acts[0](2))
#16
print(acts[1](2))
#16

def makeActions():
    acts = []
    for i in range(5): # Tries to remember each i
        acts.append(lambda x, i=i: i ** x) # But all remember same last i!
    return acts

acts = makeActions()
print(acts[0](2))
#0
print(acts[1](2))
#1

例子:average.py:計算移動平均值的高階函數

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

測試示例

>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
image.png

例子:一個簡單的裝飾器,輸出函數的運行時間

import time

def clock(func):
    def clocked(*args):
        t0 = time.time()
        result = func(*args)
        elapsed = time.time() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

使用 clock 裝飾器

import time
from clockdeco import clock

@clock
def snooze(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

if __name__=='__main__':
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))

輸出結果如下

$ python3 clockdeco_demo.py
**************************************** Calling snooze(123)
[0.12405610s] snooze(.123) -> None
**************************************** Calling factorial(6)
[0.00000191s] factorial(1) -> 1
[0.00004911s] factorial(2) -> 2
[0.00008488s] factorial(3) -> 6
[0.00013208s] factorial(4) -> 24
[0.00019193s] factorial(5) -> 120
[0.00026107s] factorial(6) -> 720
6! = 720

上述實現的 clock 裝飾器有幾個缺點:不支持關鍵字參數,而且遮蓋了被裝飾函數的 __name__ 和 __doc__ 屬性。我們使用functools.wraps 裝飾器把相關的屬性從 func 復制到 clocked 中。此外,這個新版還能正確處理關鍵字參數。

例子:改進后的 clock 裝飾器

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
        return result
    return clocked

nonlocal語句

nonlocal語句只在一個函數內有意義。當執行nonlocal語句的時候,nonlocal中列出的名稱必須在一個嵌套的def中提前定義過,否則將會產生一個錯誤。

def tester(start):
    state = start # Each call gets its own state
    def nested(label):
        nonlocal state # Remembers state in enclosing scope
        print(label, state)
        state += 1 # Allowed to change it if nonlocal
    return nested

F = tester(0)
F('spam')
#spam 0
F('eggs')
#eggs 1

例子:計算移動平均值的高階函數,不保存所有歷史值,但有
缺陷

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment

問題是,當 count 是數字或任何不可變類型時,count += 1 語句的作用其實與 count = count + 1 一樣。因此,我們在 averager 的定義體中為 count 賦值了,這會把 count 變成局部變量。total 變量也受這個問題影響。

之前的示例沒遇到這個問題,因為我們沒有給 series 賦值,我們只是調用 series.append,并把它傳給 sum 和 len。也就是說,我們利用了列表是可變的對象這一事實。

但是對數字、字符串、元組等不可變類型來說,只能讀取,不能更新。如果嘗試重新綁定,例如 count = count + 1,其實會隱式創建局部變量 count。這樣,count 就不是自由變量了,因此不會保存在閉包中。

為了解決這個問題,Python 3 引入了 nonlocal 聲明。它的作用是把變量標記為自由變量,即使在函數中為變量賦予新值了,也會變成自由變量。

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

標準庫中的裝飾器

  • 使用functools.lru_cache做備忘

functools.lru_cache 是非常實用的裝飾器,它實現了備忘(memoization)功能。這是一項優化技術,它把耗時的函數的結果保存起來,避免傳入相同的參數時重復計算。

例子:使用緩存實現生成第 n 個斐波納契數

import functools

#注意,必須像常規函數那樣調用 lru_cache。這一行中有一對括號:@functools.lru_cache()。
@functools.lru_cache()
#這里疊放了裝飾器:@lru_cache() 應用到 @clock 返回的函數上。
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

fibonacci(6)
#[0.00000000s] fibonacci(0) -> 0 
#[0.00000000s] fibonacci(1) -> 1 
#[0.00000000s] fibonacci(2) -> 1 
#[0.00000000s] fibonacci(3) -> 2 
#[0.00000000s] fibonacci(4) -> 3 
#[0.00000000s] fibonacci(5) -> 5 
#[0.00000000s] fibonacci(6) -> 8 
  • 單分派泛函數 —— functools.singledispatch

使用@singledispatch 裝飾的普通函數會變成泛函數(generic function):根據第一個參數的類型,以不同方式執行相同操作的一組函數。

from decimal import Decimal
from functools import singledispatch

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

@fun.register(int)
def _(arg, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)

@fun.register(list)
def _(arg, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, elem in enumerate(arg):
        print(i, elem)

@fun.register(float)
@fun.register(Decimal)
def fun_num(arg, verbose=False):
    if verbose:
        print("Half of your number:", end=" ")
    print(arg / 2)

def nothing(arg, verbose=False):
    print("Nothing.")

fun.register(type(None), nothing)

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

參數化裝飾器

創建一個裝飾器工廠函數,把參數傳給它,返回一個裝飾器,然后再把它應用到要裝飾的函數上。

例子:參數化clock裝飾器(為了簡單起見,基于最初實現的clock)

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

#clock 是參數化裝飾器工廠函數
def clock(fmt=DEFAULT_FMT):
    #decorate 是真正的裝飾器
    def decorate(func):
        #clocked 包裝被裝飾的函數
        def clocked(*_args):
            t0 = time.time()
            #_result 是被裝飾的函數返回的真正結果
            _result = func(*_args)
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            #這里使用 **locals() 是為了在 fmt 中引用 clocked 的局部變量
            print(fmt.format(**locals()))
            #clocked 會取代被裝飾的函數,因此它應該返回被裝飾的函數返回的值
            return _result
        return clocked
    return decorate

@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)
#snooze(0.123) dt=0.124s
#snooze(0.123) dt=0.124s
#snooze(0.123) dt=0.124s

遞歸函數

def sumtree(L):
    tot = 0
    for x in L: # For each item at this level
        if not isinstance(x, list):
            tot += x # Add numbers directly
        else:
            tot += sumtree(x) # Recur for sublists
    return tot

L = [1, [2, [3, 4], 5], 6, [7, 8]] # Arbitrary nesting
print(sumtree(L)) # Prints 36

函數內省

由于函數是對象,我們可以用常規的對象工具來處理函數。內省工具允許我們探索實現的細節。

def func(a):
    b = 'spam'
    return b * a

print(func.__name__)
print(dir(func))

reduce函數

它接受一個迭代器來處理,返回一個單個的結果。

from functools import reduce

print(reduce((lambda x, y: x + y), [1, 2, 3, 4]))
#10

def myreduce(function, sequence):
    tally = sequence[0]
    for next in sequence[1:]:
        tally = function(tally, next)
    return tally
print(myreduce((lambda x, y: x + y), [1, 2, 3, 4, 5]))
#15

生成器

  • 生成器函數
  • 生成器表達式

生成器函數

它與常規函數之間的主要代碼不同在于,生成器yields一個值,而不是返回一個值。yield語句掛起該函數并向調用者發送回一個值,但是,保留足夠的狀態以使得函數能夠從它離開的地方繼續。當繼續時,函數在上一個yield返回后立即繼續執行。

可迭代的對象定義了一個__next__方法,它要么返回迭代中的下一項,或者引發一個特殊的StopIteration異常來終止迭代。一個對象的迭代器用iter內置函數接收。生成器函數,編寫為包含yield語句的def語句,自動地支持迭代協議。

生成器函數和生成器表達式自身都是迭代器,因此是單迭代對象。

包導入模式

image.png

類的內省工具

一個簡單的內省工具

class AttrDisplay:
    """
    Provides an inheritable display overload method that shows
    instances with their class names and a name=value pair for
    each attribute stored on the instance itself (but not attrs
    inherited from its classes). Can be mixed into any class,
    and will work on any instance.
    """
    def gatherAttrs(self):
        attrs = []
        for key in sorted(self.__dict__):
            attrs.append('%s=%s' % (key, getattr(self, key)))
        return ', '.join(attrs)

    def __repr__(self):
        return '[%s: %s]' % (self.__class__.__name__, self.gatherAttrs())

class TopTest(AttrDisplay):
    count = 0
    def __init__(self):
        self.attr1 = TopTest.count
        self.attr2 = TopTest.count+1
        TopTest.count += 2

class SubTest(TopTest):
    pass

X, Y = TopTest(), SubTest()      # Make two instances
print(X)                         # Show all instance attrs
#[TopTest: attr1=0, attr2=1]
print(Y)                         # Show lowest class name
#[SubTest: attr1=2, attr2=3]

列出類樹中每個對象的屬性

顯示根據屬性所在的類來分組的屬性,它遍歷了整個類樹,在此過程中顯示了附加到每個對象上的屬性。

它這個遍歷繼承樹:從一個實例的__class__到其類,然后遞歸地從類的_bases__到其所有超類,一路掃描對象的__dict__。

def tester(listerclass, sept=False):

    class Super:
        def __init__(self):            # Superclass __init__
            self.data1 = 'spam'        # Create instance attrs
        def ham(self):
            pass

    class Sub(Super, listerclass):     # Mix in ham and a __str__
        def __init__(self):            # Listers have access to self
            Super.__init__(self)
            self.data2 = 'eggs'        # More instance attrs
            self.data3 = 42
        def spam(self):                # Define another method here
            pass

    instance = Sub()                   # Return instance with lister's __str__
    print(instance)                    # Run mixed-in __str__ (or via str(x))
    if sept: print('-' * 80)

class ListTree:
    """
    Mix-in that returns an __str__ trace of the entire class tree and all
    its objects' attrs at and above self;  run by print(), str() returns
    constructed string;  uses __X attr names to avoid impacting clients;
    recurses to superclasses explicitly, uses str.format() for clarity;
    """
    def __attrnames(self, obj, indent):
        spaces = ' ' * (indent + 1)
        result = ''
        for attr in sorted(obj.__dict__):
            if attr.startswith('__') and attr.endswith('__'):
                result += spaces + '{0}\n'.format(attr)
            else:
                result += spaces + '{0}={1}\n'.format(attr, getattr(obj, attr))
        return result

    def __listclass(self, aClass, indent):
        dots = '.' * indent
        if aClass in self.__visited:
            return '\n{0}<Class {1}:, address {2}: (see above)>\n'.format(
                           dots,
                           aClass.__name__,
                           id(aClass))
        else:
            self.__visited[aClass] = True
            here  = self.__attrnames(aClass, indent)
            above = ''
            for super in aClass.__bases__:
                above += self.__listclass(super, indent+4)
            return '\n{0}<Class {1}, address {2}:\n{3}{4}{5}>\n'.format(
                           dots,
                           aClass.__name__,
                           id(aClass),
                           here, above,
                           dots)

    def __str__(self):
        self.__visited = {}
        here  = self.__attrnames(self, 0)
        above = self.__listclass(self.__class__, 4)
        return '<Instance of {0}, address {1}:\n{2}{3}>'.format(
                           self.__class__.__name__,
                           id(self),
                           here, above)

tester(ListTree)

上述結果顯示為

<Instance of Sub, address 2053627225200:
 _ListTree__visited={}
 data1=spam
 data2=eggs
 data3=42

....<Class Sub, address 2053625967896:
     __doc__
     __init__
     __module__
     spam=<function tester.<locals>.Sub.spam at 0x000001DE25B91AE8>

........<Class Super, address 2053625972616:
         __dict__
         __doc__
         __init__
         __module__
         __weakref__
         ham=<function tester.<locals>.Super.ham at 0x000001DE25B919D8>

............<Class object, address 1391801792:
             __class__
             __delattr__
             __dir__
             __doc__
             __eq__
             __format__
             __ge__
             __getattribute__
             __gt__
             __hash__
             __init__
             __init_subclass__
             __le__
             __lt__
             __ne__
             __new__
             __reduce__
             __reduce_ex__
             __repr__
             __setattr__
             __sizeof__
             __str__
             __subclasshook__
............>
........>

........<Class ListTree, address 2053625973560:
         _ListTree__attrnames=<function ListTree.__attrnames at 0x000001DE25B917B8>
         _ListTree__listclass=<function ListTree.__listclass at 0x000001DE25B91840>
         __dict__
         __doc__
         __module__
         __str__
         __weakref__

............<Class object:, address 1391801792: (see above)>
........>
....>
>

類接口技術

class Super:
    def method(self):
        print('in Super.method') # Default behavior

    def delegate(self):
        self.action() # Expected to be defined

class Inheritor(Super): # Inherit method verbatim
    pass

class Replacer(Super): # Replace method completely
    def method(self):
        print('in Replacer.method')

class Extender(Super): # Extend method behavior
    def method(self):
        print('starting Extender.method')
        Super.method(self)
        print('ending Extender.method')

class Provider(Super): # Fill in a required method
    def action(self):
        print('in Provider.action')

for klass in (Inheritor, Replacer, Extender):
    print('\n' + klass.__name__ + '...')
    klass().method()

print('\nProvider...')
x = Provider()
x.delegate()

#Inheritor...
#in Super.method

#Replacer...
#in Replacer.method

#Extender...
#starting Extender.method
#in Super.method
#ending Extender.method

#Provider...
#in Provider.action

用戶定義迭代器

Python中所有的迭代環境都會先嘗試__iter__方法,再嘗試__getitem__。

從技術角度來說,迭代環境是通過調用內置函數iter去嘗試尋找__iter__方法來實現,而這種方法應該返回一個迭代器對象。如果已經提供了,Python就會重復調用這個迭代器對象的next方法,直到發生StopIteration異常。如果沒有找到這類__iter__方法,Python會改用__getitem__機制,就像之前那樣通過偏移量重復索引,直到引發InderError異常。

class Squares:
    def __init__(self, start, stop):    # Save state when created
        self.value = start - 1
        self.stop  = stop

    def __iter__(self):                 # Get iterator object on iter
        return self

    def __next__(self):                 # Return a square on each iteration
        if self.value == self.stop:     # Also called by next built-in
            raise StopIteration
        self.value += 1
        return self.value ** 2

for i in Squares(1,5):
    print(i, end=' ')
#1 4 9 16 25

上述的列子里,迭代器對象就是實例self,self里實現了__next__方法。要注意的是__iter__只循環一次,而不是多次。

sq = Squares(1,5)
print([n for n in sq])
#[1, 4, 9, 16, 25]
print([n for n in sq])
#[]

屬性引用:__getattr__和__setattr__

__getattr__方法是攔截未定義(不存在)屬性點號運算。如果Python可通過其繼承樹搜索流程找到這個屬性,該方法就不會被調用。

class Empty:
    def __getattr__(self, attrname): # On self.undefined
        if attrname == 'age':
            return 40
        else:
            raise AttributeError(attrname)
X = Empty()
print(X.age)
#40

而__setattr__會攔截所有屬性的賦值語句。如果定義了這個方法,self.attr=value會變成self.__setattr__('attr',value)。這一點技巧性很高,因為在__setattr__中對任何self屬性做賦值,都會再調用__setattr__,導致了無窮遞歸循環。如果想要使用這個方法,要確定是通過對屬性字典做索引運算來賦值,也就是說,是使用self.__dict__['name'] = x,而不是self.name=x。

class Accesscontrol:
    def __setattr__(self, attr, value):
        if attr == 'age':
            self.__dict__[attr] = value + 10 # Not self.name=val or setattr
        else:
            raise AttributeError(attr + ' not allowed')

X = Accesscontrol()
X.age = 40
print(X.age)
#50

委托(delegation)

委托通常是以__getattr__鉤子方法實現的。

class Wrapper:
    def __init__(self, object):
        self.wrapped = object                   # Save object

    def __getattr__(self, attrname):
        print('Trace: ' + attrname)              # Trace fetch
        return getattr(self.wrapped, attrname)   # Delegate fetch

x = Wrapper([1,2,3])
x.append(4)
#Trace: append
print(x.wrapped)
#[1, 2, 3, 4]

slots實例

將字符串屬性名稱順序賦值為特殊的__slots__類屬性,能夠優化內存和速度性能。

使用slots的時候,實例通常沒有一個屬性字典。

class C:
    __slots__ = ['a', 'b']

X = C()
X.a = 1
print(X.a)
#1
print(X.__dict__)
#AttributeError: 'C' object has no attribute '__dict__'

可以通過在__slots__中包含__dict__,仍然可以容納額外的屬性,從而考慮到一個屬性空間字典的需求。下面這個例子中,兩種儲存機制都用到了。

class D:
    __slots__ = ['a', 'b', '__dict__'] # Name __dict__ to include one too
    c = 3 # Class attrs work normally

    def __init__(self):
        self.d = 4 # d stored in __dict__, a is a slot

X = D()
X.a = 1
X.b = 2
print(X.d)
#4
print(X.c)
#3
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
    print(attr, '=>', getattr(X, attr))
#d => 4
#a => 1
#b => 2
#__dict__ => {'d': 4}

靜態方法和類方法

使用類方法統計每個類的實例

class Spam:
    numInstances = 0
    def count(cls): # Per-class instance counters
        cls.numInstances += 1 # cls is lowest class above instance

    def __init__(self):
        self.count() # Passes self.__class__ to count
    count = classmethod(count)

class Sub(Spam):
    numInstances = 0
    def __init__(self): # Redefines __init__
        Spam.__init__(self)

class Other(Spam): # Inherits __init__
    numInstances = 0

x = Spam()
y1, y2 = Sub(), Sub()
z1, z2, z3 = Other(), Other(), Other()
print(Spam.numInstances, Sub.numInstances, Other.numInstances)
#1 2 3

異常

異常基礎——捕獲異常

在try語句內,自行捕捉異常。

def fetcher(obj, index):
    return obj[index]

x = 'spam'

def catcher():
    try:
        fetcher(x, 4)
    except IndexError:
        print('got exception')
    print('continuing')

catcher()
#got exception
#continuing

異常基礎——引發異常

要手動觸發異常,直接執行raise語句。

try:
    raise IndexError
except IndexError:
    print('got exception!')
#got exception!

終止行為

try/finally的組合,可以定義一定會在最后執行時的收尾行為,無論try代碼塊中是否發生了異常。

try:
    fetcher(x, 3)
finally:
    print('after fetch!')
#after fetch!

try語句分句

image.png

with/as環境管理器

image.png

文件對象有環境管理器,可在with代碼塊后自動關閉文件,無論是否引發異常。

with語句實際的工作方式

  1. 計算表達式,所得到的對象稱為環境管理器,它必須有__enter__和__exit__方法。
  2. 環境管理器的__enter__方法會被調用。如果as子句存在,其返回值會賦值為as子句中的變量,否則直接丟棄。
  3. 代碼塊中嵌套的代碼會執行。
  4. 如果with代碼塊引發異常,__exit__(type, value, traceback)方法就會被調用。如果此方法返回值為假,則異常會重新引發。否則,異常會終止。正常情況下異常是應該被重新引發,這樣的話才能傳遞到with語句之外。
  5. 如果with代碼塊沒有引發異常,__exit__方法依然會被調用,其type、value以及traceback參數都會以None傳遞。
class TraceBlock:
    def message(self, arg):
        print('running ' + arg)

    def __enter__(self):
        print('starting with block')
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        if exc_type is None:
            print('exited normally\n')
        else:
            print('raise an exception! ' + str(exc_type))
            return False  # Propagate

with TraceBlock() as action:
    action.message('test 1')
    print('reached')

#starting with block
#running test 1
#reached
#exited normally

內置Exception類

  • BaseException—— 異常的頂級根類,它不能由用戶定義的類直接繼承。
  • Exception —— 與應用相關的異常的頂層根超類。這是BaseException的一個直接子類,并且是所有其他內置異常的超類。

管理屬性

  • __getattr__和__setattr__方法,把未定義的屬性獲取和所有的屬性賦值指向通用的處理器方法
  • __getattribute__方法,把所有屬性獲取都指向一個泛型處理器方法
  • property內置函數,把特定屬性訪問定位到get和set處理器函數,也叫特性
  • 描述符協議,把特定屬性訪問定位到具體任意get和set處理器方法

特性

特性協議允許我們把一個特定屬性的get和set操作指向我們提供的函數或方法,使得我們能夠插入在屬性訪問的時候自動運行代碼。

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self): # name = property(name)
        "name property docs"
        print('fetch...')
        return self._name

    @name.setter
    def name(self, value):  # name = name.setter(name)
        print('change...')
        self._name = value

    @name.deleter
    def name(self):  # name = name.deleter(name)
        print('remove...')
        del self._name

bob = Person('Bob Smith')  # bob has a managed attribute
print(bob.name)  # Runs name getter (name 1)
#fetch...
#Bob Smith
bob.name = 'Robert Smith'  # Runs name setter (name 2)
#change...
print(bob.name)
#fetch...
#Robert Smith

描述符

描述符提供了攔截屬性訪問的一種替代方法。實際上,特性是描述符的一種——從技術上講,property內置函數只是創建一個特定類型的描述符的一種簡化方式,而這種描述符在屬性訪問時運行方法函數。

從功能上講,描述符協議允許我們把一個特定屬性的get和set操作指向我們提供的一個單獨類對象的方法:它們提供了一種方式來插入在訪問屬性的時候自動運行的代碼。

描述符作為獨立的類創建,它提供了一種更為通用的解決方案。

image.png
class Name:
    "name descriptor docs"
    def __get__(self, instance, owner):
        print('fetch...')
        return instance._name

    def __set__(self, instance, value):
        print('change...')
        instance._name = value

    def __delete__(self, instance):
        print('remove...')
        del instance._name

class Person:
    def __init__(self, name):
        self._name = name
    name = Name() # Assign descriptor to attr

bob = Person('Bob Smith') # bob has a managed attribute
print(bob.name) # Runs Name.__get__
#fetch...
#Bob Smith
bob.name = 'Robert Smith' # Runs Name.__set__
#change...
print(bob.name)
#fetch...
#Robert Smith

描述符可以使用實例狀態和描述符狀態,或者兩者的任何組合

  • 描述符狀態用來管理內部用于描述符工作的數據
  • 實例狀態記錄了和客戶類相關的信息
class DescState: # Use descriptor state
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner): # On attr fetch
        print('DescState get')
        return self.value * 10

    def __set__(self, instance, value): # On attr assign
        print('DescState set')
        self.value = value

# Client class
class CalcAttrs:
    X = DescState(2) # Descriptor class attr
    Y = 3 # Class attr
    
    def __init__(self):
        self.Z = 4 # Instance attr

obj = CalcAttrs()
print(obj.X, obj.Y, obj.Z) # X is computed, others are not
#DescState get
#20 3 4
obj.X = 5 # X assignment is intercepted
#DescState set
obj.Y = 6
obj.Z = 7
print(obj.X, obj.Y, obj.Z)
#DescState get
#50 6 7

__getattr__和__getattribute__

屬性獲取攔截表現為兩種形式,可用兩種不同的方法來編寫:

  • __getattr__針對未定義的屬性運行
  • __getattribute__針對每個屬性,因此,當使用它的時候,必須小心避免通過把屬性訪問傳遞給超類而導致遞歸循環
image.png

避免屬性攔截方法中的循環。要解決這個問題,把獲取指向一個更高的超類,而不是跳過這個層級的版本——object類總是一個超類,并且它在這里可以很好地起作用。

def __getattribute__(self, name):
    x = object.__getattribute__(self, 'other') # Force higher to avoid me

對于__setattr__,情況是類似的。在這個方法內賦值任何屬性,都會再次觸發__setattr__并創建一個類似的循環。要解決這個問題,把屬性作為實例的__dict__命名空間字典中的一個鍵賦值。這樣就避免了直接的屬性賦值。

def __setattr__(self, name, value):
    self.__dict__['other'] = value # Use atttr dict to avoid me

盡管這種方法比較少用到,但__setattr__也可以把自己的屬性賦值傳遞給一個更高的超類而避免循環,就像__getattribute__一樣。

def __setattr__(self, name, value):
    object.__setattr__(self, 'other', value) # Force higher to avoid me

相反,我們不能使用__dict__技巧在__getattribute__中避免循環。

def __getattribute__(self, name):
    x = self.__dict__['other'] # LOOPS!

獲取__dict__屬性本身會再次觸發__getattribute__,導致一個遞歸循環。很奇怪,但確實如此。

例子:比較兩種get方法

class GetAttr:
    attr1 = 1

    def __init__(self):
        self.attr2 = 2

    def __getattr__(self, attr):  # On undefined attrs only
        print('get: ' + attr)  # Not on attr1: inherited from class
        if attr == 'attr3':  # Not on attr2: stored on instance
            return 3
        else:
            raise AttributeError(attr)


X = GetAttr()
print(X.attr1)
#1
print(X.attr2)
#2
print(X.attr3)
#get: attr3
#3
print('-' * 20)

class GetAttribute(object):  # (object) needed in 2.X only
    attr1 = 1

    def __init__(self):
        self.attr2 = 2

    def __getattribute__(self, attr):  # On all attr fetches
        print('get: ' + attr)  # Use superclass to avoid looping here
        if attr == 'attr3':
            return 3
        else:
            return object.__getattribute__(self, attr)


X = GetAttribute()
print(X.attr1)
#get: attr1
#1
print(X.attr2)
#get: attr2
#2
print(X.attr3)
#get: attr3
#3

示例:屬性驗證

  • 方法一:使用特性來驗證

要理解這段代碼,關鍵是要注意到,__init__構造函數方法內部的屬性賦值也觸發了特性的setter方法。例如,當這個方法分配給self.name時,它自動調用setName方法,該方法轉換值并將其賦給一個叫做__name的實例屬性,以便它不會與特性的名稱沖突。

class CardHolder:
    acctlen = 8                                # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct                       # Instance data
        self.name = name                       # These trigger prop setters too!
        self.age  = age                        # __X mangled to have class name
        self.addr = addr                       # addr is not managed
                                               # remain has no data
    def getName(self):
        return self.__name
    def setName(self, value):
        value = value.lower().replace(' ', '_')
        self.__name = value
    name = property(getName, setName)

    def getAge(self):
        return self.__age
    def setAge(self, value):
        if value < 0 or value > 150:
            raise ValueError('invalid age')
        else:
            self.__age = value
    age = property(getAge, setAge)

    def getAcct(self):
        return self.__acct[:-3] + '***'
    def setAcct(self, value):
        value = value.replace('-', '')
        if len(value) != self.acctlen:
            raise TypeError('invald acct number')
        else:
            self.__acct = value
    acct = property(getAcct, setAcct)

    def remainGet(self):                       # Could be a method, not attr
        return self.retireage - self.age       # Unless already using as attr
    remain = property(remainGet)

def printholder(who):
    print(who.acct, who.name, who.age, who.remain, who.addr, sep=' / ')

bob = CardHolder('1234-5678', 'Bob Smith', 40, '123 main st')
printholder(bob)
#12345*** / bob_smith / 40 / 19.5 / 123 main st

bob.name = 'Bob Q. Smith'
bob.age  = 50
bob.acct = '23-45-67-89'
printholder(bob)
#23456*** / bob_q._smith / 50 / 9.5 / 123 main st

sue = CardHolder('5678-12-34', 'Sue Jones', 35, '124 main st')
printholder(sue)
#56781*** / sue_jones / 35 / 24.5 / 124 main st

try:
    sue.age = 200
except:
    print('Bad age for Sue')
#Bad age for Sue

try:
    sue.remain = 5
except:
    print("Can't set sue.remain")
#Can't set sue.remain

try:
    sue.acct = '1234567'
except:
    print('Bad acct for Sue')
#Bad acct for Sue

方法二:使用描述符驗證

要理解這段代碼,注意__init__構造函數方法內部的屬性賦值觸發了描述符的__set__操作符方法,這一點還是很重要。例如,當構造函數方法分配給self.name時,它自動調用Name.__set__()方法,該方法轉換值,并且將其賦給了叫做name的一個描述符屬性。

class CardHolder:                        
    acctlen = 8                                  # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct                         # Instance data
        self.name = name                         # These trigger __set__ calls too!
        self.age  = age                          # __X not needed: in descriptor
        self.addr = addr                         # addr is not managed
                                                 # remain has no data
    class Name(object):
        def __get__(self, instance, owner):      # Class names: CardHolder locals
            return self.name
        def __set__(self, instance, value):
            value = value.lower().replace(' ', '_')
            self.name = value
    name = Name()

    class Age(object):
        def __get__(self, instance, owner):
            return self.age                             # Use descriptor data
        def __set__(self, instance, value):
            if value < 0 or value > 150:
                raise ValueError('invalid age')
            else:
                self.age = value
    age = Age()

    class Acct(object):
        def __get__(self, instance, owner):
            return self.acct[:-3] + '***'
        def __set__(self, instance, value):
            value = value.replace('-', '')
            if len(value) != instance.acctlen:          # Use instance class data
                raise TypeError('invald acct number')
            else:
                self.acct = value
    acct = Acct()

    class Remain(object):
        def __get__(self, instance, owner):
            return instance.retireage - instance.age    # Triggers Age.__get__
        def __set__(self, instance, value):
            raise TypeError('cannot set remain')        # Else set allowed here
    remain = Remain()

def printholder(who):
    print(who.acct, who.name, who.age, who.remain, who.addr, sep=' / ')

bob = CardHolder('1234-5678', 'Bob Smith', 40, '123 main st')
printholder(bob)
#12345*** / bob_smith / 40 / 19.5 / 123 main st

bob.name = 'Bob Q. Smith'
bob.age  = 50
bob.acct = '23-45-67-89'
printholder(bob)
#23456*** / bob_q._smith / 50 / 9.5 / 123 main st

sue = CardHolder('5678-12-34', 'Sue Jones', 35, '124 main st')
printholder(sue)
#56781*** / sue_jones / 35 / 24.5 / 124 main st

try:
    sue.age = 200
except:
    print('Bad age for Sue')
#Bad age for Sue

try:
    sue.remain = 5
except:
    print("Can't set sue.remain")
#Can't set sue.remain

try:
    sue.acct = '1234567'
except:
    print('Bad acct for Sue')
#Bad acct for Sue

**方法三:使用__getattr__來驗證

對于這個例子的特性和描述符版本,注意__init__構造函數方法中的屬性賦值觸發了類的__setattr__方法,這還是很關鍵的。例如,當這個方法分配給self.name時,它自動地調用__setattr__方法,該方法轉換值,并將其分配給一個名為name的實例屬性。通過在該實例上存儲name,它確保了未來的訪問不會觸發__getattr__。相反,acct存儲為_acct,因此隨后對acct的訪問會調用__getattr__。

class CardHolder:
    acctlen = 8                                  # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct                         # Instance data
        self.name = name                         # These trigger __setattr__ too
        self.age  = age                          # _acct not mangled: name tested
        self.addr = addr                         # addr is not managed
                                                 # remain has no data
    def __getattr__(self, name):
        if name == 'acct':                           # On undefined attr fetches
            return self._acct[:-3] + '***'           # name, age, addr are defined
        elif name == 'remain':
            return self.retireage - self.age         # Doesn't trigger __getattr__
        else:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        if name == 'name':                           # On all attr assignments
            value = value.lower().replace(' ', '_')  # addr stored directly
        elif name == 'age':                          # acct mangled to _acct
            if value < 0 or value > 150:
                raise ValueError('invalid age')
        elif name == 'acct':
            name  = '_acct'
            value = value.replace('-', '')
            if len(value) != self.acctlen:
                raise TypeError('invald acct number')
        elif name == 'remain':
            raise TypeError('cannot set remain')
        self.__dict__[name] = value                  # Avoid looping (or via object)

方法四:使用__getattribute__驗證

注意,由于每個屬性獲取都指向了__getattribute__,所以這里我們不需要壓縮名稱以攔截它們(acct存儲為acct)。另一方面,這段代碼必須負責把未壓縮的屬性獲取指向一個超類以避免循環。

class CardHolder:                        
    acctlen = 8                                  # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct                         # Instance data
        self.name = name                         # These trigger __setattr__ too
        self.age  = age                          # acct not mangled: name tested
        self.addr = addr                         # addr is not managed
                                                 # remain has no data
    def __getattribute__(self, name):
        superget = object.__getattribute__             # Don't loop: one level up
        if name == 'acct':                             # On all attr fetches
            return superget(self, 'acct')[:-3] + '***'
        elif name == 'remain':
            return superget(self, 'retireage') - superget(self, 'age')
        else:
            return superget(self, name)                # name, age, addr: stored

    def __setattr__(self, name, value):
        if name == 'name':                             # On all attr assignments
            value = value.lower().replace(' ', '_')    # addr stored directly
        elif name == 'age':
            if value < 0 or value > 150:
                raise ValueError('invalid age')
        elif name == 'acct':
            value = value.replace('-', '')
            if len(value) != self.acctlen:
                raise TypeError('invald acct number')
        elif name == 'remain':
            raise TypeError('cannot set remain')
        self.__dict__[name] = value                     # Avoid loops, orig names

裝飾器

裝飾是為函數和類指定管理代碼的一種方式。裝飾器本身的形式是處理其他的可調用對象的可調用的對象(如函數)。

  • 函數裝飾器
  • 類裝飾器

簡而言之,裝飾器提供了一種方法,在函數和類定義語句的末尾插入自動運行代碼——對于函數裝飾器,在def的末尾;對于類裝飾器,在class的末尾。這樣的代碼可以扮演不同的角色。

管理函數和類

  • 函數裝飾器也可以用來管理函數對象,而不是隨后對它們的調用——例如,把一個函數注冊到一個API。
  • 類裝飾器也可以用來直接管理類對象,而不是實例創建調用——例如,用新的方法擴展類。

為了支持函數和方法,嵌套函數的替代方法工作得更好。

image.png

Python何時執行裝飾器

裝飾器的一個關鍵特性是,它們在被裝飾的函數定義之后立即運行。這通常是在導入時(即 Python 加載模塊時)。

例子:registration.py 模塊

registry = []  # registry will hold references to functions decorated by @register.

def register(func):  # register takes a function as argument.
    print('running register(%s)' % func)
    registry.append(func) 
    return func 

@register 
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3(): 
    print('running f3()')

def main():  # main displays the registry, then calls f1(), f2(), and f3().
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main()

把 registration.py 當作腳本運行得到的輸出如下

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

如果導入 registration.py 模塊(不作為腳本運行),輸出如下

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

函數裝飾器在導入模塊時立即執行,而被裝飾的函數只在明確調用時運行。這突出了 Python 程序員所說的導入時和運行時之間的區別。

用bisect來管理已排序的序列

bisect 模塊包含兩個主要函數,bisectinsort,兩個函數都利用二分查找算法來在有序序列中查找或插入元素。

例子:在有序序列中用 bisect 查找某個元素的插入位置

import bisect

HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]

ROW_FMT = '{0:2d} @ {1:2d}    {2}{0:<2d}'

def demo(bisect_fn):
    for needle in reversed(NEEDLES):
        # 用特定的 bisect 函數來計算元素應該出現的位置。
        position = bisect_fn(HAYSTACK, needle)
        # 利用該位置來算出需要幾個分隔符號。
        offset = position * '  |'
        # 把元素和其應該出現的位置打印出來。
        print(ROW_FMT.format(needle, position, offset))

bisect_fn = bisect.bisect
#把選定的函數在抬頭打印出來。
print('DEMO:', bisect_fn.__name__)
print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)
#DEMO: bisect
#haystack ->  1  4  5  6  8 12 15 20 21 23 23 26 29 30
#31 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  |31
#30 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  |30
#29 @ 13      |  |  |  |  |  |  |  |  |  |  |  |  |29
#23 @ 11      |  |  |  |  |  |  |  |  |  |  |23
#22 @  9      |  |  |  |  |  |  |  |  |22
#10 @  5      |  |  |  |  |10
# 8 @  5      |  |  |  |  |8
# 5 @  3      |  |  |5
# 2 @  1      |2
# 1 @  1      |1
# 0 @  0    0

bisect 函數其實是 bisect_right 函數的別名,后者還有個姊妹函數叫 bisect_left。它們的區別在于,bisect_left 返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素會被放置于它相等的元素的前面,而 bisect_right 返回的則是跟它相等的元素之后的位置。

bisect 可以用來建立一個用數字作為索引的查詢表格,比如說把分數
和成績。

def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
    i = bisect.bisect(breakpoints, score)
    return grades[i]

print([grade(score) for score in [33, 99, 77, 70, 89, 90, 100]])
#['F', 'A', 'C', 'C', 'B', 'A', 'A']

排序很耗時,因此在得到一個有序序列之后,我們最好能夠保持它的有序。bisect.insort 就是為了這個而存在的。insort(seq, item) 把變量 item 插入到序列 seq 中,并能保持 seq的升序順序。

例子:insort 可以保持有序序列的順序

import random

SIZE = 7

random.seed(1729)

my_list = []
for i in range(SIZE):
    new_item = random.randrange(SIZE*2)
    bisect.insort(my_list, new_item)
    print('%2d ->' % new_item, my_list)
#10 -> [10]
# 0 -> [0, 10]
# 6 -> [0, 6, 10]
# 8 -> [0, 6, 8, 10]
# 7 -> [0, 6, 7, 8, 10]
# 2 -> [0, 2, 6, 7, 8, 10]
#10 -> [0, 2, 6, 7, 8, 10, 10]

數組

如果我們需要一個只包含數字的列表,那么 array.array 比 list 更高效。數組支持所有跟可變序列有關的操作,包括 .pop、.insert 和.extend。另外,數組還提供從文件讀取和存入文件的更快的方法,如.frombytes 和 .tofile。

from array import array
floats = array('d', (random.random() for i in range(10**7)))
print(floats[-1])
#0.5963321947530882

內存視圖

memoryview 是一個內置類,它能讓用戶在不復制內容的情況下操作同一個數組的不同切片。

>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)
>>> len(memv)
5
>>> memv[0]
-2
>>> memv_oct = memv.cast('B')
>>> memv_oct.tolist()
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4

隊列

collections.deque 類(雙向隊列)是一個線程安全、可以快速從兩端添加或者刪除元素的數據類型。而且如果想要有一種數據類型來存放“最近用到的幾個元素”,deque 也是一個很好的選擇。

>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33])
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40])
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

可散列的數據類型

如果一個對象是可散列的,那么在這個對象的生命周期中,它的散列值是不變的,而且這個對象需要實現__hash__() 方法。另外可散列對象還要有__qe__() 方法,這樣才能跟其他鍵做比較。如果兩個可散列對象是相等的,那么它們的散列值一定是一樣的。

原子不可變數據類型(str、bytes 和數值類型)都是可散列類
型,frozenset 也是可散列的。元組的話,只有當一個元組包含的所有元素都是可散列類型的情況下,它才是可散列的。

處理字典中找不到的鍵

  • 利用setdefault
my_dict.setdefault(key, []).append(new_value)

相當于

if key not in my_dict:
    my_dict[key] = []
    my_dict[key].append(new_value)
  • 利用defaultdict
from collections import defaultdict

s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list)
for k, v in s:
    d[k].append(v)

print(sorted(d.items()))
#[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]

所有的映射類型在處理找不到的鍵的時候,都會牽扯到__missing__方法。

例子:當有非字符串的鍵被查找的時候,StrKeyDict0 是如何
在該鍵不存在的情況下,把它轉換為字符串的

class StrKeyDict0(dict):  

    def __missing__(self, key):
        if isinstance(key, str):  
            raise KeyError(key)
        return self[str(key)] 

    def get(self, key, default=None):
        try:
            return self[key] 
        except KeyError:
            return default 

    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()

>>> d = StrKeyDict0([('2', 'two'), ('4', 'four')])
>>> d['2']
'two'
>>> d[1]
Traceback (most recent call last):
...
KeyError: '1'
>>> d.get('2')
'two'
>>> d.get(4)
'four'
>>> d.get(1, 'N/A')
'N/A'
>>> 2 in d
True
>>> 1 in d
False

字典的變種

  • collections.OrderedDict
  • collections.ChainMap —— 該類型可以容納數個不同的映射對象,然后在進行鍵查找操作的時候,這些對象會被當作一個整體被逐個查找,直到鍵被找到為止。
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))
  • collections.Counter —— 這個映射類型會給鍵準備一個整數計數器。每次更新一個鍵的時候都會增加這個計數器。
>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.update('aaaaazzz')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(2)
[('a', 10), ('z', 3)]
  • collections.UserDict —— 更傾向于從 UserDict 而不是從 dict 繼承的主要原因是,后者有時會在某些方法的實現上走一些捷徑,導致我們不得不在它的子類中重寫這些方法,但是 UserDict 就不會帶來這些問題。
import collections

class StrKeyDict(collections.UserDict): 

    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]

    def __contains__(self, key):
        return str(key) in self.data  

    def __setitem__(self, key, item):
        self.data[str(key)] = item 

不可變映射類型:MappingProxyType

types 模塊中引入了一個封裝類名叫MappingProxyType。如果給這個類一個映射,它會返回一個只讀的映射視圖。雖然是個只讀視圖,但是它是動態的。這意味著如果對原映射做出了改動,我們通過這個視圖可以觀察到,但是無法通過這個視圖對原映射做出修改。

>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1]
'A'
>>> d_proxy[2] = 'x'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d[2] = 'B'
>>> d_proxy
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'

獲取關于函數參數的信息

使用 inspect 模塊

例子:clip函數

def clip(text, max_len=80):
    """Return text clipped at the last space before or after max_len
    """
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
            if space_after >= 0:
                end = space_after
    if end is None:  # no spaces were found
        end = len(text)
    return text[:end].rstrip()

from inspect import signature

sig = signature(clip)
print(sig)
#(text, max_len=80)

for name, param in sig.parameters.items():
    print(param.kind, ':', name, '=', param.default)
#POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
#POSITIONAL_OR_KEYWORD : max_len = 80

支持函數式編程的包——operator模塊

Python 的目標不是變成函數式編程語言,但是得益于operator和functools等包的支持,函數式編程風格也可以信手拈來。

operator 模塊為多個算術運算符提供了對應的函數。

例子:使用 reduce 和 operator.mul 函數計算階乘

from functools import reduce
from operator import mul

def fact(n):
    return reduce(mul, range(1, n+1))

operator 模塊中還有一類函數,能替代從序列中取出元素或讀取對象
屬性的 lambda 表達式:因此,itemgetter 和 attrgetter 其實會自行構建函數。

例子:使用 itemgetter 排序一個元組列表

metro_data = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

from operator import itemgetter

for city in sorted(metro_data, key=itemgetter(1)):
    print(city)
#('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
#('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
#('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
#('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
#('New York-Newark', 'US', 20.104, (40.808611, -74.020386))

methodcaller它的作用與 attrgetter 和 itemgetter 類似,它會自行創建函數。methodcaller 創建的函數會在對象上調用參數指定的方法。

>>> from operator import methodcaller
>>> s = 'The time has come'
>>> upcase = methodcaller('upper')
>>> upcase(s)
'THE TIME HAS COME'
>>> hiphenate = methodcaller('replace', ' ', '-')
>>> hiphenate(s)
'The-time-has-come'

支持函數式編程的包——functools模塊

functools 模塊提供了一系列高階函數,其中最為人熟知的或許是
reduce。

functools.partial 這個高階函數用于部分應用一個函數。部分應用是指,基于一個函數創建一個新的可調用對象,把原函數的某些參數固定。使用這個函數可以把接受一個或多個參數的函數改編成需要回調的API,這樣參數更少。

>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3)
>>> triple(7)
21
>>> list(map(triple, range(1, 10)))
[3, 6, 9, 12, 15, 18, 21, 24, 27]

classmethod與staticmethod

classmethod 最常見的用途是定義備選構造方法。staticmethod靜態方法就是普通的函數,只是碰巧在類的定義體中,而不是在模塊層定義。

標準庫中的抽象基類

ABC中的抽象基類

image.png
image.png
image.png

抽象基類的數字塔——numbers包

其中 Number 是位于最頂端的超類,隨后是 Complex 子類,依次往下,最底端是 Integral類:

  • Number
  • Complex
  • Real
  • Rational
  • Integral

如果想檢查一個數是不是整數,可以使用 isinstance(x, numbers.Integral),這樣代碼就能接受 int、bool(int 的子類)

接口:從協議到抽象基類

接口在動態類型語言中是怎么運作的呢?首先,基本的事實是,Python語言沒有 interface 關鍵字,而且除了抽象基類,每個類都有接口:類實現或繼承的公開屬性(方法或數據屬性),包括特殊方法,如__getitem__ 或 __add__。

協議是接口,但不是正式的(只由文檔和約定定義),因此協議不能像正式接口那樣施加限制。

序列協議是 Python 最基礎的協議之一。即便對象只實現了那個協議最基本的一部分,解釋器也會負責任地處理。

這里簡單介紹序列接口的情況。

image.png

示例中的 Foo 類。它沒有繼承 abc.Sequence,而且只實現了序列協議的一個方法:__getitem__ (沒有實現 __len__ 方法)。

例子:定義 __getitem__ 方法,只實現序列協議的一部分,這樣足夠訪問元素、迭代和使用 in 運算符了。

class Foo:
    def __getitem__(self, pos):
        return range(0, 30, 10)[pos]

f = Foo()
for i in f:
    print(i)
#0
#10
#20
print(20 in f)
#True

雖然沒有 __iter__ 方法,但是 Foo 實例是可迭代的對象,因為發現有__getitem__ 方法時,Python 會調用它,傳入從 0 開始的整數索引,嘗試迭代對象(這是一種后備機制)。盡管沒有實現 __contains__ 方法,但是 Python 足夠智能,能迭代 Foo 實例,因此也能使用 in 運算符:Python 會做全面檢查,看看有沒有指定的元素。

綜上,鑒于序列協議的重要性,如果沒有 __iter__ 和 __contains__方法,Python 會調用 __getitem__ 方法,設法讓迭代和 in 運算符可用。

另一個例子:實現序列協議的 FrenchDeck 類

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

猴子補丁

上述FrenchDeck 類有個重大缺陷:無法洗牌。

>>> from random import shuffle
>>> from frenchdeck import FrenchDeck
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../python3.3/random.py", line 265, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

因為,shuffle 函數要調換集合中元素的位置,而 FrenchDeck 只實現了不可變的序列協議。可變的序列還必須提供 __setitem__ 方法。

例子:為FrenchDeck 打猴子補丁,把它變成可變的,讓
random.shuffle 函數能處理

from random import shuffle

deck = FrenchDeck()

def set_card(deck, position, card):
    deck._cards[position] = card

FrenchDeck.__setitem__ = set_card
shuffle(deck)
print(deck[:5])
#[Card(rank='2', suit='spades'), Card(rank='9', suit='spades'), Card(rank='9', suit='hearts'), Card(rank='5', suit='diamonds'), Card(rank='3', suit='clubs')]

這里的關鍵是,set_card 函數要知道 deck 對象有一個名為 _cards 的屬性,而且 _cards 的值必須是可變序列。然后,我們把 set_card 函數賦值給特殊方法 __setitem__,從而把它依附到 FrenchDeck 類上。這種技術叫猴子補丁:在運行時修改類或模塊,而不改動源碼。

白鵝類型

白鵝類型指,只要 cls 是抽象基類,即 cls 的元類是abc.ABCMeta,就可以使用 isinstance(obj, cls)。

其實,抽象基類的本質就是幾個特殊方法。

然而,即便是抽象基類,也不能濫用 isinstance 檢查,用得多了可能導致代碼異味,即表明面向對象設計得不好。在一連串 if/elif/elif中使用 isinstance 做檢查,然后根據對象的類型執行不同的操作,通常是不好的做法;此時應該使用多態,即采用一定的方式定義類,讓解釋器把調用分派給正確的方法,而不使用 if/elif/elif 塊硬編碼分派邏輯。

定義抽象基類的子類。例子:FrenchDeck2,collections.MutableSequence的子類

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck2(collections.MutableSequence):
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

    #為了支持洗牌,只需實現__setitem__方法
    def __setitem__(self, position, value):
        self._cards[position] = value

    #但是繼承MutableSequence的類必須實現__delitem__方法,這是MutableSequence 類的一個抽象方法。
    def __delitem__(self, position):
        del self._cards[position]

    #此外,還要實現insert方法,這是MutableSequence類的第三個抽象方法。
    def insert(self, position, value):
        self._cards.insert(position, value)

參考文獻

  • Python學習手冊
  • Python Cookbook
  • Fluent Python
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容