Python进阶、并行编程、Python中文指南

Python 进阶:https://eastlakeside.gitbook.io/interpy-zh/
github:https://github.com/iswbm/magic-python

1、Python 进阶

《Python进阶》是《Intermediate Python》的中文译本, 谨以此献给进击的 Python 和 Python 程序员们!

快速阅读传送门

github :https://github.com/eastlakeside/interpy-zh
Gitbook:https://eastlakeside.gitbooks.io/interpy-zh/content/
pdf/epub/mobi:https://github.com/eastlakeside/interpy-zh/releases
在线中文版本:http://docs.pythontab.com/interpy/
英文版
https://book.pythontips.com/en/latest/
https://github.com/IntermediatePython/intermediatePython

1. *args 和 **kwargs

"魔法变量 *args 和 **kwargs" 其实并不是必须写成 *args 和 **kwargs。 只有变量前面的*(星号)才是必须的。你也可以写成 *var和 **vars。而写成 *args 和 **kwargs 只是一个通俗的命名约定。*args 和 **kwargs 主要用于函数定义时传递不定数量的参数。

  • *args 相当于把一个不定长的 Python元组 作为参数,传递给函数
  • **kwargs 相当于把一个不定长的 Python字典 作为参数,传递给函数

如果同时使用三种参数, 参数顺序是:some_func(fargs, *args, **kwargs)

*args 示例:

def test_var_args(f_arg, *argv):
    print("first normal arg:", f_arg)
    for arg in argv:
        print("another arg through *argv:", arg)


test_var_args('yasoob', 'python', 'eggs', 'test')

'''
first normal arg: yasoob
another arg through *argv: python
another arg through *argv: eggs
another arg through *argv: test
'''

**kwargs 示例

def greet_me(**kwargs):
    for key, value in kwargs.items():
        print("{0} == {1}".format(key, value))

>>> greet_me(name="yasoob")
name == yasoob

2. 调试(Debugging)

Python调试:ipdb、pdbpp、rpdb 、pudb、ripdb、Py-Spy:https://blog.csdn.net/freeking101/article/details/83752823

安装:pip install -i https://pypi.douban.com/simple ipdb pdbpp pudb rpdb ripdb wdb.server wdb

利用好调试,能大大提高你捕捉代码Bug的。

从命令行运行:$ python -m pdb my_script.py

从脚本内部运行:

import pdb

def make_bread():
    pdb.set_trace()
    return "I don't have time"

print(make_bread())

试下保存上面的脚本后运行之。你会在运行时马上进入debugger模式。

3. 生成器(Generators)

首先我们要理解迭代器(iterators)。根据维基百科,迭代器是一个让程序员可以遍历一个容器(特别是列表)的对象。然而,一个迭代器在遍历并读取一个容器的数据元素时,并不会执行一个迭代。你可能有点晕了,那我们来个慢动作。换句话说这里有三个部分:

  • 可迭代对象(Iterable):Python中任意的对象,只要它定义了可以返回一个迭代器的__iter__方法,或者定义了可以支持下标索引的__getitem__方法(这些双下划线方法会在其他章节中全面解释),那么它就是一个可迭代对象。简单说,可迭代对象就是能提供迭代器的任意对象。那迭代器又是什么呢?
  • 迭代器(Iterator):任意对象,只要定义了next(Python2) 或者__next__方法,它就是一个迭代器。就这么简单。现在我们来理解迭代(iteration)
  • 迭代(Iteration):用简单的话讲,它就是从某个地方(比如一个列表)取出一个元素的过程。当我们使用一个循环来遍历某个东西时,这个过程本身就叫迭代。

生成器:也是一种迭代器,但是只能对其迭代一次。这是因为它们并没有把所有的值存在内存中,而是在运行时生成值,通过遍历来使用它们,要么用一个“for”循环,要么将它们传递给任意可以进行迭代的函数和结构。大多数时候生成器是以函数来实现的。然而,它们并不返回一个值,而是yield(暂且译作“生出”)一个值。这里有个生成器函数的简单例子:

def generator_function():
    for i in range(10):
        yield i

for item in generator_function():
    print(item)

# Output: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

这个案例并不是非常实用。生成器最佳应用场景是:你不想同一时间将所有计算出来的大量结果集分配到内存当中,特别是结果集里还包含循环。

许多Python 2里的标准库函数都会返回列表,而Python 3都修改成了返回生成器,因为生成器占用更少的资源。

下面是一个计算斐波那契数列的生成器:

# generator version
def fibon(n):
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b

函数使用方法如下:

for x in fibon(1000000):
    print(x)

用这种方式,我们可以不用担心它会使用大量资源。然而,之前如果我们这样来实现的话:

def fibon(n):
    a = b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result

这也许会在计算很大的输入参数时,用尽所有的资源。我们已经讨论过生成器使用一次迭代,但我们并没有测试过。在测试前你需要再知道一个Python内置函数:next()。它允许我们获取一个序列的下一个元素。那我们来验证下我们的理解:

def generator_function():
    for i in range(3):
        yield i

gen = generator_function()
print(next(gen))
# Output: 0
print(next(gen))
# Output: 1
print(next(gen))
# Output: 2
print(next(gen))
# Output: Traceback (most recent call last):
#            File "<stdin>", line 1, in <module>
#         StopIteration

我们可以看到,在yield掉所有的值后,next()触发了一个StopIteration的异常。基本上这个异常告诉我们,所有的值都已经被yield完了。你也许会奇怪,为什么我们在使用for循环时没有这个异常呢?啊哈,答案很简单。for循环会自动捕捉到这个异常并停止调用next()。你知不知道Python中一些内置数据类型也支持迭代哦?我们这就去看看:

my_string = "Yasoob"
next(my_string)
# Output: Traceback (most recent call last):
#      File "<stdin>", line 1, in <module>
#    TypeError: str object is not an iterator

好吧,这不是我们预期的。这个异常说那个str对象不是一个迭代器。对,就是这样!它是一个可迭代对象,而不是一个迭代器。这意味着它支持迭代,但我们不能直接对其进行迭代操作。那我们怎样才能对它实施迭代呢?是时候学习下另一个内置函数,iter。它将根据一个可迭代对象返回一个迭代器对象。这里是我们如何使用它:

my_string = "Yasoob"
my_iter = iter(my_string)
next(my_iter)
# Output: 'Y'

现在好多啦。我肯定你已经爱上了学习生成器。一定要记住,想要完全掌握这个概念,你只有使用它。确保你按照这个模式,并在生成器对你有意义的任何时候都使用它。你绝对不会失望的!

4. map,filter 和 reduce

Map,FilterReduce 三个函数能为函数式编程提供便利。

  • map:将 一个函数映射到列表的所有元素上。map(function_to_apply, list_of_inputs)
    items = [1, 2, 3, 4, 5]
    squared = list(map(lambda x: x**2, items))  # 在python3中返回迭代器,需要使用 list 转换
    匿名函数
    temp_list = [1, 2, 3, 4, 5]
    list(map(lambda i=None: print(i), temp_list))
  • filter:过滤列表中的元素,并且返回一个由所有符合要求的元素所构成的列表,符合要求即函数映射到该元素时返回值为True
    number_list = range(-5, 5)
    less_than_zero = filter(lambda x: x < 0, number_list)
    print(list(less_than_zero))
  • reducereduce 函数用于对一个序列中的元素进行累积计算,最终返回一个结果。基本语法:reduce(function, sequence[, initial])
    function:一个接受两个参数的函数,用于对序列中的元素进行累积计算。该函数的第一个参数代表累积结果,第二个参数代表当前要处理的元素。
    sequence:一个可迭代的序列,如列表、元组等。
    initial:可选参数,表示初始的累积值。
    from functools import reduce
    product = reduce((lambda x, y: x * y), [1, 2, 3, 4])

    reduce 函数和匿名函数来计算列表 numbers 中所有元素的累积乘积。初始累积值默认为列表的第一个元素,然后依次执行匿名函数,将累积结果与下一个元素相乘,直到遍历完整个列表,得到最终的结果。

5. set (集合)

与列表(list)的行为类似,但是 set 不能包含重复的值。这在很多情况下非常有用。例如你可能想检查列表中是否包含重复的元素,你有两个选择,第一个需要使用for循环,就像这样:

some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']

duplicates = []
for value in some_list:
    if some_list.count(value) > 1:
        if value not in duplicates:
            duplicates.append(value)

print(duplicates)
### 输出: ['b', 'n']

但还有一种更简单更优雅的解决方案,那就是使用集合(sets),你直接这样做:

some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']
duplicates = set([x for x in some_list if some_list.count(x) > 1])
print(duplicates)
### 输出: set(['b', 'n'])

交集。就是两个集合相同的数据

valid = set(['yellow', 'red', 'blue', 'green', 'black'])
input_set = set(['red', 'brown'])
print(input_set.intersection(valid))
### 输出: set(['red'])

差集。相当于用一个集合减去另一个集合的数据

valid = set(['yellow', 'red', 'blue', 'green', 'black'])
input_set = set(['red', 'brown'])
print(input_set.difference(valid))
### 输出: set(['brown'])

用  {}  来创建集合

a_set = {'red', 'blue', 'green'}
print(type(a_set))
### 输出: <type 'set'>

集合还有一些其它方法,建议访问官方文档

6. 三元运算符

三元运算符通常在 Python 里被称为条件表达式,这些表达式基于 真(true) / 假(false) 的条件判断

is_fat = True
state = "fat" if is_fat else "not fat"

它允许用简单的一行快速判断,而不是使用复杂的多行if语句。 这在大多数时候非常有用,而且可以使代码简单可维护。

另一个晦涩一点的用法比较少见,它使用了元组,请继续看:

fat = True
fitness = ("skinny", "fat")[fat]
print("Ali is", fitness)
#输出: Ali is fat

这之所以能正常工作,是因为在Python中,True 等于1,而 False 等于0。这样的用法很容易把真正的数据与 True/False 弄混。所以最好不要用。

7. 装饰器

装饰器(Decorators)是用来在 "其他函数执行 '之前或者之后" 添加功能的函数,所以叫装饰器。

Python 一切皆对象。首先我们来理解下 Python 中的函数

def hi(name="yasoob"):
    return "hi " + name

print(hi())
# output: 'hi yasoob'

# 我们甚至可以将一个函数赋值给一个变量,比如
greet = hi
# 我们这里没有在使用小括号,因为我们并不是在调用hi函数
# 而是在将它放在greet变量里头。我们尝试运行下这个

print(greet())
# output: 'hi yasoob'

# 如果我们删掉旧的hi函数,看看会发生什么!
del hi
print(hi())
#outputs: NameError

print(greet())
#outputs: 'hi yasoob'

在函数中定义函数。现在更进一步。在 Python 中我们可以在 一个函数中定义另一个函数

def hi(name="yasoob"):
    print("now you are inside the hi() function")

    def greet():
        return "now you are in the greet() function"

    def welcome():
        return "now you are in the welcome() function"

    print(greet())
    print(welcome())
    print("now you are back in the hi() function")

hi()
#output:now you are inside the hi() function
#       now you are in the greet() function
#       now you are in the welcome() function
#       now you are back in the hi() function

# 上面展示了无论何时你调用hi(), greet()和welcome()将会同时被调用。
# 然后greet()和welcome()函数在hi()函数之外是不能访问的,比如:

greet()
#outputs: NameError: name 'greet' is not defined

知道了可以在函数中定义另外的函数。也就是说:我们可以创建嵌套的函数。

从函数中返回函数。不需要在一个函数里去执行另一个函数,也可以将其作为输出返回:

def hi(name="yasoob"):
    def greet():
        return "now you are in the greet() function"

    def welcome():
        return "now you are in the welcome() function"

    if name == "yasoob":
        return greet
    else:
        return welcome

a = hi()
print(a)
#outputs: <function greet at 0x7f2143c01500>

#上面清晰地展示了`a`现在指向到hi()函数中的greet()函数
#现在试试这个

print(a())
#outputs: now you are in the greet() function

再次看看这个代码。在if/else语句中我们返回greetwelcome,而不是greet()welcome()。为什么那样?这是因为当你把一对小括号放在后面,这个函数就会执行;然而如果你不放括号在它后面,那它可以被到处传递,并且可以赋值给别的变量而不去执行它。

你明白了吗?让我再稍微多解释点细节。

当我们写下a = hi()hi()会被执行,而由于name参数默认是yasoob,所以函数greet被返回了。如果我们把语句改为a = hi(name = "ali"),那么welcome函数将被返回。我们还可以打印出hi()(),这会输出now you are in the greet() function

将函数作为参数传给另一个函数

def hi():
    return "hi yasoob!"

def doSomethingBeforeHi(func):
    print("I am doing some boring work before executing hi()")
    print(func())

doSomethingBeforeHi(hi)
#outputs:I am doing some boring work before executing hi()
#        hi yasoob!

现在已经具备所有必需知识,来进一步学习装饰器是什么。

第一个装饰器

上面示例其实已经创建了一个装饰器!现在编写一个稍微更有用点的程序:

def a_new_decorator(a_func):

    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")

        a_func()

        print("I am doing some boring work after executing a_func()")

    return wrapTheFunction

def a_function_requiring_decoration():
    print("I am the function which needs some decoration to remove my foul smell")

a_function_requiring_decoration()
#outputs: "I am the function which needs some decoration to remove my foul smell"

a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
#now a_function_requiring_decoration is wrapped by wrapTheFunction()

a_function_requiring_decoration()
#outputs:I am doing some boring work before executing a_func()
#        I am the function which needs some decoration to remove my foul smell
#        I am doing some boring work after executing a_func()

你看明白了吗?我们刚刚应用了之前学习到的原理。这正是python中装饰器做的事情!它们封装一个函数,并且用这样或者那样的方式来修改它的行为。现在你也许疑惑,我们在代码里并没有使用@符号?那只是一个简短的方式来生成一个被装饰的函数。这里是我们如何使用@来运行之前的代码:

@a_new_decorator
def a_function_requiring_decoration():
    """Hey you! Decorate me!"""
    print("I am the function which needs some decoration to "
          "remove my foul smell")

a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
#         I am the function which needs some decoration to remove my foul smell
#         I am doing some boring work after executing a_func()

#the @a_new_decorator is just a short way of saying:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

希望你现在对Python装饰器的工作原理有一个基本的理解。如果我们运行如下代码会存在一个问题:

print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction

这并不是我们想要的!Ouput输出应该是“a_function_requiring_decoration”。这里的函数被warpTheFunction替代了。它重写了我们函数的名字和注释文档(docstring)。幸运的是Python提供给我们一个简单的函数来解决这个问题,那就是functools.wraps。我们修改上一个例子来使用functools.wraps:

from functools import wraps

def a_new_decorator(a_func):
    @wraps(a_func)
    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")
    return wrapTheFunction

@a_new_decorator
def a_function_requiring_decoration():
    """Hey yo! Decorate me!"""
    print("I am the function which needs some decoration to "
          "remove my foul smell")

print(a_function_requiring_decoration.__name__)
# Output: a_function_requiring_decoration

现在好多了。我们接下来学习装饰器的一些常用场景。

蓝本规范:

from functools import wraps
def decorator_name(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not can_run:
            return "Function will not run"
        return f(*args, **kwargs)
    return decorated

@decorator_name
def func():
    return("Function is running")

can_run = True
print(func())
# Output: Function is running

can_run = False
print(func())
# Output: Function will not run

注意:@wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。

使用场景:授权(Authorization)

装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量使用于Flask和Django web框架中。这里是一个例子来使用基于装饰器的授权:

from functools import wraps

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated

使用场景:日志 (Logging)

日志是装饰器运用的另一个亮点。这是个例子:

from functools import wraps

def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logit
def addition_func(x):
   """Do some math."""
   return x + x

result = addition_func(4)
# Output: addition_func was called

带参数的装饰器

在函数中嵌入装饰器

我们回到日志的例子,并创建一个包裹函数,能让我们指定一个用于输出的日志文件。

from functools import wraps

def logit(logfile='out.log'):
    def logging_decorator(func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # 打开logfile,并写入内容
            with open(logfile, 'a') as opened_file:
                # 现在将日志打到指定的logfile
                opened_file.write(log_string + '\n')
            return func(*args, **kwargs)
        return wrapped_function
    return logging_decorator

@logit()
def myfunc1():
    pass

myfunc1()
# Output: myfunc1 was called
# 现在一个叫做 out.log 的文件出现了,里面的内容就是上面的字符串

@logit(logfile='func2.log')
def myfunc2():
    pass

myfunc2()
# Output: myfunc2 was called
# 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串

装饰器类

现在我们有了能用于正式环境的logit装饰器,但当我们的应用的某些部分还比较脆弱时,异常也许是需要更紧急关注的事情。比方说有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个email,同时也保留日志,留个记录。这是一个使用继承的场景,但目前为止我们只看到过用来构建装饰器的函数。

幸运的是,类也可以用来构建装饰器。那我们现在以一个类而不是一个函数的方式,来重新构建logit

from functools import wraps

class logit(object):
    def __init__(self, logfile='out.log'):
        self.logfile = logfile

    def __call__(self, func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # 打开logfile并写入
            with open(self.logfile, 'a') as opened_file:
                # 现在将日志打到指定的文件
                opened_file.write(log_string + '\n')
            # 现在,发送一个通知
            self.notify()
            return func(*args, **kwargs)
        return wrapped_function

    def notify(self):
        # logit只打日志,不做别的
        pass

这个实现有一个附加优势,在于比嵌套函数的方式更加整洁,而且包裹一个函数还是使用跟以前一样的语法:

@logit()
def myfunc1():
    pass

现在,我们给logit创建子类,来添加email的功能(虽然email这个话题不会在这里展开)。

class email_logit(logit):
    '''
    一个logit的实现版本,可以在函数调用时发送email给管理员
    '''
    def __init__(self, email='admin@myproject.com', *args, **kwargs):
        self.email = email
        super(email_logit, self).__init__(*args, **kwargs)

    def notify(self):
        # 发送一封email到self.email
        # 这里就不做实现了
        pass

从现在起,@email_logit 将会和 @logit 产生同样的效果,但是在打日志的基础上,还会多发送一封邮件给管理员。

8. Global 和 Return

global 变量意味着我们可以在函数以外的区域都能访问这个变量。

return 返回后,函数直接结束

还可以 返回 tuple(元组),list(列表)或者dict(字典) 等

def profile_1():
    name = "Danny"
    age = 30
    return (name, age)

def profile_2():
    name = "Danny"
    age = 30
    return name, age

9. 对象变动 (Mutation)

Python中 可变(mutable) 与 不可变(immutable) 的数据类型让新手很是头痛。

简单的说,可变(mutable)意味着 "可以被改动",不可变(immutable) 意味着 "常量(constant)"。

foo = ['hi']
print(foo)  # ['hi']

bar = foo
bar += ['bye']
print(foo)  # ['hi', 'bye']

'''
['hi']
['hi', 'bye']
'''

刚刚发生了什么?我们预期的不是那样!我们期望看到是这样的:

foo = ['hi']
print(foo)  # ['hi']

bar = foo
bar += ['bye']

print(foo)  # ['hi', 'bye']
print(bar)  # ['hi', 'bye']

'''
['hi']
['hi', 'bye']
['hi', 'bye']
'''

这不是一个bug。这是对象可变性(mutability)在作怪。每当你将一个变量赋值为另一个可变类型的变量时,对这个数据的任意改动会同时反映到这两个变量上去。新变量只不过是老变量的一个别名而已。这个情况只是针对可变数据类型。下面的函数和可变数据类型让你一下就明白了:

def add_to(num, target=[]):
    target.append(num)
    return target


print(add_to(1))  # [1]
print(add_to(2))  # [1, 2]
print(add_to(3))  # [1, 2, 3]

'''
[1]
[1, 2]
[1, 2, 3]
'''

你可能预期它表现的不是这样子。你可能希望,当你调用add_to时,有一个新的列表被创建,就像这样:

def add_to(num, target=[]):
    target.append(num)
    return target

add_to(1)
# Output: [1]

add_to(2)
# Output: [2]

add_to(3)
# Output: [3]

这次又没有达到预期,是列表的可变性在作怪。在 Python 中当函数被定义时,默认参数只会运算一次,而不是每次被调用时都会重新运算你应该永远不要定义可变类型的默认参数,除非你知道你正在做什么。你应该像这样做:

def add_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

现在每当你在调用这个函数不传入target参数的时候,一个新的列表会被创建。举个例子:

add_to(42)
# Output: [42]

add_to(42)
# Output: [42]

add_to(42)
# Output: [42]

10.  __slots__ 魔法

在 Python 中,每个类都有实例属性。默认情况下Python用一个字典来保存一个对象的实例属性。这非常有用,因为它允许我们在运行时去设置任意的新属性。

然而,对于有着已知属性的小类来说,它可能是个瓶颈。这个字典浪费了很多内存。Python不能在对象创建时直接分配一个固定量的内存来保存所有的属性。因此如果你创建许多对象(我指的是成千上万个),它会消耗掉很多内存。
不过还是有一个方法来规避这个问题。这个方法需要使用 __slots__ 来告诉 Python 不要使用字典,而且只给一个固定集合的属性分配空间。

这里是一个 使用 与 不使用__slots__的例子:

  • 不使用 __slots__:

    class MyClass(object):
      def __init__(self, name, identifier):
          self.name = name
          self.identifier = identifier
          self.set_up()
      # ...
  • 使用 __slots__:
    class MyClass(object):
      __slots__ = ['name', 'identifier']
      def __init__(self, name, identifier):
          self.name = name
          self.identifier = identifier
          self.set_up()
      # ...

第二段代码会为你的内存减轻负担

11. 虚拟环境 (virtualenv)

推荐使用 conda

12. Collections 容器类型

Python 附带一个模块,它包含许多容器数据类型,名字叫作 collections

  • defaultdict:不需要检查 key 是否存在
  • OrderedDict:在 Python 3.7 之前的版本中是有用的,因为在这之前的 dict 实现中,元素的插入顺序是不确定的。从 Python 3.7 开始,dict 成为了默认有序的,因此 OrderedDict 可能在某些情况下并不是必需的。
  • Counter:一个计数器,针对某项数据进行计数。
  • deque:deque 提供了一个双端队列,你可以从头/尾两端添加或删除元素。
  • namedtuple:命名元组。不能修改元组中的数据。需要使用整数作为索引来获取元组中的数据
  • enum.Enum (outside of the module; Python 3.4+):枚举对象,它属于enum模块。Enums(枚举类型)基本上是一种组织各种东西的方式。

defaultdict 示例

from collections import defaultdict

colours = (
    ('Yasoob', 'Yellow'),
    ('Ali', 'Blue'),
    ('Arham', 'Green'),
    ('Ali', 'Black'),
    ('Yasoob', 'Red'),
    ('Ahmed', 'Silver'),
)

favourite_colours = defaultdict(list)

for name, colour in colours:
    favourite_colours[name].append(colour)

print(favourite_colours)
list(map(lambda k_v=None: print(f'{k_v[0]}:{k_v[1]}'), favourite_colours.items()))

'''
defaultdict(<class 'list'>, {'Yasoob': ['Yellow', 'Red'], 'Ali': ['Blue', 'Black'], 'Arham': ['Green'], 'Ahmed': ['Silver']})
Yasoob:['Yellow', 'Red']
Ali:['Blue', 'Black']
Arham:['Green']
Ahmed:['Silver']
'''

另一种重要的是例子就是:当你在一个字典中对一个键进行嵌套赋值时,如果这个键不存在,会触发keyError异常。 defaultdict允许我们用一个聪明的方式绕过这个问题。 首先我分享一个使用dict触发KeyError的例子,然后提供一个使用defaultdict的解决方案。

问题

some_dict = {}
some_dict['colours']['favourite'] = "yellow"

## 异常输出:KeyError: 'colours'

解决方案

import json
import collections

tree = lambda: collections.defaultdict(tree)
some_dict = tree()
some_dict['colours']['favourite'] = "yellow"

# 运行正常
print(some_dict)
print(dict(some_dict))
print(json.dumps(some_dict))

'''
defaultdict(<function <lambda> at 0x0000020C74E36280>, {'colours': defaultdict(<function <lambda> at 0x0000020C74E36280>, {'favourite': 'yellow'})})
{'colours': defaultdict(<function <lambda> at 0x0000020C74E36280>, {'favourite': 'yellow'})}
{"colours": {"favourite": "yellow"}}
'''

可以用 json.dumps 打印出 some_dict,例如:

import json
print(json.dumps(some_dict))

## 输出: {"colours": {"favourite": "yellow"}}

OrderedDict 示例:

from collections import OrderedDict

# 创建一个空的有序字典
my_dict = OrderedDict()

# 添加元素到有序字典中
my_dict['a'] = 1
my_dict['b'] = 2
my_dict['c'] = 3

# 迭代有序字典中的元素
for key, value in my_dict.items():
    print(key, value)

# 输出结果:
# a 1
# b 2
# c 3

counter 示例:

from collections import Counter

colours = (
    ('Yasoob', 'Yellow'),
    ('Ali', 'Blue'),
    ('Arham', 'Green'),
    ('Ali', 'Black'),
    ('Yasoob', 'Red'),
    ('Ahmed', 'Silver'),
)

favs = Counter(name for name, colour in colours)
print(favs)

'''
Counter({'Yasoob': 2, 'Ali': 2, 'Arham': 1, 'Ahmed': 1})
'''

我们也可以在利用它统计一个文件,例如:

from collections import Counter

with open('filename', 'rb') as f:
    line_count = Counter(f)
print(line_count)

deque 示例:

用法就像 python 的 list,并且提供了类似的方法,例如:

from collections import deque

d = deque()
d.append('1')
d.append('2')
d.append('3')

print(len(d))
print(d[0])
print(d[-1])

'''
3
1
3
'''

可以从两端取出 (pop) 数据:

from collections import deque

d = deque(range(5))
print(len(d))

d.popleft()
d.pop()
print(d)

'''
5
deque([1, 2, 3])
'''

也可以限制这个列表的大小,当超出你设定的限制时,数据会从对队列另一端被挤出去 (pop)。最好的解释是给出一个例子:

d = deque(maxlen=30)

现在当你插入30条数据时,最左边一端的数据将从队列中删除。还可以从任一端扩展这个队列中的数据:

from collections import deque

d = deque([1,2,3,4,5])
d.extendleft([0])
d.extend([6,7,8])
print(d)

'''
deque([0, 1, 2, 3, 4, 5, 6, 7, 8])
'''

namedtuple 示例:

元组是一个不可变的列表,你可以存储一个数据的序列,它和命名元组(namedtuples)非常像,但有几个关键的不同。主要相似点是都不像列表,不能修改元组中的数据。为了获取元组中的数据,需要使用整数作为索引:

man = ('Ali', 30)
print(man[0])

'''
Ali
'''

嗯,那 namedtuples 是什么呢?它把元组变成一个针对简单任务的容器。你不必使用整数索引来访问一个 namedtuples 的数据。你可以像字典(dict)一样访问 namedtuples,但 namedtuples 是不可变的。

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")

print(perry)
print(perry.name)

'''
Animal(name='perry', age=31, type='cat')
perry
'''

现在你可以看到,我们可以用名字来访问 namedtuple 中的数据。我们再继续分析它。一个命名元组(namedtuple)有两个必需的参数。它们是元组名称和字段名称。

在上面的例子中,我们的元组名称是Animal,字段名称是'name','age'和'type'。
namedtuple 让你的元组变得自文档了。你只要看一眼就很容易理解代码是做什么的。
你也不必使用整数索引来访问一个命名元组,这让你的代码更易于维护。
而且,namedtuple 的每个实例没有对象字典,所以它们很轻量,与普通的元组比,并不需要更多的内存。这使得它们比字典更快。

然而,要记住它是一个元组,属性值在 namedtuple 中是不可变的,所以下面的代码不能工作:

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")
perry.age = 42

'''
Traceback (most recent call last):
  File "test.py", line 5, in <module>
    perry.age = 42
AttributeError: can't set attribute

'''

你应该使用命名元组来让代码自文档它们向后兼容于普通的元组,这意味着你可以既使用整数索引,也可以使用名称来访问 namedtuple

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")
print(perry[0])

'''
perry
'''

最后,你可以将一个命名元组转换为字典,方法如下:

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="Perry", age=31, type="cat")
print(perry._asdict())

'''
{'name': 'Perry', 'age': 31, 'type': 'cat'}
'''

enum.Enum 示例:

另一个有用的容器是枚举对象,它属于 enum 模块,存在于Python 3.4以上版本中(同时作为一个独立的 PyPI包enum34供老版本使用)。Enums(枚举类型)基本上是一种组织各种东西的方式。

让我们回顾一下上一个'Animal'命名元组的例子。
它有一个type字段,问题是,type是一个字符串。
那么问题来了,万一程序员输入了Cat,因为他按到了Shift键,或者输入了'CAT',甚至'kitten'?

枚举可以帮助我们避免这个问题,通过不使用字符串。考虑以下这个例子:

from collections import namedtuple
from enum import Enum


class Species(Enum):
    cat = 1
    dog = 2
    horse = 3
    aardvark = 4
    butterfly = 5
    owl = 6
    platypus = 7
    dragon = 8
    unicorn = 9
    # 依次类推

    # 但我们并不想关心同一物种的年龄,所以我们可以使用一个别名
    kitten = 1  # (译者注:幼小的猫咪)
    puppy = 2  # (译者注:幼小的狗狗)


Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="Perry", age=31, type=Species.cat)
drogon = Animal(name="Drogon", age=4, type=Species.dragon)
tom = Animal(name="Tom", age=75, type=Species.cat)
charlie = Animal(name="Charlie", age=2, type=Species.kitten)


print(charlie.type == tom.type)  # True
print(charlie.type)  # <Species.cat: 1>

'''
True
Species.cat
'''

有三种方法访问枚举数据,例如以下方法都可以获取到'cat'的值:

Species(1)
Species['cat']
Species.cat

13. 枚举 (enumerate)

enumerate 是 Python 中的一个内置函数,它常用于在迭代过程中获取序列的索引和对应的值

示例:遍历数据并自动计数

my_list = ['apple', 'banana', 'grapes', 'pear']
for c, value in enumerate(my_list, 1):
    print(c, value)

'''
1 apple
2 banana
3 grapes
4 pear
'''

my_list = ['apple', 'banana', 'grapes', 'pear']
counter_list = list(enumerate(my_list, 1))
print(counter_list)

counter_list = list(enumerate(my_list, 100))
print(counter_list)
'''
[(1, 'apple'), (2, 'banana'), (3, 'grapes'), (4, 'pear')]
[(100, 'apple'), (101, 'banana'), (102, 'grapes'), (103, 'pear')]

14、zip、unzip

Zip 组合两个列表。zip 的一个优点是它提高了 for 循环的可读性。

first_name = ['Joe', 'Earnst', 'Thomas', 'Martin', 'Charles']
last_name = ['Schmoe', 'Ehlmann', 'Fischer', 'Walter', 'Rogan', 'Green']
age = [23, 65, 11, 36, 83]
print(list(zip(first_name, last_name, age)))
for first_name, last_name, age in zip(first_name, last_name, age):
    print(f"{first_name} {last_name} is {age} years old")

Unzip 解 压缩

full_name_list = [
    ('Joe', 'Schmoe', 23),
    ('Earnst', 'Ehlmann', 65),
    ('Thomas', 'Fischer', 11),
    ('Martin', 'Walter', 36),
    ('Charles', 'Rogan', 83)
]
first_name, last_name, age = list(zip(*full_name_list))
print(f"first name: {first_name}\nlast name: {last_name} \nage: {age}")

15. 对象 自省

自省(introspection),在计算机编程领域里,是指在运行时来判断一个对象的类型的能力。它是Python的强项之一。Python中所有一切都是一个对象。

  • dir():自省最重要的函数之一。它返回一个列表,列出了一个对象所拥有的属性和方法。
  • type() 返回一个对象的类型。
  • id() 返回任意不同种类对象的唯一ID
  • inspect 模块供了许多有用的函数,来获取活跃对象的信息。
    import inspect
    print(inspect.getmembers(str))
print(type(''))    # <class 'str'> 
print(type([]))    # <class 'list'> 
print(type({}))    # <class 'dict'> 
print(type(dict))  # <class 'type'> 
print(type(3))     # <class 'int'> 

'''
<class 'str'>
<class 'list'>
<class 'dict'>
<class 'type'>
<class 'int'>
'''

16. "列表、字典、集合" 推导式

推导式(又称解析式)是Python的一种独有特性,推导式是可以从一个数据序列构建另一个新的数据序列的结构体。 共有三种推导

  • 列表(list) 推导式。提供了一种简明扼要的方法来创建列表。它的结构是在一个中括号里包含一个表达式,然后是一个for语句,然后是0个或多个for或者if语句。那个表达式可以是任意的,意思是你可以在列表中放入任意类型的对象。返回结果将是一个新的列表,在这个以if和for语句为上下文的表达式运行完成之后产生。
    语法:variable = [out_exp for out_exp in input_list if out_exp == 2]
  • 字典(dict) 推导式
  • 集合(set) 推导式
multiples = [i for i in range(30) if i % 3 is 0]
print(multiples)
# Output: [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]

这将对快速生成列表非常有用。有些人甚至更喜欢使用它而不是 filter 函数。
列表推导式在有些情况下超赞,特别是当你需要使用for循环来生成一个新列表。举个例子,你通常会这样做:

squared = []
for x in range(10):
    squared.append(x**2)

你可以使用列表推导式来简化它,就像这样:

squared = [x**2 for x in range(10)]

字典推导 和 列表推导 的使用方法是类似的。

mcase = {'a': 10, 'b': 34, 'A': 7, 'Z': 3}

mcase_frequency = { 
    k.lower(): mcase.get(k.lower(), 0) + mcase.get(k.upper(), 0) for k in mcase.keys() 
}

print(mcase_frequency)

'''
{'a': 17, 'b': 34, 'z': 3}
'''

还可以快速对换一个字典的键和值:

{v: k for k, v in some_dict.items()}

集合推导式 跟 列表推导式 也是类似的。 唯一的区别在于它们使用大括号{}

squared = {x**2 for x in [1, 1, 2]}
print(squared)

'''
{1, 4}
'''

17. 异常 ( try/except、finally、try/else )

最基本 try/except 从句

try:
    file = open('test.txt', 'rb')
except IOError as e:
    print('An IOError occurred. {}'.format(e.args[-1]))

处理多个异常

try:
    file = open('test.txt', 'rb')
except (IOError, EOFError) as e:
    print("An error occurred. {}".format(e.args[-1]))

多个except语句

try:
    file = open('test.txt', 'rb')
except EOFError as e:
    print("An EOF error occurred.")
    raise e
except IOError as e:
    print("An error occurred.")
    raise e

如果异常没有被第一个except处理,那么它也许被下一个语句块处理,或者根本不会被处理。

捕获 所有 异常:

try:
    file = open('test.txt', 'rb')
except Exception:
    # 打印一些异常日志,如果你想要的话
    raise

# 或者

try:
    pass
except BaseException as e:
    print(e)

finally 从句

包裹到 finally 中的代码不管异常是否触发都将会被执行。这可以被用来在脚本执行之后做清理工作。这里是个简单的例子:

try:
    file = open('test.txt', 'rb')
except IOError as e:
    print('An IOError occurred. {}'.format(e.args[-1]))
finally:
    print("This would be printed whether or not an exception occurred!")

'''
An IOError occurred. No such file or directory
This would be printed whether or not an exception occurred!
'''

try/else 从句

else 从句只会在没有异常的情况下执行,而且它会在 finally 语句之前执行。有人也许问了:如果你只是想让一些代码在没有触发异常的情况下执行,为啥你不直接把代码放在try里面呢?回答是,那样的话这段代码中的任意异常都还是会被try捕获,而你并不一定想要那样。

try:
    print('I am sure no exception is going to occur!')
except Exception:
    print('exception')
else:
    # 这里的代码只会在try语句里没有触发异常时运行,
    # 但是这里的异常将 *不会* 被捕获
    print('This would only run if no exception occurs. And an error here '
          'would NOT be caught.')
finally:
    print('This would be printed in every case.')

'''
I am sure no exception is going to occur!
This would only run if no exception occurs. And an error here would NOT be caught.
This would be printed in every case.
'''

18. 类

类是 Python 的核心。

实例变量、类变量

  • 实例变量 用于每个对象唯一的数据。
  • 类变量 用于在类的不同实例之间共享的数据
class Cal(object):
    # pi is a class variable
    pi = 3.142

    def __init__(self, radius):
        # self.radius is an instance variable
        self.radius = radius

    def area(self):
        return self.pi * (self.radius ** 2)

a = Cal(32)
a.area()
# Output: 3217.408
a.pi
# Output: 3.142
a.pi = 43
a.pi
# Output: 43

b = Cal(44)
b.area()
# Output: 6082.912
b.pi
# Output: 3.142
b.pi = 50
b.pi
# Output: 50

New style classes (新样式类)

  • 旧的基类不继承自任何内容
  • 新样式基类继承自 object
class OldClass():
    def __init__(self):
        print('I am an old class')

class NewClass(object):
    def __init__(self):
        print('I am a jazzy new class')

old = OldClass()
# Output: I am an old class

new = NewClass()
# Output: I am a jazzy new class

Magic Methods (魔术方法)

Python 的类以其神奇的方法而闻名,通常称为 dunder(双下划线)方法

  • __init__ 类初始值设定项。每当创建类的实例时,都会调用其 __init__ 方法。
  • __getitem__ 在类中实现 getitem 允许其实例使用 [](索引器)运算符。

19. lambda 表达式

lambda 表达式 是 一行函数。它们在其他语言中也被称为匿名函数。如果你不想在程序中对一个函数使用两次,你也许会想用lambda表达式,它们和普通的函数完全一样。

原型

    lambda 参数:操作(参数)

例子

add = lambda x, y: x + y

print(add(3, 5))  # 8

这还有一些 lambda 表达式的应用案例,可以在一些特殊情况下使用:

列表排序

a = [(1, 2), (4, 1), (9, 10), (13, -3)]
a.sort(key=lambda x: x[1])

print(a)  # [(13, -3), (4, 1), (1, 2), (9, 10)]

列表并行排序

list_1 = [3, 1, 2, 5, 4]
list_2 = [13, 11, 12, 15, 14]

data = list(zip(list_1, list_2))
print(list(data))

data = sorted(data)
list1, list2 = map(lambda t: list(t), list(zip(*data)))

print(list1)
print(list2)

'''
[(3, 13), (1, 11), (2, 12), (5, 15), (4, 14)]
[1, 2, 3, 4, 5]
[11, 12, 13, 14, 15]
'''

关于 zip

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象,这样做的好处是节约了不少的内存。

我们可以使用 list() 转换来输出列表。

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,利用 * 号操作符,可以将元组解压为列表。

zip 方法在 Python 2 和 Python 3 中的不同:在 Python 2.x zip() 返回的是一个列表。在 Python 3.x 中为了减少内存,zip() 返回的是一个对象。如需展示列表,需手动 list() 转换。

如果需要了解 Pyhton2 的应用,可以参考 Python zip()

下面针对 Python 中 zip 的使用:

zip 语法:zip([iterable, ...])

参数说明:iterabl : 一个或多个迭代器

返回值:返回一个对象。

Python 中 zip 的使用方法:

>>>a = [1,2,3]
>>> b = [4,5,6]
>>> c = [4,5,6,7,8]
>>> zipped = zip(a,b)     # 返回一个对象
>>> zipped
<zip object at 0x103abc288>
>>> list(zipped)  # list() 转换为列表
[(1, 4), (2, 5), (3, 6)]
>>> list(zip(a,c))              # 元素个数与最短的列表一致
[(1, 4), (2, 5), (3, 6)]
 
>>> a1, a2 = zip(*zip(a,b))          # 与 zip 相反,zip(*) 可理解为解压,返回二维矩阵式
>>> list(a1)
[1, 2, 3]
>>> list(a2)
[4, 5, 6]
>>>

20. 一行式

简易 Web Server

你是否想过通过网络快速共享文件?进入到你要共享文件的目录下并在命令行中运行下面的代码:

Python2:python -m SimpleHTTPServer
Python3:python -m http.server

漂亮的打印

你可以在 Python REPL 漂亮的打印出列表和字典。这里是相关的代码:

from pprint import pprint

my_dict = {'name': 'Yasoob', 'age': 'undefined', 'personality': 'awesome'}
pprint(my_dict)

'''
{'age': 'undefined', 'name': 'Yasoob', 'personality': 'awesome'}
'''

这种方法在字典上更为有效。此外,如果你想快速漂亮的从文件打印出 json 数据,那么你可以这么做:cat file.json | python -m json.tool

脚本性能分析 这可能在定位你的脚本中的性能瓶颈时,会非常奏效:

执行:python -m cProfile my_script.py

备注:cProfile 是一个比 profile 更快的实现,因为它是用c写的

CSV 转换为 json

执行命令:python -c "import csv,json;print json.dumps(list(csv.reader(open('csv_file.csv'))))"

确保更换 csv_file.csv 为你想要转换的 csv 文件

List Flattening 列表扁平化

import itertools

a_list = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain.from_iterable(a_list)))
# Output: [1, 2, 3, 4, 5, 6]

# or
print(list(itertools.chain(*a_list)))
# Output: [1, 2, 3, 4, 5, 6]

一行式的构造器

避免类初始化时大量重复的赋值语句

class A(object):
    def __init__(self, a, b, c, d, e, f):
        self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})

更多的一行方法请参考: Python website

21. for - else

else 从句:else从句会在循环正常结束时执行。这意味着,循环没有遇到任何break。

有两个场景会让循环停下来。

  • 第一个是当一个元素被找到,break被触发。
  • 第二个场景是循环结束。

for/else 循环的基本结构:

for item in container:
    if search_something(item):
        # Found it!
        process(item)
        break
else:
    # Didn't find anything..
    not_found_in_container()

考虑下这个简单的案例,它是我从官方文档里拿来的:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'equals', x, '*', n / x)
            break

'''
4 equals 2 * 2.0
6 equals 2 * 3.0
8 equals 2 * 4.0
9 equals 3 * 3.0
'''

它会找出 2 到 10 之间的数字的因子。现在是趣味环节了。我们可以加上一个附加的 else 语句块,来抓住质数,并且告诉我们:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'equals', x, '*', n / x)
            break
    else:
        # loop fell through without finding a factor
        print(n, 'is a prime number')

'''
2 is a prime number
3 is a prime number
4 equals 2 * 2.0
5 is a prime number
6 equals 2 * 3.0
7 is a prime number
8 equals 2 * 4.0
9 equals 3 * 3.0
'''        

22. 使用 C扩展

CPython 实现为开发人员提供的一个有趣的功能是易于将 C 代码连接到 Python。开发者有三种方法可以在自己的 Python 代码中来调用 C 编写的函数:ctypes,SWIG,Python/C API。每种方式也都有各自的利弊。首先,我们要明确为什么要在 Python 中调用 C ?常见原因如下:

  • 你要提升代码的运行速度,而且你知道C要比Python快50倍以上
  • C语言中有很多传统类库,而且有些正是你想要的,但你又不想用Python去重写它们
  • 想对从内存到文件接口这样的底层资源进行访问
  • 不需要理由,就是想这样做

CTypes

Python中的 ctypes 模块 可能是 Python 调用 C 方法中最简单的一种。ctypes 模块提供了和 C 语言兼容的数据类型和函数来加载 dll 文件,因此在调用时不需对源文件做任何的修改。也正是如此奠定了这种方法的简单性。

示例:实现两数求和的 C 代码,保存为 add.c

//sample C file to add 2 numbers - int and floats

#include <stdio.h>

int add_int(int, int);
float add_float(float, float);

int add_int(int num1, int num2){
    return num1 + num2;

}

float add_float(float num1, float num2){
    return num1 + num2;

}

接下来将 C文件编译为 .so 文件 ( windows 下为 DL L)。下面操作会生成 adder.so 文件

# For Linux
$  gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c

# For Mac
$  gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c

现在在你的 Python 代码中来调用它

from ctypes import *

# load the shared object file
adder = CDLL('./adder.so')

# Find sum of integers
res_int = adder.add_int(4, 5)
print("Sum of 4 and 5 = " + str(res_int))

# Find sum of floats
a = c_float(5.5)
b = c_float(4.1)

add_float = adder.add_float
add_float.restype = c_float
print("Sum of 5.5 and 4.1 = ", str(add_float(a, b)))

输出如下

Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 =  9.60000038147

在这个例子中,C 文件是自解释的,它包含两个函数,分别实现了整形求和和浮点型求和。

在Python文件中,一开始先导入ctypes模块,然后使用CDLL函数来加载我们创建的库文件。这样我们就可以通过变量adder来使用C类库中的函数了。当adder.add_int()被调用时,内部将发起一个对C函数add_int的调用。ctypes接口允许我们在调用C函数时使用原生Python中默认的字符串型和整型。

而对于其他类似布尔型和浮点型这样的类型,必须要使用正确的ctype类型才可以。如向adder.add_float()函数传参时, 我们要先将Python中的十进制值转化为c_float类型,然后才能传送给C函数。这种方法虽然简单,清晰,但是却很受限。例如,并不能在C中对对象进行操作。

SWIG

Simplified Wrapper and Interface Generator 简称 SWIG,是将 C 代码连接到 Python 的另一种方式。在这种方法中,开发人员必须开发一个额外的接口文件,该文件是 SWIG(命令行实用程序)的输入。

Python开发者一般不会采用这种方法,因为大多数情况它会带来不必要的复杂。而当你有一个C/C++ 代码库需要被多种语言调用时,这将是个非常不错的选择。

示例如下(来自SWIG官网)

example.c 文件中的 C 代码包含了不同的变量和函数

#include <time.h>
double My_variable = 3.0;

int fact(int n) {
    if (n <= 1) return 1;
    else return n*fact(n-1);

}

int my_mod(int x, int y) {
    return (x%y);

}

char *get_time()
{
    time_t ltime;
    time(&ltime);
    return ctime(&ltime);

}

编译它

unix % swig -python example.i
unix % gcc -c example.c example_wrap.c \
    -I/usr/local/include/python2.1
unix % ld -shared example.o example_wrap.o -o _example.so

最后,Python 的输出

>>> import example
>>> example.fact(5)
120
>>> example.my_mod(7,3)
1
>>> example.get_time()
'Sun Feb 11 23:01:07 1996'
>>>

可以看到使用 SWIG 确实达到了同样的效果。

Python / C API

Python/C API 可能是被最广泛使用的方法。它不仅简单,而且可以在C代码中操作你的Python对象。

这种方法需要以特定的方式来编写C代码以供Python去调用它。所有的Python对象都被表示为一种叫做PyObject的结构体,并且Python.h头文件中提供了各种操作它的函数。例如,如果PyObject表示为PyListType(列表类型)时,那么我们便可以使用PyList_Size()函数来获取该结构的长度,类似Python中的len(list)函数。大部分对Python原生对象的基础函数和操作在Python.h头文件中都能找到。

示例:编写一个C扩展,添加所有元素到一个Python列表(所有元素都是数字)

来看一下我们要实现的效果,这里演示了用Python调用C扩展的代码

# Though it looks like an ordinary python import, the addList module is implemented in C
import addList

l = [1,2,3,4,5]
print "Sum of List - " + str(l) + " = " +  str(addList.add(l))

上面的代码和普通的Python文件并没有什么分别,导入并使用了另一个叫做 addList 的 Python 模块。唯一差别就是这个模块并不是用 Python 编写的,而是 C。

接下来我们看看如何用 C 编写 addList 模块,这可能看起来有点让人难以接受,但是一旦你了解了这之中的各种组成,你就可以一往无前了。

//Python.h has all the required function definitions to manipulate the Python objects
#include <Python.h>

//This is the function that is called from your python code
static PyObject* addList_add(PyObject* self, PyObject* args){

    PyObject * listObj;

    //The input arguments come as a tuple, we parse the args to get the various variables
    //In this case it's only one list variable, which will now be referenced by listObj
    if (! PyArg_ParseTuple( args, "O", &listObj ))
        return NULL;

    //length of the list
    long length = PyList_Size(listObj);

    //iterate over all the elements
    int i, sum =0;
    for (i = 0; i < length; i++) {
        //get an element out of the list - the element is also a python objects
        PyObject* temp = PyList_GetItem(listObj, i);
        //we know that object represents an integer - so convert it into C long
        long elem = PyInt_AsLong(temp);
        sum += elem;
    }

    //value returned back to python code - another python object
    //build value here converts the C long to a python integer
    return Py_BuildValue("i", sum);

}

//This is the docstring that corresponds to our 'add' function.
static char addList_docs[] =
"add(  ): add all elements of the list\n";

/* This table contains the relavent info mapping -
   <function-name in python module>, <actual-function>,
   <type-of-args the function expects>, <docstring associated with the function>
 */
static PyMethodDef addList_funcs[] = {
    {"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
    {NULL, NULL, 0, NULL}

};

/*
   addList is the module name, and this is the initialization block of the module.
   <desired module name>, <the-info-table>, <module's-docstring>
 */
PyMODINIT_FUNC initaddList(void){
    Py_InitModule3("addList", addList_funcs,
            "Add all ze lists");

}

逐步解释

  • Python.h头文件中包含了所有需要的类型(Python对象类型的表示)和函数定义(对Python对象的操作)
  • 接下来我们编写将要在Python调用的函数, 函数传统的命名方式由{模块名}_{函数名}组成,所以我们将其命名为addList_add
  • 然后填写想在模块内实现函数的相关信息表,每行一个函数,以空行作为结束
  • 最后的模块初始化块签名为PyMODINIT_FUNC init{模块名}

函数addList_add接受的参数类型为PyObject类型结构(同时也表示为元组类型,因为Python中万物皆为对象,所以我们先用PyObject来定义)。传入的参数则通过PyArg_ParseTuple()来解析。第一个参数是被解析的参数变量。第二个参数是一个字符串,告诉我们如何去解析元组中每一个元素。字符串的第n个字母正是代表着元组中第n个参数的类型。例如,"i"代表整形,"s"代表字符串类型, "O"则代表一个Python对象。接下来的参数都是你想要通过PyArg_ParseTuple()函数解析并保存的元素。这样参数的数量和模块中函数期待得到的参数数量就可以保持一致,并保证了位置的完整性。例如,我们想传入一个字符串,一个整数和一个Python列表,可以这样去写

int n;
char *s;
PyObject* list;
PyArg_ParseTuple(args, "siO", &n, &s, &list);

在这种情况下,我们只需要提取一个列表对象,并将它存储在listObj变量中。然后用列表对象中的PyList_Size()函数来获取它的长度。就像Python中调用len(list)

现在我们通过循环列表,使用PyList_GetItem(list, index)函数来获取每个元素。这将返回一个PyObject*对象。既然Python对象也能表示PyIntType,我们只要使用PyInt_AsLong(PyObj *)函数便可获得我们所需要的值。我们对每个元素都这样处理,最后再得到它们的总和。

总和将被转化为一个Python对象并通过Py_BuildValue()返回给Python代码,这里的i表示我们要返回一个Python整形对象。

现在我们已经编写完C模块了。将下列代码保存为setup.py

#build the modules

from distutils.core import setup, Extension

setup(name='addList', version='1.0',  \
      ext_modules=[Extension('addList', ['adder.c'])])

并且运行

python setup.py install

现在应该已经将我们的C文件编译安装到我们的 Python 模块中了。

在一番辛苦后,让我们来验证下我们的模块是否有效

#module that talks to the C code
import addList

l = [1,2,3,4,5]
print "Sum of List - " + str(l) + " = " +  str(addList.add(l))

输出结果如下

Sum of List - [1, 2, 3, 4, 5] = 15

如你所见,我们已经使用 Python.h API 成功开发出了我们第一个 Python C 扩展。这种方法看似复杂,但你一旦习惯,它将变的非常有效。

Python调用C代码的另一种方式便是使用 Cython 让 Python 编译的更快。但是 Cython 和传统的 Python 比起来可以将它理解为另一种语言,所以我们就不在这里过多描述了。

23. open 函数

大多数时候,我们看到它这样被使用:

f = open('photo.jpg', 'r+')
jpgdata = f.read()
f.close()

我现在写这篇文章的原因,是大部分时间我看到open被这样使用。有三个错误存在于上面的代码中。你能把它们全指出来吗?如不能,请读下去。在这篇文章的结尾,你会知道上面的代码错在哪里,而且,更重要的是,你能在自己的代码里避免这些错误。现在我们从基础开始:

open的返回值是一个文件句柄,从操作系统托付给你的Python程序。一旦你处理完文件,你会想要归还这个文件句柄,只有这样你的程序不会超出一次能打开的文件句柄的数量上限。

显式地调用close关闭了这个文件句柄,但前提是只有在read成功的情况下。如果有任意异常正好在f = open(...)之后产生,f.close()将不会被调用(取决于Python解释器的做法,文件句柄可能还是会被归还,但那是另外的话题了)。为了确保不管异常是否触发,文件都能关闭,我们将其包裹成一个with语句:

with open('photo.jpg', 'r+') as f:
    jpgdata = f.read()

open 的第一个参数是文件名。第二个(mode 打开模式)决定了这个文件如何被打开。

  • 如果你想读取文件,传入r
  • 如果你想读取并写入文件,传入r+
  • 如果你想覆盖写入文件,传入w
  • 如果你想在文件末尾附加内容,传入a

虽然有若干个其他的有效的mode字符串,但有可能你将永远不会使用它们。mode很重要,不仅因为它改变了行为,而且它可能导致权限错误。举个例子,我们要是在一个写保护的目录里打开一个jpg文件, open(.., 'r+')就失败了。mode可能包含一个扩展字符;让我们还可以以二进制方式打开文件(你将得到字节串)或者文本模式(字符串)

一般来说,如果文件格式是由人写的,那么它更可能是文本模式。jpg图像文件一般不是人写的(而且其实不是人直接可读的),因此你应该以二进制模式来打开它们,方法是在mode字符串后加一个b(你可以看看开头的例子里,正确的方式应该是rb)。
如果你以文本模式打开一些东西(比如,加一个t,或者就用r/r+/w/a),你还必须知道要使用哪种编码。对于计算机来说,所有的文件都是字节,而不是字符。

可惜,在Pyhon 2.x版本里,open不支持显示地指定编码。然而,io.open函数在Python 2.x中和3.x(其中它是open的别名)中都有提供,它能做正确的事。你可以传入encoding这个关键字参数来传入编码。
如果你不传入任意编码,一个系统 - 以及Python -指定的默认选项将被选中。你也许被诱惑去依赖这个默认选项,但这个默认选项经常是错误的,或者默认编码实际上不能表达文件里的所有字符(这将经常发生在Python 2.x和/或Windows)。
所以去挑选一个编码吧。utf-8是一个非常好的编码。当你写入一个文件,你可以选一个你喜欢的编码(或者最终读你文件的程序所喜欢的编码)。

那你怎么找出正在读的文件是用哪种编码写的呢?好吧,不幸的是,并没有一个十分简单的方式来检测编码。在不同的编码中,同样的字节可以表示不同,但同样有效的字符。因此,你必须依赖一个元数据(比如,在HTTP头信息里)来找出编码。越来越多的是,文件格式将编码定义成UTF-8

有了这些基础知识,我们来写一个程序,读取一个文件,检测它是否是JPG(提示:这些文件头部以字节FF D8开始),把对输入文件的描述写入一个文本文件。

import io

with open('photo.jpg', 'rb') as inf:
    jpgdata = inf.read()

if jpgdata.startswith(b'\xff\xd8'):
    text = u'This is a JPEG file (%d bytes long)\n'
else:
    text = u'This is a random file (%d bytes long)\n'

with io.open('summary.txt', 'w', encoding='utf-8') as outf:
    outf.write(text % len(jpgdata))

现在你会正确地使用 open 啦!

24. 兼容 Python2+3

推荐使用 Python3,放弃 Python2

25. Coroutines 协程

Python中的协程和生成器很相似但又稍有不同。主要区别在于:

  • 生成器 是 数据的生产者
  • 协程 是 数据的消费者

首先我们先来回顾下生成器的创建过程。我们可以这样去创建一个生成器:

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

然后我们经常在 for 循环中这样使用它:

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b


for i in fib():
    print(i)

这样做不仅快而且不会给内存带来压力,因为我们所需要的值都是动态生成的而不是将他们存储在一个列表中。更概括的说如果现在我们在上面的例子中使用 yield 便可获得了一个协程。协程会消费掉发送给它的值。Python 实现的 grep 就是个很好的例子:

def grep(pattern):
    print("Searching for", pattern)
    while True:
        line = (yield)
        if pattern in line:
            print(line)

等等!yield 返回了什么?啊哈,我们已经把它变成了一个协程。它将不再包含任何初始值,相反要从外部传值给它。我们可以通过 send()方法向它传值。这有个例子:

def grep(pattern):
    print("Searching for", pattern)
    while True:
        line = (yield)
        if pattern in line:
            print(line)


search = grep('coroutine')
next(search)  # output: Searching for coroutine

search.send("I love you")
search.send("Don't you love me?")
search.send("I love coroutine instead!")  # output: I love coroutine instead!

'''
Searching for coroutine
I love coroutine instead!
'''

发送的值会被 yield 接收。我们为什么要运行 next()方法呢?这样做正是为了启动一个协程。就像协程中包含的生成器并不是立刻执行,而是通过 next()方法来响应 send()方法。因此,你必须通过 next()方法来执行 yield 表达式。

我们可以通过调用 close()方法来关闭一个协程。像这样:

search = grep('coroutine')
search.close()

更多协程相关知识的学习大家可以参考David Beazley的这份精彩演讲

26. Function caching 函数缓存

函数缓存允许我们根据参数缓存函数的返回值。当使用相同的参数定期调用 I/O 绑定函数时,它可以节省时间。在 Python 3.2+ 中,有一个 lru_cache 装饰器,它允许我们快速缓存和取消缓存函数的返回值。

Python 3.2+ 示例:实现一个斐波那契计算器

from functools import lru_cache


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


print([fib(n) for n in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

那个 maxsize 参数是告诉 lru_cache,最多缓存最近多少个返回值。

我们也可以轻松地对返回值清空缓存,通过这样:

fib.cache_clear()

27. 上下文管理器(Context managers)

上下文管理器允许您在需要时精确地分配和释放资源。上下文管理器使用最广泛的示例是 with 语句。

with open('some_file', 'w') as opened_file:
    opened_file.write('Hola!')

上面这段代码打开了一个文件,往里面写入了一些数据,然后关闭该文件。如果在往文件写数据时发生异常,它也会尝试去关闭文件。上面那段代码与下面这段是等价的:

file = open('some_file', 'w')
try:
    file.write('Hola!')
finally:
    file.close()

当与第一个例子对比时,我们可以看到,通过使用 with,许多样板代码(boilerplate code)被消掉了。 这就是 with 语句的主要优势,它确保我们的文件会被关闭,而不用关注嵌套代码如何退出。上下文管理器的一个常见用例,是资源的加锁和解锁,以及关闭已打开的文件。让我们看看如何来实现我们自己的上下文管理器。这会让我们更完全地理解在这些场景背后都发生着什么。

基于类的实现( with 语句 )

一个上下文管理器的类,最起码要定义 __enter__ 和 __exit__ 方法。
让我们来构造我们自己的开启文件的上下文管理器,并学习下基础知识。

class TestFile(object):
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)

    def __enter__(self):
        return self.file_obj

    def __exit__(self, type, value, traceback):
        self.file_obj.close()

通过定义 __enter__ 和 __exit__ 方法,我们可以在 with 语句里使用它。我们来试试:

class TestFile(object):
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)

    def __enter__(self):
        return self.file_obj

    def __exit__(self, type, value, traceback):
        self.file_obj.close()


with TestFile('demo.txt', 'w') as opened_file:
    opened_file.write('Hola!')

我们的 __exit__函数接受三个参数。这些参数对于每个上下文管理器类中的 __exit__ 方法都是必须的。我们来谈谈在底层都发生了什么。

  1. with 语句先暂存了 TestFile 类的 __exit__ 方法
  2. 然后它调用 File 类的 __enter__ 方法
  3. __enter__ 方法打开文件并返回给 with 语句
  4. 打开的文件句柄被传递给 opened_file参 数
  5. 我们使用 .write()来写文件
  6. with 语句调用之前暂存的 __exit__ 方法
  7. __exit__ 方法关闭了文件

with 语句中异常的处理

我们还没有谈到_ _exit__ 方法的这三个参数:typevalue 和 traceback
在第4步和第6步之间,如果发生异常,Python 会将异常的 type,value 和 traceback 传递给 __exit__ 方法。
它让 __exit__ 方法来决定如何关闭文件以及是否需要其他步骤。在我们的案例中,我们并没有注意它们。

那如果我们的文件对象抛出一个异常呢?万一我们尝试访问文件对象的一个不支持的方法。举个例子:

with TestFile('demo.txt', 'w') as opened_file:
    opened_file.undefined_function('Hola!')

我们来列一下,当异常发生时,with 语句会采取哪些步骤。

  1. 它把异常的 type,value 和 traceback 传递给 __exit__ 方法
  2. 它让 __exit__ 方法来处理异常
  3. 如果 __exit__ 返回的是 True,那么这个异常就被优雅地处理了。
  4. 如果 __exit__ 返回的是 True 以外的任何东西,那么这个异常将被 with 语句抛出。

在我们的案例中,__exit__ 方法返回的是 None ( 如果没有 return 语句那么方法会返回 None)。因此,with 语句抛出了那个异常。

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'file' object has no attribute 'undefined_function'

我们尝试下在 __exit__ 方法中处理异常:

class TestFile(object):
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)

    def __enter__(self):
        return self.file_obj

    def __exit__(self, type, value, traceback):
        print("Exception has been handled")
        self.file_obj.close()
        return True


with TestFile('demo.txt', 'w') as opened_file:
    opened_file.undefined_function()

'''
Exception has been handled
'''

我们的 __exit__ 方法返回了 True,因此没有异常会被 with 语句抛出。

这还不是实现上下文管理器的唯一方式。还有一种方式,我们会在下一节中一起看看。

基于生成器的实现 ( contextlib 模块)

我们还可以用 装饰器(decorators) 和 生成器(generators) 来实现上下文管理器。
Python 有个 contextlib 模块专门用于这个目的。我们可以使用一个生成器函数来实现一个上下文管理器,而不是使用一个类。
让我们看看一个基本的,没用的例子:

from contextlib import contextmanager


@contextmanager
def open_file(name):
    f = open(name, 'w')
    yield f
    f.close()

OK啦!这个实现方式看起来更加直观和简单。然而,这个方法需要关于生成器、yield 和 装饰器的一些知识。在这个例子中我们还没有捕捉可能产生的任何异常。它的工作方式和之前的方法大致相同。

让我们小小地剖析下这个方法。

  1. Python 解释器遇到了 yield 关键字。因为这个缘故它创建了一个生成器而不是一个普通的函数。
  2. 因为这个装饰器,contextmanager 会被调用并传入函数名(open_file)作为参数。
  3. contextmanager 函数返回一个以 GeneratorContextManager 对象封装过的生成器。
  4. 这个 GeneratorContextManager 被赋值给 open_file 函数,我们实际上是在调用 GeneratorContextManager 对象。

那现在我们既然知道了所有这些,我们可以用这个新生成的上下文管理器了,像这样:

from contextlib import contextmanager


@contextmanager
def open_file(name):
    f = open(name, 'w')
    yield f
    f.close()


with open_file('some_file') as ff:
    ff.write('hola!')

2、Python 并行编程

参考:python-parallel-programming-cookbook-cn:Python并行编程 中文版 — python-parallel-programming-cookbook-cn 1.0 文档

1. 认识并行计算和Python

2. 基于线程的并行

3. 基于进程的并行

4. 异步编程

5. 分布式Python编程

6. Python GPU编程

3、Python 黑魔法手册

github:https://github.com/iswbm/magic-python

1、 冷知识

省略号 ...

...
print(type(...))
"""
Ellipsis是一个特殊的对象,用三个连续的句点表示(...)。
它可以在代码中用作占位符,表示某些代码的部分未给出或被省略。
当使用切片操作或扩展切片操作时,也可以使用Ellipsis。
"""

用 end 标明结束。其实在 Python 这种严格缩进的语言里并没有必要这样做。

__builtins__.end = None


def my_abs(x):
    if x > 0:
        return x
    else:
        return -x
    end
end

print(my_abs(10))
print(my_abs(-10))

直接运行的 zip 包

# demo/__main__.py
import calc
print(calc.add(2, 3))

# cat demo/calc.py
def add(x, y):
    return x+y

打包成zip包:python -m zipfile -c demo.zip demo/*

执行:python demo.zip

链式比较

print(False == False == True)
# 和上面等价
print(False == False and False == True)
score = 85
if 80 < score <= 90:
    print("成绩良好")

#!/usr/bin/python

#!/usr/bin/python  或者  #!/usr/bin/env python

接触过 linux 的人都知道 /usr/bin/python 就是我们执行 python 进入console 模式里的 python。在可执行文件头里使用 #! + /usr/bin/python ,意思就是说你得用哪个软件 (python)来执行这个文件。不加时每次执行这个脚本都得这样: python xx.py ,

执行 env python 时,它其实会去 env | grep PATH 里(也就是 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin )这几个路径里去依次查找名为python的可执行文件。

dict() 与 {}

python -m timeit -n 1000000 -r 5 -v "dict()"
python -m timeit -n 1000000 -r 5 -v "{}"

$ cat demo.py
{}
$ python -m dis demo.py
$ cat demo.py
dict()
$ python -m dis demo.py

发现使用 dict(),会多了个调用函数的过程,而这个过程会有进出栈的操作,相对更加耗时。

2、命令行

使用 "_" 占位符,程序中使用时,只是站位,不绑定任何变量

在交互式模式下的应用,可以返回上一次的运行结果。

使用 json.tool 来格式化 JSON:python -m json.tool demo.json

命令行式执行 Python 代码
python -c "import hashlib;print(hashlib.md5('hello').hexdigest())"

方法 1:import pdb;pdb.set_trace()
方法 2:python -m pdb demo.py

快速搭建 HTTP 服务器
python2:python -m SimpleHTTPServer 8888
python3:python3 -m http.server 8888

离线学习 Python 模块的方法:python -m pydoc -p 5200

用 pip 来安装第三方的模块时,通常会使用这样的命令:pip install requests
此时如果环境中有 Python2 也有 Python 3,使用这条命令安装的包是安装 Python2 呢?还是安装到 Python 3 呢?就算你的环境上没有安装 Python2,那也有可能存在着多个版本的 Python 吧?比如安装了 Python3.8,也安装了 Python3.9,那你安装包时就会很困惑,我到底把包安装在了哪里?但若你使用这样的命令去安装,就没有了这样的烦恼了
在 python2 中安装:python -m pip install requests
在 python3 中安装:python3 -m pip install requests
在 python3.8 中安装:python3.8 -m pip install requests
在 python3.9 中安装:python3.9 -m pip install requests

直接执行 模块

原理:__main__.py 是一个特殊的文件,用来标记模块是一个可执行模块。既可以通过 "python -m 模块名" 来执行 __main__.py 文件。

Python 模块或者包 SimpleHTTPServer, http.server, pydoc,pdb,pip, json.tool,site ,timeit 通常都是用做工具包由其他模块导入使用,很少使用命令来执行。

Python 使用 "-m" 参数可以 "将模块里的部分功能抽取出来" 在命令行直接执行调用。

1、 快速搭建一个 HTTP 服务
        # python2
        $ python -m SimpleHTTPServer 8888
        # python3
        $ python3 -m http.server 8888
2、快速构建 HTML 帮助文档
        $ python -m pydoc -p 5200
3、快速进入 pdb 调试模式
        $ python -m pdb demo.py
4、最优雅且正确的包安装方法
        $ python3 -m pip install requests
5、快速美化 JSON 字符串
        $ echo '{"name": "MING"}' | python -m json.tool
6、快速打印包的搜索路径
        $ python -m site
7、用于快速计算程序执行时长
        $ python3 -m timeit '"-".join(map(str, range(100)))'

快速计算字符串 base64编码:
        $ echo "hello, world" | python3 -m base64
        aGVsbG8sIHdvcmxkCg==
        $ echo "aGVsbG8sIHdvcmxkCg==" | python3 -m base64 -d
        hello, world
对文件进行编码和解码:

        python3 -m base64 demo.py

        echo "编码内容" | python3 -m base64 -d

识别文件类型:
        python -m mimetypes https://movie.douban.com/top250
        python -m mimetypes index.html
        python -m mimetypes https://img-blog.csdnimg.cn/20201112144653191.png
        python -m mimetypes sample.py
        python -m mimetypes sample.py.gz

原理剖析:最好的学习方式莫过于模仿,直接以 pip 和 json 模块为学习对象,看看目录结构和代码都有什么特点。

先看一下 pip 的源码目录,发现在其下有一个 __main__.py 的文件,难道这是 -m 的入口?

再看一下 json.tool 的源码文件, tool 模块的源代码有一个名为 main 的函数

main 函数是在模块中直接被调用的。只有当 __name__ 为 __main___ 时,main 函数才会被调用

if __name__ == '__main__':
    main()

先把当前路径设置追加到 PATH 的环境变量中:export PATH=${PATH}:`pwd`

  • 当模块被导入时,__name__ 的值为模块名。验证:在当前目录下新建一个 demo 文件夹,并且在 demo 目录下新建一个 __main__.py 的文件,随便打印点东西 print("hello, world"),然后执行:python3 -m demo
  • 当模块被直接执行,__name__ 的值就变成了 __main__验证:在 demo 目录下再新建一个 foobar.py 文件:

    # foobar.py
    def main():
        print("hello, world")

    if __name__ == "__main__":
        main()
    执行命令:python3 -m demo.foobar

总结:-m <package> 理解为 -m <package.__main__> 的简写形式。

解压、压缩

创建一个 tar 压缩包
        # 将 demo 文件夹压缩成 demo.tar
        $ python3 -m tarfile -c demo.tar demo
        # 解压 demo.tar 到 demo_new 文件夹下
        $ python3 -m tarfile -e demo.tar demo_new
创建一个 gzip 格式的压缩包(gzip 的输入,只能是一个文件,而不能是一个目录)     
        # 将 message.html 文件夹压缩成 message.gz
        $  python3 -m gzip message
        # 解压 message.gz
        $ python3 -m gzip -d message.gz
压缩demo文件夹为 demo.zip:python3 -m zipfile -c demo.zip demo
解压一个 zip 格式的压缩包:python3 -m zipfile -e demo.zip demo

telnet 端口检测工具

检查 192.168.56.200 上的 22 端口有没有开放:python3 -m telnetlib -d 192.168.56.200 22

打包项目成 pyz 并运行

当前有一个 demo 项目,目录结构树及相关文件的的代码如下

用如下命令,将该项目进行打包,其中 demo 是项目的文件夹名,main:main 中的第一个 main 指的 main.py,而第二个 main 指的是 main 函数:python3 -m zipapp demo -m "main:main"

执行完成后,会生成一个 demo.pyz 文件,可直接执行:python demo.pyz

3、炫技 操作

list 合并 的8种方式

  • 使用 + 对多个列表进行相加
  • 借助 itertools,使用 itertools.chain() 函数先将可迭代对象(在这里指的是列表)串联起来,组成一个更大的可迭代对象。
    >>> from itertools import chain
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>> list03 = [7,8,9]
    >>>
    >>> list(chain(list01, list02, list03))
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 使用 * 可以解包列表,解包后再合并。
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>>
    >>> [*list01, *list02]
    [1, 2, 3, 4, 5, 6]
    >>>
  • 在字典中使用 update 可实现原地更新,而在列表中使用 extend 可实现列表的自我扩展。
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>>
    >>> list01.extend(list02)
    >>> list01
    [1, 2, 3, 4, 5, 6]
  • 使用列表推导式

    list01 = [1, 2, 3]
    list02 = [4, 5, 6]
    list03 = [7, 8, 9]

    temp = [x for temp_list in (list01, list02, list03) for x in temp_list]
    print(temp)

  • 使用 heapq。heapq 是 Python 的一个标准模块,它提供了堆排序算法的实现。
    该模块里有一个 merge 方法,可以用于合并多个列表。heapq.merge 除了合并多个列表外,它还会将合并后的最终的列表进行排序。等价 sorted(itertools.chain(*iterables))
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>> list03 = [7,8,9]
    >>>
    >>> from heapq import merge
    >>>
    >>> list(merge(list01, list02, list03))
    [1, 2, 3, 4, 5, 6, 7, 8, 9]

  • 借助魔法方法。有一个魔法方法叫 __add__,当我们使用第一种方法 list01 + list02 的时候,内部实际上是作用在 __add__ 这个魔法方法上的。
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>>
    >>> list01 + list02
    [1, 2, 3, 4, 5, 6]
    >>>
    >>>
    >>> list01.__add__(list02)
    [1, 2, 3, 4, 5, 6]
    借用这个魔法特性,我们可以配合 reduce 这个方法来对多个列表进行合并,示例代码如下
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>> list03 = [7,8,9]
    >>>
    >>> from functools import reduce
    >>> reduce(list.__add__, (list01, list02, list03))
    [1, 2, 3, 4, 5, 6, 7, 8, 9]

  • 使用 yield from
    >>> list01 = [1,2,3]
    >>> list02 = [4,5,6]
    >>> list03 = [7,8,9]
    >>>
    >>> def merge(*lists):
    ...   for l in lists:
    ...     yield from l
    ...
    >>> list(merge(list01, list02, list03))
    [1, 2, 3, 4, 5, 6, 7, 8, 9]

连接多个列表

import timeit
a = [1, 2]
b = [3, 4]
c = [5, 6]
print(a + b + c)
print(sum((a, b, c), []))
print(timeit.timeit("a+b+c", number=1000000, globals=globals()))
print(timeit.timeit("sum((a,b,c), [])", number=1000000, globals=locals()))

dict 合并 的7种方法

  • 原地更新。字典对象内置了一个 update 方法,用于把另一个字典更新到自己身上。
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    profile.update(ext_info)
    print(profile)
    不想更新到自己身上,而是生成一个新的对象,那使用深拷贝。
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    from copy import deepcopy
    full_profile = deepcopy(profile)
    full_profile.update(ext_info)
    print(full_profile)
  • 先解包再合并字典。使用 ** 可以解包字典,解包完后再使用 dict 或者 {} 就可以合并。
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    full_profile01 = {**profile, **ext_info}
    print(full_profile01)
    {'name': 'xiaoming', 'age': 27, 'gender': 'male'}
    dict(**profile, **ext_info)   等价于 dict((("name", "xiaoming"), ("age", 27), ("gender", "male")))
  • 借助 itertools。可以使用 itertools.chain() 函数先将多个字典(可迭代对象)串联起来,组成一个更大的可迭代对象,然后再使用 dict 转成字典。
    import itertools
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    dict(itertools.chain(profile.items(), ext_info.items()))
  • 借助 ChainMap。 ChainMap 也可以达到和 itertools 同样的效果。
    from collections import ChainMap
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    dict(ChainMap(profile, ext_info))
  • 使用 dict.items() 合并。| 操作符用于对集合(set)取并集。先利用 items 方法将 dict 转成 dict_items,再对这两个 dict_items 取并集,最后利用 dict 函数,转成字典。
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    full_profile = dict(profile.items() | ext_info.items())
    直接使用 list 函数再合并
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    dict(list(profile.items()) + list(ext_info.items()))
  • 最酷炫的字典解析式。Python 里对于生成列表、集合、字典,有一套非常 Pythonnic 的写法。那就是列表解析式,集合解析式和字典解析式,通常是 Python 发烧友的最爱,
    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    temp = {k:v for d in [profile, ext_info] for k,v in d.items()}
    print(temp)
  • Python 3.9 新特性。合并操作符(Union Operator) ||= 类似于原地更新。

    profile = {"name": "xiaoming", "age": 27}
    ext_info = {"gender": "male"}
    profile | ext_info
    temp = {'name': 'xiaoming', 'age': 27, 'gender': 'male'}

    ext_info |= profile
    ext_info

导包 的 5种方法

  • 直接 import
  • __import__ 函数可用于导入模块,import 语句也会调用函数。其定义为:__import__(name[, globals[, locals[, fromlist[, level]]]])
    参数介绍:
    name (required): 被加载 module 的名称
    globals (optional): 包含全局变量的字典,该选项很少使用,采用默认值 global()
    locals (optional): 包含局部变量的字典,内部标准实现未用到该变量,采用默认值 - local()
    fromlist (Optional): 被导入的 submodule 名称
    level (Optional): 导入路径选项,Python 2 中默认为 -1,表示同时支持 absolute import 和 relative import。Python 3 中默认为 0,表示仅支持 absolute import。如果大于 0,则表示相对导入的父目录的级数,即 1 类似于 ‘.’,2 类似于 ‘..’。

    >>> os = __import__('os')
    >>> os.getcwd()

    import os as myos 等价下面
    >>> myos = __import__('os')
    >>> myos.getcwd()

    __import__ 是一个内建函数,既然是内建函数的话,那么这个内建函数必将存在于 __buildins__ 中,因此我们还可以这样导入 os 的模块:>>> __builtins__.__dict__['__import__']('os').getcwd()

  • importlib 模块。
    >>> import importlib
    >>> os=importlib.import_module("os")
    >>> os.getcwd()
    实现 import xx as yy效果,可以这样
    >>> import importlib
    >>> myos = importlib.import_module("os")
    >>> myos.getcwd()

  • import_from_github_com 可以从 github 下载安装并导入的包。
    安装:python3 -m pip install import_from_github_com
    使用:from github_com.zzzeek import sqlalchemy

  • 远程导入模块

对于普通开发者来说,其实只要掌握 import 这种方法足够了,而对于那些想要自己开发框架的人来说,深入学习__import__以及 importlib 是非常有必要的。

判断是否包含子串

  • in 和 not in
  • "hello, python".find("llo") != -1
  • 字符串对象有一个 index 方法,可以返回指定子串在该字符串中第一次出现的索引,如果没有找到会抛出异常,因此使用时需要注意捕获。
  • 利用和 index 这种曲线救国的思路,同样我们可以使用 count 的方法来判断。只要判断结果大于 0 就说明子串存在于字符串中。

  • 在 operator 中有一个方法 contains 可以很方便地判断子串是否在字符串中。
    import operator
    operator.contains("hello, python", "llo")

  • 使用正则匹配。
    import re

    def is_in(full_str, sub_str):
        if re.findall(sub_str, full_str):
            return True
        else:
            return False

    print(is_in("hello, python", "llo"))  # True
    print(is_in("hello, python", "lol"))  # False

海象 运算符

它的英文原名叫 Assignment Expressions,翻译过来也就是 赋值表达式,不过现在大家更普遍地称之为海象运算符,就是因为它长得真的太像海象了。

也可以叫 :=  短变量声明,意思是声明并初始化

用法 1:if/else

在 Python 3.8 之前,Python 这样子写

age = 20
if age > 18:
    print("已经成年了")

但有了海象运算符之后,

if (age:= 20) > 18:
    print("已经成年了")

用法 2:while

在不使用 海象运算符之前,使用 while 循环来读取文件的时候,你也许会这么写

file = open("demo.txt", "r")
while True:
    line = file.readline()
    if not line:
        break
    print(line.strip())
但有了海象运算符之后,可以这样写,
file = open("demo.txt", "r")
while (line := file.readline()):
    print(line.strip())    

实现一个需要命令行交互输入密码并检验的代码,你也许会这样子写

while True:
   p = input("Enter the password: ")
   if p == "youpassword":
      break
有了海象运算符之后,这样子写更为舒服
while (p := input("Enter the password: ")) != "youpassword":
   continue      

用法3:推导式

如下这段代码中,使用列表推导式得出所有会员中过于肥胖的人的 bmi 指数

members = [
    {"name": "小五", "age": 23, "height": 1.75, "weight": 72},
    {"name": "小李", "age": 17, "height": 1.72, "weight": 63},
    {"name": "小陈", "age": 20, "height": 1.78, "weight": 82},
]

count = 0

def get_bmi(info):
    global count
    count += 1

    print(f"执行了 {count} 次")

    height = info["height"]
    weight = info["weight"]

    return weight / (height**2)

# 查出所有会员中过于肥胖的人的 bmi 指数
fat_bmis = [get_bmi(m) for m in members if get_bmi(m) > 24]

print(fat_bmis)

可以看到,会员数只有 3 个,但是 get_bmi 函数却执行了 4 次,原因是在判断时执行了 3 次,而在构造新的列表时又重复执行了一遍。

如果所有会员都是过于肥胖的,那最终将执行 6 次,这种在大量的数据下是比较浪费性能的,因此对于这种结构,我通常会使用传统的for 循环 + if 判断。

fat_bmis = []

# 查出所有会员中过于肥胖的人的 bmi 指数
for m in members:
    bmi = get_bmi(m)
    if bmi > 24:
        fat_bmis.append(bmi)

有了海象运算符之后,你就可以不用在这种场景下做出妥协。字典推导式和集合推导式中同样适用

# 查出所有会员中过于肥胖的人的 bmi 指数
fat_bmis = [bmi for m in members if (bmi := get_bmi(m)) > 24]

装饰器 6种写法

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

装饰器的使用方法很固定

  • 先定义一个装饰器(帽子)
  • 再定义你的业务函数或者类(人)
  • 最后把这装饰器(帽子)扣在这个函数(人)头上
# 定义装饰器
def decorator(func):
    def wrapper(*args, **kw):
        return func()
    return wrapper

# 定义业务函数并进行装饰
@decorator
def function():
    print("hello, decorator")

第一种:普通装饰器

写一个最普通的装饰器,它实现的功能是:

  • 在函数执行前,先记录一行日志
  • 在函数执行完,再记录一行日志
# 这是装饰器函数,参数 func 是被装饰的函数
def logger(func):
    def wrapper(*args, **kw):
        print('我准备开始执行:{} 函数了:'.format(func.__name__))

        # 真正执行的是这行。
        func(*args, **kw)

        print('主人,我执行完啦。')
    return wrapper

计算两个数之和。写好后,直接给它带上帽子。

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

然后执行一下 add 函数:add(200, 50)

第二种:带参数的函数装饰器

看上面的例子,装饰器是不能接收参数的。其用法,只能适用于一些简单的场景。不传参的装饰器,只能对被装饰函数,执行固定逻辑。

装饰器本身是一个函数,做为一个函数,如果不能传参,那这个函数的功能就会很受限,只能执行固定的逻辑。这意味着,如果装饰器的逻辑代码的执行需要根据不同场景进行调整,若不能传参的话,我们就要写两个装饰器,这显然是不合理的。

比如我们要实现一个场景,可以在装饰器里传入一个参数,指明国籍,并在函数执行前,用自己国家的母语打一个招呼。

# 小明,中国人
@say_hello("China")
def xiaoming():
    pass

# jack,美国人
@say_hello("America")
def jack():
    pass

实现 传参 装饰,需要两层嵌套。

def say_hello(country):
    def wrapper(func):
        def deco(*args, **kwargs):
            if country == "China":
                print("你好!")
            elif country == "America":
                print('hello.')
            else:
                return

            # 真正执行函数的地方
            func(*args, **kwargs)
        return deco
    return wrapper

第三种:不带参数的类装饰器

还可以基于类实现的装饰器。基于类装饰器的实现必须实现 __call__ 和 __init__两个内置函数。 __init__ :接收被装饰函数 __call__ :实现装饰逻辑。

class logger(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[INFO]: the function {func}() is running..."\
            .format(func=self.func.__name__))
        return self.func(*args, **kwargs)

@logger
def say(something):
    print("say {}!".format(something))

say("hello")

第四种:带参数的类装饰器

上面不带参数的例子,你发现没有,只能打印INFO级别的日志,正常情况下,我们还需要打印DEBUG WARNING等级别的日志。这就需要给类装饰器传入参数,给这个函数指定级别了。

带参数和不带参数的类装饰器有很大的不同。__init__ :不再接收被装饰函数,而是接收传入参数。 __call__ :接收被装饰函数,实现装饰逻辑。指定WARNING级别,运行一下,来看看输出。

class logger(object):
    def __init__(self, level='INFO'):
        self.level = level

    def __call__(self, func): # 接受函数
        def wrapper(*args, **kwargs):
            print("[{level}]: the function {func}() is running..."\
                .format(level=self.level, func=func.__name__))
            func(*args, **kwargs)
        return wrapper  #返回函数

@logger(level='WARNING')
def say(something):
    print("say {}!".format(something))

say("hello")

第五种:使用偏函数与类实现装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。

事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个可被调用(callable)的对象

对于这个 callable 对象,我们最熟悉的就是函数了。

除函数之外,类也可以是 callable 对象,只要实现了__call__ 函数(上面几个例子已经接触过了)。

还有容易被人忽略的偏函数其实也是 callable 对象。

接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。

如下所示,DelayFunc 是一个实现了 __call__ 的类,delay 返回一个偏函数,在这里 delay 就可以作为一个装饰器。(以下代码摘自 Python工匠:使用装饰器的小技巧)

import time
import functools

class DelayFunc:
    def __init__(self,  duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)

def delay(duration):
    """
    装饰器:推迟某个函数的执行。
    同时提供 .eager_call 方法立即执行
    """
    # 此处为了避免定义额外函数,
    # 直接使用 functools.partial 帮助构造 DelayFunc 实例
    return functools.partial(DelayFunc, duration)

业务函数很简单,就是相加

@delay(duration=2)
def add(a, b):
    return a+b

执行过程

>>> add    # 可见 add 变成了 Delay 的实例
<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5)  # 直接调用实例,进入 __call__
Wait for 2 seconds...
8
>>>
>>> add.func # 实现实例方法
<function add at 0x107bef1e0>

第六种:能装饰类的装饰器

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

instances = {}

def singleton(cls):
    def get_instance(*args, **kw):
        cls_name = cls.__name__
        print('===== 1 ====')
        if not cls_name in instances:
            print('===== 2 ====')
            instance = cls(*args, **kw)
            instances[cls_name] = instance
        return instances[cls_name]
    return get_instance

@singleton
class User:
    _instance = None

    def __init__(self, name):
        print('===== 3 ====')
        self.name = name

调用 函数 的9种方式

  • 方法一:直接调用函数运行

    def task():
        print("running task")

    task()
    class Task:
        def task(self):
            print("running task")

    Task().task()

  • 方法二:使用偏函数来执行。在 functools 内置库中 partial 方法专门用来生成偏函数。
    def power(x, n):
        s = 1
        while n > 0:
            n = n - 1
            s = s * x
        return s

    from functools import partial

    power_2=partial(power, n=2)
    power_2(2)  # output: 4
    power_2(3)  # output: 9

  • 方法三:使用 eval 动态执行

  • 方法四:使用 getattr 动态获取执行
    import sys

    class Task:
        @staticmethod
        def pre_task():
            print("running pre_task")

        @staticmethod
        def task():
            print("running task")

        @staticmethod
        def post_task():
            print("running post_task")

    argvs = sys.argv[1:]

    task = Task()

    for action in argvs:
        func = getattr(task, action)
        func()

  • 方法五:使用类本身的字典。__dict__() 的魔法方法,存放所有对象的属性及方法。静态方法并没有与实例进行绑定,因此静态方法是属于类的,但是不是属于实例的,实例虽然有使用权(可以调用),但是并没有拥有权。因此要想通过 __dict__ 获取函数,得通过类本身 Task,取出来的函数,调用方法和平时的也不一样,必须先用 __func__ 获取才能调用。
    import sys

    class Task:
        @staticmethod
        def pre_task():
            print("running pre_task")

    func = Task.__dict__.get("pre_task")
    func.__func__()

  • 方法六:使用 global() 获取执行。上面放入类中,只是为了方便使用 getattr 的方法,其实不放入类中,也是可以的。此时你需要借助 globals() 或者 locals() ,它们本质上就是一个字典,你可以直接 get 来获得函数。
    import sys

    def pre_task():
        print("running pre_task")

    def task():
        print("running task")

    def post_task():
        print("running post_task")

    argvs = sys.argv[1:]

    for action in argvs:
        globals().get(action)()

  • 方法七:从文本中编译运行。先定义一个字符串,内容是你函数的内容,比如上面的 pre_task ,再通过 compile 函数编进 编译,转化为字节代码,最后再使用 exec 去执行它。
    pre_task = """
    print("running pre_task")
    """
    exec(compile(pre_task, '<string>', 'exec'))
    若代码是放在一个 txt 文本中,虽然无法直接导入运行,但仍然可以通过 open 来读取,最后使用 compile 函数编译运行。
    with open('source.txt') as f:
        source = f.read()
        exec(compile(source, 'source.txt', 'exec'))

  • 方法八:使用 attrgetter 获取执行。在 operator 这个内置库中,有一个获取属性的方法,叫 attrgetter ,获取到函数后再执行。
    from operator import attrgetter

    class People:
        def speak(self, dest):
            print("Hello, %s" %dest)

    p = People()
    caller = attrgetter("speak")
    caller(p)("king")

  • 方法九:使用 methodcaller 执行。同样还是 operator 这个内置库,有一个 methodcaller 方法,使用它,也可以做到动态调用实例方法的效果。
    from operator import methodcaller

    class People:
        def speak(self, dest):
            print("Hello, %s" %dest)

    caller = methodcaller("speak", "king")
    p = People()
    caller(p)

创造 "新语法"

from functools import partial


class Magic(object):
    def __init__(self, func):
        self.func = func

    def __or__(self, other):
        return self.func(other)

    def __ror__(self, other):
        self.func = partial(self.func, other)
        return self


到 = Magic(range)
for i in 5 | 到 | 10:
    print(i)

for i in range(5, 11):
    print(i)

|到| 应该分为 | 、| 这三个部分

  • 第一和第三的 | 是同个意思,它就是一个普通的运算符,通常我们使用 or 关键字来替代它。控制 | 的魔法方法是 __or__ 和 __xor__
  • 第二个字符  实际上是一个类的实例,定义一个 Magic 的类,用于改变 range 的 | 方法

总结

  •  是 Magic 类的一个实例

  • __or__ 定义的是  实例右侧遇到 | 的行为

  • __xor__ 定义的是  实例左侧遇到 | 的行为

4、魔法 进阶

精通上下文管理器

with 这个关键字,对于每一学习Python的人,都不会陌生。操作文本对象的时候,几乎所有的人都会让我们要用 with open ,这就是一个上下文管理的例子。

with open('test.txt') as f:
    print(f.readlines())

一个类里,实现了__enter____exit__的方法,这个类的实例就是一个上下文管理器。

class Resource():
    def __enter__(self):
        print('===connect to resource===')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('===close resource connection===')

    def operate(self):
        print('===in operation===')

with Resource() as res:
    res.operate()

执行一下,通过日志的打印顺序。可以知道其执行过程。

===connect to resource===
===in operation===
===close resource connection===

修改上面代码

class Resource():
    def __enter__(self):
        print('===connect to resource===')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('===close resource connection===')
        return True

    def operate(self):
        1/0

with Resource() as res:
    res.operate()

运行一下,惊奇地发现,居然不会报错。这就是上下文管理协议的一个强大之处,异常可以在__exit__ 进行捕获并由你自己决定如何处理,是抛出呢还是在这里就解决了。在__exit__ 里返回 True(没有return 就默认为 return False),就相当于告诉 Python解释器,这个异常我们已经捕获了,不需要再往外抛了。

在 写__exit__ 函数时,需要注意的事,它必须要有这三个参数:

  • exc_type:异常类型

  • exc_val:异常值

  • exc_tb:异常的错误栈信息

当主逻辑代码没有报异常时,这三个参数将都为None。

Python 提供了 提供了一个装饰器,你只要按照它的代码协议来实现函数内容,就可以将这个函数对象变成一个上下文管理器。

按照 contextlib 的协议来自己实现一个打开文件(with open)的上下文管理器。

import contextlib


@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    # 【重点】:yield
    yield file_handler

    # __exit__方法
    print('close file:', file_name, 'in __exit__')
    file_handler.close()
    return


with open_func('/Users/MING/mytest.txt') as file_in:
    for line in file_in:
        print(line)

在被装饰函数里,必须是一个生成器(带有yield),而yield之前的代码,就相当于__enter__里的内容。yield 之后的代码,就相当于__exit__ 里的内容。

import contextlib


@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    # 【重点】:yield
    yield file_handler

    # __exit__方法
    print('close file:', file_name, 'in __exit__')
    file_handler.close()
    return


with open_func('/Users/MING/mytest.txt') as file_in:
    for line in file_in:
        print(line)

在被装饰函数里,必须是一个生成器(带有yield),而yield之前的代码,就相当于__enter__里的内容。yield 之后的代码,就相当于__exit__ 里的内容。

上面这段代码只能实现上下文管理器的第一个目的(管理资源),并不能实现第二个目的(处理异常)。

如果要处理异常,可以改成下面这个样子。

import contextlib


@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    try:
        yield file_handler
    except Exception as exc:
        # deal with exception
        print('the exception was thrown')
    finally:
        print('close file:', file_name, 'in __exit__')
        file_handler.close()
        return


with open_func('/Users/MING/mytest.txt') as file_in:
    for line in file_in:
        1 / 0
        print(line)

@property  描述符

假想你正在给学校写一个成绩管理系统,并没有太多编码经验的你,可能会这样子写。

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                self.name, self.math, self.chinese, self.english
            )

看起来一切都很合理

>>> std1 = Student('小明', 76, 87, 68)
>>> std1
<Student: 小明, math:76, chinese: 87, english:68>

但是程序并不像人那么智能,不会自动根据使用场景判断数据的合法性,如果老师在录入成绩的时候,不小心将成绩录成了负数,或者超过100,程序是无法感知的。

聪明的你,马上在代码中加入了判断逻辑。

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        if 0 <= math <= 100:
            self.math = math
        else:
            raise ValueError("Valid value must be in [0, 100]")

        if 0 <= chinese <= 100:
            self.chinese = chinese
        else:
            raise ValueError("Valid value must be in [0, 100]")

        if 0 <= english <= 100:
            self.english = english
        else:
            raise ValueError("Valid value must be in [0, 100]")

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
            self.name, self.math, self.chinese, self.english
        )

但在__init__里有太多的判断逻辑,很影响代码的可读性。巧的是,你刚好学过 Property 特性,可以很好地应用在这里。于是你将代码修改成如下,代码的可读性瞬间提升了不少

class Student:
    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    @property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @property
    def chinese(self):
        return self._chinese

    @chinese.setter
    def chinese(self, value):
        if 0 <= value <= 100:
            self._chinese = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @property
    def english(self):
        return self._english

    @english.setter
    def english(self, value):
        if 0 <= value <= 100:
            self._english = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
                self.name, self.math, self.chinese, self.english
            )

你以为你写的代码,已经非常优秀,无懈可击了。没想到,人外有天,你的主管看了你的代码后,深深地叹了口气:类里的三个属性,math、chinese、english,都使用了 Property 对属性的合法性进行了有效控制。功能上,没有问题,但就是太啰嗦了,三个变量的合法性逻辑都是一样的,只要大于等于0,小于等于100 就可以,代码重复率太高了,这里三个成绩还好,但假设还有地理、生物、历史、化学等十几门的成绩呢,这代码简直没法忍。去了解一下 Python 的描述符吧。

经过主管的指点,你知道了「描述符」这个东西。怀着一颗敬畏之心,你去搜索了下关于 描述符的用法。其实也很简单,一个实现了 描述符协议 的类就是一个描述符。

什么是描述符协议:在类里实现了 __get__()__set__()__delete__() 其中至少一个方法。

  • __get__: 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。

  • __set__:将在属性分配操作中调用。不会返回任何内容。

  • __delete__:控制删除操作。不会返回内容。

对描述符有了大概的了解后,你开始重写上面的方法。

class Score:
    def __init__(self, default=0):
        self._score = default

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Score must be integer')
        if not 0 <= value <= 100:
            raise ValueError('Valid value must be in [0, 100]')

        self._score = value

    def __get__(self, instance, owner):
        return self._score

    def __delete__(self):
        del self._score


class Student:
    math = Score(0)
    chinese = Score(0)
    english = Score(0)

    def __init__(self, name, math, chinese, english):
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
            self.name, self.math, self.chinese, self.english
        )

实现的效果和前面的一样,可以对数据的合法性进行有效控制(字段类型、数值区间等)

到这里,你需要记住的只有一点,就是描述符给我们带来的编码上的便利,它在实现 保护属性不受修改属性类型检查 的基本功能,同时又大大提高代码的复用率。

描述符的访问规则

描述符分两种:

  • 数据描述符:实现了__get__ 和 __set__ 两种方法的描述符

  • 非数据描述符:只实现了__get__ 方法的描述符

数据描述器和非数据描述器的区别在于:它们相对于实例的字典的优先级不同

如果实例字典中有与描述符同名的属性,那么:

  • 描述符是数据描述符的话,优先使用数据描述符

  • 描述符是非数据描述符的话,优先使用字典中的属性。

这边还是以上节的成绩管理的例子来说明,方便你理解。

# 数据描述符
class DataDes:
    def __init__(self, default=0):
        self._score = default

    def __set__(self, instance, value):
        self._score = value

    def __get__(self, instance, owner):
        print("访问数据描述符里的 __get__")
        return self._score

# 非数据描述符
class NoDataDes:
    def __init__(self, default=0):
        self._score = default

    def __get__(self, instance, owner):
        print("访问非数据描述符里的 __get__")
        return self._score


class Student:
    math = DataDes(0)
    chinese = NoDataDes(0)

    def __init__(self, name, math, chinese):
        self.name = name
        self.math = math
        self.chinese = chinese

    def __getattribute__(self, item):
        print("调用 __getattribute__")
        return super(Student, self).__getattribute__(item)

    def __repr__(self):
        return "<Student: {}, math:{}, chinese: {},>".format(
                self.name, self.math, self.chinese)

需要注意的是,math 是数据描述符,而 chinese 是非数据描述符。从下面的验证中,可以看出,当实例属性和数据描述符同名时,会优先访问数据描述符(如下面的math),而当实例属性和非数据描述符同名时,会优先访问实例属性(__getattribute__

>>> std = Student('xm', 88, 99)
>>>
>>> std.math
调用 __getattribute__
访问数据描述符里的 __get__
88
>>> std.chinese
调用 __getattribute__
99

讲完了数据描述符和非数据描述符,我们还需要了解的对象属性的查找规律。

当我们对一个实例属性进行访问时,Python 会按 obj.__dict__ → type(obj).__dict__ → type(obj)的父类.__dict__ 顺序进行查找,如果查找到目标属性并发现是一个描述符,Python 会调用描述符协议来改变默认的控制行为。

基于描述符如何实现property

正常人所见过的描述符的用法就是上面提到的那些,我想说的是那只是描述符协议最常见的应用之一,或许你还不知道,其实有很多 Python 的特性的底层实现机制都是基于 描述符协议 的,比如我们熟悉的@property 、@classmethod 、@staticmethod 和 super 等。

property 的基本用法。

class Student:
    def __init__(self, name):
        self.name = name

    @property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

过property装饰的函数,如例子中的 math 会变成 Student 实例的属性。而对 math 属性赋值会进入 使用 math.setter 装饰函数的逻辑代码块。

为什么说 property 底层是基于描述符协议的呢?通过 PyCharm 点击进入 property 的源码,很可惜,只是一份类似文档一样的伪源码,并没有其具体的实现逻辑。

不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。这里我自己通过模仿其函数结构,结合「描述符协议」来自己实现类 property 特性。

class TestProperty(object):

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        print("in __get__")
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError
        return self.fget(obj)

    def __set__(self, obj, value):
        print("in __set__")
        if self.fset is None:
            raise AttributeError
        self.fset(obj, value)

    def __delete__(self, obj):
        print("in __delete__")
        if self.fdel is None:
            raise AttributeError
        self.fdel(obj)

    def getter(self, fget):
        print("in getter")
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        print("in setter")
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        print("in deleter")
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

然后 Student 类,我们也相应改成如下

class Student:
    def __init__(self, name):
        self.name = name

    # 其实只有这里改变
    @TestProperty
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

为了尽量让你少产生一点疑惑,我这里做两点说明:

  1. 使用TestProperty装饰后,math 不再是一个函数,而是TestProperty 类的一个实例。所以第二个math函数可以使用 math.setter 来装饰,本质是调用TestProperty.setter 来产生一个新的 TestProperty 实例赋值给第二个math

  2. 第一个 math 和第二个 math 是两个不同 TestProperty 实例。但他们都属于同一个描述符类(TestProperty),当对 math 赋值时,就会进入 TestProperty.__set__,当对math 进行取值里,就会进入 TestProperty.__get__。仔细一看,其实最终访问的还是Student实例的 _math 属性。

说了这么多,还是运行一下,更加直观一点。

# 运行后,会直接打印这一行,这是在实例化 TestProperty 并赋值给第二个math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90

@staticmethod

说完了 property ,这里再来讲讲 @classmethod 和 @staticmethod 的实现原理。

定义了一个类,用了两种方式来实现静态方法。

class Test:
    @staticmethod
    def myfunc():
        print("hello")

# 上下两种写法等价

class Test:
    def myfunc():
        print("hello")
    # 重点:这就是描述符的体现
    myfunc = staticmethod(myfunc)

这两种写法是等价的,就好像在 property 一样,其实以下两种写法也是等价的。

@TestProperty
def math(self):
    return self._math

math = TestProperty(fget=math)

话题还是转回到 staticmethod 这边来吧。

由上面的注释,可以看出 staticmethod 其实就相当于一个描述符类,而myfunc 在此刻变成了一个描述符。关于 staticmethod 的实现,你可以参照下面这段我自己写的代码,加以理解。

调用这个方法可以知道,每调用一次,它都会经过描述符类的 __get__ 。

>>> Test.myfunc()
in staticmethod __get__
hello
>>> Test().myfunc()
in staticmethod __get__
hello

@classmethod

同样的 classmethod 也是一样。

class classmethod(object):
    def __init__(self, f):
        self.f = f

    def __get__(self, instance, owner=None):
        print("in classmethod __get__")

        def newfunc(*args):
            return self.f(owner, *args)
        return newfunc

class Test:
    def myfunc(cls):
        print("hello")

    # 重点:这就是描述符的体现
    myfunc = classmethod(myfunc)

验证结果如下

>>> Test.myfunc()
in classmethod __get__
hello
>>> Test().myfunc()
in classmethod __get__
hello

讲完了 propertystaticmethodclassmethod 与 描述符的关系。我想你应该对描述符在 Python 中的应用有了更深的理解。对于 super 的实现原理,就交由你来自己完成。

所有实例共享描述符

通过以上内容的学习,你是不是觉得自己已经对描述符足够了解了呢?可在这里,我想说以上的描述符代码都有问题。问题在哪里呢?请看下面这个例子。

class Score:
    def __init__(self, default=0):
        self._value = default

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            self._value = value
        else:
            raise ValueError


class Student:
    math = Score(0)
    chinese = Score(0)
    english = Score(0)

    def __repr__(self):
        return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.chinese, self.english)

Student 里没有像前面那样写了构造函数,但是关键不在这儿,没写只是因为没必要写。然后来看一下会出现什么样的问题呢

>>> std1 = Student()
>>> std1
<Student math:0, chinese:0, english:0>
>>> std1.math = 85
>>> std1
<Student math:85, chinese:0, english:0>
>>> std2 = Student()
>>> std2 # std2 居然共享了std1 的属性值
<Student math:85, chinese:0, english:0>
>>> std2.math = 100
>>> std1 # std2 也会改变std1 的属性值
<Student math:100, chinese:0, english:0>

从结果上来看,std2 居然共享了 std1 的属性值,只要其中一个实例的变量发生改变,另一个实例的变量也会跟着改变。
探其根因,是由于此时 math,chinese,english 三个全部是类变量,导致 std2 和 std1 在访问 math,chinese,english 这三个变量时,其实都是访问类变量。
问题是不是来了?小明和小强的分数怎么可能是绑定的呢?这很明显与实际业务不符。
使用描述符给我们制造了便利,却无形中给我们带来了麻烦,难道这也是描述符的特性吗?
描述符是个很好用的特性,会出现这个问题,是由于我们之前写的描述符代码都是错误的。
描述符的机制,在我看来,只是抢占了访问顺序,而具体的逻辑却要因地制宜,视情况而定。
如果要把 math,chinese,english 这三个变量变成实例之间相互隔离的属性,应该这么写。

class Score:
    def __init__(self, subject):
        self.name = subject

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            instance.__dict__[self.name] = value
        else:
            raise ValueError


class Student:
    math = Score("math")
    chinese = Score("chinese")
    english = Score("english")

    def __init__(self, math, chinese, english):
        self.math = math
        self.chinese = chinese
        self.english = english

    def __repr__(self):
        return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.chinese, self.english)

引导程序逻辑进入描述符之后,不管你是获取属性,还是设置属性,都是直接作用于 instance 的。

不难看出:

  • 之前的错误代码,更像是把描述符当做了存储节点。

  • 之后的正确代码,则是把描述符直接当做代理,本身不存储值。

元类 编程

类是如何产生?这个问题也许你会觉得很傻。实则不然,很多初学者只知道使用继承的表面形式来创建一个类,却不知道其内部真正的创建是由 type 来创建的。

type?这不是判断对象类型的函数吗?是的,type通常用法就是用来判断对象的类型。但除此之外,他最大的用途是用来动态创建类。当Python扫描到class的语法的时候,就会调用type函数进行类的创建。

如何使用 type 创建类

首先,type() 需要接收三个参数

  1. 类的名称,若不指定,也要传入空字符串:""

  2. 父类,注意以tuple的形式传入,若没有父类也要传入空tuple:(),默认继承object

  3. 绑定的方法或属性,注意以dict的形式传入

来看个例子

# 准备一个基类(父类)
class BaseClass:
    def talk(self):
        print("i am people")

# 准备一个方法
def say(self):
    print("hello")

# 使用type来创建User类
User = type("User", (BaseClass, ), {"name":"user", "say":say})

理解什么是元类

什么是类?可能谁都知道,类就是用来创建对象的「模板」。

那什么是元类呢?一句话通俗来说,元类就是创建类的「模板」。

为什么type能用来创建类?因为它本身是一个元类。使用元类创建类,那就合理了。

type是Python在背后用来创建所有类的元类,我们熟知的类的始祖 object 也是由type创建的。更有甚者,连type自己也是由type自己创建的,这就过份了。

>>> type(type)
<class 'type'>

>>> type(object)
<class 'type'>

>>> type(int)
<class 'type'>

>>> type(str)
<class 'type'>

如果要形象地来理解的话,就看下面这三行话。

  • str:用来创建字符串对象的类。

  • int:是用来创建整数对象的类。

  • type:是用来创建类对象的类。

反过来看

  • 一个实例的类型,是类

  • 一个类的类型,是元类

  • 一个元类的类型,是type

写个简单的小示例来验证下

>>> class MetaPerson(type):
...     pass
...
>>> class Person(metaclass=MetaPerson):
...     pass
...
>>> Tom = Person()
>>> print(type(Tom))
<class '__main__.Person'>
>>> print(type(Tom.__class__))
<class '__main__.MetaPerson'>
>>> print(type(Tom.__class__.__class__))
<class 'type'>

下面再来看一个稍微完整的

# 注意要从type继承
class BaseClass(type):
    def __new__(cls, *args, **kwargs):
        print("in BaseClass")
        return super().__new__(cls, *args, **kwargs)

class User(metaclass=BaseClass):
    def __init__(self, name):
        print("in User")
        self.name = name

# in BaseClass

user = User("wangbm")
# in User

综上,我们知道了类是元类的实例,所以在创建一个普通类时,其实会走元类的 __new__

同时,我们又知道在类里实现了 __call__ 就可以让这个类的实例变成可调用。

所以在我们对普通类进行实例化时,实际是对一个元类的实例(也就是普通类)进行直接调用,所以会走进元类的 __call__

在这里可以借助 「单例的实现」举一个例子,你就清楚了

class MetaSingleton(type):
    def __call__(cls, *args, **kwargs):
        print("cls:{}".format(cls.__name__))
        print("====1====")
        if not hasattr(cls, "_instance"):
            print("====2====")
            cls._instance = type.__call__(cls, *args, **kwargs)
        return cls._instance

class User(metaclass=MetaSingleton):
    def __init__(self, *args, **kw):
        print("====3====")
        for k,v in kw:
            setattr(self, k, v)

验证结果

>>> u1 = User('wangbm1')
cls:User
====1====
====2====
====3====
>>> u1.age = 20
>>> u2 = User('wangbm2')
cls:User
====1====
>>> u2.age
20
>>> u1 is u2
True

使用元类的意义

正常情况下,我们都不会使用到元类。但是这并不意味着,它不重要。假如某一天,我们需要写一个框架,很有可能就需要你对元类要有进一步的研究。

元类有啥用,用我通俗的理解,元类的作用过程:

  1. 拦截类的创建

  2. 拦截下后,进行修改

  3. 修改完后,返回修改后的类

所以,很明显,为什么要用它呢?不要它会怎样?

使用元类,是要对类进行定制修改。使用元类来动态生成元类的实例,而99%的开发人员是不需要动态修改类的,因为这应该是框架才需要考虑的事。

但是,这样说,你一定不会服气,到底元类用来干什么?其实元类的作用就是创建API,一个最典型的应用是 Django ORM

元类实战:ORM

使用过Django ORM的人都知道,有了ORM,使得我们操作数据库,变得异常简单。

ORM的一个类(User),就对应数据库中的一张表。id,name,email,password 就是字段。

class User(BaseModel):
    id = IntField('id')
    name = StrField('username')
    email = StrField('email')
    password = StrField('password')

    class Meta:
        db_table = "user"

如果我们要插入一条数据,我们只需这样做

# 实例化成一条记录
u = User(id=20180424, name="xiaoming",
         email="xiaoming@163.com", password="abc123")

# 保存这条记录
u.save()

通常用户层面,只需要懂应用,就像上面这样操作就可以了。

但是今天我并不是来教大家如何使用ORM,我们是用来探究ORM内部究竟是如何实现的。我们也可以自己写一个简易的ORM。

从上面的User类中,我们看到StrFieldIntField,从字段意思上看,我们很容易看出这代表两个字段类型。字段名分别是id,username,email,password

StrFieldIntField在这里的用法,叫做属性描述符。 简单来说呢,属性描述符可以实现对属性值的类型,范围等一切做约束,意思就是说变量id只能是int类型,变量name只能是str类型,否则将会抛出异常。

那如何实现这两个属性描述符呢?请看代码。

import numbers

class Field:
    pass

class IntField(Field):
    def __init__(self, name):
        self.name = name
        self._value = None

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")
        self._value = value

class StrField(Field):
    def __init__(self, name):
        self.name = name
        self._value = None

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError("string value need")
        self._value = value

我们看到User类继承自BaseModel,这个BaseModel里,定义了数据库操作的各种方法,譬如我们使用的save函数,也可以放在这里面的。所以我们就可以来写一下这个BaseModel

class BaseModel(metaclass=ModelMetaClass):
    def __init__(self, *args, **kw):
        for k,v in kw.items():
            # 这里执行赋值操作,会进行数据描述符的__set__逻辑
            setattr(self, k, v)
        return super().__init__()

    def save(self):
        db_columns=[]
        db_values=[]
        for column, value in self.fields.items():
            db_columns.append(str(column))
            db_values.append(str(getattr(self, column)))
        sql = "insert into {table} ({columns}) values({values})".format(
                table=self.db_table, columns=','.join(db_columns),
                values=','.join(db_values))
        pass

BaseModel类中,save函数里面有几个新变量。

  1. fields: 存放所有的字段属性

  2. db_table:表名

我们思考一下这个u实例的创建过程:

type -> ModelMetaClass -> BaseModel -> User -> u

这里会有几个问题。

  • init的参数是User实例时传入的,所以传入的id是int类型,name是str类型。看起来没啥问题,若是这样,我上面的数据描述符就失效了,不能起约束作用。所以我们希望init接收到的id是IntField类型,name是StrField类型。

  • 同时,我们希望这些字段属性,能够自动归类到fields变量中。因为 BaseModel,它可不是专门为User类服务的,它还要兼容各种各样的表。不同的表,表里有不同数量,不同属性的字段,这些都要能自动分类并归类整理到一起。这是一个ORM框架最基本的。

  • 我们希望对表名有两种选择,一个是User中若指定Meta信息,比如表名,就以此为表名,若未指定就以类名的小写 做为表名。虽然BaseModel可以直接取到User的db_table属性,但是如果在数据库业务逻辑中,加入这段复杂的逻辑,显然是很不优雅的。

上面这几个问题,其实都可以通过元类的__new__函数来完成。

下面就来看看,如何用元类来解决这些问题呢?请看代码。

class ModelMetaClass(type):
    def __new__(cls, name, bases, attrs):
        if name == "BaseModel":
            # 第一次进入__new__是创建BaseModel类,name="BaseModel"
            # 第二次进入__new__是创建User类及其实例,name="User"
            return super().__new__(cls, name, bases, attrs)

        # 根据属性类型,取出字段
        fields = {k:v for k,v in attrs.items() if isinstance(v, Field)}

        # 如果User中有指定Meta信息,比如表名,就以此为准
        # 如果没有指定,就默认以 类名的小写 做为表名,比如User类,表名就是user
        _meta = attrs.get("Meta", None)
        db_table = name.lower()
        if _meta is not None:
            table = getattr(_meta, "db_table", None)
            if table is not None:
                db_table = table

        # 注意原来由User传递过来的各项参数attrs,最好原模原样的返回,
        # 如果不返回,有可能下面的数据描述符不起作用
        # 除此之外,我们可以往里面添加我们自定义的参数
        attrs["db_table"] = db_table
        attrs["fields"] = fields
        return super().__new__(cls, name, bases, attrs)

__new__ 有什么用

在没有元类的情况下,每次创建实例,在先进入 __init__ 之前都会先进入 __new__

class User:
    def __new__(cls, *args, **kwargs):
        print("in BaseClass")
        return super().__new__(cls)

    def __init__(self, name):
        print("in User")
        self.name = name

使用如下

>>> u = User('wangbm')
in BaseClass
in User
>>> u.name
'wangbm'

在有元类的情况下,每次创建类时,会都先进入 元类的 __new__ 方法,如果你要对类进行定制,可以在这时做一些手脚。

综上,元类的__new__和普通类的不一样:

  • 元类的__new__ 在创建类时就会进入,它可以获取到上层类的一切属性和方法,包括类名,魔法方法。

  • 而普通类的__new__ 在实例化时就会进入,它仅能获取到实例化时外界传入的属性。

5、开发技巧

嵌套 上下文管理

写一个嵌套的上下文管理器时,可能会这样写

import contextlib

@contextlib.contextmanager
def test_context(name):
    print('enter, my name is {}'.format(name))

    yield

    print('exit, my name is {}'.format(name))

with test_context('aaa'):
    with test_context('bbb'):
        print('========== in main ============')

除此之外,你可知道,还有另一种嵌套写法

with test_context('aaa'), test_context('bbb'):
    print('========== in main ============')

嵌套 for

常会写如下这种嵌套的 for 循环代码

list1 = range(1,3)
list2 = range(4,6)
list3 = range(7,9)
for item1 in list1:
    for item2 in list2:
        for item3 in list3:
              print(item1+item2+item3)

这里仅仅是三个 for 循环,在实际编码中,有可能会有更多层。这样的代码,可读性非常的差,很多人不想这么写,可又没有更好的写法。这里介绍一种我常用的写法,使用 itertools 这个库来实现更优雅易读的代码。

from itertools import product
list1 = range(1,3)
list2 = range(4,6)
list3 = range(7,9)
for item1,item2,item3 in product(list1, list2, list3):
    print(item1+item2+item3)

单行实现 for 死循环

for i in iter(int, 1):pass

iter有两种使用方法。

  • 通常我们的认知是第一种,将一个列表转化为一个迭代器。

  • 而第二种方法,他接收一个 callable对象,和一个sentinel 参数。第一个对象会一直运行,直到它返回 sentinel 值才结束。

int 是一个内建方法。通过看注释,可以看出它是有默认值0,由于int() 永远返回0,永远返回不了1,所以这个 for 循环会没有终点。一直运行下去。

关闭异常自动关联上下文

于处理不当或者其他问题,再次抛出另一个异常时,往外抛出的异常也会携带原始的异常信息。

try:
    print(1 / 0)
except Exception as exc:
    raise RuntimeError("Something bad happened")

如果在异常处理程序或 finally 块中引发异常,默认情况下,异常机制会隐式工作会将先前的异常附加为新异常的 __context__属性。这就是 Python 默认开启的自动关联异常上下文。

如果你想自己控制这个上下文,可以加个 from 关键字(from 语法会有个限制,就是第二个表达式必须是另一个异常类或实例。),来表明你的新异常是直接由哪个异常引起的。

try:
    print(1 / 0)
except Exception as exc:
    raise RuntimeError("Something bad happened") from exc

也可以通过with_traceback()方法为异常设置上下文__context__属性,这也能在traceback更好地显示异常信息。

try:
    print(1 / 0)
except Exception as exc:
    raise RuntimeError("bad thing").with_traceback(exc)

彻底关闭这个自动关联异常上下文,可以使用 raise...from None

try:
    print(1 / 0)
except Exception as exc:
    raise RuntimeError("Something bad happened") from None

自带的缓存机制

缓存是一种将定量数据加以保存,以备迎合后续获取需求的处理方式,旨在加快数据获取的速度。

数据的生成过程可能需要经过计算,规整,远程获取等操作,如果是同一份数据需要多次使用,每次都重新生成会大大浪费时间。所以,如果将计算或者远程请求等操作获得的数据缓存下来,会加快后续的数据获取需求。

functool 模块中的 lru_cache 装饰器。@functools.lru_cache(maxsize=None, typed=False)

参数解读:

  • maxsize:最多可以缓存多少个此函数的调用结果,如果为None,则无限制,设置为 2 的幂时,性能最佳

  • typed:若为 True,则不同参数类型的调用将分别缓存。

举个例子

from functools import lru_cache

@lru_cache(None)
def add(x, y):
    print("calculating: %s + %s" % (x, y))
    return x + y

print(add(1, 2))
print(add(1, 2))
print(add(2, 3))

流式读取数G超大文件

使用 read 函数 Python 会将文件的内容一次性地全部载入内存中,如果文件有 10 个G甚至更多,那么你的电脑就要消耗的内存非常巨大。

# 一次性读取
with open("big_file.txt", "r") as fp:
    content = fp.read()

改进,readline 去做一个生成器来逐行返回。

def read_from_file(filename):
    with open(filename, "r") as fp:
        yield fp.readline()

如果这个文件内容就一行呢,一行就 10个G,其实你还是会一次性读取全部内容。

最优雅的解决方法是,在使用 read 方法时,指定每次只读取固定大小的内容,比如下面的代码中,每次只读取 8kb 返回。

def read_from_file(filename, block_size = 1024 * 8):
    with open(filename, "r") as fp:
        while True:
            chunk = fp.read(block_size)
            if not chunk:
                break

            yield chunk

借助偏函数 和 iter 函数可以优化一下代码

from functools import partial

def read_from_file(filename, block_size = 1024 * 8):
    with open(filename, "r") as fp:
        for chunk in iter(partial(fp.read, block_size), ""):
            yield chunk

Python 3.8 + 利用 海象运算符就可以

def read_from_file(filename, block_size = 1024 * 8):
    with open(filename, "r") as fp:
        while chunk := fp.read(block_size):
            yield chunk

延迟调用

调用会在函数返回前一步完成。在 Python 可以使用 上下文管理器 达到这种效果

import contextlib

def callback():
    print('B')

with contextlib.ExitStack() as stack:
    stack.callback(callback)
    print('A')

输出如下

A
B

计算函数运行时间

import timeit

def run_sleep(second):
    print(second)
    time.sleep(second)

# 只用这一行
print(timeit.timeit(lambda :run_sleep(2), number=5))

重定向标准输出到日志

示例:

import contextlib

log_file="/var/log/you.log"

def you_task():
    pass

@contextlib.contextmanager
def close_stdout():
    raw_stdout = sys.stdout
    file = open(log_file, 'a+')
    sys.stdout = file

    yield

    sys.stdout = raw_stdout
    file.close()

with close_stdout():
    you_task()

在程序退出前执行代码

使用 atexit 这个内置模块,可以很方便地注册退出函数。不管在哪个地方导致程序崩溃,都会执行那些你注册过的函数。

如果clean()函数有参数,那么你可以不用装饰器,而是直接调用atexit.register(clean_1, 参数1, 参数2, 参数3='xxx')

但是使用 atexit 仍然有一些局限性,比如:

  • 如果程序是被你没有处理过的系统信号杀死的,那么注册的函数无法正常执行。

  • 如果发生了严重的 Python 内部错误,你注册的函数无法正常执行。

  • 如果你手动调用了os._exit(),你注册的函数无法正常执行。

运行状态查看源代码

# demo.py
import inspect


def add(x, y):
    return x + y

print("===================")
print(inspect.getsource(add))

单分派泛函数

Python中只能实现基于单个(第一个)参数的数据类型来选择具体的实现方式,官方名称 是 single-dispatch。你或许听不懂,说人话,就是可以实现第一个参数的数据类型不同,其调用的函数也就不同。

使用方法极其简单,只要被singledispatch 装饰的函数,就是一个single-dispatch 的泛函数(generic functions)。

  • 单分派:根据一个参数的类型,以不同方式执行相同的操作的行为。

  • 多分派:可根据多个参数的类型选择专门的函数的行为。

  • 泛函数:多个函数绑在一起组合成一个泛函数。

from functools import singledispatch

@singledispatch
def age(obj):
    print('请传入合法类型的参数!')

@age.register(int)
def _(age):
    print('我已经{}岁了。'.format(age))

@age.register(str)
def _(age):
    print('I am {} years old.'.format(age))


age(23)  # int
age('twenty three')  # str
age(['23'])  # list

Python 中有许许多的数据类型,比如 str,list, dict, tuple 等,不同数据类型的拼接方式各不相同,所以这里写了一个通用的函数,可以根据对应的数据类型对选择对应的拼接方式拼接,而且不同数据类型我还应该提示无法拼接。以下是简单的实现。

def check_type(func):
    def wrapper(*args):
        arg1, arg2 = args[:2]
        if type(arg1) != type(arg2):
            return '【错误】:参数类型不同,无法拼接!!'
        return func(*args)
    return wrapper


@singledispatch
def add(obj, new_obj):
    raise TypeError

@add.register(str)
@check_type
def _(obj, new_obj):
    obj += new_obj
    return obj


@add.register(list)
@check_type
def _(obj, new_obj):
    obj.extend(new_obj)
    return obj

@add.register(dict)
@check_type
def _(obj, new_obj):
    obj.update(new_obj)
    return obj

@add.register(tuple)
@check_type
def _(obj, new_obj):
    return (*obj, *new_obj)

print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))

# list 和 字符串 无法拼接
print(add([1,2,3], '4,5,6'))

如果不使用 singledispatch 的话,你可能会写出这样的代码。

def check_type(func):
    def wrapper(*args):
        arg1, arg2 = args[:2]
        if type(arg1) != type(arg2):
            return '【错误】:参数类型不同,无法拼接!!'
        return func(*args)
    return wrapper

@check_type
def add(obj, new_obj):
    if isinstance(obj, str) :
        obj += new_obj
        return obj

    if isinstance(obj, list) :
        obj.extend(new_obj)
        return obj

    if isinstance(obj, dict) :
        obj.update(new_obj)
        return obj

    if isinstance(obj, tuple) :
        return (*obj, *new_obj)

print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))

# list 和 字符串 无法拼接
print(add([1,2,3], '4,5,6'))

反转字符串

mstr1 = 'abc'
ml1 = list(mstr1)
ml1.reverse()
mstr2 = str(ml1)

更好的方法

>>> mstr = 'abc'
>>> ml = [1,2,3]
>>> mstr[::-1]
'cba'
>>> ml[::-1]
[3, 2, 1]

函数的连续调用

 __call__ 魔法方法。

def add(x):
    class AddInt(int):
        def __call__(self, x):
            return AddInt(self.numerator + x)

    return AddInt(x)


print(add(2))
print(add(2)(3)(4)(5)(6)(7))

字典的多级排序

在一个列表中,每个元素都是一个字典,里面的每个字典结构都是一样的。里面的每个字典会有多个键值对,根据某个 key 或 value 的值大小,对该列表进行排序,使用 sort 函数就可以轻松实现。

students = [
    {'name': 'Jack', 'age': 17, 'score': 89}, 
    {'name': 'Julia', 'age': 17, 'score': 80}, 
    {'name': 'Tom', 'age': 16, 'score': 80}
]
students.sort(key=lambda student: student['score'])
print(students)

规则:先按成绩升序,如果成绩一致,再按年龄升序。这样的规则,代码该如何实现呢?

用字典本身的 sort 函数也能实现,方法如下:

students = [
    {'name': 'Jack', 'age': 17, 'score': 89}, 
    {'name': 'Julia', 'age': 17, 'score': 80}, 
    {'name': 'Tom', 'age': 16, 'score': 80}
]
students.sort(key=lambda student: (student['score'], student['age']))
print(students)

如果一个降序,而另一个是升序,很简单,只要在对应的 key 上,前面加一个负号,就会把顺序给颠倒过来。

students = [
    {'name': 'Jack', 'age': 17, 'score': 89},
    {'name': 'Julia', 'age': 17, 'score': 80},
    {'name': 'Tom', 'age': 16, 'score': 80}
]
students.sort(key=lambda student: (-student['score'], student['age']))
print(students)

位置参数变成关键字参数

在 Python 中,参数的种类,大概可以分为四种:

  1. 必选参数,也叫位置参数,调用函数时一定指定的参数,并且在传参的时候必须按函数定义时的顺序来

  2. 可选参数,也叫默认参数,调用函数时,可以指定也可以不指定,不指定就按默认的参数值来。

  3. 可变参数,就是参数个数可变,可以是 0 个或者任意个,但是传参时不能指定参数名,通常使用 *args 来表示。

  4. 关键字参数,就是参数个数可变,可以是 0 个或者任意个,但是传参时必须指定参数名,通常使用 **kw 来表示

使用单独的 *,可以将后面的位置参数变成关键字参数,关键字参数在你传参时,必须要写参数名,不然会报错。

def demo_func(a, b, *, c):
    print(a)
    print(b)
    print(c)


# demo_func(1, 2, 3)
demo_func(1, 2, c=3)

实现的方式就是依靠两个符号:

  • /:在 / 之前的参数都是位置参数,不能以 key-value 传参,至于后面是什么参数它不管
  • *:在 * 之后都是关键字参数,都应该以 key-value 传参,至于前面是什么参数它也不管
def func(a, b, /, c, d):
    pass


func("a", "b", c="c", d="d")

获取一个函数设定的参数

>>> from inspect import signature
>>>
>>> sig = signature(demo) # # 获取函数签名
>>> sig
<Signature (name, age, gender='male', *args, **kw)>

检查传参是否匹配签名

>>> sig.bind("测试", 27)
<BoundArguments (name='测试', age=27)>
>>>
>>> sig.bind("测试")

禁止对象深拷贝

使用 copy 模块的 deepcopy 拷贝一个对象后,会创建出来一个全新的的对象。

如果希望基于我们的类实例化后对象,禁止被深拷贝,这时候就要用到 Python 的魔法方法了。

在如下代码中,我们重写了 Sentinel 类的 __deepcopy__ 和 __copy__ 方法

class Sentinel(object):
    def __deepcopy__(self, memo):
        # Always return the same object because this is essentially a constant.
        return self

    def __copy__(self):
        # called via copy.copy(x)
        return self

如果对它进行深度拷贝的话,会发现返回的永远都是原来的对象

>>> obj = Sentinel()
>>> id(obj)
140151569169808
>>>
>>> new_obj = deepcopy(obj)
>>> id(new_obj)
140151569169808

将变量名和变量值转为字典

import re
import inspect


def varname(*args):
    current_frame = inspect.currentframe()
    back_frame = current_frame.f_back
    back_frame_info = inspect.getframeinfo(back_frame)

    current_func_name = current_frame.f_code.co_name

    caller_file_path = back_frame_info[0]
    caller_line_no = back_frame_info[1]
    caller_type = back_frame_info[2]
    caller_expression = back_frame_info[3]

    keys = []

    for line in caller_expression:
        re_match = re.search(r'\b{}\((.*?)\)'.format(current_func_name), line)
        match_string = re_match.groups(1)[0]
        keys = [match.strip() for match in match_string.split(',') if match]

    return dict(zip(keys, args))


name = "king"
age = 28
gender = "男"
convert_vars_to_dict = varname(name, age, gender)
print(convert_vars_to_dict)

 :inspect 学习文档

替换 实例 方法

当你想对类实例的方法进行替换时,你可能想到的是直接对他进行粗暴地替换

class People:
    def speak(self):
        print("hello, world")


def speak(self):
    print("hello, python")

p = People()
p.speak = speak
p.speak()

执行这段代码的时候,就会发现行不通,它提示我们要传入 self 参数。这么替换,speak 就变成了一个 function,而不是一个和实例绑定的 method ,可以把替换前后的 speak 打印出来

p = People()
print(p.speak)
p.speak = speak
print(p.speak)

使用 实例的类

p.class.speak = speak
p.__class__.speak = speak

利用 types 绑定方法。这种方法,最为安全,不会影响其他实例。是官方推荐的一种做法。

在 types 中有一个 MethodType,可以将普通方法与实例进行绑定。绑定后,就可以直接替换掉原实例的 speak 方法了,完整代码如下:

import types


class People:
    def speak(self):
        print("hello, world")


def speak(self):
    print("hello, python")


p = People()
p.speak = types.MethodType(speak, p)
p.speak()

动态 创建 函数

在下面的代码中,每一次 for 循环都会创建一个返回特定字符串的函数。

from types import FunctionType

for name in ("world", "python"):
    func = FunctionType(compile(
        ("def hello():\n"
         "    return '{}'".format(name)),
        "<string>",
        "exec").co_consts[0], globals())

    print(func())

调用类的私有方法

类中可供直接调用的方法,只有公有方法(protected类型的方法也可以,但是不建议)。也就是说,类的私有方法是无法直接调用的。

class Kls():
    def public(self):
        print('Hello public world!')

    def __private(self):
        print('Hello private world!')

    def call_private(self):
        self.__private()

ins = Kls()

# 调用公有方法,没问题
ins.public()

# 直接调用私有方法,不行
ins.__private()

# 但你可以通过内部公有方法,进行代理
ins.call_private()

# 调用私有方法,以下两种等价,这里只是普及,不推荐
ins._Kls__private()
ins.call_private()

利用 any 代替 for 循环

在某些场景下,我们需要判断是否满足某一组集合中任意一个条件,这时候,很多同学自然会想到使用 for 循环。

found = False
for thing in things:
    if thing == other_thing:
        found = True
        break

其实更好的写法,是使用 any() 函数,能够使这段代码变得更加清晰、简洁

found = any(thing == other_thing for thing in things)

使用 any 并不会减少 for 循环的次数,只要有一个条件为 True,any 就能得到结果。

理,当你需要判断是否满足某一组集合中所有条件,也可以使用 all() 函数。

found = all(thing == other_thing for thing in things)

只要有一个不满足条件,all 函数的结果就会立刻返回 False

4、Python 中文指南

Contents:

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值