几个有趣的python技巧

标题 | python-is-coolhtml

做者 | chiphuyennode

原文 | github.com/chiphuyen/p…python

译者 | kbsc13("算法猿的成长"公众号做者)git

声明 | 翻译是出于交流学习的目的,欢迎转载,但请保留本文出于,请勿用做商业或者非法用途github

导读

这篇文章主要是介绍一些 python 的技巧。算法

采用的python版本是 3.6+微信

本文的目录以下:多线程

  • Lambda, map, filter, reduce
  • 列表操做
  • 类和魔法方法
  • 本地命名空间和对象的属性
  • 疯狂的导入

1. Lambda, map, filter, reduce

lambda 是建立匿名函数,下面是一个使用的例子,其中 square_fnsquare_ld 这两个都是相同做用的函数:app

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
复制代码

须要注意的是,mapfilter 返回的对象都是迭代器,也就是说它们的数值并无被存储下来,只是在须要的时候生成,因此若是调用了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] 表示在一个列表中,从索引 xy 中取出元素,步长是 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 object ngrams at 0x1069b26d0>

for ngram in ngrams_generator:
    print(ngram)

==> ['i', 'want', 'to']
    ['want', 'to', 'go']
    ['to', 'go', 'to']
    ['go', 'to', 'school']
复制代码

另一种方式生成 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 object at 0x1069a7dc8> # 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 0x1069c4518>
复制代码

理想的状况是,能够打印一个节点的数值以及其包含的全部子节点,要实现这个功能,能够采用 __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
复制代码

在下面这篇文章给出了全部的魔法方法列表:

www.tutorialsteacher.com/python/magi…

固然也能够查看官方文档的说明,不过阅读起来会有些难度:

docs.python.org/3/reference…

其中推荐如下这些方法:

  • __len__ :重写 len() 方法
  • __str__:重写str() 方法
  • __iter__:若是想让对象能够迭代,能够继承这个方法,而且还能够调用 next() 方法

对于相似 Node 这样的类,即咱们肯定其支持的全部属性(好比对于 Node ,这里就是指 value, left, right 着三个属性),能够采用 __slots__ 来表示这些数值,这有利于提高性能和节省内存空间。想更详细了解 __slots__ ,能够看看这篇 Stackoverflow 上的回答:

stackoverflow.com/questions/4…

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 0x1069b1470>}
复制代码

一个对象的全部属性都保存在它的 __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,以及 numpytensorflow

若是咱们只想让 Encoder, Decoder, Loss 被导入到另外一个模块中使用,那么就须要指定 __all__ 参数:

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

class Encoder:
    ...
复制代码

经过上述代码,当有另外一个文件也是直接采用 from part import * 的作法,那么只会导入给定的 Encoder, Decoder, Loss ,同时 __all__ 也是对一个模块的一个概览。


参考


欢迎关注个人微信公众号--算法猿的成长,或者扫描下方的二维码,你们一块儿交流,学习和进步!

推荐阅读

相关文章
相关标签/搜索