幾個有趣的python技巧


標題 | python-is-cool

作者 | chiphuyen

原文 | https://github.com/chiphuyen/python-is-cool

譯者 | kbsc13("算法猿的成長"公眾號作者)

聲明 | 翻譯是出於交流學習的目的,歡迎轉載,但請保留本文出於,請勿用作商業或者非法用途

導讀

這篇文章主要是介紹一些 python 的技巧。

採用的python版本是 3.6+

本文的目錄如下:

  • Lambda, map, filter, reduce
  • 列表操作
  • 類和魔法方法
  • 本地命名空間和對象的屬性
  • 瘋狂的導入

1. Lambda, map, filter, reduce

lambda 是創建匿名函數,下面是一個使用的例子,其中 square_fn 和 square_ld 這兩個都是相同作用的函數:

def square_fn(x):
return x * x

square_ld = lambda x: x * x

for i in range(10):
assert square_fn(i) == square_ld(i)

因為快速聲明的特點使得 lambda 非常適合用於回調函數以及作為一個參數傳入其他函數中。此外,它還可以很好的和 map, filter , reduce 這幾個函數一起使用。

map(fn, iterable) 是將 iterable 參數的所有元素都傳給函數fn ,這裡可以作為iterable參數的有列表、集合、字典、元祖和字符串,返回的是一個 map 對象,例子如下所示:

nums = [1/3, 333/7, 2323/2230, 40/34, 2/3]
nums_squared = [num * num for num in nums]
print(nums_squared)

==> [0.1111111, 2263.04081632, 1.085147, 1.384083, 0.44444444]

如果用 map 函數作為回調函數,則代碼為:

nums_squared_1 = map(square_fn, nums)
nums_squared_2 = map(lambda x: x * x, nums)
print(list(nums_squared_1))

==> [0.1111111, 2263.04081632, 1.085147, 1.384083, 0.44444444]

還可以使用多個迭代對象,例如,如果想計算一個簡單的線性函數f(x)=ax+b 和真實標籤 labels 的均方差,下面有兩個相同作用的實現方法:

a, b = 3, -0.5
xs = [2, 3, 4, 5]
labels = [6.4, 8.9, 10.9, 15.3]

# Method 1: using a loop
errors = []
for i, x in enumerate(xs):
errors.append((a * x + b - labels[i]) ** 2)
result1 = sum(errors) ** 0.5 / len(xs)

# Method 2: using map
diffs = map(lambda x, y: (a * x + b - y) ** 2, xs, labels)
result2 = sum(diffs) ** 0.5 / len(xs)

print(result1, result2)

==> 0.35089172119045514 0.35089172119045514

需要注意的是,map 和 filter 返回的對象都是迭代器,也就是說它們的數值並沒有被存儲下來,只是在需要的時候生成,所以如果調用了sum(diffs) ,diffs 將變為空,如果想保存所有diffs的元素,需要轉為列表的類型--list(diffs)。

filter(fn, iterable) 的使用方式和 map 一樣,不同的是 fn 返回的是布爾類型的數值,然後 filter 函數返回的就是 fn 會返回 True 的元素,一個例子如下所示:

bad_preds = filter(lambda x: x > 0.5, errors)
print(list(bad_preds))

==> [0.8100000000000006, 0.6400000000000011]

reduce(fn, iterable, initializer) 是在我們想對一個列表的元素都迭代地採用一個操作器的使用。比如,我們想計算一個列表的所有元素的乘積:

product = 1
for num in nums:
product *= num
print(product)

==> 12.95564683272412

這等價於:

from functools import reduce
product = reduce(lambda x, y: x * y, nums)
print(product)

==> 12.95564683272412

注意

lambda 函數的運算時間並不是很好,和用 def 定義的有名字函數相比,會稍微慢一些,因此更建議使用帶名字的函數。


2. 列表操作

python 中的列表也是有很多特別的技巧。

2.1 Unpacking

對於展開列表的每個元素,可以這麼實現:

elems = [1, 2, 3, 4]
a, b, c, d = elems
print(a, b, c, d)

==> 1 2 3 4

也可以這麼做:

a, *new_elems, d = elems
print(a)
print(new_elems)
print(d)

==> 1
[2, 3]
4

2.2 Slicing

反轉一個列表可以通過切片方式實現--[::-1]:

elems = list(range(10))
print(elems)

==> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(elems[::-1])

==> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

這個語法 [x:y:z] 表示在一個列表中,從索引 x 到 y 中取出元素,步長是 z 。當 z 是負數,它表示從後往前,x 沒有指定的時候,默認從第一個元素開始遍歷列表。如果沒有指定 y ,則默認採用最後一個元素。因此,如果我們希望每隔2個元素進行採樣,可以採用 [::2]:

evens = elems[::2]
print(evens)

reversed_evens = elems[-2::-2]
print(reversed_evens)

==> [0, 2, 4, 6, 8]
[8, 6, 4, 2, 0]

也可以通過切片的方式來刪除列表的元素:

del elems[::2]
print(elems)

==> [1, 3, 5, 7, 9]

2.3 Insertion

改變列表中一個元素的代碼實現如下所示:

elems = list(range(10))
elems[1] = 10
print(elems)

==> [0, 10, 2, 3, 4, 5, 6, 7, 8, 9]

而如果希望修改特定範圍內的多個元素,比如用 3 個數值 20,30,40 來替換數值 1 ,代碼如下所示:

elems = list(range(10))
elems[1:2] = [20, 30, 40]
print(elems)

==> [0, 20, 30, 40, 2, 3, 4, 5, 6, 7, 8, 9]

還可以在索引為0和索引為1之間插入 3 個數值 [0.2, 0.3, 0.5]:

elems = list(range(10))
elems[1:1] = [0.2, 0.3, 0.5]
print(elems)

==> [0, 0.2, 0.3, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9]

2.4 Flattening

通過採用 sum 方法來碾平一個嵌套列表的對象:

list_of_lists = [[1], [2, 3], [4, 5, 6]]
sum(list_of_lists, [])

==> [1, 2, 3, 4, 5, 6]

但如果嵌套的層次太多,就需要遞歸的操作,這裡介紹另一個通過 lambda 實現的方法:

nested_lists = [[1, 2], [[3, 4], [5, 6], [[7, 8], [9, 10], [[11, [12, 13]]]]]]
flatten = lambda x: [y for l in x for y in flatten(l)] if type(x) is list else [x]
flatten(nested_lists)

# This line of code is from
# https://github.com/sahands/python-by-example/blob/master/python-by-example.rst#flattening-lists

2.5 List vs generator

為了解釋列表和生成器的區別,這裡用一個創建一個列表的所有字符串的 n-grams 作為例子:

其中一個實現方法是採用滑動窗口:

tokens = ['i', 'want', 'to', 'go', 'to', 'school']

def ngrams(tokens, n):
length = len(tokens)
grams = []
for i in range(length - n + 1):
grams.append(tokens[i:i+n])
return grams

print(ngrams(tokens, 3))

==> [['i', 'want', 'to'],
['want', 'to', 'go'],
['to', 'go', 'to'],
['go', 'to', 'school']]

在上述例子中,我們需要同時存儲所有的 n-grams,如果文本是有 m 個字符,那麼內存大小就是 O(nm) ,這在 m 很大的時候問題會很大。

因此,可以考慮通過生成器在需要的時候才生成新的 n-gram ,所以我們可以創建一個函數 ngrams 通過關鍵詞 yield 返回一個生成器,此內存只需要 O(m+n):

def ngrams(tokens, n):
length = len(tokens)
for i in range(length - n + 1):
yield tokens[i:i+n]

ngrams_generator = ngrams(tokens, 3)
print(ngrams_generator)

==> <generator>

for ngram in ngrams_generator:
print(ngram)

==> ['i', 'want', 'to']
['want', 'to', 'go']
['to', 'go', 'to']
['go', 'to', 'school']
/<generator>

另外一種方式生成 n-grams 是通過切片方式來生成列表 [0, 1, ..., -n], [1, 2, ..., -n+1], …, [n-1, n, ..., -1] ,然後通過 zip 來包裝到一起:

def ngrams(tokens, n):
length = len(tokens)
slices = (tokens[i:length-n+i+1] for i in range(n))
return zip(*slices)

ngrams_generator = ngrams(tokens, 3)
print(ngrams_generator)

==> # zip objects are generators

for ngram in ngrams_generator:
print(ngram)

==> ('i', 'want', 'to')
('want', 'to', 'go')
('to', 'go', 'to')
('go', 'to', 'school')

注意,這裡生成切片的方法是 (tokens[...] for i in range(n)) ,而不是 [tokens[...] for i in range(n)],因為 [] 是列表生成式,而 () 會返回一個生成器。


3. 類和魔法方法

在 python 中,魔法方法是前綴和後綴都帶有兩個下劃線的 __,最有名的一個魔法方法可能就是 __init__ 了,下面是實現一個 Node 類,表示一個二叉樹:

class Node:
""" A struct to denote the node of a binary tree.
It contains a value and pointers to left and right children.
"""
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right

如果我們要打印一個 Node 對象,不過輸出結果並非很好解釋:

root = Node(5)
print(root) # <__main__.node object="" at="">

理想的情況是,可以打印一個節點的數值以及其包含的所有子節點,要實現這個功能,可以採用 __repr__ 方法,它會返回一個可解釋的對象,比如字符串。

class Node:
""" A struct to denote the node of a binary tree.
It contains a value and pointers to left and right children.
"""
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right

def __repr__(self):
strings = [f'value: {self.value}']
strings.append(f'left: {self.left.value}' if self.left else 'left: None')
strings.append(f'right: {self.right.value}' if self.right else 'right: None')
return ', '.join(strings)

left = Node(4)
root = Node(5, left)
print(root) # value: 5, left: 4, right: None

接著,我們可能想進一步實現兩個節點的比較數值的功能,這裡通過 __eq__ 實現相等 == ,__lt__實現小於 < ,__ge__ 實現 大於等於 >= 。

class Node:
""" A struct to denote the node of a binary tree.
It contains a value and pointers to left and right children.
"""
def __init__(self, value, left=None, right=None):

self.value = value
self.left = left
self.right = right

def __eq__(self, other):
return self.value == other.value

def __lt__(self, other):
return self.value < other.value

def __ge__(self, other):
return self.value >= other.value


left = Node(4)
root = Node(5, left)
print(left == root) # False
print(left < root) # True
print(left >= root) # False

在下面這篇文章給出了所有的魔法方法列表:

https://www.tutorialsteacher.com/python/magic-methods-in-python

當然也可以查看官方文檔的說明,不過閱讀起來會有些難度:

https://docs.python.org/3/reference/datamodel.html#special-method-names

其中推薦以下這些方法:

  • len :重寫 len() 方法
  • str:重寫str() 方法
  • iter:如果想讓對象可以迭代,可以繼承這個方法,並且還可以調用 next() 方法

對於類似 Node 這樣的類,即我們確定其支持的所有屬性(比如對於 Node ,這裡就是指 value, left, right 著三個屬性),可以採用 __slots__ 來表示這些數值,這有利於提升性能和節省內存空間。想更詳細瞭解 __slots__ ,可以看看這篇 Stackoverflow 上的回答:

https://stackoverflow.com/questions/472000/usage-of-slots/28059785#28059785

class Node:
""" A struct to denote the node of a binary tree.
It contains a value and pointers to left and right children.
"""
__slots__ = ('value', 'left', 'right')
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right

4. 本地命名空間,對象的屬性

locals() 函數會返回一個字典,它包含了所有定義在本地命名空間的變量,例子如下所示:

class Model1:
def __init__(self, hidden_size=100, num_layers=3, learning_rate=3e-4):
print(locals())
self.hidden_size = hidden_size
self.num_layers = num_layers
self.learning_rate = learning_rate

model1 = Model1()

==> {'learning_rate': 0.0003, 'num_layers': 3, 'hidden_size': 100, 'self': <__main__.model1 object="" at="">}

一個對象的所有屬性都保存在它的 __dict__ :

print(model1.__dict__)

==> {'hidden_size': 100, 'num_layers': 3, 'learning_rate': 0.0003}

注意手動將所有參數分配到對應的屬性會非常麻煩,特別是在參數列表比較大的時候。為了避免這種情況,可以利用對象的 __dict__:

class Model2:
def __init__(self, hidden_size=100, num_layers=3, learning_rate=3e-4):
params = locals()
del params['self']
self.__dict__ = params

model2 = Model2()
print(model2.__dict__)

==> {'learning_rate': 0.0003, 'num_layers': 3, 'hidden_size': 100}

如果對象是通過 **kwargs 來進行初始化,會更加的方便,不過**kwargs 應該儘量少使用:

class Model3:
def __init__(self, **kwargs):
self.__dict__ = kwargs

model3 = Model3(hidden_size=100, num_layers=3, learning_rate=3e-4)
print(model3.__dict__)

==> {'hidden_size': 100, 'num_layers': 3, 'learning_rate': 0.0003}

5. 瘋狂的導入

通常會陷入這種瘋狂的導入操作* 的例子如下所示:

在 file.py 文件中

from parts import *

這個寫法非常不負責任,它是將另一個模塊的一切都導入到當前的模塊,包括那個模塊的導入的內容,比如說,parts.py 模塊可能是這樣的:

import numpy
import tensorflow

class Encoder:
...

class Decoder:
...

class Loss:
...

def helper(*args, **kwargs):
...

def utils(*args, **kwargs):
...

由於 parts.py 沒有指定 __all__ ,所以 file.py 會導入 Encoder, Decoder, Loss, utils, helper,以及 numpy 和 tensorflow 。

如果我們只想讓 Encoder, Decoder, Loss 被導入到另一個模塊中使用,那麼就需要指定 __all__ 參數:

 __all__ = ['Encoder', 'Decoder', 'Loss']
import numpy
import tensorflow

class Encoder:
...

通過上述代碼,當有另一個文件也是直接採用 from part import * 的做法,那麼只會導入給定的 Encoder, Decoder, Loss ,同時 __all__ 也是對一個模塊的一個概覽。


參考

  • https://www.tutorialsteacher.com/python/magic-methods-in-python
  • https://docs.python.org/3/reference/datamodel.html#special-method-names
  • https://stackoverflow.com/questions/472000/usage-of-slots/28059785#28059785


分享到:


相關文章: