主页»Python»改进 Python 程序的 91 个主张(四)

改进 Python 程序的 91 个主张(四)

来历:驭风者 发布时刻:2017-05-17 阅览次数:

第 6 章 内部机制

主张 61:运用愈加安全的 property

property 实践上是一种完结了 __get__() 、 __set__() 办法的类,用户也能够依据自己的需求界说个性化的 property,其实质是一种特别的数据描绘符(数据描绘符:假如一个方针一起界说了 __get__() 和 __set__() 办法,则称为数据描绘符,假如仅界说了__get__() 办法,则称为非数据描绘符)。它和一般描绘符的差异在于:一般描绘符供给的是一种较为初级的操控特色拜访的机制,而 property 是它的高档运用,它以规范库的办法供给描绘符的完结,其签名办法为:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

property 有两种常用的办法:

1、第一种办法

class Some_Class(object):
    def __init__(self):
        self._somevalue = 0
    def get_value(self):
        print('calling get method to return value')
        return self._somevalue
    def set_value(self, value):
        print('calling set method to set value')
        self._somevalue = value
    def def_attr(self):
        print('calling delete method to delete value')
        def self._somevalue
    x = property(get_value, set_value, del_attr, "I'm the 'x' property.")
obj = Some_Class()
obj.x = 10
print(obj.x + 2)
del obj.x
obj.x

2、第二种办法

class Some_Class(self):
    _x = None
    def __init__(self):
        self._x = None
    @property
    def x(self):
        print('calling get method to return value')
        return self._x
    @x.setter
    def x(self, value):
        print('calling set method to set value')
        self._x = value
    @x.deleter
    def x(self):
        print('calling delete method to delete value')
        del self._x

以上咱们能够总结出 property 的优势:

1、代码更简练,可读性更强

2、更好的办理特色的拜访。property 将对特色的拜访直接转化为对对应的 get、set 等相关函数的调用,特色能够更好地被操控和办理,常见的运用场景如设置校验(如检查电子邮件地址是否合法)、检查赋值的规模(某个变量的赋值规模必须在 0 到 10 之间)以及对某个特色进行二次核算之后再回来给用户(将 RGB 办法表明的色彩转化为#******)或许核算某个依靠于其他特色的特色。

class Date(object):
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    def get_date(self):
        return self.year + '-' + self.month + '-' + self.day
    def set_date(self, date_as_string):
        year, month, day = date_as_string.split('-')
        if not (2000 <= year <= 2017 and 0 <= month <= 12 and 0 <= day <= 31):
            print('year should be in [2000:2017]')
            print('month should be in [0:12]')
            print('day should be in [0, 31]')
            raise AssertionError
        self.year = year
        self.month = month
        self.day = day
    date = property(get_date, set_date)

创立一个 property 实践上便是将其特色的拜访与特定的函数相关起来,相关于规范特色的拜访,property 的作用相当于一个分发器,对某个特色的拜访并不直接操作详细的方针,而对规范特色的拜访没有中心这一层,直接拜访存储特色的方针:

3、代码可保护性更好。property 对特色进行再包装,以相似于接口的办法呈现给用户,以一致的语法来拜访特色,当详细完结需求改动的时分,拜访的办法依然能够坚持一致。

4、操控特色拜访权限,进步数据安全性。假如用户想设置某个特色为只读,来看看 property 是怎么完结的。

class PropertyTest(object):
    def __init__(self):
        self.__var1 = 20
    @property
    def x(self):
        return self.__var1
pt = PropertyTest()
print(pt.x)
pt.x = 12

留意这样运用 property 并不能真实含义到达特色只读的意图,正如以双下划线指令的变量并不是真实的私有变量相同,咱们仍是能够经过pt._PropertyTest__var1 = 30来批改特色。稍后咱们会谈论怎么完结真实含义上的只读和私有变量。

已然 property 实质是特别类,那么就能够被承继,咱们就能够自界说 property:

def update_meta(self, other):
    self.__name__ = other.__name__
    self.__doc__ = other.__doc__
    self.__dict__.update(other.__dict__)
    return self
class UserProperty(property):
    def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
        if fget is not None:
            def __get__(obj, objtype=None, name=fget.__name__):
                fegt = getattr(obj, name)
                return fget()
            fget = update_meta(__get__, fget)
        if fset is not None:
            def __set__(obj, value, name=fset.__name__):
                fset = getattr(obj, name)
                return fset(value)
            fset = update_meta(__set__, fset)
        if fdel is not None:
            def __delete__(obj, name=fdel.__name__):
                fdel = getattr(obj, name)
                return fdel()
            fdel = update_meta(__delete__, fdel)
        return property(fget, fset, fdel, doc)
class C(object):
    def get(self):
        return self._x
    def set(self, x):
        self._x = x
    def delete(self):
        del self._x
    x = UserProperty(get, set, delete)
c = C()
c.x = 1
print(c.x)
def c.x

UserProperty 承继自 property,其结构函数 __new__(cls, fget=None, fset=None, fdel=None, doc=None) 中从头界说了 fget() 、 fset() 以及 fdel() 办法以满意用户特定的需求,最终回来的方针实践仍是 property 的实例,因而用户能够像运用 property 相同运用 UserProperty。

运用 property 并不能真实彻底到达特色只读的意图,用户依然能够绕过阻止来批改动量。咱们来看看一个可行的完结:

def ro_property(obj, name, value):
    setattr(obj.__class__, name, property(lambda obj: obj.__dict__["__" + name]))
    setattr(obj, "__" + name, value)

class ROClass(object):
    def __init__(self, name, available):
        ro_property(self, "name", name)
        self.available = available

a = ROClass("read only", True)
print(a.name)
a._Article__name = "modify"
print(a.__dict__)
print(ROClass.__dict__)
print(a.name)

主张 62:把握 metaclass

关于元类这知识点,引荐stackoverflow上Jerub答复

这儿有中文翻译

主张 63:了解 Python 方针协议

因为 Python 是一门动态言语,Duck Typing 的概念遍及其间,所以其间的 Concept 并不以类型的束缚为载体,而别的运用称为协议的概念。

In [1]: class Object(object):
   ...:     def __str__(self):
   ...:         print('calling __str__')
   ...:         return super(Object, self).__str__()
   ...:     

In [2]: o = Object()

In [3]: print('%s' % o)
calling __str__
<__main__.Object object at 0x7f133ff20160>

比方在字符串格局化中,假如有占位符 %s,那么依照字符串转化的协议,Python 会主动地调用相应方针的 __str__() 办法。

总结一下 Python 中的协议:

1、类型转化协议:__str__() 、__repr__()、__init__()、__long__()、__float__()、__nonzero__() 等。

2、比较巨细的协议:__cmp__(),当两者持平时,回来 0,当 self < other 时回来负值,反之回来正值。一起 Python 又有 __eq__()、__ne__()、__lt__()、__gt__() 等办法来完结持平、不等、小于和大于的断定。这也便是 Python 对 ==、!=、< 和 > 等操作符的进行重载的支撑机制。

3、数值相关的协议:

其间有个 Python 中特有的概念:反运算。以something + other为例,调用的是something的__add__(),若没有界说__add__(),这时分 Python 有一个反运算的协议,检查other有没有__radd__(),假如有,则以something为参数调用。

4、容器类型协议:容器的协议是十分粗浅的,已然为容器,那么必定要有协议查询内含多少方针,在 Python 中,便是要支撑内置函数 len(),经过 __len__() 来完结,一望而知。而 __getitem__()、__setitem__()、__delitem__() 则对应读、写和删去,也很好了解。__iter__() 完结了迭代器协议,而 __reversed__() 则供给对内置函数 reversed() 的支撑。容器类型中最有特征的是对成员联系的判别符 in 和 not in 的支撑,这个办法叫 __contains__(),只需支撑这个函数就能够运用 in 和 not in 运算符了。

5、可调用方针协议:所谓可调用方针,即相似函数方针,能够让类实例体现得像函数相同,这样就能够让每一个函数调用都有所不同。

In [1]: class Functor(object):
   ...:     def __init__(self, context):
   ...:         self._context = context
   ...:     def __call__(self):
   ...:         print('do something with %s' % self._context)
   ...:         

In [2]: lai_functor = Functor('lai')

In [3]: yong_functor = Functor('yong')

In [4]: lai_functor()
do something with lai

In [5]: yong_functor()
do something with yong

6、还有一个可哈希方针,它是经过 __hash__() 办法来支撑 hash() 这个内置函数的,这在创立自己的类型时十分有用,因为只需支撑可哈希协议的类型才干作为 dict 的键类型(不过只需承继自 object 的新式类就默许支撑了)。

7、上下文办理器协议:也便是对with句子的支撑,该协议经过__enter__()和__exit__()两个办法来完结对资源的整理,保证资源不管在什么状况下都会正常整理:

class Closer:
    def __init__(self):
        self.obj = obj
    def __enter__(self):
        return self.obj
    def __exit__(self, exception_type, exception_val, trace):
        try:
            self.obj.close()
        except AttributeError:
            print('Not closeable.')
            return True

这儿 Closer 相似的类现已在规范库中存在,便是 contextlib 里的 closing。

以上便是常用的方针协议,灵敏地用这些协议,咱们能够写出更为 Pythonic 的代码,它更像是声明,没有言语上的束缚,需求咱们一起恪守。

主张 64:运用操作符重载完结中缀语法

了解 Shell 脚本编程应该了解|管道符号,用以衔接两个程序的输入输出。如按字母表反序遍历当时目录的文件与子目录:

$ ls | sort -r
Videos/
Templates/
Public/
Pictures/
Music/
examples.desktop
Dropbox/
Downloads/
Documents/
Desktop/

管道的处理十分明晰,因为它是中缀语法。而咱们常用的 Python 是前缀语法,比方相似的 Python 代码应该是 sort(ls(), reverse=True)。

Julien Palard 开发了一个 pipe 库,运用|来简化代码,也便是重载了 __ror__() 办法:

class Pipe:
    def __init__(self, function):
        self.function = function
    def __ror__(self, other):
        return self.function(other)
    def __call__(self, *args, **kwargs):
        return Pipe(lambda x: self.function(x, *args, **kwargs))

这个 Pipe 类能够当成函数的 decorator 来运用。比方在列表中挑选数据:

@Pipe
def where(iterable, predicate):
    return (x for x in iterable if (predicate(x)))

pipe 库内置了一堆这样的处理函数,比方 sum、select、where 等函数尽在其间,请看以下代码:

fib() | take_while(lambda x: x < 1000000) \
      | where(lambda x: x % 2) \
      | select(lambda x: x * x) \
      | sum()

这样写的代码,含义是不是一望而知呢?便是找出小于 1000000 的斐波那契数,并核算其间的偶数的平方之和。

咱们能够运用pip3 install pipe装置,装置完后测验:

In [1]: from pipe import *

In [2]: [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | select(lambda x: x * x) | add
Out[2]: 34

此外,pipe 是慵懒求值的,所以咱们彻底能够弄一个无量生成器而不必忧虑内存被用完:

In [3]: def fib():
   ...:     a, b = 0, 1
   ...:     while True:
   ...:         yield a
   ...:         a, b = b, a + b
   ...:         

In [4]: euler2 = fib() | where(lambda x: x % 2 ==0) | take_while(lambda x: x < 400000) | add

In [5]: euler2
Out[5]: 257114

读取文件,核算文件中每个单词呈现的次数,然后依照次数从高到低对单词排序:

from __future__ import print_function
from re import split
from pipe import *
with open("test_descriptor.py") as f:
    print(f.read()
          | Pipe(lambda x: split("/W+", x))
          | Pipe(lambda x:(i for i in x if i.strip()))
          | groupby(lambda x:x)
          | select(lambda x:(x[0], (x[1] | count)))
          | sort(key=lambda x: x[1], reverse=True)
          )

主张 65:了解 Python 的迭代器协议

首要介绍一下 iter() 函数,iter() 能够输入两个实参,为了简化,第二个可选参数能够疏忽。iter() 函数回来一个迭代器方针,承受的参数是一个完结了 __iter__() 办法的容器或迭代器(准确来说,还支撑仅有 __getitem__() 办法的容器)。关于容器而言,__iter__() 办法回来一个迭代器方针,而对迭代器而言,它的 __iter__() 办法回来其自身。

所谓协议,是一种松懈的约好,并没有相应的接口界说,所以把协议简略归纳如下:

  1. 完结 __iter__() 办法,回来一个迭代器

  2. 完结 next() 办法,回来当时的元素,并指向下一个元素的方位,假如当时方位已无元素,则抛出 StopIteration 反常

没错,其实 for 句子便是对获取容器的迭代器、调用迭代器的 next() 办法以及对 StopIteration 进行处理等流程进行封装的语法糖(相似的语法糖还有 in/not in 句子)。

迭代器最大的优点是界说了一致的拜访容器(或调集)的一致接口,所以程序员能够随时界说自己的迭代器,只需完结了迭代器协议即可。除此之外,迭代器还有慵懒求值的特性,它仅能够在迭代至当时元素时才核算(或读取)该元素的值,在此之前能够不存在,在此之后也能够毁掉,也便是说不需求在遍历之前完结预备好整个迭代进程中的一切元素,所以十分适宜遍历无量个元素的调集或或巨大的事物(斐波那契数列、文件):

class Fib(object):
    def __init__(self):
        self._a, self._b = 0, 1
    def __iter__(self):
        return self
    def next(self):
        self._a, self._b = self._b, self._a + self._b
        return self._a
for i, f in enumerate(Fib()):
    print(f)
    if i > 10:
        break

下面来看看与迭代有关的规范库 itertools。

itertools 的方针是供给一系列核算快速、内存高效的函数,这些函数能够独自运用,也能够进行组合,这个模块受到了 Haskell 等函数式编程言语的启示,所以很多运用 itertools 模块中的函数的代码,看起来有点像函数式编程言语。比方 sum(imap(operator.mul, vector1, vector2)) 能够用来运转两个向量的对应元素乘积之和。

itertools 供给了以下几个有用的函数:chain() 用以一起接连地迭代多个序列;compress()、dropwhile() 和 takewhile() 能用遴选序列元素;tee() 就像同名的 UNIX 运用程序,对序列作 n 次迭代;而 groupby 的作用相似 SQL 中相同拼写的要害字所带的作用。

[k for k, g in groupby("AAAABBBCCDAABB")] --> A B C D A B
[list(g) for k, g in groupby("AAAABBBCCD")] --> AAAA BBB CC D

除了这些针对有限元素的迭代协助函数之外,还有 count()、cycle()、repeat() 等函数发生无量序列,这 3 个函数就别离能够发生算术递加数列、无限重复实参的序列和重复发生同一个值的序列。

组合函数含义product()核算 m 个序列的 n 次笛卡尔积permutations()发生全摆放combinations()发生无重复元素的组合combinations_with_replacement()发生有重复元素的组合

In [1]: from itertools import *

In [2]: list(product('ABCD', repeat=2))
Out[2]: 
[('A', 'A'),
 ('A', 'B'),
 ('A', 'C'),
 ('A', 'D'),
 ('B', 'A'),
 ('B', 'B'),
 ('B', 'C'),
 ('B', 'D'),
 ('C', 'A'),
 ('C', 'B'),
 ('C', 'C'),
 ('C', 'D'),
 ('D', 'A'),
 ('D', 'B'),
 ('D', 'C'),
 ('D', 'D')]
# 其间 product() 能够承受多个序列
In [5]: for i in product('ABC', '123', repeat=2):
   ...:     print(''.join(i))
   ...:     
A1A1
A1A2
A1A3
A1B1
A1B2
A1B3
A1C1
A1C2
...

主张 66:了解 Python 的生成器

生成器,望文生义,便是按必定的算法生成一个序列。

迭代器尽管在某些场景体现得像生成器,但它绝非生成器;反而是生成器完结了迭代器协议的,能够在必定程度上看作迭代器。

假如一个函数,运用了 yield 要害字,那么它便是一个生成器函数。当调用生成器函数时,它回来一个迭代器,不过这个迭代器是以生成器方针的办法呈现的:

In [1]: def fib(n):
   ...:     a, b = 0, 1
   ...:     while a < n:
   ...:         yield a
   ...:         a, b = b, a + b
   ...: for i, f in enumerate(fib(10)):
   ...:     print(f)
   ...:     
0
1
1
2
3
5
8

In [2]: f = fib(10)

In [3]: type(f)
Out[3]: generator

In [4]: dir(f)
Out[4]: 
['__class__',
 '__del__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__iter__',
 '__le__',
 '__lt__',
 '__name__',
 '__ne__',
 '__new__',
 '__next__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'gi_yieldfrom',
 'send',
 'throw']

能够看到它回来的是一个 generator 类型的方针,这个方针带有__iter__()和__next__()办法,可见的确是一个迭代器。

剖析:

  1. 每一个生成器函数调用之后,它的函数并不履行,而是到第一次调用 next() 的时分才开端履行;

  2. yield 表达式的默许回来值为 None,当第一次调用 next() 办法时,生成器函数开端履行,履行到 yield 表达式间断;

  3. 再次调用next()办法,函数将在前次间断的当地持续履行。

send() 是全功用版别的 next(),或许说 next() 是 send()的快捷办法,相当于 send(None)。还记得 yield 表达式有一个回来值吗?send() 办法的作用便是操控这个回来值,使得 yield 表达式的回来值是它的实参。

除了能 yield 表达式的“回来值”之外,也能够让它抛出反常,这便是 throw() 办法的才能。

关于惯例事务逻辑的代码来说,对特定的反常有很好的处理(比方将反常信息写入日志后高雅的回来),然后完结从外部影响生成器内部的操控流。

当调用 close() 办法时,yield 表达式就抛出 GeneratorExit 反常,生成器方针会自行处理这个反常。当调用 close() 办法,再次调用 next()、send() 会使生成器方针抛出 StopIteration 反常。换言之,这个生成器方针现已不再可用。当生成器方针被 GC 收回时,会主动调用 close()。

生成器还有两个很棒的用途:

  • 完结 with 句子的上下文办理协议,运用的是调用生成器函数时函数体并不履行,当第一次调用 next() 办法时才开端履行,并履行到 yield 表达式后间断,直到下一次调用 next() 办法这个特性;

  • 完结协程,运用的是 send()、throw()、close() 等特性。

第二个用途鄙人一个末节解说,先看第一个:

In [1]: with open('/tmp/test.txt', 'w') as f:
   ...:     f.write('Hello, context manager.')
   ...:     

In [2]: from contextlib import contextmanager

In [3]: @contextmanager
   ...: def tag(name):
   ...:     print('<%s>' % name)
   ...:     yield
   ...:     print('<%s>' % name)
   ...:     

In [4]: with tag('h1'):
   ...:     print('foo')
   ...:     
<h1>
foo
<h1>

这是 Python 文档中的比方。经过 contextmanager 对 next()、throw()、close() 的封装,yield 大大简化了上下文办理器的编程杂乱度,对进步代码可保护性有着极大的含义。除此之外,yield 和 contextmanager 也能够用以“池”办法中对资源的办理和收回,详细的完结留给咱们去考虑。

主张 67:依据生成器的协程及 greenlet

先介绍一下协程的概念:

协程,又称微线程和纤程等,听说源于 Simula 和 Modula-2 言语,现代编程言语根本上都支撑这个特性,比方 Lua 和 ruby 都有相似的概念。

协程往往完结在言语的运转时库或虚拟机中,操作体系对其存在一窍不通,所以又被称为用户空间线程或绿色线程。又因为大部分协程的完结是协作式而非抢占式的,需求用户自己去调度,所以一般无法运用多核,但用来履行协作式多任务十分适宜。用协程来做的东西,用线程或进程一般也是相同能够做的,但往往多了许多加锁和通讯的操作。

依据出产着顾客模型,比较抢占式多线程编程完结和协程编程完结。线程完结至少有两点硬伤:

  • 对行列的操作需求有显式/隐式(运用线程安全的行列)的加锁操作。

  • 顾客线程还要经过 sleep 把 CPU 资源当令地“推让”给出产者线程运用,其间的当令是多久,根本上只能静态地运用经历,作用往往不尽善尽美。

下面来看看协程的处理方案,代码来自廖雪峰 Python3 教程

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

履行成果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

留意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首要调用c.send(None)发动生成器;

  2. 然后,一旦出产了东西,经过c.send(n)切换到consumer履行;

  3. consumer经过yield拿到音讯,处理,又经过yield把成果传回;

  4. produce拿到consumer处理的成果,持续出产下一条音讯;

  5. produce决议不出产了,经过c.close()封闭consumer,整个进程完毕。

整个流程无锁,由一个线程履行,produce和consumer协作完结任务,所以称为“协程”,而非线程的抢占式多任务。

最终套用Donald Knuth的一句话总结协程的特色:

“子程序便是协程的一种特例。”

greenlet 是一个 C 言语编写的程序库,它与 yield 要害字没有亲近的联系。greenlet 这个库里最为要害的一个类型便是 PyGreenlet 方针,它是一个 C 结构体,每一个 PyGreenlet 都能够看到一个调用栈,从它的进口函数开端,一切的代码都在这个调用栈上运转。它能够随时记载代码运转现场,并随时刻断,以及康复。它跟 yield 所能够做到的相似,但更好的是它供给从一个 PyGreenlet 切换到另一个 PyGreenlet 的机制。

from greenlet import greenlet
def test1():
    print(12)
    gr2.switch()
    print(34)
def test2():
    print(56)
    gr1.switch()
    print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

协程尽管不能充分运用多核,但它跟异步 I/O 结合起来今后编写 I/O 密集型运用十分简略,能够在同步的代码外表下完结异步的履行,其间的代表当属将 greenlet 与 libevent/libev 结合起来的 gevent 程序库,它是 Python 网络编程库。最终,以 gevent 并发查询 DNS 的比方为例,运用它进行并发查询 n 个域名,能够取得简直 n 倍的功用提高:

In [1]: import gevent

In [2]: from gevent import socket

In [3]: urls = ['www.baidu.com', 'www.python.org', 'www.qq.com']

In [4]: jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]

In [5]: gevent.joinall(jobs, timeout=2)
Out[5]: 
[<Greenlet at 0x7f37e439c508>,
 <Greenlet at 0x7f37e439c5a0>,
 <Greenlet at 0x7f37e439c340>]

In [6]: [job.value for job in jobs]
Out[6]: ['115.239.211.112', '151.101.24.223', '182.254.34.74']

主张 68:了解 GIL 的限制性

多线程 Python 程序运转的速度比只需一个线程的时分还要慢,除了程序自身的并行性之外,很大程度上与 GIL 有关。因为 GIL 的存在,多线程编程在 Python 中并不抱负。GIL 被称为大局解说器锁(Global Interpreter Lock),是 Python 虚拟机上用作互斥线程的一种机制,它的作用是保证任何状况下虚拟机中只会有一个线程被运转,而其他线程都处于等候 GIL 锁被开释的状况。不管是在单核体系仍是多核体系中,一直只需一个取得了 GIL 锁的线程在运转,每次遇到 I/O 操作便会进行 GIL 锁的开释。

但假如是纯核算的程序,没有I/O操作,解说器则会依据sys.setcheckinterval的设置来主动进行线程间的切换,默许是每隔100个内部时钟就会开释GIL锁然后轮换到其他线程:

在单核 CPU 中,GIL 对多线程的履行并没有太大影响,因为单核上的多线程实质上便是次序履行的。但关于多核 CPU,多线程并不能真实发挥优势带来功率上显着的提高,甚至在频频 I/O 操作的状况下因为存在需求屡次开释和请求 GIL 的景象,功率反而会下降。

那么 Python 解说器为什么要引进 GIL 呢?

咱们知道 Python 中方针的办理与引证计数器亲近相关,当计数器变为 0 的时分,该方针便会被废物收回器收回。当吊销一个方针的引证时,Python 解说器对方针以及其计数器的办理分为以下两步:

  1. 使引证计数值减1

  2. 判别该计数值是否为 0,假如为0,则毁掉该方针

鉴于此,Python 引进了 GIL,以保证对虚拟机内部同享资源拜访的互斥性。

GIL 的引进的确使得多线程不能再多核体系中发挥优势,但它也带来了一些优点:大大简化了 Python 线程中同享资源的办理,在单核 CPU 上,因为其实质是次序履行的,一般状况下多线程能够取得较好的功用。此外,关于扩展的 C 程序的外部调用,即便其不是线程安全的,但因为 GIL 的存在,线程会堵塞直到外部调用函数回来,线程安全不再是一个问题。

在 Python3.2 中从头完结了 GIL,其完结机制首要会集在两个方面:一方面是运用固定的时刻而不是固定数量的操作指令来进行线程的强制切换;另一个方面是在线程开释 GIL 后,开端等候,直到某个其他线程获取 GIL 后,再开端测验去获取 GIL,这样尽管能够防止此前取得 GIL 的线程,不会当即再次获取 GIL,但依然无法保证优先级高的线程优先获取 GIL。这种办法只能处理部分问题,并未改动 GIL 的实质。

Python 供给了其他办法能够绕过 GIL 的限制,比方运用多进程 multiprocess 模块或许选用 C 言语扩展的办法,以及经过 ctypes 和 C 动态库来充分运用物理内核的核算才能。

主张 69:方针的办理与废物收回

class Leak(object):
    def __init__(self):
        print('object with id %d was born' % id(self))
while(True):
    A = Leak()
    B = Leak()
    A.b = B
    B.a = A
    A = None
    B = None

运转上述程序,咱们会发现 Python 占用的内存耗费一直在持续增长,直到最终内存耗光。

先简略谈谈 Python 中的内存办理的办法:

Python 运用引证计数器(Reference counting)的办法来办理内存中的方针,即针对每一个方针保护一个引证计数值来表明该方针当时有多少个引证。

当其他方针引证该方针时,其引证计数会增加 1,而删去一个队当时方针的引证,其引证计数会减 1。只需当引证计数的值为 0 时的时分该方针才会被废物搜集器收回,因为它表明这个方针不再被其他方针引证,是个不可达方针。引证计数算法最显着的缺点是无法处理循环引证的问题,即两个方针彼此引证。好像上述代码中A、B方针之间彼此循环引证形成了内存走漏,因为两个方针的引证计数都不为 0,该方针也不会被废物收回器收回,而无限循环导致一直在请求内存而没有开释。

循环引证常常会在列表、元组、字典、实例以及函数运用时呈现。关于由循环引证而导致的内存走漏的状况,能够运用 Python 自带的一个 gc 模块,它能够用来盯梢方针的“入引证(incoming reference)“和”出引证(outgoing reference)”,并找出杂乱数据结构之间的循环引证,一起收回内存废物。有两种办法能够触发废物收回:一种是经过显式地调用 gc.collect() 进行废物收回;还有一种是在创立新的方针为其分配内存的时分,检查 threshold 阈值,当方针的数量超越 threshold 的时分便主动进行废物收回。默许状况下阈值设为(700,10,10),而且 gc 的主动收回功用是敞开的,这些能够经过 gc.isenabled() 检查:

In [1]: import gc

In [2]: print(gc.isenabled())
True

In [3]: gc.isenabled()
Out[3]: True

In [4]: gc.get_threshold()
Out[4]: (700, 10, 10)

所以批改之前的代码:

def main():
    collected = gc.collect()
    print("Garbage collector before running: collected {} objects.".format(collected))
    print("Creating reference cycles...")
    A = Leak()
    B = Leak()
    A.b = B
    B.a = A
    A = None
    B = None
    collected = gc.collect()
    print(gc.garbage)
    print("Garbage collector after running: collected {} objects".format(collected))

if __name__ == "__main__":
    ret = main()
    sys.exit(ret)

gc.garbage 回来的是因为循环引证而发生的不可达的废物方针的列表,输出为空表明内存中此刻不存在废物方针。gc.collect() 显现一切搜集和毁掉的方针的数目,此处为 4(2 个方针 A、B,以及其实例特色 dict)。

咱们再来考虑一个问题:假如在类 Leak 中增加析构办法 __del__(),会发现 gc.garbage 的输出不再为空,而是方针 A、B 的内存地址,也便是说这两个方针在内存中依然以“废物”的办法存在。

这是什么原因呢?实践受骗存在循环引证而且当这个环中存在多个析构办法时,废物收回器不能确认方针析构的次序,所以为了安全起见依然坚持这些方针不被毁掉。而当环被打破时,gc 在收回方针的时分便会再次主动调用 __del__() 办法。

gc 模块一起支撑 DEBUG 办法,当设置 DEBUG 办法之后,关于循环引证形成的内存走漏,gc 并不开释内存,而是输出更为详细的确诊信息为发现内存走漏供给便当,然后便利程序员进行批改。更多 gc 模块能够参阅文档

第 7 章 运用东西辅佐项目开发

Python 项意图开发进程,其实便是一个或多个包的开发进程,而这个开发进程又由包的装置、办理、测验和发布等多个节点构成,所以这是一个杂乱的进程,运用东西进行辅佐开发有利于削减流程损耗,提高出产力。本章将介绍几个常用的、先进的东西,比方 setuptools、pip、paster、nose 和 Flask-PyPI-Proxy 等。

主张 70:从 PyPI 装置包

PyPI 全称 Python Package Index,直译过来便是“Python 包索引”,它是 Python 编程言语的软件库房,相似 Perl 的 CPAN 或 Ruby 的 Gems。

$ tar zxvf requests-1.2.3.tar.gz
$ cd requests-1.2.3
$ python setup.py install
$ sudo aptitude install python-setuptools   # 主动装置包

主张 71:运用 pip 和 yolk 装置、办理包

pip 常用指令:

$ pip install package_name
$ pip uninstall package_name
$ pip show package_name
$ pip freeze

主张 72:做 paster 创立包

distutils 规范库,至少供给了以下几方面的内容:

  • 支撑包的构建、装置、发布(打包)

  • 支撑 PyPI 的挂号、上传

  • 界说了扩展指令的协议,包括 distutils.cmd.Command 基类、distutils.commands 和 distutils.key_words 等进口点,为 setuptools 和 pip 等供给了基础设施。

要运用 distutils,按习气需求编写一个 setup.py 文件,作为后续操作的进口点。在arithmetic.py同层目录下树立一个setup.py文件,内容如下:

from distutils.core import setup
setup(name="arithmetic",
     version='1.0',
     py_modules=["your_script_name"],
     )

setup.py 文件的含义是履行时调用 distutils.core.setup() 函数,而实参是经过命名参数指定的。name 参数指定的是包名;version 指定的是版别;而 py_modules 参数是一个序列类型,里边包括需求装置的 Python 文件。

编写好 setup.py 文件今后,就能够运用 python setup.py install 进行装置了。

distutils 还带有其他指令,能够经过 python setup.py --help-commands 进行查询。

实践上若要把包提交到 PyPI,还要遵从 PEP241,给出足够多的元数据才行,比方对包的简略描绘、详细描绘、作者、作者邮箱、主页和授权办法等:

setup(
    name='requests',
    version=requests.__version__,
    description='Python HTTP for Humans.',
    long_description=open('README.rst').read() + '\n\n' +
    				open('HISTORY.rst').read(),
    author='Kenneth Reitz',
    author_email='[email protected]',
    url='http://python-requests.org',
    packages=packages,
    package_data={'': ['LICENSE', 'NOTICE'], 'requests': ['*.pem']},
    package_dir={'requests': 'requests'},
    include_package_data=True,
    install_requires=requires,
    license=open('LICENSE').read(),
    zip_safe=False,
    classifiers=(
    	'Development Status :: 5 - Production/Stable',
        'Intended Audience :: Developers',
        'Natural Language :: English',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2.6',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.3',
        ),
)

包括太多内容了,假如每一个项目都手写很困难,最好找一个东西能够主动创立项意图 setup.py 文件以及相关的装备、目录等。Python 中做这种事的东西有好几个,做得最好的是 pastescript。pastescript 是一个有着杰出插件机制的指令行东西,装置今后就能够运用 paster 指令,创立适用于 setuptools 的包文件结构。

装置好 pastescript 今后能够看到它注册了一个指令行进口 paster:

$ paster create --list-template     # 查询目录装置的模板
$ paster create -o arithmethc-2 -t basic_package atithmetic     # 为了 atithmetic 生成项目包

简略地填写几个问题今后,paster 就在 arithmetic-2 目录生成了名为 arithmetic 的包项目。

用上 --config 参数,它是一个相似 ini 文件格局的装备文件,能够在里边填好各个模板变量的值(查询模板有哪些变量用 --list-variables参数),然后就能够运用了。

[pastescript]
description = corp-prj
license_name = 
keywords = Python
long_description = corp-prj
author = xxx corp
author_email = [email protected]
url = http://example.com
version = 0.0.1

以上装备文件运用paster create -t basic_package --config="corp-prj-setup.cfg" arithmetic

主张 73:了解单元测验概念

单元测验用来验证程序单元的正确性,一般由开发人员完结,是测验进程的第一个环节,以保证缩写的代码契合软件需求和遵从开发方针。好的单元测验有以下优点:

  • 削减了潜在 bug,进步了代码的质量。

  • 大大减缩软件批改的本钱

  • 为集成测验供给根本保障

有用的单元测验应该从以下几个方面考虑:

  • 测验先行,遵从单元测验进程:

    • 创立测验方案(Test Plan)

    • 编写测验用例,预备测验数据

    • 编写测验脚本

    • 编写被测代码,在代码完结之后履行测验脚本

    • 批改代码缺点,从头测验直到代码可承受间断

  • 遵从单元测验根本原则:

    • 一致性:防止currenttime = time.localtime()这种不确认履行成果的句子

    • 原子性:履行成果只需 True 或 False 两种

    • 单一责任:测验应该依据情形(scenario)和行为,而不是办法。假如一个办法对应着多种行为,应该有多个测验用例;而一个行为即便对应多个办法也只能有一个测验用例

    • 阻隔性:不能依靠于详细的环境设置,如数据库的拜访、环境变量的设置、体系的时刻等;也不能依靠于其他的测验用例以及测验履行的次序,而且无条件逻辑依靠。单元测验的一切输入应该是确认的,办法的行为和结构应是能够猜测的。

  • 运用单元测验结构,在单元测验方面常见的测验结构有 PyUnit 等,它是 JUnit 的 Python 版别,在 Python2.1 之前需求独自装置,在 Python2.1 之后它成为了一个规范库,名为 unittest。它支撑单元测验主动化,能够同享地进行测验环境的设置和整理,支撑测验用例的调集以及独立的测验陈述结构。unittest 相关的概念首要有以下 4 个:

    • 测验固件(test fixtures):测验相关的预备作业和整理作业,依据类 TestCase 创立测验固件的时分一般需求从头完结 setUp() 和 tearDown() 办法。当界说了这些办法的时分,测验运转器会在运转测验之前和之后别离调用这两个办法

    • 测验用例(test case):最小的测验单元,一般依据 TestCase 构建

    • 测验用例集(test suite):测验用例的调集,运用 TestSuite 类来完结,除了能够包括 TestCase 外,也能够包括 TestSuite

    • 测验运转器(test runner):操控和驱动整个单元测验进程,一般运用 TestRunner 类作为测验用例的根本履行环境,常用的运转器为 TextTestRunner,它是 TestRunner 的子类,以文字办法运转测验并陈述成果。

# 测验以下类
class MyCal(object):
    def add(self, a, b):
        return a + b
    def sub(self, a, b):
        return a - b
# 测验
class MyCalTest(unittest.TestCase):
    def setUp(self):
        print('running set up')
    def tearDown(self):
        print('running teardown')
        self.mycal = None
    def testAdd(self):
        self.assertEqual(self.mycal.add(-1, 7), 6)
    def testSub(self):
        self.assertEqual(self.mycal.sub(10, 2), 8)
suite = unittest.TestSuite()
suite.addTest(MyCalTest("testAdd"))
suite.addTest(MyCalTest("testSub"))
runner = unittest.TextTestRunner()
runner.run(suite)

运转 python3 -m unittest -v MyCalTest 得到测验成果。

QQ群:凯发娱乐官网官方群(515171538),验证音讯:10000
微信群:加小编微信 849023636 邀请您参加,验证音讯:10000
提示:更多精彩内容重视微信大众号:全栈开发者中心(fsder-com)
m88 188bet uedbet 威廉希尔 明升 bwin 明升88 bodog bwin 明升m88.com 18luck 188bet unibet unibet Ladbrokes Ladbrokes casino m88明升 明升 明升 m88.com 188bet m88 明陞 uedbet赫塔菲官网 365bet官网 m88 help
网友谈论(共0条谈论) 正在载入谈论......
沉着谈论文明上网,回绝歹意咒骂 宣布谈论 / 共0条谈论
登录会员中心