SegmentFault 编程进阶之路最新的文章
2022-09-18T23:23:20+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
关于 php-fpm reload 会取消正在处理的请求的解决方案
https://segmentfault.com/a/1190000042507650
2022-09-18T23:23:20+08:00
2022-09-18T23:23:20+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2><p>在测试中,发现 php-fpm reload 会强制 kill 掉正在处理的请求。网上查了一下,发现其他人也有这个问题并反馈给了官方:<a href="https://link.segmentfault.com/?enc=ISRmZFtOz9MHOfIvYl%2FT6g%3D%3D.JzOfI863HiVjgelMYoviDzNefo672ghyb4cfzAnZVF6DIFaCC5MT6cp4tNjRj7Vd" rel="nofollow">https://bugs.php.net/bug.php?id=75440</a> 和 <a href="https://link.segmentfault.com/?enc=3LjicKfRIUybRVOGEYsQgQ%3D%3D.amZEg5Io34T1D%2FqIKEmkK2AiS9uou2jd%2Bo6aFOCwrAzRpzmdHXlxRvDehwhWIZ27" rel="nofollow">https://bugs.php.net/bug.php?id=60961</a>,帖子是 2017 和 2012 年的,到现在还没解决。</p><p>官方帮助手册还说 reload 是 <code>graceful</code> ,啊哈哈,不要太相信:</p><pre><code>man php-fpm
...
SIGINT,SIGTERM
immediate termination
SIGQUIT
graceful stop
SIGUSR1
re-open log file
SIGUSR2
graceful reload of all workers + reload of fpm conf/binary
...</code></pre><h2>reload 流程简介</h2><p><code>php-fpm</code> 是 <code>master worker</code> 的工作方式。</p><p>php-fpm master 进程通过接受用户发送的 <code>SIGUSR2</code> 信号实现自身服务的 <code>reload</code>:</p><pre><code>kill -USR2 <pid></code></pre><p>主进程(master进程)收到 <code>reload</code> 信号,会向所有子进程发送 <code>SIGGUIT</code> 信号,同时注册定时器时间,timeout 的值为 <code>fpm_global_config.process_control_timeout</code> 。在规定时间之内子进程还没有结束,则子进程将被 kill 。比如 timeout 值设为1秒,如果在 1 秒之内还没有结束,则直接向子进程发送 <code>SIGKILL</code> 信号,强制杀死。</p><p>最后 master 等待所有的子进程结束后,根据之前保存的启动参数重新启动一个进程,并继承父进程的 socket 文件描述符。</p><h2>缓兵之计</h2><p>注意,这只是缓解的方案,依然不能保证请求不会丢失。这个方案在于 <code>process_control_timeout</code> 这个配置选项,配置文件在 <code>php-fpm.conf</code> (我的是在 <code>/usr/local/etc/php-fpm.conf</code>)中,默认值是 <code>0</code> ,会立即将子进程 kill 掉,这里我改为了 60s 进行测试:</p><pre><code>; Time limit for child processes to wait for a reaction on signals from master.
; Available units: s(econds), m(inutes), h(ours), or d(ays)
; Default Unit: seconds
; Default Value: 0
process_control_timeout = 60s</code></pre><p>测试结果,正在处理的请求只要在该时间内完成请求,就能正常返回。</p><p>这不是 100% 的方案是因为,master 进程要等待所有子进程结束才会重新创建 worker 进程,而 <code>process_control_timeout</code> 等待的时候,worker 进程不接受请求了,因此这段时间内新的请求进不来,这些新请求将由 fpm 排队,nginx 若超时会报 502 给用户,保险起见,nginx 的超时时间的值应该是 <code>process_control_timeout</code> 的两倍。</p><p>尽管可能会报 502 ,但这样的处理方式比杀死正在处理的请求让人接受的多了。</p><h2>总结</h2><p>尽管设置了 <code>process_control_timeout</code> ,在上述情况之上,PHP-FPM 在 reload 完成之前不会为新请求提供服务。但是,所有这些新请求将由 fpm 排队,并在重新加载完成后立即执行。最终用户的结果是,在此期间,他们看到浏览器显示加载中。另一点是设置的超时,也不能保证请求在这个时间内处理完,还是需要程序员保证自己的脚本运行时间在合理范围内。</p>
CPython中处理 is 与 is not 语句
https://segmentfault.com/a/1190000040417933
2021-07-28T16:10:21+08:00
2021-07-28T16:10:21+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2><p>文档中 <a href="https://link.segmentfault.com/?enc=hn0bhKhXtDDnAQX0uTEZEw%3D%3D.Zv6wc9d7fT5w4ok0xnPfH28C7TewHRSOK6Y7sDlIl6WLAcgXtSwaZ4m%2Fa%2BJI8n%2BpQy5Z5JuqQI3P23OAVhH9zQ%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=rVleBPbxu%2F%2Fc5iLQ%2F7KAEA%3D%3D.9qr%2FS0OQbm9Cm5Yg20SaTH%2B%2BFoCqFqZTBHf7A2DgyokUjbbr0%2Bn3S3aNOlk2F7wnIMy8PD6XCgXI33tk51d5TA%3D%3D" rel="nofollow">https://docs.python.org/3.8/r...</a> 表示对于 <code>x is y</code> 当且仅当两个变量指向同一对象时才为真。对象可以通过 <code>id()</code> 函数来查看它的身份(<code>id()</code> 函数返回了对象在内存中的映射)。</p><h2>is 与 is not 的字节码</h2><p><code>is</code> 与 <code>is not</code> 都是操作符。 <code>is not</code> 是整体的,千万别把 <code>x is not y</code> 当做是 <code>x is (not y)</code> 。</p><p>来看看这两个操作符对应的字节码(基于 Python 3.8):</p><pre><code>>>> def fun():
... x is y
... x is not y
...
>>> import dis
>>> dis.dis(fun)
2 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 8 (is)
6 POP_TOP
3 8 LOAD_GLOBAL 0 (x)
10 LOAD_GLOBAL 1 (y)
12 COMPARE_OP 9 (is not)
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
>>></code></pre><p>对于 <code>COMPARE_OP</code> 对应的动作</p><pre><code>case TARGET(COMPARE_OP): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *res = cmp_outcome(tstate, oparg, left, right);
Py_DECREF(left);
Py_DECREF(right);
SET_TOP(res);
if (res == NULL)
goto error;
PREDICT(POP_JUMP_IF_FALSE);
PREDICT(POP_JUMP_IF_TRUE);
DISPATCH();
}</code></pre><p>这部分的代码的含义是先将代对比的两个操作数从栈中取出,通过 <code>cmp_outcome(tstate, oparg, left, right)</code> 得到两数的操作结果,再将结果 <code>res</code> 放入栈顶。</p><p><code>cmp_outcome</code> 函数的相关代码是:</p><pre><code>static PyObject *
cmp_outcome(PyThreadState *tstate, int op, PyObject *v, PyObject *w)
{
int res = 0;
switch (op) {
case PyCmp_IS:
res = (v == w);
break;
case PyCmp_IS_NOT:
res = (v != w);
break;
...
}
v = res ? Py_True : Py_False;
Py_INCREF(v);
return v;
}</code></pre><p>在 <code>cmp_outcome()</code> 函数中,仅通过对比两个指针的值是否相等来判断它们是否是指向同一对象。</p><h2>纯Python代码解释</h2><p>通过 <code>id()</code> 函数可以来判断某一对象在内存中对应的地址,因此用它也可以来判断两个变量是否指向了同一对象:</p><pre><code>def _is(a, b):
return id(a) == id(b)
def _is_not(a, b):
return id(a) != id(b)</code></pre>
请谨慎使用 datetime.utcnow()
https://segmentfault.com/a/1190000040374683
2021-07-20T14:38:02+08:00
2021-07-20T14:38:02+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2><p>执行下面代码:</p><pre><code>import time
from datetime import datetime, timezone, timedelta
print(time.time())
print(datetime.utcnow().timestamp())
print(datetime.now(timezone.utc).timestamp())
print(datetime.now(timezone(timedelta(hours=2))).timestamp())
==== output ====
1626687759.9081082
1626658959.908108
1626687759.908108
1626687759.908108</code></pre><p>发现,输出的时间戳中只有 <code>utcnow()</code> 是不一样,如果对比相差的时间能发现正好差8小时,而我电脑所在的时区正好是东八区。</p><h2>原因</h2><p>正如 <code>utcnow()</code> <a href="https://link.segmentfault.com/?enc=pfEidCck5kQhV3oc5gImNg%3D%3D.XWS%2BiGDtUfYzmkcbX9Crppn5gU9m4u8RNFidkNyplX1qhEJA4DZNuvtI2wjdLNV1oa%2FmLVpXcfj720aFtob9kcFhxqyaiqYhBtFwlITijJw%3D" rel="nofollow">文档</a> 所表明的那样,它返回的是 <code>naive time</code> ,Naive datetime 实例被认为为表示本地时间,因此它的时间戳会比使用 <code>now(None)</code> 相差的时间正好是该电脑所在时区。</p><p>造成这种诡异处理方式的是有历史原因的,在 Python 2 转 Python 3 的过渡阶段中,<a href="https://link.segmentfault.com/?enc=bxRxAp2SYJn2ot4NLJfaIA%3D%3D.UXl%2FkFh1kbvLNAF7M9jOXlbgEMBmhTf6bzcshsBG9DAuzhsfUAsnNS5CRuE3tcdvH7mK8QRTIWPyq72UOOmr%2B6roWRV8fd93xMrVwh%2Fs%2FEg%3D" rel="nofollow">datetime.timezone</a> 作为 3.2 版中的新功能被设计了出来,因此有了更为清晰明确的标记日期所在的时区。旧的接口 <code>utcnow()</code> 则保留了原先的处理方式。</p><p>新的时区的模型的处理方式与Python 2 存在兼容问题:</p><pre><code>==== Python 2 ====
>>> from datetime import datetime
>>> from dateutil import tz
>>> datetime(2021, 5, 1).astimezone(tz.UTC)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: astimezone() cannot be applied to a naive datetime
==== Python 3 ====
>>> from datetime import datetime
>>> from dateutil import tz
>>> datetime(2021, 5, 1).astimezone(tz.UTC)
datetime.datetime(2021, 5, 1, 4, 0, tzinfo=tzutc())</code></pre><h2>总结</h2><p>综上所述, <code>utcnow()</code> 可能是一个常见的陷阱。我建议不要再使用 <code>utcnow()</code> 和 <code>utcfromtimestamp()</code> 。</p>
动手来自己实现一下 namedtuple
https://segmentfault.com/a/1190000022777780
2020-05-29T00:07:05+08:00
2020-05-29T00:07:05+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2>
<p><code>namedtuple</code> 是 <code>collections</code> 模块下的一个功能,它是类工厂函数,能返回 <code>tuple</code> 子类,允许通过字段名向元组中取值,性能上接近于元组。</p>
<pre><code>Person = namedtuple('Person', 'name age')
p = Person(name='Tony', age=18)
print(p.name) # 'Tony'
print(p.age) # 18</code></pre>
<p>我们来试着自己动手来实现这个 <code>namedtuple</code> 功能。因为这个功能需求明确,没什么模块依赖,通过自己的实现后再去看看真正在 <code>cpython</code> 里的源码就会比较清晰。</p>
<h2>需求分析</h2>
<p>对于代码 <code>Person = namedtuple('Person', 'name age')</code> 让它等价于:</p>
<pre><code>class Person(tuple):
def __new__(cls, name, age):
return tuple.__new__(cls, (name, age))
@property
def name(self):
return self[0]
@property
def age(self):
return self[1]</code></pre>
<p>那么有一种实现就是,拼凑成这样的代码块字符串,再通过 <code>exec(code)</code> 来创建类,旧版本(小于 <code>3.7</code>)的 <code>namedtuple</code> 在 cpython 中还真就是这么实现的。</p>
<p>PS: 重写了 <code>__new__</code> 而不是 <code>__init__</code> ,有一个原因是因为元组一旦创建就不可变。为了能够通过字段名取值,这里引入了 <code>property</code> 修饰符。</p>
<h2>构造类的字符串模板创建类</h2>
<p>基于这个思路一个简陋的就能写了出来:</p>
<pre><code># 类名称模板
_class_template = '''
class {typename}(tuple):
def __new__(_cls, {arg_list}):
return tuple.__new__(_cls, ({arg_list}))
{field_defs}
'''
# 属性模板
_field_template = '''
@property
def {name}(self):
return self[{index}]
'''
def namedtuple(typename, field_names):
field_names = field_names.split()
class_definition = _class_template.format(
typename=typename,
arg_list=arg_list = repr(field_names).replace("'", "")[1:-1],
field_defs=''.join(_field_template.format(index=index, name=name)
for index, name in enumerate(field_names))
)
namespace = {}
exec(class_definition, namespace)
return namespace[typename]
# use demo
Person = namedtuple('Person', 'name age')
p = Person(name='Tony', age=18)
print(isinstance(p, tuple)) # True
print(p.name) # Tony
print(p.age) # 18</code></pre>
<p><code>cpython</code> 中的 <code>namedtuple</code> 便是基于这个思路实现的,源码见:<a href="https://link.segmentfault.com/?enc=PPKnXxYAN%2F0HYUgj7%2F5gbQ%3D%3D.JqsiEaazK2w8N%2BIOgpwKtUwtbW5YXDVBKdYEAFua8CmHPdxU3vWfh4JUQbyLWgCuTklcn7QSsxrACxfr8025x7uNPUrtR%2Bc5AKQjdPTI31VHw36gDZ%2FN%2Be1yLbpgLEnqWNxGgwtogpkhWU33SyrvgeQAKIujNFiqe1wL%2B%2BZuNdk%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=QgoWWN2T0%2B4C6U9qO%2BgX%2Bw%3D%3D.DVpueAWlm5yhTcnIEgFLohv9NWyGnKhdqjd1MyMvyAnqzTn9uz0kZro38k83UdAn3C3I6U4Gg2I17u5NwkErvbYald7xGiyCKC3GybNo4%2FM%3D" rel="nofollow">https://github.com/python/cpy...</a>,这个实现方式一直沿用到了 3.6.x 。直到因为性能原因而进行了改版,PR见:<a href="https://link.segmentfault.com/?enc=vcjhCi0MJaIcvFF7hl5tRg%3D%3D.26otBxtLqck0%2Bdi54gfYGhjuVVzrKXjl9uoZnm2qmufrU1AGspUbExepevTP5PD8" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=9eBXrVSS9AVC93cg00EzjQ%3D%3D.GtwhN%2FpUdjocNCBc2fSc1kY8N7Aboz%2BlLLEbLzhRWi8n6N8Xyd7lmR4AVIYILdbn" rel="nofollow">https://github.com/python/cpy...</a></p>
<h2>基于元类编程</h2>
<p>改进后的 <code>namedtuple</code> 更能体现元类编程的思想,对性能也有明显的改善。我用简化的代码来展示改版后的 <code>namedtuple</code> 的工作内容:</p>
<pre><code>def _tuplegetter(index):
@property
def _getter(self):
return self[index]
return _getter
def namedtuple(typename, field_names):
field_names = field_names.split()
arg_list = repr(field_names).replace("'", "")[1:-1]
s = f'def __new__(_cls, {arg_list}): return tuple.__new__(_cls, ({arg_list}))'
namespace = {}
exec(s, namespace)
__new__ = namespace['__new__']
class_namespace = {'__new__': __new__}
for index, name in enumerate(field_names):
class_namespace[name] = _tuplegetter(index)
result = type(typename, (tuple,), class_namespace)
return result</code></pre>
<p>到此,一个简易版的 <code>namedtuple</code> 就结构就完成了,改进后的 <code>exec</code> 调用中只有一行代码,性能会更好,旧版本的还会额外 import 其他依赖。然后就是构造元类编程中类属性和方法了。属性的获取是通过写的 <code>_getter</code> 来完成,而实际上源码上会委托给 <code>operator.itemgetter</code> 函数。</p>
<h2>总结</h2>
<p>相信从本文中理解了 <code>namedtuple</code> 的设计和实现原理,再去阅读源代码,能更快的理解源码,起到事半功倍的效果。</p>
Python 中 staticmethod 和 classmethod 原理探究
https://segmentfault.com/a/1190000022735289
2020-05-25T10:41:18+08:00
2020-05-25T10:41:18+08:00
weapon
https://segmentfault.com/u/weapon
4
<h2>起步</h2>
<p>文章 <a href="https://link.segmentfault.com/?enc=JpnxlP1JzG6WevqI7y9%2Byg%3D%3D.MHAQYeO%2B%2Bn2vef%2Bv%2BJbF9JiQmJ%2BOZkaua%2BGYJObFUzUNOUL2Bd3KgiAYWRfGFTBKkFDpjeeqPn%2FeuoC5NYWYmA%3D%3D" rel="nofollow">《Python中 property 的实现原理及实现纯 Python 版》</a> 中探究了 <code>property</code> 的实现原理。如果能理解那边描述符的使用方式,那也能很快理解本篇中的 <code>staticmethod</code> 和 <code>classmethod</code> 。</p>
<h2>函数与方法</h2>
<p>对于类中定义的方法来说,通过类来调用与实例调用是不一样的:</p>
<pre><code>class C:
def f(self): pass
print(C.f) # <function C.f at >
print(C().f) # <bound method C.f of ></code></pre>
<p>一个返回的是 <code>function</code> 类型,一个返回的是 <code>method</code> 类型。他们的主要区别在于,函数的 <code>传参都是显式传递的</code> 而方法则方法中 <code>传参往往都会有隐式传递的,具体根据于调用方</code>。例如示例中的 <code>C().f</code> 通过实例调用的方式会隐式传递 <code>self</code> 数据。</p>
<h2>staticmethod 的实现</h2>
<p><code>staticmethod</code> 的效果是让 <code>C.f</code> 与 <code>c.f</code> 都返回函数,等价于 <code>object.__getattribute__(c, "f")</code> 或 <code>object.__getattribute__(C, "f")</code> ,运行代码如下:</p>
<pre><code>class C:
@staticmethod
def sf(): pass
c = C()
print(C.sf) # <function C.sf at 0x000001AEDDA64040>
print(c.sf) # <function C.sf at 0x000001AEDDA64040>
print(C.sf is c.sf) # True</code></pre>
<p>要实现这样的方式也可以依托于描述符的机制,在 <code>__get__</code> 中返回原始的函数,因此它的 Python 实现版本异常的简单:</p>
<pre><code>class staticmethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f</code></pre>
<p>这么简单的代码也已经是 C 实现版本对应的Python完整代码了。</p>
<h2>classmethod 的实现</h2>
<p><code>classmethod</code> 则是要让 <code>C.f</code> 和 <code>c.f</code> 都返回方法,并且传递隐式参数 <code>cls</code> , 运行代码如下:</p>
<pre><code>class C:
@classmethod
def cf(cls): pass
c = C()
print(C.cf) # <bound method C.cf>
print(c.cf) # <bound method C.cf>
print(C.cf is c.cf) # False</code></pre>
<p><code>classmethod</code> 不仅要隐式传递参数,还需要每次创建新的 <code><bound method></code> 对象。因此它的实现上需要用闭包,将闭包函数作为返回值以便得到新的对象:</p>
<pre><code>class classmethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc</code></pre>
<p>这里的技巧就在于闭包将隐式的 <code>cls</code> 通过闭包空间进行绑定。这个纯python实现版本在功能上没什么问题,仅有个小缺陷:</p>
<pre><code>c = C()
print(C.cf) # <function classmethod.__get__.<locals>.newfunc at 0x000001EDF2527EE0>
print(c.cf) # <function classmethod.__get__.<locals>.newfunc at 0x000001EDF2527EE0>
print(C.cf is c.cf) # False</code></pre>
<p>尽管我们用闭包绑定了个隐式参数,但通过 <code>c.cf</code> 获取的依然是 <code>function</code> 对象。我没有找到可以在Python代码中创建 <code><bound method></code> 实例的方式。</p>
<h2>总结</h2>
<p><code>staticmethod</code> 和 <code>classmethod</code> 都运用了描述符的机制,学习描述符不仅能提供接触到更多工具集的方法,还能更深地理解 Python 工作的原理并更加体会到其设计的优雅性。</p>
Python中 property 的实现原理及实现纯 Python 版
https://segmentfault.com/a/1190000022727234
2020-05-24T01:24:14+08:00
2020-05-24T01:24:14+08:00
weapon
https://segmentfault.com/u/weapon
3
<h2>起步</h2>
<p><code>property</code> 是 Python 内置的功能,常用来修饰类方法,用于已访问属性的方式调用函数。</p>
<pre><code>class C(object):
def __init__(self):
self._x = 'Tom'
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
c = C()
print(c.x) # Tom
c.x = 'Tony'
print(c.x) # Tony</code></pre>
<p>尽管 <code>property</code> 的实现是 C 实现,但仍不妨碍探究它的实现原理,本文最后也会给出它的纯 Python 版本的实现。</p>
<h2>描述符对象</h2>
<p>为了能够实现访问属性就调用某个函数,这里将利用 <a href="https://link.segmentfault.com/?enc=rFOUPpxBUKR%2FPL5Ba8TslQ%3D%3D.uLOQ3lPscYq2fLSrIwtRP%2Bu6tDIiPBqi4UHY3zjZ%2Bz8MQSF96mvpl5EE8IUk6DHE" rel="nofollow">描述符对象</a> 作为本文的实现起点,当某个类定义了 <code>__get__</code> 方法后,通过其方法名称可以直接调用 <code>__get__</code> ,例如:</p>
<pre><code>class Desc:
def __init__(self, name):
self.name = name
def __get__(self, obj, objtype):
print('Retrieving', self.name)
return self.name
class A:
x = Desc('Tom')
a = A()
print(a.x) # 打印了 'Retrieving'</code></pre>
<p>从这点来看,如果我们自行实现 <code>property</code> ,那它将会是类而不是函数,同样的为了能够完成属性的赋值操作,该类还要设置 <code>__set__</code> 函数。</p>
<h2>setter 函数的实现</h2>
<p>这个的实现需要脑子转个弯。对于修饰符 <code>@x.setter</code> ,因为 <code>x</code> 已经是 <code>property()</code> 的实例,所以我们要完成的 <code>property</code> 要实现 <code>setter</code> 函数,那函数体会是什么呢?</p>
<p>函数体也是要返回描述符对象,并该对象是有 <code>__set__</code> 的。那 <code>property</code> 不就正好满足吗,所以这里的处理方式是 <code>setter</code> 函数会返回一个新的 <code>property</code> 实例。</p>
<h2>property 的简易实现</h2>
<p>基于上述分析,对于开头中的实例代码可运行的简易版本:</p>
<pre><code>class property:
def __init__(self, fget=None, fset=None):
self.fget = fget
self.fset = fset
def __get__(self, inst, owner=None):
if inst is None:
return self
return self.fget(inst)
def __set__(self, inst, value):
self.fset(inst, value)
def setter(self, fset):
return property(self.fget, fset)</code></pre>
<h2>property 的完整实现</h2>
<p>这个基本是依据 C 实现的纯 Python 版本,纯 C 实现在文件 <code>Objects/descrobject.c</code> 中。</p>
<p>Python 实现版本:</p>
<pre><code>class property:
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)</code></pre>
<p>在创建新的 <code>proptery</code> 实例中使用的是 <code>type(self)(...)</code> ,这是因为考虑到了 <code>proptery</code> 可能被继承。</p>
<h2>总结</h2>
<p><code>proptery</code> 主要依赖于描述符的机制。<code>proptery</code> 内置也成为了 Python 的一个特性,它的内部实现原理很简单,但在应用上却很方面,可读性也十分友好。</p>
字符串在Python内部是如何省内存的
https://segmentfault.com/a/1190000021677331
2020-02-03T14:47:02+08:00
2020-02-03T14:47:02+08:00
weapon
https://segmentfault.com/u/weapon
3
<h2>起步</h2>
<p>Python3 起,<code>str</code> 就采用了 <code>Unicode</code> 编码(注意这里并不是 <code>utf8</code> 编码,尽管 <code>.py</code> 文件默认编码是 <code>utf8</code> )。 每个标准 <code>Unicode</code> 字符占用 4 个字节。这对于内存来说,无疑是一种浪费。</p>
<p><code>Unicode</code> 是表示了一种字符集,而为了传输方便,衍生出里如 <code>utf8</code> , <code>utf16</code> 等编码方案来节省存储空间。Python内部存储字符串也采用了类似的形式。</p>
<h2>三种内部表示Unicode字符串</h2>
<p>为了减少内存的消耗,Python使用了三种不同单位长度来表示字符串:</p>
<ul>
<li>每个字符 1 个字节(Latin-1)</li>
<li>每个字符 2 个字节(UCS-2)</li>
<li>每个字符 4 个字节(UCS-4)</li>
</ul>
<p>源码中定义字符串结构体:</p>
<pre><code># Include/unicodeobject.h
typedef uint32_t Py_UCS4;
typedef uint16_t Py_UCS2;
typedef uint8_t Py_UCS1;
# Include/cpython/unicodeobject.h
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;</code></pre>
<p>如果字符串中所有字符都在 <code>ascii</code> 码范围内,那么就可以用占用 1 个字节的 <code>Latin-1</code> 编码进行存储。而如果字符串中存在了需要占用两个字节(比如中文字符),那么整个字符串就将采用占用 2 个字节 <code>UCS-2</code> 编码进行存储。</p>
<p>这点可以通过 <code>sys.getsizeof</code> 函数外部窥探来验证这个结论:</p>
<p><img src="/img/remote/1460000021677334" alt="20200110170427.png" title="20200110170427.png"></p>
<p>如图,存储 <code>'zh'</code> 所需的存储空间比 <code>'z'</code> 多 1 个字节, <code>h</code> 在这里占了 1 个字节;</p>
<p>存储 <code>'z中'</code> 所需的存储空间比 <code>'中'</code> 多了 2 个字节,<code>z</code> 在这里占了 2 个字节。</p>
<p>大多数的自然语言采用 2 字节的编码就够了。但如果有一个 1G 的 ascii 文本加载到内存后,在文本中插入了一个 emoji 表情,那么字符串所需的空间将扩大到 4 倍,是不是很惊喜。</p>
<h2>为什么内部不采用 utf8 进行编码</h2>
<p>最受欢迎的 Unicode 编码方案,Python内部却不使用它,为什么?</p>
<p>这里就得说下 utf8 编码带来的缺点。这种编码方案每个字符的占用字节长度是变化的,这就导致了无法按所以随机访问单个字符,例如 <code>string[n]</code> (使用utf8编码)则需要先统计前n个字符占用的字节长度。所以由 O(1) 变成了 O(n) ,这更无法让人接受。</p>
<p>因此Python内部采用了定长的方式存储字符串。</p>
<h2>字符串驻留机制</h2>
<p>另一个节省内存的方式就是将一些短小的字符串做成池,当程序要创建字符串对象前检查池中是否有满足的字符串。在内部中,仅包含下划线(<code>_</code>)、<code>字母</code> 和 <code>数字</code> 的长度不高过 <code>20</code> 的字符串才能驻留。驻留是在代码编译期间进行的,代码中的如下会进行驻留检查:</p>
<ul>
<li>空字符串 <code>''</code> 及所有;</li>
<li>变量名;</li>
<li>参数名;</li>
<li>字符串常量(代码中定义的所有字符串);</li>
<li>字典键;</li>
<li>属性名称;</li>
</ul>
<p>驻留机制节省大量的重复字符串内存。在内部,字符串驻留池由一个全局的 <code>dict</code> 维护,该字段将字符串用作键:</p>
<pre><code>void PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
PyObject *t;
if (s == NULL || !PyUnicode_Check(s))
return;
// 对PyUnicodeObjec进行类型和状态检查
if (!PyUnicode_CheckExact(s))
return;
if (PyUnicode_CHECK_INTERNED(s))
return;
// 创建intern机制的dict
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
// 对象是否存在于inter中
t = PyDict_SetDefault(interned, s, s);
// 存在, 调整引用计数
if (t != s) {
Py_INCREF(t);
Py_SETREF(*p, t);
return;
}
/* The two references in interned are not counted by refcnt.
The deallocator will take care of this */
Py_REFCNT(s) -= 2;
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}</code></pre>
<p>变量 <code>interned</code> 就是全局存放字符串池的字典的变量名 <code>interned = PyDict_New()</code>,为了让 <code>intern</code> 机制中的字符串不被回收,设置字典时 <code>PyDict_SetDefault(interned, s, s);</code> 将字符串作为键同时也作为值进行设置,这样对于字符串对象的引用计数就会进行两次 <code>+1</code> 操作,这样存于字典中的对象在程序结束前永远不会为 0,这也是 <code>y_REFCNT(s) -= 2;</code> 将计数减 2 的原因。</p>
<p>从函数参数中可以看到其实字符串对象还是被创建了,内部其实始终会为字符串创建对象,但经过 inter 机制检查后,临时创建的字符串会因引用计数为 0 而被销毁,临时变量在内存中昙花一现然后迅速消失。</p>
<h2>字符串缓冲池</h2>
<p>除了字符串驻留池,Python 还会保存所有 ascii 码内的单个字符:</p>
<pre><code>static PyObject *unicode_latin1[256] = {NULL};</code></pre>
<p>如果字符串其实是一个字符,那么优先从缓冲池中获取:</p>
<pre><code>[unicodeobjec.c]
PyObject * PyUnicode_DecodeUTF8Stateful(const char *s,
Py_ssize_t size,
const char *errors,
Py_ssize_t *consumed)
{
...
/* ASCII is equivalent to the first 128 ordinals in Unicode. */
if (size == 1 && (unsigned char)s[0] < 128) {
return get_latin1_char((unsigned char)s[0]);
}
...
}</code></pre>
<p>然后再经过 intern 机制后被保存到 intern 池中,这样驻留池中和缓冲池中,两者都是指向同一个字符串对象了。</p>
<p>严格来说,这个单字符缓冲池并不是省内存的方案,因为从中取出的对象几乎都会保存到缓冲池中,这个方案是为了减少字符串对象的创建。</p>
<h2>总结</h2>
<p>本文介绍了两种是节省内存的方案。一个字符串的每个字符在占用空间大小是相同的,取决于字符串中的最大字符。</p>
<p>短字符串会放到一个全局的字典中,该字典中的字符串成了单例模式,从而节省内存。</p>
我的2019年开源贡献
https://segmentfault.com/a/1190000021622032
2020-01-19T15:09:26+08:00
2020-01-19T15:09:26+08:00
weapon
https://segmentfault.com/u/weapon
3
<h2>起步</h2>
<p>年末时候往往可以总结下今年做了什么。我想说说一年来在开源软件的贡献。我的想法也比较简单,开源软件帮助我解决非常多的问题,如今我也有所成长,应该反馈于开源软件。</p>
<h2>代码贡献</h2>
<p>几年的贡献的项目只有两个:</p>
<p>一个是为 <a href="https://link.segmentfault.com/?enc=FqKOigkIo%2B8L4g%2BlyFITpw%3D%3D.FqrcuYgaDZTccLgF8qDyjmgn%2Bu3CuWiBy28UHpgt2eCA4qM8Zj5n2sdI1HiI%2BPdp" rel="nofollow">CPython</a> 贡献了 <code>8</code> 个补丁,得益于秋季那会有比较多富余的业余时间:</p>
<p><img src="/img/bVbCSZs" alt="20191226105049.png" title="20191226105049.png"></p>
<p>Python 是我最喜欢的编程语言,去年第一次成功打上补丁,那时感觉短时间内很难再达到那个高度。因为修复一个问题往往需要重新整理那方面的知识,因此往往比较耗时,没有富余的时间不太可能完成。</p>
<p>这个过程中也能够明显感觉到自己不懂的地方越来越多。Python源码我是前年就着书看的,当初认为掌握了,十分自信。如今再次看看那些代码,有了不一样的体会。要花很多的时间和精力去体会,有时候睡前脑子里都在想着如何修复会更好。</p>
<p>另一个项目是 <a href="https://link.segmentfault.com/?enc=lJmRpR8xZGMV7E921SBUPA%3D%3D.j6KOexYCsE9kHkWyHtI%2FYQMvOf2AX9LlDS7MpjXCwUqgiaw%2FU6BclWiowTGfcr9M" rel="nofollow">thinkphp</a> ,数了下有 15 个PR成功合并。而且由于限流组件 <a href="https://link.segmentfault.com/?enc=M7lcN6aVz4IO5cVqkfXmoQ%3D%3D.x79QpnXqD%2FRMA%2B%2FfLwR6YLzQt6EXdi7Comef4qk%2FWibSZM8V6CQKsewXREkmdGfq" rel="nofollow">think-throttle</a> 收录为官方组件我也加入了组织:</p>
<p><img src="/img/bVbCS1I" alt="20191226113900.png" title="20191226113900.png"></p>
<h2>总结</h2>
<p>我热爱开源,为开源项目作出贡献能够让我体会到从无到有构建成果的满足感,这种感觉很赞,真的很赞。</p>
PHP 理清 foreach 潜规则
https://segmentfault.com/a/1190000019637833
2019-07-02T10:25:04+08:00
2019-07-02T10:25:04+08:00
weapon
https://segmentfault.com/u/weapon
27
<p>原文地址:<a href="https://link.segmentfault.com/?enc=Bj%2FkKnyksFQjO8B8SSddHw%3D%3D.UZxy0VoJ2KbCd1Vey52C8fT5U%2F1eR7J2ETsyYip2yfKhUOjfuNVNzZz%2FH55GeR%2BykOWc2aVgfYftgo0biX7m%2FQ%3D%3D" rel="nofollow">https://www.hongweipeng.com/i...</a></p>
<h2>起步</h2>
<p>在相当长的一段时间里,我认为 <code>foreach</code> 在循环期间使用的是原数组的副本。但最近经过一些实验后发现这并不是百分百正确。</p>
<p>比如副本的说法说得通的:</p>
<pre><code>$array = array(1, 2, 3, 4, 5);
foreach ($array as $item) {
echo "$item\n";
$array[] = $item;
}
print_r($array);
/* Output in loop: 1 2 3 4 5
$array after loop: 1 2 3 4 5 1 2 3 4 5 */</code></pre>
<p>这个例子在循环体中修改数组不影响循环过程,副本的说法说得通。</p>
<p>然而</p>
<pre><code>$arr = [1, 2, 3, 4, 5];
$obj = [6, 7, 8, 9, 10];
$ref = &$arr;
foreach ($ref as $val) {
echo $val;
if ($val == 3) {
$ref = $obj;
}
}
// output in php5.x: 123678910
// output in php7.x: 12345</code></pre>
<p>对于不同的PHP版本输出会有差异,php7 提及 foreach 的改变有三点:</p>
<ol>
<li>foreach 不再改变内部数组指针;</li>
<li>foreach 通过值遍历时,操作的值为数组的副本;</li>
<li>foreach 通过引用遍历时,有更好的迭代特性。</li>
</ol>
<p>因此,在讨论 <code>foreach</code> 里的数组副本问题,得分开版本来说明。在此,<a href="https://link.segmentfault.com/?enc=dAwzR%2FPsBnQHm2Mwb0Q4kg%3D%3D.6yuhPHRlGum1heut5nv77dWILc3wekw4S8Q64t30Y27k7psN%2FR5oiccTKOlENfjcnJkEqeBKwKNbWfi3B8DGcWD8GNBEi91lf9egYD4ZkrY%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=bQhstCdmORG699NW0Uix5g%3D%3D.%2BvXS%2Fgb5Dv0x6hce15U283i19URq2Nb7dvE%2BfNBLzubS%2BLxCQ9YkpqVRs0PMWCOBvhE%2FxofStivPINA4NcZYTUQ6GEBzhsuAePeL7WFYjhE%3D" rel="nofollow">https://stackoverflow.com/que...</a> 有了比较详细的说明,并举例了大多数情况。本文就进行一些整理与总结。</p>
<h2>写时复制</h2>
<p>造成运行差异和与预期不同的原因一部分就是因为触发了写时复制,另一部分是 foreach 本身的机制。</p>
<p>php底层有两个属性来处理引用计数(refcount)与完全引用计数(is_ref)。</p>
<p>当类似 <code>$a = [1, 2, 3];</code> 创建并初始化后,该对象 <code>is_ref</code> 会设为 0, <code>refcount</code> 会设为 1; 当进行引用传递类似 <code>$b = &$a</code> 时,is_ref 和 refcount 都会 +1 ; 当类似 <code>$c = $a</code> 时,refcount 会 +1。</p>
<p><strong>什么情况下会触发写时复制?</strong></p>
<p>当变量被重新赋值 <code>$a = 1;</code> 时,如果此时的 $a 的 is_ref=0 且 refcount>1,那么就会触发复制;否则在原对象上进行修改。</p>
<pre><code>$a = [1, 2, 3]
$b = $a;
$a[] = 5; // $a 的 is_ref=0,refcount>1 触发了写时复制,之后$a与$b是两个不同数组</code></pre>
<p><strong>什么情况下可以跳过写时复制而可以直接对原数组进行操作呢?</strong></p>
<p>根据写时复制的触发规则,一个简单的跳过改机制就是进行引用复制使得 <code>is_ref > 0</code> 。</p>
<p>那么可以在迭代期间进行修改:</p>
<pre><code><?php
$arr = [0, 1, 2, 3, 4, 5];
$ref = &$arr;
foreach ($arr as $v) {
if ($v === 0) {
unset($arr[3]);
}
echo $v;
}
// output in 5.x: 01245
// output in 7.x: 012345 // 7.x版本下,通过值遍历时,底层操作的始终是数组的副本</code></pre>
<p>7.x 版本好像还是写时复制的对吧。这是因为 7.x 版本对foreach 的改变 <code>"foreach 通过值遍历时,操作的值为数组的副本"</code> 。</p>
<p>另外一种可以在迭代中修改的是依靠 <code>foreach</code> 的机制的,即通过引用来进行迭代 <code>foreach ($arr as &$v)</code> :</p>
<pre><code><?php
$arr = [0, 1, 2, 3, 4, 5];
foreach ($arr as &$v) {
if ($v === 0) {
unset($arr[3]);
}
echo $v;
}
// output in 5.x: 01245
// output in 7.x: 01245</code></pre>
<h2>数组副本</h2>
<p>数组内部指针(IAP)我们可以通过且只能 <code>current($arr)</code> 函数观察它的移动,因为修改IAP也是在写时复制的语义下进行了。这也就意味着大多数情况下,<code>foreach</code> 都会被迫拷贝它正在迭代的数组。在此强调:写时复制条件是操作对象的计数为 <code>isref = 0</code> 且 <code>refcount > 1</code> 。</p>
<p><strong>foreach 对 <code>current</code> 的影响</strong></p>
<p>7.x 的foreach已经不会修改内部指针了,所以讨论 current 影响的这部分都指 5.x 版本。</p>
<h3>current 的例子1</h3>
<pre><code><?php
$arr = [0, 1, 2, 3, 4, 5];
foreach ($arr as $v) {
echo current($arr);
}
// output in 5.x: 11111</code></pre>
<p>这里有两个问题,一个是为什么第一次循环时 current 指向是第二个元素;另一个问题就是为什么都是指向第二个元素。</p>
<p>先来解释第一个问题,为什么第一次循环时 current 指向是第二个元素?</p>
<p>在 <code>foreach</code> 启动前,此时 $arr (is_ref=0, refcount=1),达不到写时复制的条件,因此用的是$arr本身。<br>这里有个细节,循环遍历某个数据结构的“正常”方式常常看起来像这样:</p>
<pre><code>reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
code();
move_forward(arr);
}</code></pre>
<p>而 PHP 的 <code>foreach</code> 做的事情有些不同:</p>
<pre><code>reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
move_forward(arr);
code();
}</code></pre>
<p>也就是说,在执行 foreach 的循环体之前,数组指针已经向前移动了。这意味着当循环体正在处理 <code>$i</code> 是,IAP 已经处于元素 <code>$i+1</code> 了。这也就是为什么第一次循环 <code>current</code> 得到的是第二个元素了。</p>
<p>那么,为什么下一个循环里 current 还是第二个元素呢?</p>
<p>这是因为底层会在 foreach 启动后对 refcount 进行 +1 ,因此在第一次循环后第二次循环启动时,foreach 又要修改内部指针了,但此时 $arr 为 is_ref=0 refcount=2,修改内部指针又在写时复制的语义下,因此触发了写时复制,所以从第二次循环开始,底层用的都是另外的一份副本,不再对原数组进行修改,所以 <code>current($arr)</code> 就一直停留在第二个元素上了。</p>
<h3>current 的例子2</h3>
<pre><code><?php
$arr = [0, 1, 2, 3, 4, 5];
foreach ($arr as &$v) {
echo current($arr);
}
// output in 5.x: 12345</code></pre>
<p>这是foreach的运行机制导致的,只要是用引用进行迭代,foreach 操作的始终是原数组。这规则在 7.x 版本也适用。</p>
<h3>current 的例子3</h3>
<pre><code><?php
$arr = [0, 1, 2, 3, 4, 5];
$foo = $arr;
foreach ($arr as $v) {
echo current($arr);
}
// output in 5.x: 000000</code></pre>
<p>这个比 例子 1 就是多个一个将数组分配给另一个变量。这里,循环启动时 refcount=2,并且内部数组指针的移动又发生在循环体之前,所以一开始就触发了写时复制,foreach 始终都是在副本上操作。因此 <code>current($arr)</code> 总还是指向第一个元素。</p>
<p>关于 foreach 对 current 的影响鸟哥似乎有分享:</p>
<p><img src="/img/remote/1460000019637836" alt="3649802502-572708cb15c1c.png" title="3649802502-572708cb15c1c.png"></p>
<p>说是在Think 2015 PHP技术峰会,但我没找到视频,十分遗憾。</p>
<h2>在迭代过程中修改原数组</h2>
<p>为了确保我们对数组的修改能够实时生效,我们就要避免写时复制的情况,让foreach始终都操作原数组。这里最方便的就是用引用来迭代即 <code>foreach ($arr as &$v)</code> 的形式,但尽管如此,对于操作后的数组,5和7的版本在处理上也有差异:</p>
<pre><code>$array = array(1, 2, 3, 4, 5);
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// output in 5.x: (1, 1) (1, 3) (1, 4) (1, 5)
// output in 7.x: (1, 1) (1, 3) (1, 4) (1, 5)
// (3, 1) (3, 3) (3, 4) (3, 5)
// (4, 1) (4, 3) (4, 4) (4, 5)
// (5, 1) (5, 3) (5, 4) (5, 5) </code></pre>
<p>此处的 <code>(1, 2)</code> 是缺少的部分,因为元素 <code>$array[1]</code> 已经被删除。但对于删除后的处理,5和7不同,5 在外循环第一次迭代后就中止了,这是因此 5.x 的循环中,当前的IAP位置会被备份到 <code>HashPointer</code> 中(这点在额外章节中有具体说明),循环体结束后当且仅当元素仍然存在时进行恢复,否则使用当前的IAP位置。而7.x的两个循环都具有完全独立的散列表迭代器,不再通过共享IAP进行交叉污染。</p>
<pre><code>$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
var_dump($val);
$array[2] = 0;
}
/* output in 5.x: 1, 2, 0, 4, 5 */
/* output in 7.x: 1, 2, 3, 4, 5 */</code></pre>
<p>对于 5.x 版本,原数组有被引用,因此不会触发写时复制,foreach 操作始终是原数组。</p>
<p>对于 7.x 版本,foreach 通过值遍历时,操作的都是数组的副本,这点在升级文档有提及。</p>
<p>现在有一个比较奇怪的边缘问题:</p>
<pre><code>$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
var_dump($value);
}
// output in 5.x: 1, 4
// output in 7.x: 1, 3, 4</code></pre>
<p>在5.x版本中,由于 HashPointer恢复机制会直接跳到新元素(这应该算是bug)。而版本 7.x 不再依赖元素哈希,所以感觉 7.x 的运行结果更为正确。</p>
<p><strong>在循环期间替换迭代的实体</strong></p>
<p>php允许在循环期间替换迭代的实体,因此对于操作原数组来说,也会将其替换为另一个数组,开始它的迭代。</p>
<pre><code>$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];
$ref = &$arr;
foreach ($ref as $val) {
echo "$val\n";
if ($val == 3) {
$ref = $obj;
}
}
// Output in 5.x: 1 2 3 6 7 8 9 10
// output in 7.x: 1, 2, 3, 4, 5 值传递,始终操作的是副本,替换实体不起作用</code></pre>
<p>尽管操作上是允许的,但我想没有人会这么做。</p>
<h2>额外</h2>
<p><strong>内部指针与 HashPointer</strong></p>
<p>为了引出指针恢复的概念,我们可以先从一个问题来入手,只有一个内部数组指针的要怎么同时满足两个循环:</p>
<pre><code>// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
foreach ($arr as &$v) {
// ...
}
}</code></pre>
<p>解决的办法是,在循环体执行之前,将当前元素的指针和指向的元素保存起来,在循环体运行后,如果元素仍然存在,就把IAP恢复为之前保存的指针;如果元素已被删除,则IAP就使用当前的位置。这个保存的指针和元素地方就是 <code>HashPointer</code> 。</p>
<p><code>HashPointer</code> 备份恢复机制带来的方便就是我们可以临时修改数组的指针:</p>
<pre><code>$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
var_dump($value);
reset($array);
}
// output in 5.x and 7.x: 1, 2, 3, 4, 5</code></pre>
<p>如果要干涉这个机制,就要让他恢复失败:</p>
<pre><code>$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
var_dump($value);
unset($array[1]);
reset($array);
}
// output in 5.x: 1, 1, 3, 4, 5
// output in 7.x: 1, 2, 3, 4, 5 值传递,始终操作的是副本</code></pre>
PHP 对输入变量名的自动转换的问题与源码分析
https://segmentfault.com/a/1190000019633304
2019-07-01T17:57:18+08:00
2019-07-01T17:57:18+08:00
weapon
https://segmentfault.com/u/weapon
3
<p>原文地址:<a href="https://link.segmentfault.com/?enc=rm2iI3vIlPAIfUx7ieMSdQ%3D%3D.jLCcFveVCiJhSuwsqK46y9LKHzXiGZEwcZVNMsR%2B2JDB9Q2a%2BmhUcI1DDKOghdPyu%2FNUJj4HulusAvojLOshZA%3D%3D" rel="nofollow">https://www.hongweipeng.com/i...</a></p>
<h2>起步</h2>
<p>表单提交到PHP脚本时,底层的PHP会做一层转换。将一些符号转成下划线 <code>_</code> 。</p>
<p><img src="/img/remote/1460000019633307?w=777&h=214" alt="20190701132308.png" title="20190701132308.png"></p>
<p>实际上这层转换中会发生很多意想不到的情况。</p>
<h2>列举这些情况</h2>
<p><img src="/img/remote/1460000019633308" alt="20190701134005.png" title="20190701134005.png"></p>
<p>一个简单的测试就出现了意外,一个是单个 <code>[</code> 也会被替换,对于 <code>array</code> 的输入, key 不会做转换。于是我多多测了一下,得出如下列表:</p>
<pre><code><input name="a.b" /> 转为: $_REQUEST["a_b"]
<input name="a b" /> 转为: $_REQUEST["a_b"]
<input name="a[b" /> 转为: $_REQUEST["a_b"]
<input name="a]b" /> 转为: $_REQUEST["a]b"]
<input name="a-b" /> 转为: $_REQUEST["a-b"]
<input name=" ab" /> 转为: $_REQUEST["ab"]
<input name="ab " /> 转为: $_REQUEST["ab "]
<input name="arr[a.b]" /> 转为: $_REQUEST["arr"]["a.b"]
<input name="ar.r[a.b]" /> 转为: $_REQUEST["ar_r"]["a.b"]
<input name="arr[a[b]]" /> 转为: $_REQUEST["arr"]["a[b"]
<input name="arr[a[]x]" /> 转为: $_REQUEST["arr"]["a["]
<input name="arr[]ab" /> 转为: $_REQUEST["arr"][0]
<input name="arr[a]b" /> 转为: $_REQUEST["arr"]["a"]
<input name="arr[a.b" /> 转为: $_REQUEST["arr_a.b"]
<input name="arr[[a.b" /> 转为: $_REQUEST["arr_[a.b"]</code></pre>
<p>这个转换机制十分诡异是吧。查了一下,在 <a href="https://link.segmentfault.com/?enc=uKMaqYzbjtWshGn3P%2F6fOg%3D%3D.B%2BeVxE99oEUrPrIFnNnLCcmU3CAypjbdLxESZoShjPhfrkxrqLGmyrWWUcSja896" rel="nofollow">Bug#77172 convert error on receiving variables from external sources</a> 中提出了 <code>id[]_text</code> 转换成 <code>id[]</code> 的问题,采取的结果是补全文档上的说明。</p>
<p>另外也有几个讨论是否关闭这层转换:</p>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=FLG%2FZihGb2RStODu1x6iQw%3D%3D.rpFBN9YbWRUeMyiSFif%2Fbx%2FzqQxvHwF1wTPVB4Fp2lOf0Y1wnK8kj1oR6Wp8vukA" rel="nofollow">Request #34882 Unable to access <em>original</em> posted variable name with dot in</a> ;</li>
<li>
<a href="https://link.segmentfault.com/?enc=DKbNW2DyUbdjayPQJOlXgw%3D%3D.uJuZfGTCVMGStNOK16VxkZ7d3566LeXVvLYppsxuk5SILfnZguuK%2B1uMJJ%2BBeZkg" rel="nofollow">Request #37040 autoconversion of variable names should be turned off</a> 建议取消这个转换的讨论;</li>
<li>
<a href="https://link.segmentfault.com/?enc=GikPYsBY8JQYjaZXLhOAGg%3D%3D.IC0cqVfY3zsQZROFV7qpkZ3jPyK%2FWK7g32w6m%2FRcgtoP9Ia3kJnskMu8F2%2FJbEgo" rel="nofollow">Request #65252 Input string parsing - allow ' ' and '.' chars as hash key</a> 讨论转换的hash冲突问题;</li>
</ul>
<p>这三个 <code>Request</code> 都还是 open 状态,还没有结果,其中关于关闭转换的讨论早在06年就提出来了。我不清楚 PHP 为什么会做这个转换,目的是什么。据我所知的 java,Django 都不会做转换的。</p>
<p>PHP对于外部输入的变量都会转换的,这就涉及到了 <code>$_POST, $_GET, $_FILES, $_COOKIE, $_REQUEST</code> 这些变量了。</p>
<h2>源码分析</h2>
<p>虽然我没有阅读过php源码,在朋友的帮助下,关于这部分的转换代码在 <code>main/php_variables.c</code> 的 <code>php_register_variable_ex</code> 函数中 <a href="https://link.segmentfault.com/?enc=7oagwI5n%2BwI%2BdINZv%2Fyv%2Bg%3D%3D.OGwhqAvMlqgMyg6bAnwyg9YIkoO4NF2kaNjqNimXrGl%2BTz9T3v5E42bDKpTdB2OZVExKJic59PTjDzEovssJcsgycxa4DLsRQCebS9tNfLlWQ4QW1d7HSlM5H6THZWzYsb8uZEj%2Ft9mXIGISy97v1A%3D%3D" rel="nofollow">php_variables.c#L68</a> ,源码精简了下流程:</p>
<pre><code>PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars_array)
{
char *p = NULL;
char *ip = NULL; /* index pointer */
char *index;
char *var, *var_orig;
/* ignore leading spaces in the variable name */
while (*var_name==' ') { // 忽略前置空格
var_name++;
}
for (p = var; *p; p++) {
if (*p == ' ' || *p == '.') { // 空格和点替换成下划线
*p='_';
} else if (*p == '[') {
is_array = 1; // 如果遇到 [ 则视为数组,is_array 设为1
ip = p;
*p = 0;
break;
}
}
...
}</code></pre>
<p>这里可以看出,忽略前置空格是最先做的动作;当遇到第一个 <code>[</code> 时,php则认为数数组,不再进行转换,设置了 <code>is_array = 1</code> 就 break 了。</p>
<p>这个 <code>is_array</code> 有什么用呢,往下看:</p>
<pre><code>if (is_array) {
int nest_level = 0;
while (1) {
char *index_s;
size_t new_idx_len = 0;
ip++; // [ 的下一个字符
index_s = ip;
if (*ip==']') { // 如果下一个字符就已经是],表示没有设置key
index_s = NULL;
} else {
ip = strchr(ip, ']'); // 查找剩余字符串中的 ]
if (!ip) {
/* PHP variables cannot contain '[' in their names, so we replace the character with a '_' */
*(index_s - 1) = '_'; // 如果没找到,则将 [ 替换成下划线
index_len = 0;
if (index) {
index_len = strlen(index);
}
goto plain_var;
return;
}
*ip = 0;
new_idx_len = strlen(index_s); // key 的长度到第一个出现 ] 为止
}
}
...
}</code></pre>
<p>到此,转化处理的过程就很清晰了,对于数组情况的变量名,分为两种:</p>
<ol>
<li>没找到 <code>]</code> 与其匹配,该变量名不是数组,将 <code>[</code> 替换成下划线,后续字符串不做处理;</li>
<li>有 <code>]</code> 与其匹配,取到第一个出现 <code>]</code> 的位置作为 key ,舍弃后面的字符。</li>
</ol>
<p>对于情况1 就很奇怪了,如果输入是 <code>arr[[a.b</code> 那么就会转成成 <code>arr_[a.b</code> 了。</p>
<h2>总结</h2>
<p>鉴于当前的转换规则总结的规律如下:</p>
<ol>
<li>在第一个 <code>[</code> 之前的字符中,忽略前置的空格,将 <code>.</code> 和 <code>空格</code> 替换成下划线 <code>_</code> ;</li>
<li>
<p>在第一个 <code>[</code> 之后的字符,不再进行替换处理:</p>
<ul>
<li>若后续字符中<strong>没有</strong> <code>]</code> 时,第一个 <code>[</code> 替换成 <code>_</code> ,后续字符串不做转换;</li>
<li>若后续字符中<strong>有</strong> <code>]</code> 时,取到第一次出现 <code>]</code> 的位置作为 key,舍弃后续字符。</li>
</ul>
</li>
</ol>
<p>另外,谁能告诉我PHP的这层转换的设计初衷是什么啊。</p>
Python 中 is 语法带来的误解
https://segmentfault.com/a/1190000019085547
2019-05-06T15:10:20+08:00
2019-05-06T15:10:20+08:00
weapon
https://segmentfault.com/u/weapon
3
<h2>起步</h2>
<p>Python 的成功一个原因是它的可读性,代码清晰易懂,更容易被人类所理解,但有时可读性会产生误解。</p>
<p>假如要判断一个变量是不是 17,那可以:</p>
<pre><code>if x is 17:</code></pre>
<p><code>x 是 17</code> 肯定是比 <code>x == 17</code> 更加口语化的。</p>
<h2>is的误解</h2>
<p>但是如果你尝试:</p>
<pre><code>if name is "weapon":</code></pre>
<p>这个判断不见得管用。<code>is</code> 用来检查左侧和右侧是否是完全相同的对象。如果有两个不同的字符串对象,每个对象的值是相同的,应该使用 <code>==</code> 来判断,因为 is 的用法与口语上的区别挺大的:</p>
<pre><code>if 999 + 1 is 1000: # False</code></pre>
<p>正因为这样的误解,在 <code>if</code> 判断条件上容易让初学者掉坑:</p>
<pre><code>answer = 'yes'
if answer is 'y' or 'yes':</code></pre>
<p>你会发现不管变量是什么值,判断都是为真。因为 <code>is</code> 的优先级高,相当于 <code>if (answer is 'y') or ('yes')</code> 。</p>
<p>正确的方法应该是 <code>if answer == 'y' or answer == 'yes'</code> 或者 <code>if answer in ('y', 'yes')</code> 。</p>
<h2>is not 上的混淆</h2>
<pre><code>>>> 'something' is not None
True
>>> 'something' is (not None)
False</code></pre>
<p><code>is not</code> 是一个二元运算符,应该视为一个整体,不要因为中间空格而当成两个词。底层上,它们也是一个操作符,CPython 将 <code>s is not None</code> 翻译成的字节码为:</p>
<pre><code> 6 LOAD_NAME 0 (s)
8 LOAD_CONST 1 (None)
10 COMPARE_OP 9 (is not)</code></pre>
<p><code>is not</code> 是对 <code>is</code> 相对应的操作符。也可以视为是将 is 判断的结果再进行取反。</p>
<h2>总结</h2>
<p>我同意 Python 非常易读。每种语言的结构都存在一些“出乎意料”的使用。这并不影响我对 Python 这门语言的喜爱,每个人都应该好好学习,并小心使用选择的语言。</p>
Python 模块源码分析:queue 队列
https://segmentfault.com/a/1190000018773080
2019-04-05T19:41:25+08:00
2019-04-05T19:41:25+08:00
weapon
https://segmentfault.com/u/weapon
9
<h2>起步</h2>
<p><code>queue</code> 模块提供适用于多线程编程的先进先出(FIFO)数据结构。因为它是线程安全的,所以多个线程很轻松地使用同一个实例。</p>
<h2>源码分析</h2>
<p>先从初始化的函数来看:</p>
<pre><code>class Queue:
def __init__(self, maxsize=0):
# 设置队列的最大容量
self.maxsize = maxsize
self._init(maxsize)
# 线程锁,互斥变量
self.mutex = threading.Lock()
# 由锁衍生出三个条件变量
self.not_empty = threading.Condition(self.mutex)
self.not_full = threading.Condition(self.mutex)
self.all_tasks_done = threading.Condition(self.mutex)
self.unfinished_tasks = 0
def _init(self, maxsize):
# 初始化底层数据结构
self.queue = deque()</code></pre>
<p>从这初始化函数能得到哪些信息呢?首先,队列是可以设置其容量大小的,并且具体的底层存放元素的它使用了 <code>collections.deque()</code> 双端列表的数据结构,这使得能很方便的做先进先出操作。这里还特地抽象为 <code>_init</code> 函数是为了方便其子类进行覆盖,允许子类使用其他结构来存放元素(比如优先队列使用了 <code>list</code>)。</p>
<p>然后就是线程锁 <code>self.mutex</code> ,对于底层数据结构 <code>self.queue</code> 的操作都要先获得这把锁;再往下是三个条件变量,这三个 <code>Condition</code> 都以 <code>self.mutex</code> 作为参数,也就是说它们共用一把锁;从这可以知道诸如 <code>with self.mutex</code> 与 <code>with self.not_empty</code> 等都是互斥的。</p>
<p>基于这些锁而做的一些简单的操作:</p>
<pre><code>class Queue:
...
def qsize(self):
# 返回队列中的元素数
with self.mutex:
return self._qsize()
def empty(self):
# 队列是否为空
with self.mutex:
return not self._qsize()
def full(self):
# 队列是否已满
with self.mutex:
return 0 < self.maxsize <= self._qsize()
def _qsize(self):
return len(self.queue)</code></pre>
<p>这个代码片段挺好理解的,无需分析。</p>
<p>作为队列,主要得完成入队与出队的操作,首先是入队:</p>
<pre><code>class Queue:
...
def put(self, item, block=True, timeout=None):
with self.not_full: # 获取条件变量not_full
if self.maxsize > 0:
if not block:
if self._qsize() >= self.maxsize:
raise Full # 如果 block 是 False,并且队列已满,那么抛出 Full 异常
elif timeout is None:
while self._qsize() >= self.maxsize:
self.not_full.wait() # 阻塞直到由剩余空间
elif timeout < 0: # 不合格的参数值,抛出ValueError
raise ValueError("'timeout' must be a non-negative number")
else:
endtime = time() + timeout # 计算等待的结束时间
while self._qsize() >= self.maxsize:
remaining = endtime - time()
if remaining <= 0.0:
raise Full # 等待期间一直没空间,抛出 Full 异常
self.not_full.wait(remaining)
self._put(item) # 往底层数据结构中加入一个元素
self.unfinished_tasks += 1
self.not_empty.notify()
def _put(self, item):
self.queue.append(item)</code></pre>
<p>尽管只有二十几行的代码,但这里的逻辑还是比较复杂的。它要处理超时与队列剩余空间不足的情况,具体几种情况如下:</p>
<ol>
<li>
<p>如果 <code>block</code> 是 False,忽略timeout参数</p>
<ul>
<li>若此时队列已满,则抛出 Full 异常;</li>
<li>若此时队列未满,则立即把元素保存到底层数据结构中;</li>
</ul>
</li>
<li>
<p>如果 <code>block</code> 是 True</p>
<ul>
<li>若 <code>timeout</code> 是 <code>None</code> 时,那么put操作可能会阻塞,直到队列中有空闲的空间(默认);</li>
<li>若 <code>timeout</code> 是非负数,则会阻塞相应时间直到队列中有剩余空间,在这个期间,如果队列中一直没有空间,抛出 <code>Full</code> 异常;</li>
</ul>
</li>
</ol>
<p>处理好参数逻辑后,,将元素保存到底层数据结构中,并递增<code>unfinished_tasks</code>,同时通知 <code>not_empty</code> ,唤醒在其中等待数据的线程。</p>
<p>出队操作:</p>
<pre><code>class Queue:
...
def get(self, block=True, timeout=None):
with self.not_empty:
if not block:
if not self._qsize():
raise Empty
elif timeout is None:
while not self._qsize():
self.not_empty.wait()
elif timeout < 0:
raise ValueError("'timeout' must be a non-negative number")
else:
endtime = time() + timeout
while not self._qsize():
remaining = endtime - time()
if remaining <= 0.0:
raise Empty
self.not_empty.wait(remaining)
item = self._get()
self.not_full.notify()
return item
def _get(self):
return self.queue.popleft()</code></pre>
<p><code>get()</code> 操作是 <code>put()</code> 相反的操作,代码块也及其相似,<code>get()</code> 是从队列中移除最先插入的元素并将其返回。</p>
<ol>
<li>
<p>如果 <code>block</code> 是 False,忽略timeout参数</p>
<ul>
<li>若此时队列没有元素,则抛出 Empty 异常;</li>
<li>若此时队列由元素,则立即把元素保存到底层数据结构中;</li>
</ul>
</li>
<li>
<p>如果 <code>block</code> 是 True</p>
<ul>
<li>若 <code>timeout</code> 是 <code>None</code> 时,那么get操作可能会阻塞,直到队列中有元素(默认);</li>
<li>若 <code>timeout</code> 是非负数,则会阻塞相应时间直到队列中有元素,在这个期间,如果队列中一直没有元素,则抛出 <code>Empty</code> 异常;</li>
</ul>
</li>
</ol>
<p>最后,通过 <code>self.queue.popleft()</code> 将最早放入队列的元素移除,并通知 <code>not_full</code> ,唤醒在其中等待数据的线程。</p>
<p>这里有个值得注意的地方,在 <code>put()</code> 操作中递增了 <code>self.unfinished_tasks</code> ,而 <code>get()</code> 中却没有递减,这是为什么?</p>
<p>这其实是为了留给用户一个消费元素的时间,<code>get()</code> 仅仅是获取元素,并不代表消费者线程处理的该元素,用户需要调用 <code>task_done()</code> 来通知队列该任务处理完成了:</p>
<pre><code>class Queue:
...
def task_done(self):
with self.all_tasks_done:
unfinished = self.unfinished_tasks - 1
if unfinished <= 0:
if unfinished < 0: # 也就是成功调用put()的次数小于调用task_done()的次数时,会抛出异常
raise ValueError('task_done() called too many times')
self.all_tasks_done.notify_all() # 当unfinished为0时,会通知all_tasks_done
self.unfinished_tasks = unfinished
def join(self):
with self.all_tasks_done:
while self.unfinished_tasks: # 如果有未完成的任务,将调用wait()方法等待
self.all_tasks_done.wait()</code></pre>
<p>由于 <code>task_done()</code> 使用方调用的,当 <code>task_done()</code> 次数大于 <code>put()</code> 次数时会抛出异常。</p>
<p><code>task_done()</code> 操作的作用是唤醒正在阻塞的 <code>join()</code> 操作。<code>join()</code> 方法会一直阻塞,直到队列中所有的元素都被取出,并被处理了(和线程的join方法类似)。也就是说 <code>join()</code> 方法必须配合 <code>task_done()</code> 来使用才行。</p>
<h2>LIFO 后进先出队列</h2>
<p>LifoQueue使用后进先出顺序,与栈结构相似:</p>
<pre><code>class LifoQueue(Queue):
'''Variant of Queue that retrieves most recently added entries first.'''
def _init(self, maxsize):
self.queue = []
def _qsize(self):
return len(self.queue)
def _put(self, item):
self.queue.append(item)
def _get(self):
return self.queue.pop()</code></pre>
<p>这就是 <code>LifoQueue</code> 全部代码了,这正是 <code>Queue</code> 设计很棒的一个原因,它将底层的数据操作抽象成四个操作函数,本身来处理线程安全的问题,使得其子类只需关注底层的操作。</p>
<p>LifoQueue 底层数据结构改用 <code>list</code> 来存放,通过 <code>self.queue.pop()</code> 就能将 list 中最后一个元素移除,无需重置索引。</p>
<h2>PriorityQueue 优先队列</h2>
<pre><code>from heapq import heappush, heappop
class PriorityQueue(Queue):
'''Variant of Queue that retrieves open entries in priority order (lowest first).
Entries are typically tuples of the form: (priority number, data).
'''
def _init(self, maxsize):
self.queue = []
def _qsize(self):
return len(self.queue)
def _put(self, item):
heappush(self.queue, item)
def _get(self):
return heappop(self.queue)</code></pre>
<p>优先队列使用了 <code>heapq</code> 模块的结构,也就是最小堆的结构。优先队列更为常用,队列中项目的处理顺序需要基于这些项目的特征,一个简单的例子:</p>
<pre><code>import queue
class A:
def __init__(self, priority, value):
self.priority = priority
self.value = value
def __lt__(self, other):
return self.priority < other.priority
q = queue.PriorityQueue()
q.put(A(1, 'a'))
q.put(A(0, 'b'))
q.put(A(1, 'c'))
print(q.get().value) # 'b'</code></pre>
<p>使用优先队列的时候,需要定义 <code>__lt__</code> 魔术方法,来定义它们之间如何比较大小。若元素的 <code>priority</code> 相同,依然使用先进先出的顺序。</p>
<h2>参考</h2>
<ul><li>
<a href="https://link.segmentfault.com/?enc=alP5ZDK4YXyDcLyyk%2ByVMg%3D%3D.G0NYBPCXu3gtsF%2FZkXHwREyBbE5L35UZryK4I9fTfMRB5gdoNBA7QVKAL0J20PGu" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=rXR%2BN5UfIIqTd5fx2ZKUPw%3D%3D.gzZvhfISJWOQMIiir3yIrVtrq7swAG7XqPdu8IsUI8tH%2BcjhWCQBWYVWzhJwFerG" rel="nofollow">https://pymotw.com/3/queue/in...</a>
</li></ul>
Python 的 heapq 模块源码分析
https://segmentfault.com/a/1190000017793857
2019-01-07T15:55:32+08:00
2019-01-07T15:55:32+08:00
weapon
https://segmentfault.com/u/weapon
2
<p>原文链接:<a href="https://link.segmentfault.com/?enc=50RhxV%2FMr06ZahJIDX%2F3Ww%3D%3D.aUFFFX6Vcc1GUM0Aahs4McfDIh%2B3m7EjtfAjLN8uMZOguqBXsFbZJXxLXCFKRLIsxfRGT9MPZutTOtuZyLgF%2FQ%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=Pr2fy%2FX6qq4APx1Db876RQ%3D%3D.XR8C6wUlW43i%2Fdgk%2BCMxDS1X%2BSXDb%2BiYhnGFf831EU2%2FkW3vYWZvHbPUFzjm3GdTUXafaJqo5iD3ETnR8wcA4A%3D%3D" rel="nofollow">https://www.hongweipeng.com/i...</a></p>
<h2>起步</h2>
<p>heapq 模块实现了适用于Python列表的最小堆排序算法。</p>
<p><img src="/img/remote/1460000017793860?w=613&h=221" alt="20190107115023.jpg" title="20190107115023.jpg"></p>
<p>堆是一个树状的数据结构,其中的子节点都与父母排序顺序关系。因为堆排序中的树是满二叉树,因此可以用列表来表示树的结构,使得元素 <code>N</code> 的子元素位于 <code>2N + 1</code> 和 <code>2N + 2</code> 的位置(对于从零开始的索引)。</p>
<p>本文内容将分为三个部分,第一个部分简单介绍 <code>heapq</code> 模块的使用;第二部分回顾堆排序算法;第三部分分析heapq中的实现。</p>
<h2>heapq 的使用</h2>
<p>创建堆有两个基本的方法:<code>heappush()</code> 和 <code>heapify()</code>,取出堆顶元素用 <code>heappop()</code>。</p>
<p><code>heappush()</code> 是用来向已有的堆中添加元素,一般从空列表开始构建:</p>
<pre><code>import heapq
data = [97, 38, 27, 50, 76, 65, 49, 13]
heap = []
for n in data:
heapq.heappush(heap, n)
print('pop:', heapq.heappop(heap)) # pop: 13
print(heap) # [27, 50, 38, 97, 76, 65, 49]</code></pre>
<p>如果数据已经在列表中,则使用 <code>heapify()</code> 进行重排:</p>
<pre><code>import heapq
data = [97, 38, 27, 50, 76, 65, 49, 13]
heapq.heapify(data)
print('pop:', heapq.heappop(data)) # pop: 13
print(data) # [27, 38, 49, 50, 76, 65, 97]</code></pre>
<h2>回顾堆排序算法</h2>
<p>堆排序算法基本思想是:将无序序列建成一个堆,得到关键字最小(或最大的记录;输出堆顶的最小 (大)值后,使剩余的 n-1 个元素 重又建成一个堆,则可得到n个元素的次小值 ;重复执行,得到一个有序序列,这个就是堆排序的过程。</p>
<p>堆排序需要解决两个问题:</p>
<ol>
<li>如何由一个无序序列建立成一个堆?</li>
<li>如何在输出堆顶元素之后,调整剩余元素,使之成为一个新的堆?</li>
<li>新添加元素和,如何调整堆?</li>
</ol>
<p>先来看看第二个问题的解决方法。采用的方法叫“筛选”,当输出堆顶元素之后,就将堆中最后一个元素代替之;然后将根结点值与左、右子树的根结点值进行比较 ,并与其中小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”。</p>
<p><img src="/img/remote/1460000017793861?w=682&h=507" alt="20190107132151.jpg" title="20190107132151.jpg"></p>
<p>如上图所示,当堆顶 13 输出后,将堆中末尾的 97 替代为堆顶,然后堆顶与它的子节点 38 和 27 中的小者交换;元素 97 在新的位置上在和它的子节点 65 和 49 中的小者交换;直到元素97成为叶节点,就得到了新的堆。这个过程也叫 <code>下沉</code> 。</p>
<p>让堆中位置为 <code>pos</code> 元素进行下沉的如下:</p>
<pre><code>def heapdown(heap, pos):
endpos = len(heap)
while pos < endpos:
lchild = 2 * pos + 1
rchild = 2 * pos + 2
if lchild >= endpos: # 如果pos已经是叶节点,退出循环
break
childpos = lchild # 假设要交换的节点是左节点
if rchild < endpos and heap[childpos] > heap[rchild]:
childpos = rchild
if heap[pos] < heap[childpos]: # 如果节点比子节点都小,退出循环
break
heap[pos], heap[childpos] = heap[childpos], heap[pos] # 交换
pos = childpos</code></pre>
<p>再来看看如何解决第三个问题:新添加元素和,如何调整堆?这个的方法正好与 <code>下沉</code> 相反,首先将新元素放置列表的最后,然后新元素与其父节点比较,若比父节点小,与父节点交换;重复过程直到比父节点大或到根节点。这个过程使得元素从底部不断上升,从下至上恢复堆的顺序,称为 <code>上浮</code> 。</p>
<p>将位置为 <code>pos</code> 进行上浮的代码为:</p>
<pre><code>def heapup(heap, startpos, pos): # 如果是新增元素,startpos 传入 0
while pos > startpos:
parentpos = (pos - 1) // 2
if heap[pos] < heap[parentpos]:
heap[pos], heap[parentpos] = heap[parentpos], heap[pos]
pos = parentpos
else:
break</code></pre>
<p>第一个问题:如何由一个无序序列建立成一个堆?从无序序列的第 n/2 个元素 (即此无序序列对应的完全二叉树的最后一个非终端结点 )起 ,至第一个元素止,依次进行下沉:</p>
<p><img src="/img/remote/1460000017793862?w=681&h=506" alt="20190107145317.jpg" title="20190107145317.jpg"></p>
<pre><code>for i in reversed(range(len(data) // 2)):
heapdown(data, i)</code></pre>
<h2>heapq 源码分析</h2>
<p>添加新元素到堆中的 <code>heappush()</code> 函数:</p>
<pre><code>def heappush(heap, item):
"""Push item onto heap, maintaining the heap invariant."""
heap.append(item)
_siftdown(heap, 0, len(heap)-1)</code></pre>
<p>把目标元素放置列表最后,然后进行上浮。尽管它命名叫 down ,但这个过程是上浮的过程,这个命名也让我困惑,后来我才知道它是因为元素的索引不断减小,所以命名 <code>down</code> 。下沉的过程它也就命名为 <code>up</code> 了。</p>
<pre><code>def _siftdown(heap, startpos, pos):
newitem = heap[pos]
# Follow the path to the root, moving parents down until finding a place
# newitem fits.
while pos > startpos:
parentpos = (pos - 1) >> 1
parent = heap[parentpos]
if newitem < parent:
heap[pos] = parent
pos = parentpos
continue
break
heap[pos] = newitem</code></pre>
<p>一样是通过 <code>newitem</code> 不断与父节点比较。不一样的是这里缺少了元素交换的过程,而是计算出新元素最后所在的位置 <code>pos</code> 并进行的赋值。显然这是优化后的代码,减少了不断交换元素的冗余过程。</p>
<p>再来看看输出堆顶元素的函数 <code>heappop()</code>:</p>
<pre><code>def heappop(heap):
"""Pop the smallest item off the heap, maintaining the heap invariant."""
lastelt = heap.pop() # raises appropriate IndexError if heap is empty
if heap:
returnitem = heap[0]
heap[0] = lastelt
_siftup(heap, 0)
return returnitem
return lastelt</code></pre>
<p>通过 <code>heap.pop()</code> 获得列表中的最后一个元素,然后替换为堆顶 <code>heap[0] = lastelt</code> ,再进行下沉:</p>
<pre><code>def _siftup(heap, pos):
endpos = len(heap)
startpos = pos
newitem = heap[pos]
# Bubble up the smaller child until hitting a leaf.
childpos = 2*pos + 1 # 左节点,默认替换左节点
while childpos < endpos:
# Set childpos to index of smaller child.
rightpos = childpos + 1 # 右节点
if rightpos < endpos and not heap[childpos] < heap[rightpos]:
childpos = rightpos # 当右节点比较小时,应交换的是右节点
# Move the smaller child up.
heap[pos] = heap[childpos]
pos = childpos
childpos = 2*pos + 1
# The leaf at pos is empty now. Put newitem there, and bubble it up
# to its final resting place (by sifting its parents down).
heap[pos] = newitem
_siftdown(heap, startpos, pos)</code></pre>
<p>这边的代码将准备要下沉的元素视为新元素 <code>newitem</code> ,将其当前的位置 pos 视为空位置,由其子节点中的小者进行取代,反复如此,最后会在叶节点留出一个位置,这个位置放入 <code>newitem</code> ,再让新元素进行上浮。</p>
<p>再来看看让无序数列重排成堆的 <code>heapify()</code> 函数:</p>
<pre><code>def heapify(x):
"""Transform list into a heap, in-place, in O(len(x)) time."""
n = len(x)
for i in reversed(range(n//2)):
_siftup(x, i)</code></pre>
<p>这部分就和理论上的一致,从最后一个非叶节点 (n // 2) 到根节点为止,进行下沉。</p>
<h2>总结</h2>
<p>堆排序结合图来理解还是比较好理解的。这种数据结构常用于优先队列(标准库Queue的优先队列用的就是堆)。 <code>heapq</code> 模块中还有很多其他 <code>heapreplace</code> ,<code>heappushpop</code> 等大体上都很类似。</p>
raise 与 raise ... from 的区别
https://segmentfault.com/a/1190000017332255
2018-12-11T11:16:42+08:00
2018-12-11T11:16:42+08:00
weapon
https://segmentfault.com/u/weapon
6
<h2>起步</h2>
<p>Python 的 <code>raise</code> 和 <code>raise from</code> 之间的区别是什么?</p>
<pre><code>try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened")</code></pre>
<p>输出:</p>
<pre><code>Traceback (most recent call last):
File "test4.py", line 2, in <module>
print(1 / 0)
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test4.py", line 4, in <module>
raise RuntimeError("Something bad happened")
RuntimeError: Something bad happened</code></pre>
<p>而 <code>raise from</code> :</p>
<pre><code>try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened") from exc</code></pre>
<p>输出:</p>
<pre><code>Traceback (most recent call last):
File "test4.py", line 2, in <module>
print(1 / 0)
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test4.py", line 4, in <module>
raise RuntimeError("Something bad happened") from exc
RuntimeError: Something bad happened</code></pre>
<h2>分析</h2>
<p>不同之处在于,<code>from</code> 会为异常对象设置 <code>__cause__</code> 属性表明异常的是由谁直接引起的。</p>
<p>处理异常时发生了新的异常,在不使用 <code>from</code> 时更倾向于新异常与正在处理的异常没有关联。而 <code>from</code> 则是能指出新异常是因旧异常直接引起的。这样的异常之间的关联有助于后续对异常的分析和排查。<code>from</code> 语法会有个限制,就是第二个表达式必须是另一个异常类或实例。</p>
<p>如果在异常处理程序或 <code>finally</code> 块中引发异常,默认情况下,异常机制会隐式工作会将先前的异常附加为新异常的 <code>__context__</code> 属性。</p>
<p>当然,也可以通过 <code>with_traceback()</code> 方法为异常设置上下文 <code>__context__</code> 属性,这也能在 <code>traceback</code> 更好的显示异常信息。</p>
<pre><code>raise Exception("foo occurred").with_traceback(tracebackobj)</code></pre>
<h2>禁止异常关联</h2>
<p>from 还有个特别的用法:<code>raise ... from None</code> ,它通过设置 <code>__suppress_context__</code> 属性指定来明确禁止异常关联:</p>
<pre><code>try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened") from None</code></pre>
<p>输出:</p>
<pre><code>Traceback (most recent call last):
File "test4.py", line 4, in <module>
raise RuntimeError("Something bad happened") from None
RuntimeError: Something bad happened</code></pre>
<h2>总结</h2>
<p>在异常处理程序或 <code>finally</code> 块中引发异常,Python 会为异常设置上下文,可以手动通过 <code>with_traceback()</code> 设置其上下文,或者通过 <code>from</code> 来指定异常因谁引起的。这些手段都是为了得到更友好的异常回溯信息,打印清晰的异常上下文。若要忽略上下文,则可以通过 <code>raise ... from None</code> 来禁止自动显示异常上下文。</p>
<h2>参考</h2>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=%2BIwnUSWb3lOPi6kV918yIQ%3D%3D.S42nnMcaZ3AT3FWT9RxyvCl9HTol%2BEYFfAXYuwWZjSiuNHsdyBxjsBxDebaMiuWyQFYp32Qmb%2BP37jarHjDejg%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=ReMZB4%2FQXQd2lQ1JLdLGKg%3D%3D.0%2FQ7d8tRcxSE2iVZYmwD8emRyfMQWPc5TOrNJ9Ekw%2FjvACpC%2Fzl5MUkUJCJe3d5ze%2BIG8ZCWKXCUOZQ9%2BA2s1w%3D%3D" rel="nofollow">https://docs.python.org/3/ref...</a>
</li>
<li>
<a href="https://link.segmentfault.com/?enc=a46bvc5QBw5qFgsFzZfzsA%3D%3D.WnnCtBQfmWifSzpGYMLXZ9wp%2B69NG9CDxjaKQia4sW75Xoullumuef%2FF2JAk2bY%2Fouvta2aI49sTDKwWVw7cPg%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=0UIy2B4EwfR9elaxbxTWqA%3D%3D.ZcDrFZ0JYUniImbfVyBdpNZ67sbAWSqXdgOw30uVDJngvO7JPLVALV7tX7ZerKNN%2BYxqIZRkkuzWAH1iXa9DNA%3D%3D" rel="nofollow">https://docs.python.org/3/lib...</a>
</li>
<li><a href="https://link.segmentfault.com/?enc=aQQeB5v9YoQSV9UJAijehA%3D%3D.me5NNKg9QKd6xDmP4kv0jZgTB1AhlA%2ByflkQ0yo2Nh%2FeIUYHNLP2CLklGa1hCKf5" rel="nofollow">PEP 3109 -- Raising Exceptions in Python 3000</a></li>
<li><a href="https://link.segmentfault.com/?enc=TJJ7S6wHQL%2BniAgkfM7cJg%3D%3D.NHcC6lr5NI9WP1lyIUYbU6YKxnahdDpG1%2Fp8rjcjMEXGOyOfiQ0wqZ1nejR3wUgr" rel="nofollow">PEP 3134 -- Exception Chaining and Embedded Tracebacks</a></li>
</ul>
Python 的 enum 模块源码分析
https://segmentfault.com/a/1190000017327118
2018-12-10T21:14:24+08:00
2018-12-10T21:14:24+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2>
<p>上一篇 《<a href="https://segmentfault.com/a/1190000017327003">Python 的枚举类型</a>》 文末说有机会的话可以看看它的源码。那就来读一读,看看枚举的几个重要的特性是如何实现的。</p>
<p>要想阅读这部分,需要对元类编程有所了解。</p>
<h2>成员名不允许重复</h2>
<p>这部分我的第一个想法是去控制 <code>__dict__</code> 中的 key 。但这样的方式并不好,<code>__dict__</code> 范围大,它包含该类的所有属性和方法。而不单单是枚举的命名空间。我在源码中发现 enum 使用另一个方法。通过 <code>__prepare__</code> 魔术方法可以返回一个类字典实例,在该实例<br>使用 <code>__prepare__</code> 魔术方法自定义命名空间,在该空间内限定成员名不允许重复。</p>
<pre><code># 自己实现
class _Dict(dict):
def __setitem__(self, key, value):
if key in self:
raise TypeError('Attempted to reuse key: %r' % key)
super().__setitem__(key, value)
class MyMeta(type):
@classmethod
def __prepare__(metacls, name, bases):
d = _Dict()
return d
class Enum(metaclass=MyMeta):
pass
class Color(Enum):
red = 1
red = 1 # TypeError: Attempted to reuse key: 'red'</code></pre>
<p>再看看 Enum 模块的具体实现:</p>
<pre><code>class _EnumDict(dict):
def __init__(self):
super().__init__()
self._member_names = []
...
def __setitem__(self, key, value):
...
elif key in self._member_names:
# descriptor overwriting an enum?
raise TypeError('Attempted to reuse key: %r' % key)
...
self._member_names.append(key)
super().__setitem__(key, value)
class EnumMeta(type):
@classmethod
def __prepare__(metacls, cls, bases):
enum_dict = _EnumDict()
...
return enum_dict
class Enum(metaclass=EnumMeta):
...</code></pre>
<p>模块中的 <code>_EnumDict</code> 创建了 <code>_member_names</code> 列表来存储成员名,这是因为不是所有的命名空间内的成员都是枚举的成员。比如 <code>__str__</code>, <code>__new__</code> 等魔术方法就不是了,所以这边的 <code>__setitem__</code> 需要做一些过滤:</p>
<pre><code>def __setitem__(self, key, value):
if _is_sunder(key): # 下划线开头和结尾的,如 _order__
raise ValueError('_names_ are reserved for future Enum use')
elif _is_dunder(key): # 双下划线结尾的, 如 __new__
if key == '__order__':
key = '_order_'
elif key in self._member_names: # 重复定义的 key
raise TypeError('Attempted to reuse key: %r' % key)
elif not _is_descriptor(value): # value得不是描述符
self._member_names.append(key)
self._last_values.append(value)
super().__setitem__(key, value)</code></pre>
<p>模块考虑的会更全面。</p>
<h2>每个成员都有名称属性和值属性</h2>
<p>上述的代码中,<code>Color.red</code> 取得的值是 1。而 eumu 模块中,定义的枚举类中,每个成员都是有名称和属性值的;并且细心的话还会发现 <code>Color.red</code> 是 <code>Color</code> 的示例。这样的情况是如何来实现的呢。</p>
<p>还是用元类来完成,在元类的 <code>__new__</code> 中实现,具体的思路是,先创建目标类,然后为每个成员都创建一样的类,再通过 <code>setattr</code> 的方式将后续的类作为属性添加到目标类中,伪代码如下:</p>
<pre><code>def __new__(metacls, cls, bases, classdict):
__new__ = cls.__new__
# 创建枚举类
enum_class = super().__new__()
# 每个成员都是cls的示例,通过setattr注入到目标类中
for name, value in cls.members.items():
member = super().__new__()
member.name = name
member.value = value
setattr(enum_class, name, member)
return enum_class</code></pre>
<p>来看下一个可运行的demo:</p>
<pre><code>class _Dict(dict):
def __init__(self):
super().__init__()
self._member_names = []
def __setitem__(self, key, value):
if key in self:
raise TypeError('Attempted to reuse key: %r' % key)
if not key.startswith("_"):
self._member_names.append(key)
super().__setitem__(key, value)
class MyMeta(type):
@classmethod
def __prepare__(metacls, name, bases):
d = _Dict()
return d
def __new__(metacls, cls, bases, classdict):
__new__ = bases[0].__new__ if bases else object.__new__
# 创建枚举类
enum_class = super().__new__(metacls, cls, bases, classdict)
# 创建成员
for member_name in classdict._member_names:
value = classdict[member_name]
enum_member = __new__(enum_class)
enum_member.name = member_name
enum_member.value = value
setattr(enum_class, member_name, enum_member)
return enum_class
class MyEnum(metaclass=MyMeta):
pass
class Color(MyEnum):
red = 1
blue = 2
def __str__(self):
return "%s.%s" % (self.__class__.__name__, self.name)
print(Color.red) # Color.red
print(Color.red.name) # red
print(Color.red.value) # 1</code></pre>
<p>enum 模块在让每个成员都有名称和值的属性的实现思路是一样的(代码我就不贴了)。<code>EnumMeta.__new__</code> 是该模块的重点,几乎所有枚举的特性都在这个函数实现。</p>
<h2>当成员值相同时,第二个成员是第一个成员的别名</h2>
<p>从这节开始就不再使用自己实现的类的说明了,而是通过拆解 enum 模块的代码来说明其实现了,从模块的使用特性中可以知道,如果成员值相同,后者会是前者的一个别名:</p>
<pre><code>from enum import Enum
class Color(Enum):
red = 1
_red = 1
print(Color.red is Color._red) # True</code></pre>
<p>从这可以知道,red和_red是同一对象。这又要怎么实现呢?</p>
<p>元类会为枚举类创建 <code>_member_map_</code> 属性来存储成员名与成员的映射关系,如果发现创建的成员的值已经在映射关系中了,就会用映射表中的对象来取代:</p>
<pre><code>class EnumMeta(type):
def __new__(metacls, cls, bases, classdict):
...
# create our new Enum type
enum_class = super().__new__(metacls, cls, bases, classdict)
enum_class._member_names_ = [] # names in definition order
enum_class._member_map_ = OrderedDict() # name->value map
for member_name in classdict._member_names:
enum_member = __new__(enum_class)
# If another member with the same value was already defined, the
# new member becomes an alias to the existing one.
for name, canonical_member in enum_class._member_map_.items():
if canonical_member._value_ == enum_member._value_:
enum_member = canonical_member # 取代
break
else:
# Aliases don't appear in member names (only in __members__).
enum_class._member_names_.append(member_name) # 新成员,添加到_member_names_中
enum_class._member_map_[member_name] = enum_member
...</code></pre>
<p>从代码上来看,即使是成员值相同,还是会先为他们都创建对象,不过后创建的很快就会被垃圾回收掉了(我认为这边是有优化空间的)。通过与 <code>_member_map_</code> 映射表做对比,用以创建该成员值的成员取代后续,但两者成员名都会在 <code>_member_map_</code> 中,如例子中的 <code>red</code> 和 <code>_red</code> 都在该字典,但他们指向的是同一个对象。</p>
<p>属性 <code>_member_names_ </code> 只会记录第一个,这将会与枚举的迭代有关。</p>
<h2>可以通过成员值来获取成员</h2>
<pre><code>print(Color['red']) # Color.red 通过成员名来获取成员
print(Color(1)) # Color.red 通过成员值来获取成员</code></pre>
<p>枚举类中的成员都是单例模式,元类创建的枚举类中还维护了值到成员的映射关系 <code>_value2member_map_</code> :</p>
<pre><code>class EnumMeta(type):
def __new__(metacls, cls, bases, classdict):
...
# create our new Enum type
enum_class = super().__new__(metacls, cls, bases, classdict)
enum_class._value2member_map_ = {}
for member_name in classdict._member_names:
value = enum_members[member_name]
enum_member = __new__(enum_class)
enum_class._value2member_map_[value] = enum_member
...</code></pre>
<p>然后在 Enum 的 <code>__new__</code> 返回该单例即可:</p>
<pre><code>class Enum(metaclass=EnumMeta):
def __new__(cls, value):
if type(value) is cls:
return value
# 尝试从 _value2member_map_ 获取
try:
if value in cls._value2member_map_:
return cls._value2member_map_[value]
except TypeError:
# 从 _member_map_ 映射获取
for member in cls._member_map_.values():
if member._value_ == value:
return member
raise ValueError("%r is not a valid %s" % (value, cls.__name__))</code></pre>
<h2>迭代的方式遍历成员</h2>
<p>枚举类支持迭代的方式遍历成员,按定义的顺序,如果有值重复的成员,只获取重复的第一个成员。对于重复的成员值只获取第一个成员,正好属性 <code>_member_names_ </code> 只会记录第一个:</p>
<pre><code>class Enum(metaclass=EnumMeta):
def __iter__(cls):
return (cls._member_map_[name] for name in cls._member_names_)</code></pre>
<h2>总结</h2>
<p>enum 模块的核心特性的实现思路就是这样,几乎都是通过元类黑魔法来实现的。对于成员之间不能做比较大小但可以做等值比较。这反而不需要讲,这其实继承自 object 就是这样的,不用额外做什么就有的“特性”了。</p>
<p>总之,enum 模块相对独立,且代码量不多,对于想知道元类编程可以阅读一下,教科书式教学,还有单例模式等,值得一读。</p>
Python 的枚举类型
https://segmentfault.com/a/1190000017327003
2018-12-10T20:59:22+08:00
2018-12-10T20:59:22+08:00
weapon
https://segmentfault.com/u/weapon
35
<h2>起步</h2>
<p>Python 的原生类型中并不包含枚举类型。为了提供更好的解决方案,Python 通过 <a href="https://link.segmentfault.com/?enc=Sh1WxO3By6aKWLrt3m%2FlhA%3D%3D.VifzhSMP0Dq6dUEhN6VbM5RRgnlB3jCjgEGuUSu3OK47iufMQQmMDL7RQBXjKAGI" rel="nofollow">PEP 435</a> 在 3.4 版本中添加了 <code>enum</code> 标准库。</p>
<p>枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期、月份、状态等。在没有专门提供枚举类型的时候我们是怎么做呢,一般就通过字典或类来实现:</p>
<pre><code>Color = {
'RED' : 1,
'GREEN': 2,
'BLUE' : 3,
}
class Color:
RED = 1
GREEN = 2
BLUE = 3</code></pre>
<p>这种来实现枚举如果小心翼翼地使用当然没什么问题,毕竟是一种妥协的解决方案。它的隐患在于可以被修改。</p>
<h2>使用 Enum</h2>
<p>更好的方式是使用标准库提供的 <code>Enum</code> 类型,官方库值得信赖。3.4 之前的版本也可以通过 <code>pip install enum</code> 下载支持的库。简单的示例:</p>
<pre><code>from enum import Enum
class Color(Enum):
red = 1
green = 2
blue = 3</code></pre>
<p>枚举成员有值(默认可重复),枚举成员具有友好的字符串表示:</p>
<pre><code>>>> print(Color.red)
Color.red
>>> print(repr(Color.red))
<Color.red: 1>
>>> type(Color.red)
<Enum 'Color'>
>>> isinstance(Color.green, Color)
True</code></pre>
<p>枚举类型不可实例化,不可更改。</p>
<h2>定义枚举</h2>
<p><strong> 定义枚举时,成员名不允许重复</strong></p>
<pre><code>class Color(Enum):
red = 1
green = 2
red = 3 # TypeError: Attempted to reuse key: 'red'</code></pre>
<p><strong> 成员值允许相同,第二个成员的名称被视作第一个成员的别名 </strong></p>
<pre><code>class Color(Enum):
red = 1
green = 2
blue = 1
print(Color.red) # Color.red
print(Color.blue) # Color.red
print(Color.red is Color.blue)# True
print(Color(1)) # Color.red 在通过值获取枚举成员时,只能获取到第一个成员</code></pre>
<p><strong> 若要不能定义相同的成员值,可以通过 unique 装饰 </strong></p>
<pre><code>from enum import Enum, unique
@unique
class Color(Enum):
red = 1
green = 2
blue = 1 # ValueError: duplicate values found in <enum 'Color'>: blue -> red</code></pre>
<h2>枚举取值</h2>
<p>可以通过成员名来获取成员也可以通过成员值来获取成员:</p>
<pre><code>print(Color['red']) # Color.red 通过成员名来获取成员
print(Color(1)) # Color.red 通过成员值来获取成员</code></pre>
<p>每个成员都有名称属性和值属性:</p>
<pre><code>member = Color.red
print(member.name) # red
print(member.value) # 1</code></pre>
<p>支持迭代的方式遍历成员,按定义的顺序,如果有值重复的成员,只获取重复的第一个成员:</p>
<pre><code>for color in Color:
print(color)</code></pre>
<p>特殊属性 <code>__members__</code> 是一个将名称映射到成员的有序字典,也可以通过它来完成遍历:</p>
<pre><code>for color in Color.__members__.items():
print(color) # ('red', <Color.red: 1>)</code></pre>
<h2>枚举比较</h2>
<p>枚举的成员可以通过 <code>is</code> 同一性比较或通过 <code>==</code> 等值比较:</p>
<pre><code>Color.red is Color.red
Color.red is not Color.blue
Color.blue == Color.red
Color.blue != Color.red</code></pre>
<p>枚举成员不能进行大小比较:</p>
<pre><code>Color.red < Color.blue # TypeError: unorderable types: Color() < Color()</code></pre>
<h2>扩展枚举 IntEnum</h2>
<p><code>IntEnum</code> 是 <code>Enum</code> 的扩展,不同类型的整数枚举也可以相互比较:</p>
<pre><code>from enum import IntEnum
class Shape(IntEnum):
circle = 1
square = 2
class Request(IntEnum):
post = 1
get = 2
print(Shape.circle == 1) # True
print(Shape.circle < 3) # True
print(Shape.circle == Request.post) # True
print(Shape.circle >= Request.post) # True</code></pre>
<h2>总结</h2>
<p><code>enum</code> 模块功能很明确,用法也简单,其实现的方式也值得学习,有机会的话可以看看它的源码。</p>
Python中几种属性访问的区别
https://segmentfault.com/a/1190000017033331
2018-11-16T09:42:05+08:00
2018-11-16T09:42:05+08:00
weapon
https://segmentfault.com/u/weapon
6
<h2>起步</h2>
<p>python的提供一系列和属性访问有关的特殊方法:<code>__get__</code>, <code>__getattr__</code>, <code>__getattribute__</code>, <code>__getitem__</code> 。本文阐述它们的区别和用法。</p>
<h2>属性的访问机制</h2>
<p>一般情况下,属性访问的默认行为是从对象的字典中获取,并当获取不到时会沿着一定的查找链进行查找。例如 <code>a.x</code> 的查找链就是,从 <code>a.__dict__['x']</code> ,然后是 <code>type(a).__dict__['x']</code> ,再通过 <code>type(a)</code> 的基类开始查找。</p>
<p>若查找链都获取不到属性,则抛出 <code>AttributeError</code> 异常。</p>
<h2>
<code>__getattr__</code> 方法</h2>
<p>这个方法是当对象的属性不存在是调用。如果通过正常的机制能找到对象属性的话,不会调用 <code>__getattr__</code> 方法。</p>
<pre><code>class A:
a = 1
def __getattr__(self, item):
print('__getattr__ call')
return item
t = A()
print(t.a)
print(t.b)
# output
1
__getattr__ call
b</code></pre>
<h2>
<code>__getattribute__</code> 方法</h2>
<p>这个方法会被无条件调用。不管属性存不存在。如果类中还定义了 <code>__getattr__</code> ,则不会调用 <code>__getattr__()</code> 方法,除非在 <code>__getattribute__</code> 方法中显示调用<code>__getattr__()</code> 或者抛出了 <code>AttributeError</code> 。</p>
<pre><code>class A:
a = 1
def __getattribute__(self, item):
print('__getattribute__ call')
raise AttributeError
def __getattr__(self, item):
print('__getattr__ call')
return item
t = A()
print(t.a)
print(t.b)</code></pre>
<p>所以一般情况下,为了保留 <code>__getattr__</code> 的作用,<code>__getattribute__()</code> 方法中一般返回父类的同名方法:</p>
<pre><code>def __getattribute__(self, item):
return object.__getattribute__(self, item)</code></pre>
<p>使用基类的方法来获取属性能避免在方法中出现无限递归的情况。</p>
<h2>
<code>__get__</code> 方法</h2>
<p>这个方法比较简单说明,它与前面的关系不大。</p>
<p>如果一个类中定义了 <code>__get__()</code>, <code>__set__()</code> 或 <code>__delete__()</code> 中的任何方法。则这个类的对象称为描述符。</p>
<pre><code>class Descri(object):
def __get__(self, obj, type=None):
print("call get")
def __set__(self, obj, value):
print("call set")
class A(object):
x = Descri()
a = A()
a.__dict__['x'] = 1 # 不会调用 __get__
a.x # 调用 __get__</code></pre>
<p>如果查找的属性是在描述符对象中,则这个描述符会覆盖上文说的属性访问机制,体现在查找链的不同,而这个行文也会因为调用的不同而稍有不一样:</p>
<ul>
<li>如果调用是对象实例(题目中的调用方式),<code>a.x</code> 则转换为调用: 。<code>type(a).__dict__['x'].__get__(a, type(a))</code>
</li>
<li>如果调用的是类属性, <code>A.x</code> 则转换为:<code>A.__dict__['x'].__get__(None, A)</code>
</li>
<li>其他情况见文末参考资料的文档</li>
</ul>
<h2>
<code>__getitem__</code> 方法</h2>
<p>这个调用也属于无条件调用,这点与 <code>__getattribute__</code> 一致。区别在于 <code>__getitem__</code> 让类实例允许 <code>[]</code> 运算,可以这样理解:</p>
<ul>
<li>
<code>__getattribute__</code> 适用于所有 <code>.</code> 运算符;</li>
<li>
<code>__getitem__</code> 适用于所有 <code>[]</code> 运算符。</li>
</ul>
<pre><code>class A(object):
a = 1
def __getitem__(self, item):
print('__getitem__ call')
return item
t = A()
print(t['a'])
print(t['b'])</code></pre>
<p>如果仅仅想要对象能够通过 <code>[]</code> 获取对象属性可以简单的:</p>
<pre><code>def __getitem(self, item):
return object.__getattribute__(self, item)</code></pre>
<h2>总结</h2>
<p>当这几个方法同时出现可能就会扰乱你了。我在网上看到一份示例还不错,稍微改了下:</p>
<pre><code>class C(object):
a = 'abc'
def __getattribute__(self, *args, **kwargs):
print("__getattribute__() is called")
return object.__getattribute__(self, *args, **kwargs)
# return "haha"
def __getattr__(self, name):
print("__getattr__() is called ")
return name + " from getattr"
def __get__(self, instance, owner):
print("__get__() is called", instance, owner)
return self
def __getitem__(self, item):
print('__getitem__ call')
return object.__getattribute__(self, item)
def foo(self, x):
print(x)
class C2(object):
d = C()
if __name__ == '__main__':
c = C()
c2 = C2()
print(c.a)
print(c.zzzzzzzz)
c2.d
print(c2.d.a)
print(c['a'])</code></pre>
<p>可以结合输出慢慢理解,这里还没涉及继承关系呢。总之,每个以 <code>__ get</code> 为前缀的方法都是获取对象内部数据的钩子,名称不一样,用途也存在较大的差异,只有在实践中理解它们,才能真正掌握它们的用法。</p>
<h2>参考</h2>
<ul><li>
<a href="https://link.segmentfault.com/?enc=TZg%2BeE2ZnHZV6mLOWiFaWA%3D%3D.E5Tg7dv%2BEj0SqQAWvBWHQCqBiRmqyN6j%2FoQbnQA8420FwJvEP3%2FGZw%2FUw%2BPo2g3zB9nbWX7LMCpz%2B2cngRJu8c7z1mhpi1522bLWMks7%2Bqo%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=mLK3bPy1xG0BUdi59%2B6s8A%3D%3D.zIZ38rQzE%2FU3e6jK%2Bj0HzuAleevfVxQTi9AlyUzyc0kecq5i2m2adnYiXMDz2M4nb4Tfkk0TzURZNMDWWZynzPEQu7Ae55lBYbvGqcYah%2B0%3D" rel="nofollow">https://docs.python.org/3/ref...</a>
</li></ul>
用Python实现读写锁
https://segmentfault.com/a/1190000016900930
2018-11-04T20:08:38+08:00
2018-11-04T20:08:38+08:00
weapon
https://segmentfault.com/u/weapon
10
<h2>起步</h2>
<p>Python 提供的多线程模型中并没有提供读写锁,读写锁相对于单纯的互斥锁,适用性更高,<strong>可以多个线程同时占用读模式的读写锁,但是只能一个线程占用写模式的读写锁</strong>。</p>
<p>通俗点说就是当没有写锁时,就可以加读锁且任意线程可以同时加;而写锁只能有一个线程,且必须在没有读锁时才能加上。</p>
<h2>简单的实现</h2>
<pre><code>import threading
class RWlock(object):
def __init__(self):
self._lock = threading.Lock()
self._extra = threading.Lock()
self.read_num = 0
def read_acquire(self):
with self._extra:
self.read_num += 1
if self.read_num == 1:
self._lock.acquire()
def read_release(self):
with self._extra:
self.read_num -= 1
if self.read_num == 0:
self._lock.release()
def write_acquire(self):
self._lock.acquire()
def write_release(self):
self._lock.release()</code></pre>
<p>这是读写锁的一个简单的实现,<code>self.read_num</code> 用来保存获得读锁的线程数,这个属性属于临界区,对其操作也要加锁,所以这里需要一个保护内部数据的额外的锁 <code>self._extra</code> 。</p>
<p>但是这个锁是不公平的。理想情况下,线程获得所的机会应该是一样的,不管线程是读操作还是写操作。而从上述代码可以看到,读请求都会立即设置 <code>self.read_num += 1</code>,不管有没有获得锁,而写请求想要获得锁还得等待 <code>read_num</code> 为 0 。</p>
<p>所以这个就造成了<strong>只有锁没有被占用或者没有读请求时,可以获得写权限</strong>。我们应该想办法避免读模式锁长期占用。</p>
<h2>读写锁的优先级</h2>
<p>读写锁也有分 <strong>读优先</strong> 和 <strong>写优先</strong>。上面的代码就属于读优先。</p>
<p>如果要改成写优先,那就换成去记录写线程的引用计数,读和写在同时竞争时,可以让写线程增加写的计数,这样可使读线程的读锁一直获取不到, 因为读线程要先判断写的引用计数,若不为0,则等待其为 0,然后进行读。这部分代码不罗列了。</p>
<p>但这样显然不够灵活。我们不需要两个相似的读写锁类。我们希望重构我们代码,使它更强大。</p>
<h2>改进</h2>
<p>为了能够满足自定义优先级的读写锁,要记录等待的读写线程数,并且需要两个条件 <code>threading.Condition</code> 用来处理哪方优先的通知。计数引用可以扩大语义:正数:表示正在读操作的线程数,负数:表示正在写操作的线程数(最多-1)</p>
<p>在获取读操作时,先然后判断时候有等待的写线程,没有,进行读操作,有,则等待读的计数加 1 后等待 Condition 通知;等待读的计数减 1,计数引用加 1,继续读操作,若条件不成立,循环等待;</p>
<p>在获取写操作时,若锁没有被占用,引用计数减 1,若被占用,等待写线程数加 1,等待写条件 Condition 的通知。</p>
<p>读模式和写模式的释放都是一样,需要根据判断去通知对应的 Condition:</p>
<pre><code>class RWLock(object):
def __init__(self):
self.lock = threading.Lock()
self.rcond = threading.Condition(self.lock)
self.wcond = threading.Condition(self.lock)
self.read_waiter = 0 # 等待获取读锁的线程数
self.write_waiter = 0 # 等待获取写锁的线程数
self.state = 0 # 正数:表示正在读操作的线程数 负数:表示正在写操作的线程数(最多-1)
self.owners = [] # 正在操作的线程id集合
self.write_first = True # 默认写优先,False表示读优先
def write_acquire(self, blocking=True):
# 获取写锁只有当
me = threading.get_ident()
with self.lock:
while not self._write_acquire(me):
if not blocking:
return False
self.write_waiter += 1
self.wcond.wait()
self.write_waiter -= 1
return True
def _write_acquire(self, me):
# 获取写锁只有当锁没人占用,或者当前线程已经占用
if self.state == 0 or (self.state < 0 and me in self.owners):
self.state -= 1
self.owners.append(me)
return True
if self.state > 0 and me in self.owners:
raise RuntimeError('cannot recursively wrlock a rdlocked lock')
return False
def read_acquire(self, blocking=True):
me = threading.get_ident()
with self.lock:
while not self._read_acquire(me):
if not blocking:
return False
self.read_waiter += 1
self.rcond.wait()
self.read_waiter -= 1
return True
def _read_acquire(self, me):
if self.state < 0:
# 如果锁被写锁占用
return False
if not self.write_waiter:
ok = True
else:
ok = me in self.owners
if ok or not self.write_first:
self.state += 1
self.owners.append(me)
return True
return False
def unlock(self):
me = threading.get_ident()
with self.lock:
try:
self.owners.remove(me)
except ValueError:
raise RuntimeError('cannot release un-acquired lock')
if self.state > 0:
self.state -= 1
else:
self.state += 1
if not self.state:
if self.write_waiter and self.write_first: # 如果有写操作在等待(默认写优先)
self.wcond.notify()
elif self.read_waiter:
self.rcond.notify_all()
elif self.write_waiter:
self.wcond.notify()
read_release = unlock
write_release = unlock</code></pre>
让Python中类的属性具有惰性求值的能力
https://segmentfault.com/a/1190000016015066
2018-08-15T09:33:20+08:00
2018-08-15T09:33:20+08:00
weapon
https://segmentfault.com/u/weapon
8
<h2>起步</h2>
<p>我们希望将一个只读的属性定义为 <code>property</code> 属性方法,只有在访问它时才进行计算,但是,又希望把计算出的值缓存起来,不要每次访问它时都重新计算。</p>
<h2>解决方案</h2>
<p>定义一个惰性属性最有效的方法就是利用描述符类来完成它,示例如下:</p>
<pre><code>class lazyproperty:
def __init__(self, fun):
self.fun = fun
def __get__(self, instance, owner):
if instance is None:
return self
value = self.fun(instance)
setattr(instance, self.fun.__name__, value)
return value</code></pre>
<p>要使用这个工具,可以像下面的方式来使用它:</p>
<pre><code>class Circle:
def __init__(self, radius):
self.radius = radius
@lazyproperty
def area(self):
print('Computing area')
return 3.1415 * self.radius ** 2
c = Circle(5)
print(c.area)
print(c.area)</code></pre>
<p>可以看出,这里的实例方法 <code>area()</code> 只会被调用一次。</p>
<h2>为什么会这样</h2>
<p>如果类中定义了 <code>__get__()</code>、<code>__set__()</code> 、<code>__delete__()</code> 中的任何方法,那么这个就被成为描述符(descriptor)。</p>
<p>一般情况下(我是说一般情况下),访问属性的默认行为是从对象的字典中获取,并沿着一个查找链的顺序进行搜索,比如对于 <code>a.x</code> 有一个查找链,从 <code>a.__dict__['x']</code> 然后是 <code>type(a).__dict__['x']</code>,再继续通过 <code>type(a)</code> 的基类开始。</p>
<p>而如果查找的值是一个描述符对象,则会覆盖这个默认的搜索行为,优先采用描述符的行为,这个行为会因为如果调用而有些不同。这里就只说明例子中的情况。</p>
<p>如果描述符绑定的对象实例,<code>a.x</code> 则转换为调用: <code>type(a).__dict__['x'].__get__(a, type(a))</code>。</p>
<p>当一个描述符之定义 <code>__get__()</code> 方法,则它的绑定关系比一般情况下要弱化很多。特别是,只有当被访问的属性不存在对象字典中时,<code>__get__()</code> 才会被调用。</p>
<p>更多描述可见文档:<a href="https://link.segmentfault.com/?enc=%2Biqy7r%2BWhIp%2BcB073J4D7g%3D%3D.ogRXzVP4Cws291g97YWGMK%2FV4LK6u7thuaKfqgsIqyHY1kWVflDyJ62ZjwPA6kmP1U5rZ0bd68nmgjq1CbOn4%2F3NlaUf%2BE45u5l0nCChU7Q%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=khdfXPTRNQLl5unem%2FRU6g%3D%3D.MzAM5rd33Qnm3e3G4l4bgJGJBl5qEXArNqgBgKPa%2FrZ5omqYJvRgn9NwNspF0lNquGf9H6P5MXBmHJ%2FBukyf3dQx0Yh%2F6UiV5oeY9%2FMVRkA%3D" rel="nofollow">https://docs.python.org/3/ref...</a></p>
<p>这种惰性求值的方法在很多模块中都会使用,比如django中的 <code>cached_property</code>:</p>
<p><img src="/img/remote/1460000016015070?w=805&h=555" alt="20180813175532.png" title="20180813175532.png"></p>
<p>使用上与例子一致,如表单中的 <code>changed_data</code> :</p>
<p><img src="/img/remote/1460000016015071" alt="20180813175928.png" title="20180813175928.png"></p>
<h2>讨论</h2>
<p>在大部分情况下,让属性具有惰性求值能力的全部意义就在于提升程序性能。当不需要这个属性时就能避免进行无意义的计算,同时又能阻止该属性重复进行计算。</p>
<p>本文的技巧中有一个潜在的缺点,就是计算出的值后就变成可变的(mutable)。</p>
<pre><code>>>> c.area
78.53
>>> c.area = 3
>>> c.area
3</code></pre>
<p>如果考虑可变性的问题,可以使用另一种实现方式,但执行效率会稍打折扣:</p>
<pre><code>def lazyproperty(func):
name = '_lazy_' + func.__name__
@property
def lazy(self):
if hasattr(self, name):
return getattr(self, name)
value = func(self)
setattr(self, name, value)
return value
return lazy</code></pre>
<p>如果使用这种方式,就会发现 <code>set</code> 操作是不允许的,所有的 get 操作都必须经由属性的 <code>getter</code> 函数来处理,这比直接在实例字典中查找相应的值要慢一些。</p>
<h2>参考</h2>
<ol>
<li>
<a href="https://link.segmentfault.com/?enc=8rGPunbqHJOTUd%2BsOjhrdQ%3D%3D.i2Q7MMcaiNuGHB9nvXjrXIv1a6Byq4avRsQrHIDhypkzofucvtbRvufa41rd3kDSw59ZwDvFiY7mgwrH7KiZCKMCZBv%2FYtoubpqpKljauX0%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=KNJgB9dEQJLkh5oVULP3yg%3D%3D.nr0siXMBsMeT3zXsmNMhHatLI1lN5pdXBjTyZHpADrYsk4cX1X0VwQDO0yGB7WFKi4e%2Fqt7K%2BIwPOfkx8JrcOScJhN70KRl6o1%2Br241Y4m0%3D" rel="nofollow">https://docs.python.org/3/ref...</a>
</li>
<li>《Python Cookbook 第三版》</li>
</ol>
深度剖析凭什么python中整型不会溢出
https://segmentfault.com/a/1190000015284473
2018-06-14T09:58:01+08:00
2018-06-14T09:58:01+08:00
weapon
https://segmentfault.com/u/weapon
20
<h2>前言</h2>
<p><em>本次分析基于 CPython 解释器,python3.x版本</em></p>
<p>在python2时代,整型有 <code>int</code> 类型和 <code>long</code> 长整型,长整型不存在溢出问题,即可以存放任意大小的整数。在python3后,统一使用了长整型。这也是吸引科研人员的一部分了,适合大数据运算,不会溢出,也不会有其他语言那样还分短整型,整型,长整型...因此python就降低其他行业的学习门槛了。</p>
<p>那么,不溢出的整型实现上是否可行呢?</p>
<h2>不溢出的整型的可行性</h2>
<p>尽管在 C 语言中,整型所表示的大小是有范围的,但是 python 代码是保存到文本文件中的,也就是说,python代码中并不是一下子就转化成 C 语言的整型的,我们需要重新定义一种数据结构来表示和存储我们新的“整型”。</p>
<p>怎么来存储呢,既然我们要表示任意大小,那就得用动态的可变长的结构,显然,数组的形式能够胜任:</p>
<pre><code>[longintrepr.h]
struct _longobject {
PyObject_VAR_HEAD
int *ob_digit;
};</code></pre>
<p><img src="/img/remote/1460000015284476" alt="ea1cca01427d4ab8b7fb65d19426fcce.png" title="ea1cca01427d4ab8b7fb65d19426fcce.png"></p>
<h2>长整型的保存形式</h2>
<p>长整型在python内部是用一个 <code>int</code> 数组( <code>ob_digit[n]</code> )保存值的. 待存储的数值的低位信息放于低位下标, 高位信息放于高下标.比如要保存 <code>123456789</code> 较大的数字,但我们的int只能保存3位(假设):</p>
<pre><code>ob_digit[0] = 789;
ob_digit[1] = 456;
ob_digit[2] = 123;</code></pre>
<p>低索引保存的是地位,那么每个 <code>int</code> 元素保存多大的数合适?有同学会认为数组中每个int存放它的上限(2^31 - 1),这样表示大数时,数组长度更短,更省空间。但是,空间确实是更省了,但操作会代码麻烦,比方大数做乘积操作,由于元素之间存在乘法溢出问题,又得多考虑一种溢出的情况。</p>
<p>怎么来改进呢?在长整型的 <code>ob_digit</code> 中元素理论上可以保存的int类型有 <code>32</code> 位,但是我们只保存 <code>15</code> 位,这样元素之间的乘积就可以只用 <code>int</code> 类型保存即可, 结果做位移操作就能得到尾部和进位 <code>carry</code> 了,定义位移长度为 <code>15</code>:</p>
<pre><code>#define PyLong_SHIFT 15
#define PyLong_BASE ((digit)1 << PyLong_SHIFT)
#define PyLong_MASK ((digit)(PyLong_BASE - 1))</code></pre>
<p><code>PyLong_MASK</code> 也就是 <code>0b111111111111111</code> ,通过与它做位运算 <code>与</code> 的操作就能得到低位数。</p>
<p>有了这种存放方式,在内存空间允许的情况下,我们就可以存放任意大小的数字了。</p>
<p><img src="/img/remote/1460000015284477" alt="4cece29a4b8d44fe9072b1967da7bf01_th.jpeg" title="4cece29a4b8d44fe9072b1967da7bf01_th.jpeg"></p>
<h2>长整型的运算</h2>
<p>加法与乘法运算都可以使用我们小学的竖式计算方法,例如对于加法运算:</p>
<table>
<thead><tr>
<th align="center"> </th>
<th align="center"> </th>
<th align="center">ob_digit<code>[2]</code>
</th>
<th align="center">ob_digit<code>[1]</code>
</th>
<th align="center">ob_digit<code>[0]</code>
</th>
</tr></thead>
<tbody>
<tr>
<td align="center">加数a</td>
<td align="center"> </td>
<td align="center">23</td>
<td align="center">934</td>
<td align="center">543</td>
</tr>
<tr>
<td align="center">加数b</td>
<td align="center">+</td>
<td align="center"> </td>
<td align="center">454</td>
<td align="center">632</td>
</tr>
<tr>
<td align="center">结果z</td>
<td align="center"> </td>
<td align="center">24</td>
<td align="center">389</td>
<td align="center">175</td>
</tr>
</tbody>
</table>
<p>为方便理解,表格展示的是数组中每个元素保存的是 3 位十进制数,计算结果保存在变量z中,那么 z 的数组最多只要 <code>size_a + 1</code> 的空间(两个加数中数组较大的元素个数 + 1),因此对于加法运算,可以这样来处理:</p>
<pre><code>[longobject.c]
static PyLongObject * x_add(PyLongObject *a, PyLongObject *b) {
int size_a = len(a), size_b = len(b);
PyLongObject *z;
int i;
int carry = 0; // 进位
// 确保a是两个加数中较大的一个
if (size_a < size_b) {
// 交换两个加数
swap(a, b);
swap(&size_a, &size_b);
}
z = _PyLong_New(size_a + 1); // 申请一个能容纳size_a+1个元素的长整型对象
for (i = 0; i < size_b; ++i) {
carry += a->ob_digit[i] + b->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK; // 掩码
carry >>= PyLong_SHIFT; // 移除低15位, 得到进位
}
for (; i < size_a; ++i) { // 单独处理a中高位数字
carry += a->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
z->ob_digit[i] = carry;
return long_normalize(z); // 整理元素个数
}</code></pre>
<p>这部分的过程就是,先将两个加数中长度较长的作为第一个加数,再为用于保存结果的 z 申请空间,两个加数从数组从低位向高位计算,处理结果的进位,将结果的低 15 位赋值给 z 相应的位置。最后的 <code>long_normalize(z)</code> 是一个整理函数,因为我们 z 申请了 <code>a_size + 1</code> 的空间,但不意味着 z 会全部用到,因此这个函数会做一些调整,去掉多余的空间,数组长度调整至正确的数量,若不方便理解,附录将给出更利于理解的python代码。</p>
<p><strong>竖式计算不是按个位十位来计算的吗,为什么这边用整个元素?</strong></p>
<p>竖式计算方法适用与任何进制的数字,我们可以这样来理解,这是一个 32768 (2的15次方) 进制的,那么就可以把数组索引为 0 的元素当做是 “个位”,索引 1 的元素当做是 “十位”。</p>
<h2>乘法运算</h2>
<p>乘法运算一样可以用竖式的计算方式,两个乘数相乘,存放结果的 z 的元素个数为 <code>size_a + size_b</code> 即可:</p>
<table>
<thead><tr>
<th align="center"> </th>
<th align="center">操作</th>
<th align="center"> </th>
<th align="center"> </th>
<th align="center">ob_digit<code>[2]</code>
</th>
<th align="center">ob_digit<code>[1]</code>
</th>
<th align="center">ob_digit<code>[0]</code>
</th>
</tr></thead>
<tbody>
<tr>
<td align="center">乘数a</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">23</td>
<td align="center">934</td>
<td align="center">543</td>
</tr>
<tr>
<td align="center">乘数b</td>
<td align="center">*</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">454</td>
<td align="center">632</td>
</tr>
<tr>
<td align="center">结果z</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">15</td>
<td align="center">126</td>
<td align="center">631</td>
<td align="center">176</td>
</tr>
<tr>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">10</td>
<td align="center">866</td>
<td align="center">282</td>
<td align="center">522</td>
<td align="center"> </td>
</tr>
<tr>
<td align="center">结果z</td>
<td align="center"> </td>
<td align="center">10</td>
<td align="center">881</td>
<td align="center">409</td>
<td align="center">153</td>
<td align="center">176</td>
</tr>
</tbody>
</table>
<p>这里需要主意的是,当乘数 b 用索引 i 的元素进行计算时,结果 z 也是从 i 索引开始保存。先创建 z 并初始化为 0,这 z 上做累加操作,加法运算则可以利用前面的 <code>x_add</code> 函数:</p>
<pre><code>// 为方便理解,会与cpython中源码部分稍有不同
static PyLongObject * x_mul(PyLongObject *a, PyLongObject *b)
{
int size_a = len(a), size_b = len(b);
PyLongObject *z = _PyLong_New(size_a + size_b);
memset(z->ob_digit, 0, len(z) * sizeof(int)); // z 的数组清 0
for (i = 0; i < size_b; ++i) {
int carry = 0; // 用一个int保存元素之间的乘法结果
int f = b->ob_digit[i]; // 当前乘数b的元素
// 创建一个临时变量,保存当前元素的计算结果,用于累加
PyLongObject *temp = _PyLong_New(size_a + size_b);
memset(temp->ob_digit, 0, len(temp) * sizeof(int)); // temp 的数组清 0
int pz = i; // 存放到临时变量的低位
for (j = 0; j < size_a; ++j) {
carry = f * a[j] + carry;
temp[pz] = carry & PyLong_MASK; // 取低15位
carry = carry >> PyLong_SHIFT; // 保留进位
pz ++;
}
if (carry){ // 处理进位
carry += temp[pz];
temp[pz] = carry & PyLong_MASK;
carry = carry >> PyLong_SHIFT;
}
if (carry){
temp[pz] += carry & PyLong_MASK;
}
temp = long_normalize(temp);
z = x_add(z, temp);
}
return z
}</code></pre>
<p>这大致就是乘法的处理过程,竖式乘法的复杂度是n^2,当数字非常大的时候(数组元素个数超过 70 个)时,python会选择性能更好,更高效的 <code>Karatsuba multiplication</code> 乘法运算方式,这种的算法复杂度是 3nlog3≈3n1.585,当然这种计算方法已经不是今天讨论的内容了。有兴趣的小伙伴可以去了解下。</p>
<h2>总结</h2>
<p>要想支持任意大小的整数运算,首先要找到适合存放整数的方式,本篇介绍了用 int 数组来存放,当然也可以用字符串来存储。找到合适的数据结构后,要重新定义整型的所有运算操作,本篇虽然只介绍了加法和乘法的处理过程,但其实还需要做很多的工作诸如减法,除法,位运算,取模,取余等。</p>
<p>python代码以文本形式存放,因此最后,还需要一个将字符串形式的数字转换成这种整型结构:</p>
<pre><code>[longobject.c]
PyObject * PyLong_FromString(const char *str, char **pend, int base)
{
}</code></pre>
<p>这部分不是本篇的重点,有兴趣的同学可以看看这个转换的过程。</p>
<h2>参考</h2>
<ul><li><a href="https://link.segmentfault.com/?enc=EYmsQ0l77lI7N593zSGlRA%3D%3D.FJmv1cC13NEYwQWJ3l7MWIsKB8OGDFOzBA%2FbSEnc3lB8EuwvkA2e4fpQrsUjoAEP9gyWrx8iPXGQLfuL16IhteJhUA7%2FD%2FtWaFJahSzqcxo%3D" rel="nofollow">longobject.c</a></li></ul>
<h2>附录</h2>
<pre><code>
# 例子中的表格中,数组元素最多存放3位整数,因此这边设置1000
# 对应的取低位与取高位也就变成对 1000 取模和取余操作
PyLong_SHIFT = 1000
PyLong_MASK = 999
# 以15位长度的二进制
# PyLong_SHIFT = 15
# PyLong_MASK = (1 << 15) - 1
def long_normalize(num):
"""
去掉多余的空间,调整数组的到正确的长度
eg: [176, 631, 0, 0] ==> [176, 631]
:param num:
:return:
"""
end = len(num)
while end >= 1:
if num[end - 1] != 0:
break
end -= 1
num = num[:end]
return num
def x_add(a, b):
size_a = len(a)
size_b = len(b)
carry = 0
# 确保 a 是两个加数较大的,较大指的是元素的个数
if size_a < size_b:
size_a, size_b = size_b, size_a
a, b = b, a
z = [0] * (size_a + 1)
i = 0
while i < size_b:
carry += a[i] + b[i]
z[i] = carry % PyLong_SHIFT
carry //= PyLong_SHIFT
i += 1
while i < size_a:
carry += a[i]
z[i] = carry % PyLong_SHIFT
carry //= PyLong_SHIFT
i += 1
z[i] = carry
# 去掉多余的空间,数组长度调整至正确的数量
z = long_normalize(z)
return z
def x_mul(a, b):
size_a = len(a)
size_b = len(b)
z = [0] * (size_a + size_b)
for i in range(size_b):
carry = 0
f = b[i]
# 创建一个临时变量
temp = [0] * (size_a + size_b)
pz = i
for j in range(size_a):
carry += f * a[j]
temp[pz] = carry % PyLong_SHIFT
carry //= PyLong_SHIFT
pz += 1
if carry: # 处理进位
carry += temp[pz]
temp[pz] = carry % PyLong_SHIFT
carry //= PyLong_SHIFT
pz += 1
if carry:
temp[pz] += carry % PyLong_SHIFT
temp = long_normalize(temp)
z = x_add(z, temp) # 累加
return z
a = [543, 934, 23]
b = [632, 454]
print(x_add(a, b))
print(x_mul(a, b))</code></pre>
深度剖析isinstance的检查机制
https://segmentfault.com/a/1190000014713684
2018-05-03T17:23:32+08:00
2018-05-03T17:23:32+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>通过内建方法 <code>isinstance(object, classinfo)</code> 可以判断一个对象是否是某个类的实例。但你是否想过关于鸭子协议的对象是如何进行判断的呢? 比如 <code>list</code> 类的父类是继 <code>object</code> 类的,但通过 <code>isinstance([], typing.Iterable)</code> 返回的却是真,难道 list 是可迭代的子类?学过Python的面向对象应该知道,list的基类是object的。</p>
<p>根据 <a href="https://link.segmentfault.com/?enc=X5xIMxhx4YuXLyBhW%2BkVOQ%3D%3D.TZ%2FkjgfIYyXcRtOzSBiT7Eno%2FA%2BROpa7AEDUWdhblaqqHH3u4Xm4ZFiVECw4qT8w" rel="nofollow">PEP 3119</a> 的描述中得知实例的检查是允许重载的:</p>
<blockquote>The primary mechanism proposed here is to allow overloading the built-in functions isinstance() and issubclass(). The overloading works as follows: The call isinstance(x, C) first checks whether <code>C.__instancecheck__</code> exists, and if so, calls <code>C.__instancecheck__(x)</code> instead of its normal implementation.</blockquote>
<p>这段话的意思是,当调用 <code>isinstance(x, C)</code> 进行检测时,会优先检查是否存在 <code>C.__instancecheck__</code> ,如果存在则调用 <code>C.__instancecheck__(x)</code> ,返回的结果便是实例检测的结果,默认的判断方式就没有了。</p>
<p>这种方式有助于我们来检查鸭子类型,我用代码测了一下。</p>
<pre><code>class Sizeable(object):
def __instancecheck__(cls, instance):
print("__instancecheck__ call")
return hasattr(instance, "__len__")
class B(object):
pass
b = B()
print(isinstance(b, Sizeable)) # output:False</code></pre>
<p>只打印了 False,并且 <code>__instancecheck__</code> 没有调用。 这是怎么回事。可见文档描述并不清楚。打破砂锅问到底的原则我从源码中观察 <code>isinstance</code> 的检测过程。</p>
<h2>从源码来看 <code>isinstance</code> 的检测过程</h2>
<p>这部分的内容可能比较难,如果读者觉得阅读有难度可以跳过,直接看结论。<code>isinstance</code> 的源码在 <code>abstract.c</code> 文件中:</p>
<pre><code>[abstract.c]
int
PyObject_IsInstance(PyObject *inst, PyObject *cls)
{
_Py_IDENTIFIER(__instancecheck__);
PyObject *checker;
/* Quick test for an exact match */
if (Py_TYPE(inst) == (PyTypeObject *)cls)
return 1;
....
}</code></pre>
<p><code>Py_TYPE(inst) == (PyTypeObject *)cls</code> 这是一种快速匹配的方式,等价于 <code>type(inst) is cls</code> ,这种快速的方式仅当 <code>inst = cls()</code> 匹配成功,并不会去优先检查 <code>__instancecheck__</code> ,所以文档中有误。继续向下看源码:</p>
<pre><code> /* We know what type's __instancecheck__ does. */
if (PyType_CheckExact(cls)) {
return recursive_isinstance(inst, cls);
}</code></pre>
<p>展开宏 <code>PyType_CheckExact</code> :</p>
<pre><code>[object.h]
#define PyType_CheckExact(op) (Py_TYPE(op) == &PyType_Type)</code></pre>
<p>也就是说 <code>cls</code> 是由 <code>type</code> 直接构造出来的类,则判断语言成立。除了类声明里指定 <code>metaclass</code> 外基本都是由 type 直接构造的。从测试代码中得知判断成立,进入 <code>recursive_isinstance</code>。但是这个函数里面我却没找到有关 <code>__instancecheck__</code> 的代码,recursive_isinstance 的判断逻辑大致是:</p>
<pre><code>def recursive_isinstance(inst, cls):
return pyType_IsSubtype(inst, cls)
def pyType_IsSubtype(a, b):
for mro in a.__class__.__mro__:
if mro is b:
return True
return False</code></pre>
<p>是从 <code>__mro__</code> 继承顺序来判断的,<code>__mro__</code> 是一个元组,它表示类的继承顺序,这个元组的中类的顺序也决定了属性查找顺序。回到 <code>PyObject_IsInstance</code> 函数往下看:</p>
<pre><code>if (PyTuple_Check(cls)) {
...
}</code></pre>
<p>这是当 <code>instance(x, C)</code> 第二个参数是元组的情况,里面的处理方式是递归调用 <code>PyObject_IsInstance(inst, item)</code> 。继续往下看:</p>
<pre><code>checker = _PyObject_LookupSpecial(cls, &PyId___instancecheck__);
if (checker != NULL) {
res = PyObject_CallFunctionObjArgs(checker, inst, NULL);
ok = PyObject_IsTrue(res);
return ok;
}</code></pre>
<p>显然,这边才是获得 <code>__instancecheck__</code> 的地方,为了让检查流程走到这里,定义的类要指明 <code>metaclass</code> 。剩下就是跟踪下 <code>_PyObject_LookupSpecial</code> 就可以了:</p>
<pre><code>[typeobject.c]
PyObject *
_PyObject_LookupSpecial(PyObject *self, _Py_Identifier *attrid)
{
PyObject *res;
res = _PyType_LookupId(Py_TYPE(self), attrid);
// 有回调的话处理回调
// ...
return res;
}</code></pre>
<p>取的是 <code>Py_TYPE(self)</code> ,也就是说指定的 metaclass 里面需要定义 <code>__instancecheck__</code> ,获得该属性后,通过 <code>PyObject_CallFunctionObjArgs</code> 调用,调用的内容才是用户自定义的重载方法。</p>
<h2>检查机制总结</h2>
<p>至此,<code>isinstance</code> 的检测过程基本清晰了,为了便于理解,也得益于python很强的自解释能力,我用python代码来简化 <code>isinstance</code> 的过程:</p>
<pre><code>def _isinstance(x, C):
# 快速匹配
if type(x) is C:
return True
# 如果是由元类 type 直接构造的类
if type(C) is type:
return C in x.__class__.__mro__
# 如果第二个参数是元组, 则递归调用
if type(C) is tuple:
for item in C:
r = _isinstance(x, item)
if r:
return r
# 用户自定义检测规则
if hasattr(C, "__instancecheck__"):
return C.__instancecheck__(x)
# 默认行为
return C in x.__class__.__mro__</code></pre>
<p>判断的过程中有5个步骤,而用户自定义的 <code>__instancecheck__</code> 则比较靠后,这个检测过程主要还是以默认的行为来进行的,用户行为并不优先。</p>
<h2>重载 <code>isinstance(x, C)</code>
</h2>
<p>因此,要想重载 <code>isinstance(x, C)</code> ,让用户能自定义判断结果,就需要满足以下条件:</p>
<ol>
<li>x 对象不能是由 C 直接实例化;</li>
<li>C 类指定 metaclass ;</li>
<li>指定的 metaclass 类中定义了 <code>__instancecheck__</code> 。</li>
</ol>
<p>满足这些条件后,比如对鸭子协议如何判断就比较清楚了:</p>
<pre><code>class MetaSizeable(type):
def __instancecheck__(cls, instance):
print("__instancecheck__ call")
return hasattr(instance, "__len__")
class Sizeable(metaclass=MetaSizeable):
pass
class B(object):
pass
b = B()
print(isinstance(b, Sizeable)) # output: False
print(isinstance([], Sizeable)) # output: True</code></pre>
<h2>参考阅读</h2>
<p><a href="https://link.segmentfault.com/?enc=00NCF68EZjgKcRqAFtlTMg%3D%3D.Ok7eEuz7GjLVbq94yH2ToMn7vh%2F750euj%2FJ451sAUGEplZmbz7HwGw1ZgADnZanJOl7kQzcGOx4tcTGbrwGLwDseoke9ecxQfyeuZyEkY2w86D1SMv39ByLDenyPrq2V" rel="nofollow">customizing-instance-and-subclass-checks</a></p>
<p><a href="https://link.segmentfault.com/?enc=m5fOERjSniFoQ9TbWDswFg%3D%3D.rAwYAlbBQDi11QTSyY31bM2rF4LksdDMTnZTzFv0p%2BAuyIjYWMHSqPBCJw9eOerSHqEEPcQJMDRQgXe4VwSrxw%3D%3D" rel="nofollow">class.__mro__</a></p>
神经网络NN算法(理论篇)
https://segmentfault.com/a/1190000012630183
2017-12-28T21:38:17+08:00
2017-12-28T21:38:17+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2>
<p>神经网络算法( <code>Neural Network</code> )是机器学习中非常非常重要的算法。这是整个深度学习的核心算法,深度学习就是根据神经网络算法进行的一个延伸。理解这个算法的是怎么工作也能为后续的学习打下一个很好的基础。</p>
<h2>背景</h2>
<p>神经网络是受神经元启发的,对于神经元的研究由来已久,1904年生物学家就已经知晓了神经元的组成结构。</p>
<p><img src="/img/remote/1460000012630188?w=398&h=237" alt="673793-20151229121248198-818698949.jpg" title="673793-20151229121248198-818698949.jpg"></p>
<ul>
<li>1943年,心理学家McCulloch和数学家Pitts参考了生物神经元的结构,发表了抽象的神经元模型MP。</li>
<li>1949年心理学家Hebb提出了Hebb学习率,认为人脑神经细胞的突触(也就是连接)上的强度上可以变化的。于是计算科学家们开始考虑用调整权值的方法来让机器学习。这为后面的学习算法奠定了基础。</li>
<li>1958年,计算科学家Rosenblatt提出了由两层神经元组成的神经网络。他给它起了一个名字--感知器( <code>Perceptron</code> )。</li>
<li>1986年,Rumelhar和Hinton等人提出了反向传播( <code>Backpropagation</code> ,BP)算法,这是最著名的一个神经网络算法。</li>
</ul>
<h2>神经网络的构成</h2>
<p>多层神经网络由三部分组成:输入层( <code>input layer</code> ), 隐藏层 ( <code>hidden layers</code> ), 输出层 ( <code>output layers</code> )。</p>
<p><img src="/img/remote/1460000012630189?w=340&h=248" alt="Image.png" title="Image.png"></p>
<p>每一层都是有单元( <code>units</code> )组成,其中,输入层是由训练集中实例特征向量传入,根据连接点之间的权重传递到下一层,这样一层一层向前传递。</p>
<p>输入层和输出层都只有一层,隐藏层的个数可以是任意的。神经网络的层数计算中不包括输入层,比方说一个神经网络中有2个隐藏层,我们就说这是一个3层的神经网络。</p>
<p>作为多层向前神经网络,理论上,如果有足够多的隐藏层和训练集,是可以模拟出任何方程的。</p>
<p>神经网络可以用来解决分类( <code>classification</code> )问题,也可以解决回归( <code>regression</code> )问题。</p>
<h2>从单层到多层的神经网络</h2>
<p>由两层神经网络构成了单层神经网络,它还有个别名———— <code>感知器</code> 。</p>
<p><img src="/img/remote/1460000012630190?w=406&h=445" alt="673793-20151221151959015-1876891081.jpg" title="673793-20151221151959015-1876891081.jpg"></p>
<p>如图中,有3个输入,连接线的权值分别是 w1, w2, w3。将输入与权值进行乘积然后求和,作为 z 单元的输入,如果 z 单元是函数 g ,那么就有 <code>z = g(a1 * w1 + a2 * w2 + a3 * w3)</code> 。</p>
<p>单层神经网络的扩展,也是一样的计算方式:</p>
<p><img src="/img/remote/1460000012630191?w=928&h=615" alt="673793-20151230205437995-673856644.jpg" title="673793-20151230205437995-673856644.jpg"></p>
<p>在多层神经网络中,只不过是将输出作为下一层的输入,一样是乘以权重然后求和:</p>
<p><img src="/img/remote/1460000012630192?w=723&h=487" alt="673793-20151222171056156-387680541.jpg" title="673793-20151222171056156-387680541.jpg"></p>
<h2>设计神经网络结构</h2>
<p>使用神经网络进行训练之前,必须确定神经网络的层数,以及每一层中单元的个数。整个训练过程就是调整连接点之间的权重值。</p>
<p>特征向量在被传入输入层前,通常要先标准化为 0 到 1 之间的数,这是为了加速学习过程。</p>
<p>对于分类问题,如果是两类,可以用一个输出单元(0 和 1 表示分类结果)进行表示。如果是多分类问题,则每一个类别用一个输出单元表示。分类问题中,输出层单元个数通常等于类别的数量。</p>
<p>目前没有明确的规则来设计最好有多少个隐藏层,通常是根据实验测试和误差,以及准确度来进行改进。</p>
<h2>交叉验证方法</h2>
<p>如何来预测准确度呢?在SVM的应用篇中,有个方法就是将数据集分为两类,训练集和测试集,利用测试集的数据将模型的预测结果进行对比,得出准确度。这里介绍另一个常用但更科学的方法————交叉验证方法( <code>Cross-Validation</code> )。</p>
<p><img src="/img/remote/1460000012630193?w=583&h=488" alt="cross_validation.jpg" title="cross_validation.jpg"></p>
<p>这个方法不局限于将数据集分成两份,它可以分成 k 份。用第一份作为训练集,其余作为测试集,得出这一部分的准确度 ( <code>evaluation</code> )。再用第二份作为训练集,其余作为测试集,得出这第二部分的准确度。以此类推,最后取各部分的准确度的平均值。从而可以得到设计多少层最佳。</p>
<h2>BP 算法</h2>
<p>BP 算法 ( <code>BackPropagation</code> )是多层神经网络的训练一个核心的算法。目的是更新每个连接点的权重,从而减小预测值( <code>predicted value</code> )与真实值 ( <code>target value</code> )之间的差距。输入一条训练数据就会更新一次权重,反方向(从输出层=>隐藏层=>输入层)来以最小化误差(error)来更新权重(weitht)。</p>
<p>在训练神经网络之前,需要初始化权重( <code>weights</code> )和偏向( <code>bias</code> ),初始化是随机值, -1 到 1 之间,每个单元有一个偏向。</p>
<h3>算法详细介绍</h3>
<p>数据集用 <code>D</code> 表示,学习率用 <code>l</code> 表示。对于每一个训练实例 X,都是一样的步骤。</p>
<p><img src="/img/remote/1460000012630189?w=340&h=248" alt="Image.png" title="Image.png"></p>
<p>利用上一层的输入,得到本层的输入:</p>
<p>$$
I_j = \sum_i w_{i,j}O_i + \theta{j}
$$</p>
<p>得到输入值后,神经元要怎么做呢?我们先将单个神经元进行展开如图:</p>
<p><img src="/img/remote/1460000012630194?w=376&h=226" alt="Image.png" title="Image.png"></p>
<p>得到值后需要进行一个非线性转化,这个转化在神经网络中称为激活函数( <code>Activation function</code> ),这个激活函数是一个 S 函数,图中以 f 表示,它的函数为:</p>
<p>$$
O_j = \frac1{1+e^{-I_j}}
$$</p>
<h3>更新权重</h3>
<p>通过上面的传递规则,可以得到最终的输出,而训练实例中包含实际的值,因此可以得到训练和实际之间的误差。根据误差(error)反向传送。</p>
<p>对于输出层的误差为:</p>
<p>$$
Err_j = O_j(1 - O_j)(T_j - O_j)
$$</p>
<p>其中 <code>Oj</code> 表示预测值, <code>Tj</code> 表示真实值。</p>
<p>对隐藏层的误差:</p>
<p>$$
Err_j = O_j(1 - O_j)\sum_k Err_kw_{j,k}
$$</p>
<p>更新权重:</p>
<p>$$
\begin{align*}
\Delta w_{i,j} &= (l)Err_jO_i \\
w_{i,j} &= w_{i,j} + \Delta w_{i,j}
\end{align*}
$$</p>
<p>这里的 <code>l</code> 是学习率。偏向更新:</p>
<p>$$
\begin{align*}
\Delta \theta{j} &= (l)Err_j \\
\theta{j} &= \theta{j} + \Delta \theta{j}
\end{align*}
$$</p>
<h3>训练的终止条件</h3>
<p>怎样才算是一个训练好了的神经网络呢?满足下面一个情况即可:</p>
<ul>
<li>权重的更新低于某个阈值,这个阈值是可以人工指定的;</li>
<li>预测的错误率低于某个阈值;</li>
<li>达到预设一定的循环次数。</li>
</ul>
<h2>BP 算法举例</h2>
<p>假设有一个两层的神经网络,结构,权重和数据集如下:</p>
<p><img src="/img/remote/1460000012630195?w=462&h=494" alt="Image.png" title="Image.png"></p>
<p>计算误差和更新权重:</p>
<p><img src="/img/remote/1460000012630196?w=303&h=373" alt="Image.png" title="Image.png"></p>
从零开始构造邻近分类器KNN
https://segmentfault.com/a/1190000012630104
2017-12-28T21:30:36+08:00
2017-12-28T21:30:36+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>本章介绍如何自行构造 <code>KNN</code> 分类器,这个分类器的实现上算是比较简单的了。不过这可能需要你之前阅读过这方面的知识。</p>
<p><strong>前置阅读</strong></p>
<p><a href="https://segmentfault.com/a/1190000012310793">分类算法之邻近算法:KNN(理论篇)</a></p>
<p><a href="https://segmentfault.com/a/1190000012351052">分类算法之邻近算法:KNN(应用篇)</a></p>
<h2>欧拉公式衡量距离</h2>
<p>关于距离的测量方式有多种,这边采用欧拉距离的测量方式:</p>
<p>$$
d(x,y) = \sqrt{\sum_{i=0}^n(x_i-y_i)^2}
$$</p>
<p>对应的 python 代码:</p>
<pre><code>import math
def euler_distance(point1: list, point2: list) -> float:
"""
计算两点之间的欧拉距离,支持多维
"""
distance = 0.0
for a, b in zip(point1, point2):
distance += math.pow(a - b, 2)
return math.sqrt(distance)</code></pre>
<h2>KNN 分类器</h2>
<pre><code>import collections
import numpy as np
class KNeighborsClass(object):
def __init__(self, n_neighbors=5):
self.n_neighbors = n_neighbors
def fit(self, data_set, labels):
self.data_set = data_set
self.labels = labels
def predict(self, test_row):
dist = []
for v in self.data_set:
dist.append(euler_distance(v, test_row))
dist = np.array(dist)
sorted_dist_index = np.argsort(dist) # 根据元素的值从大到小对元素进行排序,返回下标
# 根据K值选出分类结果, ['A', 'B', 'B', 'A', ...]
class_list = [ self.labels[ sorted_dist_index[i] ] for i in range(self.n_neighbors)]
result_dict = collections.Counter(class_list) # 计算各个分类出现的次数
ret = sorted(result_dict.items(), key=lambda x: x[1], reverse=True) # 采用多数表决,即排序后的第一个分类
return ret[0][0]</code></pre>
<p>这个分类器不需要训练,因此在 <code>fit</code> 函数中仅仅保存其数据集和结果集即可。在预测函数中,需要依次计算测试样本与数据集中每个样本的距离。筛选出前 <code>K</code> 个,采用多数表决的方式。</p>
<h2>测试</h2>
<p>还是使用 <code>sklearn</code> 中提供的虹膜数据。</p>
<pre><code>if __name__ == "__main__":
from sklearn import datasets
iris = datasets.load_iris()
knn = KNeighborsClass(n_neighbors=5)
knn.fit(iris.data, iris.target)
predict = knn.predict([0.1, 0.2, 0.3, 0.4])
print(predict) # output: 1</code></pre>
分类算法之邻近算法:KNN(应用篇)
https://segmentfault.com/a/1190000012351052
2017-12-09T00:45:37+08:00
2017-12-09T00:45:37+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>这次使用的训练集由 <code>sklearn</code> 模块提供,关于虹膜(一种鸢尾属植物)的数据。</p>
<p><img src="/img/remote/1460000012351057?w=200&h=162" alt="1278644294.png" title="1278644294.png"></p>
<h2>数据载入</h2>
<pre><code>from sklearn import datasets
iris = datasets.load_iris()</code></pre>
<p>数据存储在 <code>.data</code> 成员中,它是一个 (n_samples, n_features) <code>numpy</code> 数组:</p>
<pre><code>print(iris.data)
# [[ 5.1 3.5 1.4 0.2]
# [ 4.9 3. 1.4 0.2]
# ...</code></pre>
<p>它有四个特征,萼片长度,萼片宽度,花瓣长度,花瓣宽度 (sepal length, sepal width, petal length and petal width)。</p>
<p><img src="/img/remote/1460000012351058" alt="kahi2.jpg" title="kahi2.jpg"></p>
<p>它的品种分类有山鸢尾,变色鸢尾,菖蒲锦葵(Iris setosa, Iris versicolor, Iris virginica.)三种。</p>
<pre><code>print iris.data.shape
# output:(150L, 4L)</code></pre>
<p>这是一个含有 150 个数据的训练集。</p>
<h2>构造 KNN 分类器</h2>
<pre><code>from sklearn import neighbors
knn = neighbors.KNeighborsClassifier(n_neighbors=5)</code></pre>
<p><code>n_neighbors</code> 参数级是指定获取 K 个邻近点。</p>
<h2>训练</h2>
<p>训练的函数一般就是 <code>fit</code> :</p>
<pre><code>knn.fit(iris.data, iris.target)</code></pre>
<h2>测试</h2>
<p>模拟一些测试数据,使用刚刚的模型进行预测:</p>
<pre><code>predict = knn.predict([[0.1, 0.2, 0.3, 0.4]])
print(predict) # output: [0]</code></pre>
从零开始构造决策树(python)
https://segmentfault.com/a/1190000012328603
2017-12-07T16:53:03+08:00
2017-12-07T16:53:03+08:00
weapon
https://segmentfault.com/u/weapon
4
<h2>起步</h2>
<p>本章介绍如何不利用第三方库,仅用python自带的标准库来构造一个决策树。不过这可能需要你之前阅读过这方面的知识。</p>
<p><strong>前置阅读</strong></p>
<p><a href="https://segmentfault.com/a/1190000012289440">分类算法之决策树(理论篇)</a></p>
<p><a href="https://segmentfault.com/a/1190000012291948">分类算法之决策树(应用篇)</a></p>
<p>本文使用将使用《应用篇》中的训练集,向量特征仅有 0 和 1 两种情况。</p>
<h2>关于熵(entropy)的一些计算</h2>
<p>对于熵,根据前面提到的计算公式:</p>
<p>$$
H(X) = -\sum_{i=1}^np_i\log_2{(p_i)}
$$</p>
<p>对应的 python 代码:</p>
<pre><code>import math
import collections
def entropy(rows: list) -> float:
"""
计算数组的熵
"""
result = collections.Counter()
result.update(rows)
rows_len = len(rows)
assert rows_len # 数组长度不能为0
# 开始计算熵值
ent = 0.0
for r in result.values():
p = float(r) / rows_len
ent -= p * math.log2(p)
return ent</code></pre>
<h3>条件熵的计算</h3>
<p>根据计算方法:</p>
<p>$$
H(Y|X) = \sum_{i=1}^np_iH(Y|X=x_i)
$$</p>
<p>对应的 python 代码:</p>
<pre><code>def condition_entropy(future_list: list, result_list: list) -> float:
"""
计算条件熵
"""
entropy_dict = collections.defaultdict(list) # {0:[], 1:[]}
for future, value in zip(future_list, result_list):
entropy_dict[future].append(value)
# 计算条件熵
ent = 0.0
future_len = len(future_list) # 数据个数
for value in entropy_dict.values():
p = len(value) / future_len * entropy(value)
ent += p
return ent</code></pre>
<p>其中参数 <code>future_list</code> 是某一特征向量组成的列表,<code>result_list</code> 是 label 列表。</p>
<h3>信息增益</h3>
<p>根据信息增益的计算方法:</p>
<p>$$
gain(A) = H(D) - H(D|A)
$$</p>
<p>对应的python代码:</p>
<pre><code>def gain(future_list: list, result_list: list) -> float:
"""
获取某特征的信息增益
"""
info = entropy(result_list)
info_condition = condition_entropy(future_list, result_list)
return info - info_condition</code></pre>
<h2>定义决策树的节点</h2>
<p>作为树的节点,要有左子树和右子树是必不可少的,除此之外还需要其他信息:</p>
<pre><code>class DecisionNode(object):
"""
决策树的节点
"""
def __init__(self, col=-1, data_set=None, labels=None, results=None, tb=None, fb=None):
self.has_calc_index = [] # 已经计算过的特征索引
self.col = col # col 是待检验的判断条件,对应列索引值
self.data_set = data_set # 节点的 待检测数据
self.labels = labels # 对应当前列必须匹配的值
self.results = results # 保存的是针对当前分支的结果,有值则表示该点是叶子节点
self.tb = tb # 当信息增益最高的特征为True时的子树
self.fb = fb # 当信息增益最高的特征为False时的子树</code></pre>
<p>树的节点会有两种状态,叶子节点中 <code>results</code> 属性将保持当前的分类结果。非叶子节点中, <code>col</code> 保存着该节点计算的特征索引,根据这个索引来创建左右子树。</p>
<p><code>has_calc_index</code> 属性表示在到达此节点时,已经计算过的特征索引。特征索引的数据集上表现是列的形式,如数据集(不包含结果集):</p>
<pre><code>[
[1, 0, 1],
[0, 1, 1],
[0, 0, 1],
]</code></pre>
<p>有三条数据,三个特征,那么第一个特征对应了第一列 <code>[1, 0, 0]</code> ,它的索引是 <code>0</code> 。</p>
<h2>递归的停止条件</h2>
<p>本章将构造出完整的决策树,所以递归的停止条件是所有待分析的训练集都属于同一类:</p>
<pre><code>def if_split_end(result_list: list) -> bool:
"""
递归的结束条件,每个分支的结果集都是相同的分类
"""
result = collections.Counter(result_list)
return len(result) == 1</code></pre>
<h2>从训练集中筛选最佳的特征</h2>
<pre><code>def choose_best_future(data_set: list, labels: list, ignore_index: list) -> int:
"""
从特征向量中筛选出最好的特征,返回它的特征索引
"""
result_dict = {} # { 索引: 信息增益值 }
future_num = len(data_set[0])
for i in range(future_num):
if i in ignore_index: # 如果已经计算过了
continue
future_list = [x[i] for x in data_set]
result_dict[i] = gain(future_list, labels) # 获取信息增益
# 排序后选择第一个
ret = sorted(result_dict.items(), key=lambda x: x[1], reverse=True)
return ret[0][0]</code></pre>
<p>因此计算节点就是调用 <code>best_index = choose_best_future(node.data_set, node.labels, node.has_calc_index)</code> 来获取最佳的信息增益的特征索引。</p>
<h2>构造决策树</h2>
<p>决策树中需要一个属性来指向树的根节点,以及特征数量。不需要保存训练集和结果集,因为这部分信息是保存在树的节点中的。</p>
<pre><code>class DecisionTreeClass():
def __init__(self):
self.future_num = 0 # 特征
self.tree_root = None # 决策树根节点</code></pre>
<h3>创建决策树</h3>
<p>这里需要递归来创建决策树:</p>
<pre><code>def build_tree(self, node: DecisionNode):
# 递归条件结束
if if_split_end(node.labels):
node.results = node.labels[0] # 表明是叶子节点
return
#print(node.data_set)
# 不是叶子节点,开始创建分支
best_index = choose_best_future(node.data_set, node.labels, node.has_calc_index)
node.col = best_index
# 根据信息增益最大进行划分
# 左子树
tb_index = [i for i, value in enumerate(node.data_set) if value[best_index]]
tb_data_set = [node.data_set[x] for x in tb_index]
tb_data_labels = [node.labels[x] for x in tb_index]
tb_node = DecisionNode(data_set=tb_data_set, labels=tb_data_labels)
tb_node.has_calc_index = list(node.has_calc_index)
tb_node.has_calc_index.append(best_index)
node.tb = tb_node
# 右子树
fb_index = [i for i, value in enumerate(node.data_set) if not value[best_index]]
fb_data_set = [node.data_set[x] for x in fb_index]
fb_data_labels = [node.labels[x] for x in fb_index]
fb_node = DecisionNode(data_set=fb_data_set, labels=fb_data_labels)
fb_node.has_calc_index = list(node.has_calc_index)
fb_node.has_calc_index.append(best_index)
node.fb = fb_node
# 递归创建子树
if tb_index:
self.build_tree(node.tb)
if fb_index:
self.build_tree(node.fb)</code></pre>
<p>根据信息增益的特征索引将训练集再划分为左右两个子树。</p>
<h3>训练函数</h3>
<p>也就是要有一个 <code>fit</code> 函数:</p>
<pre><code>def fit(self, x: list, y: list):
"""
x是训练集,二维数组。y是结果集,一维数组
"""
self.future_num = len(x[0])
self.tree_root = DecisionNode(data_set=x, labels=y)
self.build_tree(self.tree_root)
self.clear_tree_example_data(self.tree_root)</code></pre>
<h3>清理训练集</h3>
<p>训练后,树节点中数据集和结果集等就没必要的,该模型只要 <code>col</code> 和 <code>result</code> 就可以了:</p>
<pre><code>def clear_tree_example_data(self, node: DecisionNode):
"""
清理tree的训练数据
"""
del node.has_calc_index
del node.labels
del node.data_set
if node.tb:
self.clear_tree_example_data(node.tb)
if node.fb:
self.clear_tree_example_data(node.fb)</code></pre>
<h3>预测函数</h3>
<p>提供一个预测函数:</p>
<pre><code>def _predict(self, data_test: list, node: DecisionNode):
if node.results:
return node.results
col = node.col
if data_test[col]:
return self._predict(data_test, node.tb)
else:
return self._predict(data_test, node.fb)
def predict(self, data_test):
"""
预测
"""
return self._predict(data_test, self.tree_root)</code></pre>
<h2>测试</h2>
<p>数据集使用前面《应用篇》中的向量化的训练集:</p>
<pre><code>if __name__ == "__main__":
dummy_x = [
[0, 0, 1, 0, 1, 1, 0, 0, 1, 0, ],
[0, 0, 1, 1, 0, 1, 0, 0, 1, 0, ],
[1, 0, 0, 0, 1, 1, 0, 0, 1, 0, ],
[0, 1, 0, 0, 1, 0, 0, 1, 1, 0, ],
[0, 1, 0, 0, 1, 0, 1, 0, 0, 1, ],
[0, 1, 0, 1, 0, 0, 1, 0, 0, 1, ],
[1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ],
[0, 0, 1, 0, 1, 0, 0, 1, 1, 0, ],
[0, 0, 1, 0, 1, 0, 1, 0, 0, 1, ],
[0, 1, 0, 0, 1, 0, 0, 1, 0, 1, ],
[0, 0, 1, 1, 0, 0, 0, 1, 0, 1, ],
[1, 0, 0, 1, 0, 0, 0, 1, 1, 0, ],
[1, 0, 0, 0, 1, 1, 0, 0, 0, 1, ],
[0, 1, 0, 1, 0, 0, 0, 1, 1, 0, ],
]
dummy_y = [0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0]
tree = DecisionTreeClass()
tree.fit(dummy_x, dummy_y)
test_row = [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, ]
print(tree.predict(test_row)) # output: 1</code></pre>
<h2>附录</h2>
<p>本次使用的完整代码:</p>
<pre><code># coding: utf-8
import math
import collections
def entropy(rows: list) -> float:
"""
计算数组的熵
:param rows:
:return:
"""
result = collections.Counter()
result.update(rows)
rows_len = len(rows)
assert rows_len # 数组长度不能为0
# 开始计算熵值
ent = 0.0
for r in result.values():
p = float(r) / rows_len
ent -= p * math.log2(p)
return ent
def condition_entropy(future_list: list, result_list: list) -> float:
"""
计算条件熵
"""
entropy_dict = collections.defaultdict(list) # {0:[], 1:[]}
for future, value in zip(future_list, result_list):
entropy_dict[future].append(value)
# 计算条件熵
ent = 0.0
future_len = len(future_list)
for value in entropy_dict.values():
p = len(value) / future_len * entropy(value)
ent += p
return ent
def gain(future_list: list, result_list: list) -> float:
"""
获取某特征的信息增益
"""
info = entropy(result_list)
info_condition = condition_entropy(future_list, result_list)
return info - info_condition
class DecisionNode(object):
"""
决策树的节点
"""
def __init__(self, col=-1, data_set=None, labels=None, results=None, tb=None, fb=None):
self.has_calc_index = [] # 已经计算过的特征索引
self.col = col # col 是待检验的判断条件,对应列索引值
self.data_set = data_set # 节点的 待检测数据
self.labels = labels # 对应当前列必须匹配的值
self.results = results # 保存的是针对当前分支的结果,有值则表示该点是叶子节点
self.tb = tb # 当信息增益最高的特征为True时的子树
self.fb = fb # 当信息增益最高的特征为False时的子树
def if_split_end(result_list: list) -> bool:
"""
递归的结束条件,每个分支的结果集都是相同的分类
:param result_list:
:return:
"""
result = collections.Counter()
result.update(result_list)
return len(result) == 1
def choose_best_future(data_set: list, labels: list, ignore_index: list) -> int:
"""
从特征向量中筛选出最好的特征,返回它的特征索引
"""
result_dict = {} # { 索引: 信息增益值 }
future_num = len(data_set[0])
for i in range(future_num):
if i in ignore_index: # 如果已经计算过了
continue
future_list = [x[i] for x in data_set]
result_dict[i] = gain(future_list, labels) # 获取信息增益
# 排序后选择第一个
ret = sorted(result_dict.items(), key=lambda x: x[1], reverse=True)
return ret[0][0]
class DecisionTreeClass():
def __init__(self):
self.future_num = 0 # 特征
self.tree_root = None # 决策树根节点
def build_tree(self, node: DecisionNode):
# 递归条件结束
if if_split_end(node.labels):
node.results = node.labels[0] # 表明是叶子节点
return
#print(node.data_set)
# 不是叶子节点,开始创建分支
best_index = choose_best_future(node.data_set, node.labels, node.has_calc_index)
node.col = best_index
# 根据信息增益最大进行划分
# 左子树
tb_index = [i for i, value in enumerate(node.data_set) if value[best_index]]
tb_data_set = [node.data_set[x] for x in tb_index]
tb_data_labels = [node.labels[x] for x in tb_index]
tb_node = DecisionNode(data_set=tb_data_set, labels=tb_data_labels)
tb_node.has_calc_index = list(node.has_calc_index)
tb_node.has_calc_index.append(best_index)
node.tb = tb_node
# 右子树
fb_index = [i for i, value in enumerate(node.data_set) if not value[best_index]]
fb_data_set = [node.data_set[x] for x in fb_index]
fb_data_labels = [node.labels[x] for x in fb_index]
fb_node = DecisionNode(data_set=fb_data_set, labels=fb_data_labels)
fb_node.has_calc_index = list(node.has_calc_index)
fb_node.has_calc_index.append(best_index)
node.fb = fb_node
# 递归创建子树
if tb_index:
self.build_tree(node.tb)
if fb_index:
self.build_tree(node.fb)
def clear_tree_example_data(self, node: DecisionNode):
"""
清理tree的训练数据
:return:
"""
del node.has_calc_index
del node.labels
del node.data_set
if node.tb:
self.clear_tree_example_data(node.tb)
if node.fb:
self.clear_tree_example_data(node.fb)
def fit(self, x: list, y: list):
"""
x是训练集,二维数组。y是结果集,一维数组
"""
self.future_num = len(x[0])
self.tree_root = DecisionNode(data_set=x, labels=y)
self.build_tree(self.tree_root)
self.clear_tree_example_data(self.tree_root)
def _predict(self, data_test: list, node: DecisionNode):
if node.results:
return node.results
col = node.col
if data_test[col]:
return self._predict(data_test, node.tb)
else:
return self._predict(data_test, node.fb)
def predict(self, data_test):
"""
预测
"""
return self._predict(data_test, self.tree_root)
if __name__ == "__main__":
dummy_x = [
[0, 0, 1, 0, 1, 1, 0, 0, 1, 0, ],
[0, 0, 1, 1, 0, 1, 0, 0, 1, 0, ],
[1, 0, 0, 0, 1, 1, 0, 0, 1, 0, ],
[0, 1, 0, 0, 1, 0, 0, 1, 1, 0, ],
[0, 1, 0, 0, 1, 0, 1, 0, 0, 1, ],
[0, 1, 0, 1, 0, 0, 1, 0, 0, 1, ],
[1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ],
[0, 0, 1, 0, 1, 0, 0, 1, 1, 0, ],
[0, 0, 1, 0, 1, 0, 1, 0, 0, 1, ],
[0, 1, 0, 0, 1, 0, 0, 1, 0, 1, ],
[0, 0, 1, 1, 0, 0, 0, 1, 0, 1, ],
[1, 0, 0, 1, 0, 0, 0, 1, 1, 0, ],
[1, 0, 0, 0, 1, 1, 0, 0, 0, 1, ],
[0, 1, 0, 1, 0, 0, 0, 1, 1, 0, ],
]
dummy_y = [0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0]
tree = DecisionTreeClass()
tree.fit(dummy_x, dummy_y)
test_row = [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, ]
print(tree.predict(test_row))</code></pre>
分类算法之邻近算法:KNN(理论篇)
https://segmentfault.com/a/1190000012310793
2017-12-06T16:43:44+08:00
2017-12-06T16:43:44+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>今天介绍另一种分类算法,k邻近算法( <code>k-nearest neighbors</code> ),即 KNN 算法。</p>
<h2>概述</h2>
<p>Cover 和 Hart 在 1968 年提出了最初的邻近算法,用于解决分类( <code>classification</code> )的问题。关于这个算法在维基百科中也有介绍:<a href="https://link.segmentfault.com/?enc=eUUPaRZMzCLU84loon3UjQ%3D%3D.hjDpgeMZ0tPugjolHfAMiz8myQ0uzuHXQaESc%2FdfNFT96MBrYDZeO3Sl1vomt4r73qa2hlz4FuKiPHcLIwOeqKwmp9SHifmm90q5PUHHKho%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=%2BYMITsL3PTbhRVYp94JftA%3D%3D.IYqcRoiowPehjQclXgaohZQ0rS02GrJPYeVls0WGgx8ERNCvdGbnYmZjQ5VFm8dViBt3z6w9%2FD8n4ptftfkQNPml7N6KR4pi8BILiaJMwBs%3D" rel="nofollow">https://zh.wikipedia.org/wiki...</a> 。</p>
<p>KNN是一种基于实例学习( <code>instance-based learning</code> ),或者所是将所有计算推迟到分类之后的惰性学习( <code>lazy learning</code> )的一种算法,KNN是所有机器学习算法中最简单算法之一。</p>
<h2>从案例中说起</h2>
<p>一个有关电影分类的例子:</p>
<p><img src="/img/remote/1460000012310798?w=565&h=404" alt="Image.png" title="Image.png"></p>
<p>这个一个根据打斗次数和接吻次数作为特征来进行类型的分类。最后一条的记录就是待分类的数据。</p>
<p>KNN这个分类过程比较简单的一个原因是它不需要创建模型,也不需要进行训练,并且非常容易理解。把例子中打斗次数和接吻次数看成是x轴和y轴,那么就很容易能建立一个二维坐标,每条记录都是坐标中的点。对于未知点来说,寻找其最近的几个点,哪种分类数较多,未知点就属于哪一类。</p>
<h2>算法说明</h2>
<p>KNN算法的思路是: 如果一个样本在特征空间中的 <code>k</code> 个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。通常 K 的取值比较小,不会超过 20。</p>
<p><strong>算法步骤为:</strong></p>
<ul>
<li>计算未知实例到所有已知实例的距离;</li>
<li>选择参数 K;</li>
<li>根据多数表决( <code>majority-voting</code> )规则,将未知实例归类为样本中最多数的类别。</li>
</ul>
<h2>距离的衡量方法</h2>
<p>关于距离的测量方式有多种,这里只介绍两种。</p>
<p><strong>欧拉距离</strong><br>这种测量方式就是简单的平面几何中两点之间的直线距离。</p>
<p><img src="/img/remote/1460000012310799?w=277&h=182" alt="images.jpg" title="images.jpg"></p>
<p>并且这种方法可以延伸至三维或更多维的情况。它的公式可以总结为:</p>
<p>$$
d(x,y) = \sqrt{\sum_{i=0}^n(x_i-y_i)^2}
$$</p>
<p><strong>曼哈顿距离</strong><br>顾名思义,城市街区的距离就不能是点和点的直线距离,而是街区的距离。如棋盘上也会使用曼哈顿距离的计算方法:</p>
<p>$$
d(x,y) = \sqrt{\sum_{i=0}^n|x_i-y_i|}
$$</p>
<h2>K 值的选择</h2>
<p>K值的选择会影响结果,有一个经典的图如下:</p>
<p><img src="/img/remote/1460000012310800?w=279&h=252" alt="279px-KnnClassification.svg.png" title="279px-KnnClassification.svg.png"></p>
<p>图中的数据集是良好的数据,即都打好了 <code>label</code> ,一类是蓝色的正方形,一类是红色的三角形,那个绿色的圆形是待分类的数据。</p>
<ul>
<li><ol><li>= 3 时,范围内红色三角形多,这个待分类点属于红色三角形。</li></ol></li>
<li><ol><li>= 5 时,范围内蓝色正方形多,这个待分类点属于蓝色正方形。</li></ol></li>
</ul>
<p>如何选择一个最佳的K值取决于数据。一般情况下,在分类时较大的 K 值能够减小噪声的影响,但会使类别之间的界限变得模糊。因此 K 的取值一般比较小 ( <code>K < 20</code> )。</p>
<h2>改进</h2>
<p>在下面一种情况中:</p>
<p><img src="/img/remote/1460000012310801?w=522&h=464" alt="Image.png" title="Image.png"></p>
<p>在点Y的预测中,改范围内三角形分类数量占优,因此将Y点归为三角形。但是从视觉上观测,应该是分为圆形分类更为合理。根据这种情况就在距离测量中加上权重,比如 <code>1/d</code> (d: 距离)。</p>
<h2>KNN 的优缺点</h2>
<p>优点:</p>
<ul>
<li>简单,易于理解,无需建模与训练,易于实现;</li>
<li>适合对稀有事件进行分类;</li>
<li>适合与多分类问题,例如根据基因特征来判断其功能分类,kNN比SVM的表现要好。</li>
</ul>
<p>缺点:</p>
<ul>
<li>惰性算法,内存开销大,对测试样本分类时计算量大,性能较低;</li>
<li>可解释性差,无法给出决策树那样的规则。</li>
</ul>
分类算法之决策树(应用篇)
https://segmentfault.com/a/1190000012291948
2017-12-05T15:38:07+08:00
2017-12-05T15:38:07+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>在理论篇我们介绍了决策树的构建和一些关于熵的计算方法,这篇文章将根据一个例子,用代码上来实现决策树。</p>
<h2>实验环境</h2>
<ul>
<li>操作系统: win10 64</li>
<li>编程语言: Python3.6</li>
</ul>
<p>用到的第三方模块有:</p>
<pre><code>numpy (1.12.1+mkl)
scikit-learn (0.19.1)</code></pre>
<h2>数据源</h2>
<p>为了方便理解和架设,我们用理论篇中买电脑的例子:</p>
<p><img src="/img/remote/1460000012291953?w=478&h=341" alt="Image.png" title="Image.png"></p>
<p>将这些记录保存成 <code>csv</code> 文件:</p>
<pre><code>RID,age,income,student,credit_rating,class:buys_computer
1,youth,hight,no,fair,no
2,youth,hight,no,excellent,no
3,middle_aged,hight,no,fair,yes
4,senior,medium,no,fair,yes
5,senior,low,yes,fair,yes
6,senior,low,yes,excellent,no
7,middle_aged,low,yes,excellent,yes
8,youth,medium,no,fair,no
9,youth,low,yes,fair,yes
10,senior,medium,yes,fair,yes
11,youth,medium,yes,excellent,yes
12,middle_aged,medium,no,excellent,yes
13,middle_aged,hight,yes,fair,yes
14,senior,medium,no,excellent,no</code></pre>
<p>这些数据就是这次应用的数据源。</p>
<h2>数据整理</h2>
<p>可以利用python标准库中 <code>csv</code> 来对这个数据源进行读取,要对原始数据集进行整理,随机变量放在一个数组,分类结果放在另一个数组,形如:</p>
<pre><code>future_list = [
{
"age" : "youth",
"income": "hight",
...
}
...
]
answer_list = ["no", "no", "yes", ...]</code></pre>
<p>按照这个思路我们构造一下:</p>
<pre><code>data_file = open("computer_buy.csv", "r")
reader = csv.reader(data_file)
headers = next(reader)
future_list = []
label_list = []
for row in reader:
label_list.append(row[-1])
row_dict = {}
for i in range(1, len(row) -1):
row_dict[ headers[i] ] = row[i]
future_list.append(row_dict)
data_file.close()</code></pre>
<h2>随机变量向量化</h2>
<p>在 <code>sklearn</code> 提供的库中,对输入的特征有一定的要求,所有特征和分类都要是数值型的值,不能是例子中的类别的值。</p>
<p><strong>怎么转化呢?</strong><br>比方说 <code>age</code> 这个特征,它有三个值: <code>youth</code> , <code>middle_aged</code> , <code>senior</code> 。有一条记录的 <code>age=youth</code> 针对这个特征我们就变成:</p>
<table>
<thead><tr>
<th>youth</th>
<th>middle_aged</th>
<th>senior</th>
</tr></thead>
<tbody><tr>
<td>1</td>
<td>0</td>
<td>0</td>
</tr></tbody>
</table>
<p>那么第一条记录 <code>youth,hight,no,fair</code> 转化为:</p>
<table>
<thead><tr>
<th>age=middle_aged</th>
<th>age=senior</th>
<th>age=youth</th>
<th>credit_rating=excellent</th>
<th>credit_rating=fair</th>
<th>income=hight</th>
<th>income=low</th>
<th>income=medium</th>
<th>student=no</th>
<th>student=yes</th>
</tr></thead>
<tbody><tr>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr></tbody>
</table>
<h3>特征向量化</h3>
<pre><code>from sklearn.feature_extraction import DictVectorizer
dummy_x = vec.fit_transform(future_list).toarray()
print("dummy_x:", dummy_x)
print("vec.get_feature_names()", vec.get_feature_names())</code></pre>
<h3>分类结果向量化</h3>
<pre><code>from sklearn import preprocessing
lb = preprocessing.LabelBinarizer()
dummy_y = lb.fit_transform(label_list)</code></pre>
<h2>构造决策树</h2>
<p>在 <code>sklearn</code> 中提供了多种决策树构建方法,这边需要向其表明,是依据 <code>信息增益</code> 的方式来构造决策树的,因此需要传入一个参数<br><code>criterion='entropy'</code>:</p>
<pre><code>from sklearn import tree
# 构造决策树
clf = tree.DecisionTreeClassifier(criterion='entropy')
clf.fit(dummy_x, dummy_y)
print("clf: ", clf)</code></pre>
<h2>保存模型</h2>
<p>将训练好的模型保存到文件里去:</p>
<pre><code># 保存模型
with open("result.dot", "w") as f:
tree.export_graphviz(clf, feature_names=vec.get_feature_names(), out_file=f)</code></pre>
<h2>测试数据</h2>
<p>接下来就是给它随机变量,让决策树来进行分类。我们修改第一条记录来进行测试:</p>
<pre><code># 测试数据
first_row = dummy_x[0, :]
new_row = list(first_row)
new_row[0] = 1
new_row[2] = 0
predict = clf.predict([new_row])
print("predict:", predict) # output: [1]</code></pre>
<h2>模型可视化</h2>
<p>可视化用到了 <code>Graphviz</code> 软件,可以到官网:<a href="https://link.segmentfault.com/?enc=pIwxNG0LMv3xkoirAEoVgg%3D%3D.LNmP7xPcJd3%2FsIPpI3iQlWCfGjtJ5xIKl11rNpAjWSw%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=RwpJHW7nh%2FJ7lwHqHoyTGg%3D%3D.ooQ9H7clK%2B9MXaA3vEn5%2B9v%2BTrLPsrSH7ZGJqzy6yQs%3D" rel="nofollow">http://www.graphviz.org/</a> 下载,我下载的是 zip 文件,解压后将目录加到环境变量中去。</p>
<p>转化 <code>dot</code> 文件至 <code>pdf</code> 可视化决策树的命令:</p>
<pre><code>dot -Tpdf result.dot -o outpu.pdf</code></pre>
<p>得到一个pdf文件,打开可以看到决策树:</p>
<p><img src="/img/remote/1460000012291954?w=665&h=633" alt="20171205153457.png" title="20171205153457.png"></p>
<h2>附录</h2>
<p>本次应用的全部代码:</p>
<pre><code># coding: utf-8
import csv
from sklearn.feature_extraction import DictVectorizer
from sklearn import preprocessing
from sklearn import tree
data_file = open("computer_buy.csv", "r")
reader = csv.reader(data_file)
headers = next(reader)
future_list = []
label_list = []
for row in reader:
label_list.append(row[-1])
row_dict = {}
for i in range(1, len(row) -1):
row_dict[ headers[i] ] = row[i]
future_list.append(row_dict)
data_file.close()
# 向量化 x
vec = DictVectorizer()
dummy_x = vec.fit_transform(future_list).toarray()
print("dummy_x:", dummy_x)
print("vec.get_feature_names()", vec.get_feature_names())
# 向量化 y
lb = preprocessing.LabelBinarizer()
dummy_y = lb.fit_transform(label_list)
# 构造决策树
clf = tree.DecisionTreeClassifier(criterion='entropy')
clf.fit(dummy_x, dummy_y)
print("clf: ", clf)
# 保存模型
with open("result.dot", "w") as f:
tree.export_graphviz(clf, feature_names=vec.get_feature_names(), out_file=f)
# 测试数据
first_row = dummy_x[0, :]
new_row = list(first_row)
new_row[0] = 1
new_row[2] = 0
predict = clf.predict([new_row])
print("predict:", predict)</code></pre>
分类算法之决策树(理论篇)
https://segmentfault.com/a/1190000012289440
2017-12-05T13:46:53+08:00
2017-12-05T13:46:53+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2>
<p>决策树(<code>decision tree</code>)是一个树结构,可以是二叉树或非二叉树,也可以把他看作是 <code>if-else</code> 规则的集合,也可以认为是在特征空间上的条件概率分布。</p>
<h2>决策树的结构</h2>
<p>以一个简单的用于是否买电脑预测的决策树为例子:</p>
<p><img src="/img/remote/1460000012289445?w=400&h=239" alt="v2-c124b112aa3ef385d210b6c03e1ff458_hd.jpg" title="v2-c124b112aa3ef385d210b6c03e1ff458_hd.jpg"></p>
<p>树中的内部节点代表一个属性,节点引出的分支表示这个属性的所有可能的值,叶节点表示最终的分类结果。从根节点到叶节点的每一条路径构建一条规则,并且这些规则具有 <code>互斥且完备</code> 的性质,即每一个样本均被且只有一条路径所覆盖。</p>
<p>决策树的创建是根据给定的训练数据来完成的,给出下面的训练集(本章都是围着这个例子进行讲解):</p>
<p><img src="/img/remote/1460000012289446?w=478&h=341" alt="Image.png" title="Image.png"></p>
<p>这是一个是否买电脑的一个数据,数据上有4个特征:年龄( <code>age</code> ),收入( <code>income</code> ),是否学生( <code>student</code> ),信用度( <code>credit_rating</code> )。</p>
<p>案例的决策树中,为什么是以年龄作为第一个进行分类的特征呢?</p>
<h2>特征的分类能力</h2>
<p>如果一个特征对结果影响比较大,那么就可以认为这个特征的分类能力比较大。相亲时候一般会先问收入,再问长相,然后问其家庭情况。也就是说在这边收入情况影响比较大,所以作为第一个特征判断,如果不合格那可能连后续都不用询问了。</p>
<p><strong>有什么方法可以表明特征的分类能力呢?</strong><br>这时候,需要引入一个概念,<code>熵</code> 。</p>
<h3>熵(entropy)</h3>
<p>1948年,香农提出“信息熵”的概率。一条信息的信息量大小和它的不确定性有直接的关系,要搞清楚一件不确定的事,需要了解大量信息。熵(entropy)用于表示 <strong>随机变量不确定性的度量</strong>, 如果熵越大,表示不确定性越大。</p>
<p>假设变量X,它有Xi(i=1,2,3...n)种情况,pi表示第i情况的概率,那么随机变量X的熵定义为:</p>
<p>$$
H(X) = -\sum_{i=1}^np_i\log_2{(p_i)}
$$</p>
<p>熵的单位是比特(bit)。</p>
<p>比如当随机变量X只有0,1两种取值,则有: <code>H(x) = -plog(p) - (1-p)log(1-p)</code> , 可以画出一个二维坐标表示他们的关系:</p>
<p><img src="/img/remote/1460000012289447?w=498&h=426" alt="9911d23ae3bcc854e59a59365b5365be_hd.jpg" title="9911d23ae3bcc854e59a59365b5365be_hd.jpg"></p>
<p>从而可知,当 <code>p=0.5</code> 时,熵取值最大,随机变量不确定性最大。</p>
<p>回到买电脑的例子,在是否购买电脑这个结果中,数据集D,有 <code>9</code> 个yes,<code>5</code> 个no。因此它的熵是:</p>
<p>$$
info(D) = H(D) = - \frac{9}{14}\log_2(\frac{9}{14}) - \frac5{14}\log_2(\frac5{14}) = 0.940 bits
$$</p>
<p><strong>条件熵(conditional entropy)</strong></p>
<p>随机变量X给定的条件下,随机变量Y的条件熵 <code>H(Y|X)</code> 定义为:</p>
<p>$$
H(Y|X) = \sum_{i=1}^np_iH(Y|X=x_i)
$$</p>
<h3>信息增益 (Information gain)</h3>
<p>信息增益表示的是:得知 <code>特征X</code> 的信息而使得 <code>分类Y</code> 的信息的不确定性减少的程度。如果某个特征的信息增益比较大,就表示该特征对结果的影响较大,特征A对数据集D的信息增益表示为:</p>
<p>$$
gain(A) = H(D) - H(D|A)
$$</p>
<p>以那个买电脑的数据集为例,我们来计算下 <code>age</code> 这个特征的信息增益,将数据再展示一下:</p>
<p><img src="/img/remote/1460000012289446?w=478&h=341" alt="Image.png" title="Image.png"></p>
<p>从图中可以看出,有14条数据 <code>age</code> 这个特征中,年轻人 <code>youth</code> 有5人, 中年人 <code>middle_aged</code> 有4人,老年人 <code>senior</code> 有5人。分别计算这三种情况下的信息熵,再将信息熵相加就能得到 <code>H(D|A)</code>:</p>
<p>$$
\begin{align*}
info_{age}(D) = H(D|A) &= \frac5{14}\times (-\frac25\log_2\frac25 - \frac35\log_2\frac35) \\
&+\frac4{14}\times (-\frac44\log_2\frac44 - \frac04\log_2\frac04) \\
&+\frac5{14}\times (-\frac35\log_2\frac35 - \frac25\log_2\frac25) \\
&=0.694 bits
\end{align*}
$$</p>
<p>因此,<code>gain(age)</code> 的信息增益就是:</p>
<pre><code class="latex">gain(age) = info(D) - info_{age}(D) = 0.940 - 0.694 = 0.246 bits</code></pre>
<h2>决策树归纳算法 (ID3)</h2>
<p>ID3算法的核心是在决策树的各个结点上应用 <code>信息增益</code> 准则进行特征选择。这个算法也是本章主要介绍的算法。具体做法是:</p>
<ul>
<li>从根节点开始,对结点计算所有可能特征的信息增益,选择信息增益最大的特征作为结点的特征,并由该特征的不同取值构建子节点;</li>
<li>对子节点递归地调用以上方法,构建决策树;</li>
<li>直到所有特征的信息增益均很小或者没有特征可选时为止。</li>
</ul>
<p>根据上面的计算信息增量的方法,可以得出其他特征的信息增量:<br><code>gain(income) = 0.029, gain(student) = 0.151, gain(credit_rating)=0.048</code> 。</p>
<p><code>age</code> 这个特征的信息增益是最大的(0.246 bits),选择age作为第一个根节点进行分类:</p>
<p><img src="/img/remote/1460000012289448" alt="Image.png" title="Image.png"></p>
<p>然后再每个子树中,再根据其特征的信息增益量进行每个划分,递归地形成每个划分上的样本判定树。</p>
<h3>递归的停止条件</h3>
<p>递归划分步骤仅当下列条件之一成立停止:<br>(a) 给定结点的所有样本属于同一类。<br>(b) 没有剩余属性可以用来进一步划分样本。在此情况下,使用多数表决。这涉及将给定的结点转换成树叶,并用样本中的多数所在的类标记它。替换地,可以存放结点样本的类分布。<br>(c) 分枝,当所有特征的信息增益都很小,也就是没有再计算的必要了,就创建一个树叶,也是用多数表决。</p>
<h2>其他决策树归纳算法</h2>
<h3>C4.5算法</h3>
<p>C4.5算法与ID3算法的区别主要在于它在生产决策树的过程中,使用信息增益比来进行特征选择。</p>
<h3>CART算法</h3>
<p>分类与回归树(classification and regression tree,CART)与C4.5算法一样,由ID3算法演化而来。CART假设决策树是一个二叉树,它通过递归地二分每个特征,将特征空间划分为有限个单元,并在这些单元上确定预测的概率分布。</p>
<p>CART算法中,对于回归树,采用的是平方误差最小化准则;对于分类树,采用基尼指数最小化准则。</p>
<hr>
<p>这些算法共同点:都是贪心算法,自上而下的创建决策树。不同点是在于对特征的选择度量方法不同。</p>
<h2>决策树的剪枝</h2>
<p>如果树长到叶子深度太大,就会造成一种情况,在训练集上表现非常好,但是因为分的太细了,在新的数据上就表现不好了。就是出现过度拟合的现象。为了避免这个问题,有两种解决办法:</p>
<ol>
<li>先剪枝:当熵减少的数量小于某一个阈值时,就停止分支的创建。这是一种贪心算法。</li>
<li>后剪枝:先创建完整的决策树,然后再尝试消除多余的节点,也就是采用减枝的方法。</li>
</ol>
<h2>总结:决策树的优缺点</h2>
<p>优点:</p>
<ul>
<li>易于理解和解释,甚至比线性回归更直观;</li>
<li>与人类做决策思考的思维习惯契合;</li>
<li>模型可以通过树的形式进行可视化展示;</li>
<li>可以直接处理非数值型数据,不需要进行哑变量的转化,甚至可以直接处理含缺失值的数据;</li>
</ul>
<p>缺点:</p>
<ul>
<li>处理连续变量不好;</li>
<li>不好处理变量之间存在许多错综复杂的关系,如金融数据分析;</li>
<li>决定分类的因素取决于更多变量的复杂组合时;</li>
<li>可规模性一般。</li>
</ul>
python中的无穷大
https://segmentfault.com/a/1190000011908915
2017-11-08T12:12:50+08:00
2017-11-08T12:12:50+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2>
<p>python中整型不用担心溢出,因为python理论上可以表示无限大的整数,直到把内存挤爆。而无穷大在编程中常常需要的。比如,从一组数字中筛选出最小的数字。一般使用一个临时变量用于存储最后结果,变量去逐个比较和不断地更新。而这临时变量一般要初始无穷大或者去第一个元素的值。</p>
<h2>正无穷大与负无穷大</h2>
<p>python中并没有特殊的语法来表示这些值,但是可以通过 <code>float()</code> 来创建它们:</p>
<pre><code>>>> a = float("inf")
>>> b = float("-inf")
>>> a
inf
>>> b
-inf</code></pre>
<p>为了测试这些值的存在,使用 <code>math.isinf()</code> 进行判断:</p>
<pre><code>>>> import math
>>> math.isinf(a)
True
>>> math.isinf(b)
True</code></pre>
<h2>无穷大数在执行数学计算的时候会传播</h2>
<p>这个就类似于数学中讲述的,无穷大加上一个常数还是无穷大,无穷大与无穷大相等:</p>
<pre><code>>>> a = float('inf')
>>> a + 45
inf
>>> a * 10
inf
>>> 10 / a
0.0
>>> float("inf") == float("inf")
True</code></pre>
<p>无穷大在比较中比任何一个数都要大。</p>
<h2>正无穷与负无穷相加的结果是什么</h2>
<p>有些操作时未定义的并会返回一个 <code>NaN</code> 结果:</p>
<pre><code>>>> a = float('inf')
>>> a/a
nan
>>> b = float('-inf')
>>> a + b
nan</code></pre>
<h2>表示非数字的 NaN</h2>
<p><code>nan</code> 值在所有操作中也会传播,并且不会产生异常:</p>
<pre><code>>>> c = float('nan')
>>> c + 23
nan
>>> c / 2
nan
>>> c * 2
nan
>>> math.sqrt(c)
nan</code></pre>
<p>使用 <code>math.isnan()</code> 可以判断值是否是 <code>NaN</code>:</p>
<pre><code>>>> math.isnan(c)
True</code></pre>
<p><code>nan</code> 值的任何比较操作都是返回 <code>False</code> :</p>
<pre><code>>>> float("nan") == float("nan")
False
>>> c > 3
False</code></pre>
<h2>更安全的类型转换</h2>
<p>由于无穷的存在,因此字符串装浮点数就存在的一些例外,并且这个转换过程不会抛出异常。如果程序员们想改变 python 的默认行为,可以使用 <code>fpectl</code> 模块,但是它在标准的Python 构建中并没有被启用,它是平台相关的,并且针对的是专家级程序员。这里提供一个比较简单的转换,就是加一个 <code>isdigit()</code> 判断:</p>
<pre><code>def str2float(ss):
if not ss.isdigit():
raise ValueError
return float(ss)
sss = "inf"
a = str2float(sss)</code></pre>
<h2>总结</h2>
<p>以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。</p>
python中精确的浮点数运算
https://segmentfault.com/a/1190000011828463
2017-11-02T15:10:06+08:00
2017-11-02T15:10:06+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2>
<p>浮点数的一个普遍的问题是它们不能精确的表示十进制数。</p>
<pre><code>>>> a = 4.2
>>> b = 2.1
>>> a + b
6.300000000000001
>>> (a + b) == 6.3
False
>>></code></pre>
<p>这是由于底层 CPU 和 <a href="https://link.segmentfault.com/?enc=N8%2BU%2BOnYT3np0mtB1sy5fA%3D%3D.6jYUVmdQUjYft4l3WSXLU%2B8bUCzVDdKpRixjQjfHKVTq2LPvjLYoTu7lH4i%2FiPJw" rel="nofollow">IEEE 754</a> 标准通过自己的浮点单位去执行算术时的特征。看似有穷的小数, 在计算机的二进制表示里却是无穷的。</p>
<p>一般情况下,这一点点的小误差是允许存在的。如果不能容忍这种误差(比如金融领域),那么就要考虑用一些途径来解决这个问题了。</p>
<h2>Decimal</h2>
<p>使用这个模块不会出现任何小误差。</p>
<pre><code>>>> from decimal import Decimal
>>> a = Decimal('4.2')
>>> b = Decimal('2.1')
>>> a + b
Decimal('6.3')
>>> print(a + b)
6.3
>>> (a + b) == Decimal('6.3')
True</code></pre>
<p>尽管代码看起来比较奇怪,使用字符串来表示数字,但是 <code>Decimal</code> 支持所有常用的数学运算。 <code>decimal</code> 模块允许你控制计算的每一方面,包括数字位数和四舍五入。在这样做之前,需要创建一个临时上下文环境来改变这种设定:</p>
<pre><code>>>> from decimal import Decimal, localcontext
>>> a = Decimal('1.3')
>>> b = Decimal('1.7')
>>> print(a / b)
0.7647058823529411764705882353
>>> with localcontext() as ctx:
... ctx.prec = 3
... print(a / b)
...
0.765
>>> with localcontext() as ctx:
... ctx.prec = 50
... print(a / b)
...
0.76470588235294117647058823529411764705882352941176
>>></code></pre>
<p>由于 <code>Decimal</code> 的高精度数字自然也就用字符串来做展示和中转。</p>
<h2>总结</h2>
<p>总的来说,当涉及金融领域时,哪怕是一点小小的误差在计算过程中都是不允许的。因此 <code>decimal</code> 模块为解决这类问题提供了方法。</p>
使用C/C++编写Python模块扩展
https://segmentfault.com/a/1190000011693325
2017-10-24T11:53:10+08:00
2017-10-24T11:53:10+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2>
<p>由于python在底层运算中会对每个运算做类型检查, 这就影响了运行的性能,而利用扩展, 可以避免这样的情况, 获得优越的执行性能,利用Python提供的C API,如宏,类型,函数等来编写扩展。</p>
<h2>前期准备</h2>
<p>此次编写的环境为:</p>
<ul>
<li>系统:Ubuntu 15.10</li>
<li>GCC:5.2.1</li>
<li>Python:2.7.10</li>
</ul>
<p>环境版本不一致一般也不会有什么问题,确保已安装python的开发包:<code>sudo apt-get install python-dev</code></p>
<h2>开始</h2>
<p>以下已判断一个数是否为质数为例,py.c:</p>
<pre><code>#include<stdio.h>
#include<python2.7/Python.h> //有的是#include<Python.h>
//判断是否是质数
static PyObject *pr_isprime(PyObject *self, PyObject *args) {
int n, num;
//解析参数
if (!PyArg_ParseTuple(args, "i", &num)) {
return NULL;
}
if (num < 1) {
return Py_BuildValue("i", 0); //C类型转成python对象
}
n = num - 1;
while (n > 1) {
if (num % n == 0)
return Py_BuildValue("i", 0);
n--;
}
return Py_BuildValue("i", 1);
}
static PyMethodDef PrMethods[] = {
//方法名,导出函数,参数传递方式,方法描述。
{"isPrime", pr_isprime, METH_VARARGS, "check if an input number is prime or not."},
{NULL, NULL, 0, NULL}
};
void initpr(void) {
(void) Py_InitModule("pr", PrMethods);
}
</code></pre>
<p>以上代码包含了3个部分:</p>
<ul>
<li>导出函数:C模块对外暴露的接口函数为<code>pr_isprime</code>,带有self和args两个参数,args包含了python解释器要传给c函数的所有参数,通常使用PyArg_ParseTuple()来获得这些参数值。</li>
<li>初始化函数:一遍python解释器能够对模块进行正确的初始化,初始化要以<code>init</code>开头,如initp。</li>
<li>方法列表:提供给外部的python程序使用函数名称映射表<code>PrMethods</code>,它是一个<code>PyMethodDef</code>结构体,成员依次是方法名,导出函数,参数传递方式,方法描述。</li>
</ul>
<p>PyMethodDef原型:</p>
<pre><code>struct PyMethodDef {
char* ml_name; #方法名
PyCFunction ml_meth; #导出函数
int ml_flags; #参数传递方式
char* ml_doc; #方法描述
}</code></pre>
<p>参数传递方式一般设置为<code>METH_VARARGS</code>,该结构体必须设置以<code>{NULL, NULL, 0, NULL}</code>表示一条空记录作为结尾。</p>
<h2>setup.py脚本</h2>
<p>为模块写一个安装程序:</p>
<pre><code>#!/usr/bin/env python
# coding=utf-8
from distutils.core import setup, Extension
module = Extension('pr', sources = ['py.c'])
setup(name = 'Pr test', version = '1.0', ext_modules = [module])</code></pre>
<p>使用<code>python setup.py build</code>进行编译,系统会在当前目录下生产一个build目录,里面包含pr.so和pr.o文件。</p>
<p><img src="/img/remote/1460000011693328?w=1214&h=258" alt="setup_build.png" title="setup_build.png"></p>
<h2>安装模块</h2>
<p>下面三种方法任一种都可以:</p>
<ul>
<li>将生产的pr.so复制到python的site_packages目录下(我的是<code>/usr/local/lib/python2.7/dist-packages</code>,放到site_packages反而没作用)。</li>
<li>或者将pr.so路径添加到sys.path中。</li>
<li>或者用<code>python setup.py install</code>让python完成安装过程。</li>
</ul>
<h2>测试</h2>
<p><img src="/img/remote/1460000011693329?w=708&h=170" alt="pr_test.png" title="pr_test.png"></p>
<p>更多关于C模块扩展内容:<a href="https://link.segmentfault.com/?enc=2h8Uuxp2zCO5Ma%2FgR8rMlA%3D%3D.34Vn6ZGFaVvTrZnDA6Fn4qzJxYQ%2FbUYufqC5fkWUKo2hFMpXaM3kPpbAgdMZHNjH" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=F5rliqhiK1VfdaJjaABxeg%3D%3D.KIuQa3mf9caD%2FlWDNaLVYDtqIwRe5bBfZmcn9SkAcXDBSntr6iQljvtiPMCFj%2BX0" rel="nofollow">https://docs.python.org/2/c-a...</a></p>
C语言printf缓冲问题
https://segmentfault.com/a/1190000011673263
2017-10-23T10:59:03+08:00
2017-10-23T10:59:03+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p><img src="/img/remote/1460000011673268" alt="20171023101458.png" title="20171023101458.png"></p>
<p>标准输出被滞后了. 不同编译器出来的结果可能不一样. 我在windows平台的 VC++6.0 上是 121212.</p>
<h2>分析</h2>
<p>标准输出和标准出错的缓冲机制不同,标准出错不缓冲,标准输出有缓冲.</p>
<p><strong>什么情况下会刷新缓冲区?</strong></p>
<ul>
<li>程序结束时调用 <code>exit(0)</code> .</li>
<li>遇到 <code>\n</code> , <code>\r</code> 时会刷新缓冲区.</li>
<li>手动刷新 <code>fflush</code> .</li>
<li>缓冲区满时自动刷新.</li>
</ul>
<h2>附录</h2>
<p>示例代码:</p>
<pre><code>#include <stdio.h>
int main(int argc, char const *argv[])
{
int i;
for (i = 0; i < 3; ++i)
{
printf("1");
fprintf(stderr, "2");
}
}</code></pre>
<hr>
<p>找到了让 windows 平台也使用输出缓冲的方式了:</p>
<pre><code>#include <stdio.h>
char buf[512];
int main(int argc, char const *argv[])
{
setvbuf(stdout, buf, _IOLBF, 512);
int i;
for (i = 0; i < 3; ++i)
{
printf("1");
fprintf(stderr, "2");
}
}</code></pre>
《流畅的python》阅读笔记
https://segmentfault.com/a/1190000011568813
2017-10-16T10:30:37+08:00
2017-10-16T10:30:37+08:00
weapon
https://segmentfault.com/u/weapon
83
<h2>起步</h2>
<p>《流畅的python》是一本适合python进阶的书, 里面介绍的基本都是高级的python用法. 对于初学python的人来说, 基础大概也就够用了, 但往往由于够用让他们忘了深入, 去精通. 我们希望全面了解这个语言的能力边界, 可能一些高级的特性并不能马上掌握使用, 因此这本书是工作之余, 还有余力的人来阅读, 我这边就将其有用, 精妙的进阶内容整理出来.</p>
<p>这本书有21个章节, 整理也是根据这些章节过来.</p>
<h2>第一章: python数据模型</h2>
<p>这部分主要介绍了python的魔术方法, 它们经常是两个下划线包围来命名的(比如 <code>__init__</code> , <code>__lt__</code>, <code>__len__</code> ). 这些特殊方法是为了被python解释器调用的, 这些方法会注册到他们的类型中方法集合中, 相当于为cpython提供抄近路. 这些方法的速度也比普通方法要快, 当然在自己不清楚这些魔术方法的用途时, 不要随意添加.</p>
<p>关于字符串的表现形式是两种, <code>__str__</code> 与 <code>__repr__</code> . python的内置函数 <code>repr</code> 就是通过 <code>__repr__</code> 这个特殊方法来得到一个对象的字符串表示形式. 这个在交互模式下比较常用, 如果没有实现 <code>__repr__</code> , 当控制台打印一个对象时往往是 <code><A object at 0x000></code> . 而 <code>__str__</code> 则是 <code>str()</code> 函数时使用的, 或是在 <code>print</code> 函数打印一个对象的时候才被调用, 终端用户友好.</p>
<p>两者还有一个区别, 在字符串格式化时, <code>"%s"</code> 对应了 <code>__str__</code> . 而 <code>"%r"</code> 对应了 <code>__repr__</code>. <code>__str__</code> 和 <code>__repr__</code> 在使用上比较推荐的是,前者是给终端用户看,而后者则更方便我们调试和记录日志.</p>
<p>更多的特殊方法: <a href="https://link.segmentfault.com/?enc=gh5LTCoV%2B620zK6xTMf1ww%3D%3D.48d0PmcZImYnRXJ9w%2FpN1Biv5PGQ%2BUexqyJTZculnydLui6Hkv2Evf%2B4i7oZHAF%2FtE2efkmNY2KzQGK1lbBQtQ%3D%3D" rel="nofollow">https://docs.python.org/3/reference/datamodel.html</a></p>
<h2>第二章: 序列构成的数组</h2>
<p>这部分主要是介绍序列, 着重介绍数组和元组的一些高级用法. </p>
<p>序列按照容纳数据的类型可以分为:</p>
<ul>
<li>
<code>容器序列</code>: list、tuple 和 collections.deque 这些序列能存放不同类型的数据</li>
<li>
<code>扁平序列</code>: str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型.</li>
</ul>
<p>如果按照是否能被修改可以分为:</p>
<ul>
<li>
<code>可变序列</code>: list、bytearray、array.array、collections.deque 和 memoryview</li>
<li>
<code>不可变序列</code>: tuple、str 和 bytes</li>
</ul>
<h3>列表推导</h3>
<p>列表推导是构建列表的快捷方式, 可读性更好且效率更高.</p>
<p>例如, 把一个字符串变成unicode的码位列表的例子, 一般:</p>
<pre><code>symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
codes.append(ord(symbol))</code></pre>
<p>使用列表推导:</p>
<pre><code>symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]</code></pre>
<p>能用列表推导来创建一个列表, 尽量使用推导, 并且保持它简短.</p>
<h3>笛卡尔积与生成器表达式</h3>
<p>生成器表达式是能逐个产出元素, 节省内存. 例如:</p>
<pre><code>>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):
... print(tshirt)</code></pre>
<p>实例中列表元素比较少, 如果换成两个各有1000个元素的列表, 显然这样组合的笛卡尔积是一个含有100万元素的列表, 内存将会占用很大, 而是用生成器表达式就可以帮忙省掉for循环的开销.</p>
<h3>具名元组</h3>
<p>元组经常被作为 <code>不可变列表</code> 的代表. 经常只要数字索引获取元素, 但其实它还可以给元素命名:</p>
<pre><code>>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'</code></pre>
<h3>切片</h3>
<p>列表中是以0作为第一个元素的下标, 切片可以根据下标提取某一个片段.</p>
<p>用 <code>s[a:b:c]</code> 的形式对 <code>s</code> 在 <code>a</code> 和 <code>b</code> 之间以 <code>c</code> 为间隔取值。<code>c</code> 的值还可以为负, 负值意味着反向取值.</p>
<pre><code>>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'</code></pre>
<h2>第三章: 字典和集合</h2>
<p><code>dict</code> 类型不但在各种程序里广泛使用, 它也是 <code>Python</code> 语言的基石. 正是因为 <code>dict</code> 类型的重要, <code>Python</code> 对其的实现做了高度的优化, 其中最重要的原因就是背后的「散列表」 set(集合)和dict一样, 其实现基础也是依赖于散列表.</p>
<p>散列表也叫哈希表, 对于dict类型, 它的key必须是可哈希的数据类型. 什么是可哈希的数据类型呢, 它的官方解释是:</p>
<blockquote><p>如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变<br>的,而且这个对象需要实现 <code> __hash__()</code> 方法。另外可散列对象还要有<br><code>__qe__()</code> 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……</p></blockquote>
<p><code>str</code>, <code>bytes</code>, <code>frozenset</code> 和 <code>数值</code> 都是可散列类型.</p>
<h3>字典推导式</h3>
<pre><code>DIAL_CODE = [
(86, 'China'),
(91, 'India'),
(7, 'Russia'),
(81, 'Japan'),
]
### 利用字典推导快速生成字典
country_code = {country: code for code, country in DIAL_CODE}
print(country_code)
'''
OUT:
{'China': 86, 'India': 91, 'Russia': 7, 'Japan': 81}
'''</code></pre>
<h3>defaultdict:处理找不到的键的一个选择</h3>
<p>当某个键不在映射里, 我们也希望也能得到一个默认值. 这就是 <code>defaultdict</code> , 它是 <code>dict</code> 的子类, 并实现了 <code>__missing__</code> 方法.</p>
<pre><code>import collections
index = collections.defaultdict(list)
for item in nums:
key = item % 2
index[key].append(item)</code></pre>
<h3>字典的变种</h3>
<p>标准库里 <code>collections</code> 模块中,除了 <code>defaultdict</code> 之外的不同映射类型:</p>
<ul>
<li>
<strong>OrderDict</strong>: 这个类型在添加键的时候,会保存顺序,因此键的迭代顺序总是一致的</li>
<li>
<strong>ChainMap</strong>: 该类型可以容纳数个不同的映射对像,在进行键的查找时,这些对象会被当做一个整体逐个查找,直到键被找到为止 <code>pylookup = ChainMap(locals(), globals())</code>
</li>
<li>
<strong>Counter</strong>: 这个映射类型会给键准备一个整数技术器,每次更行一个键的时候都会增加这个计数器,所以这个类型可以用来给散列表对象计数,或者当成多重集来用.</li>
</ul>
<pre><code>import collections
ct = collections.Counter('abracadabra')
print(ct) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
ct.update('aaaaazzz')
print(ct) # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(ct.most_common(2)) # [('a', 10), ('z', 3)]</code></pre>
<ul><li>
<strong>UserDict</strong>: 这个类其实就是把标准 dict 用纯 Python 又实现了一遍</li></ul>
<pre><code>import collections
class StrKeyDict(collections.UserDict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]
def __contains__(self, key):
return str(key) in self.data
def __setitem__(self, key, item):
self.data[str(key)] = item</code></pre>
<h3>不可变映射类型</h3>
<p>说到不可变, 第一想到的肯定是元组, 但是对于字典来说, 要将key和value的对应关系变成不可变, <code>types</code> 模块的 <code>MappingProxyType</code> 可以做到:</p>
<pre><code>from types import MappingProxyType
d = {1:'A'}
d_proxy = MappingProxyType(d)
d_proxy[1]='B' # TypeError: 'mappingproxy' object does not support item assignment
d[2] = 'B'
print(d_proxy) # mappingproxy({1: 'A', 2: 'B'})</code></pre>
<p><code>d_proxy</code> 是动态的, 也就是说对 <code>d</code> 所做的任何改动都会反馈到它上面.</p>
<h3>集合论</h3>
<p>集合的本质是许多唯一对象的聚集. 因此, 集合可以用于去重. 集合中的元素必须是可散列的, 但是 <code>set</code> 本身是不可散列的, 而 <code>frozenset</code> 本身可以散列.</p>
<p>集合具有唯一性, 与此同时, 集合还实现了很多基础的中缀运算符. 给定两个集合 a 和 b, <code>a | b</code> 返<br>回的是它们的合集, <code>a & b</code> 得到的是交集, 而 <code>a - b</code> 得到的是差集.</p>
<p>合理的利用这些特性, 不仅能减少代码的数量, 更能增加运行效率.</p>
<pre><code># 集合的创建
s = set([1, 2, 2, 3])
# 空集合
s = set()
# 集合字面量
s = {1, 2}
# 集合推导
s = {chr(i) for i in range(23, 45)}</code></pre>
<h2>第四章: 文本和字节序列</h2>
<p>本章讨论了文本字符串和字节序列, 以及一些编码上的转换. 本章讨论的 <code>str</code> 指的是python3下的.</p>
<h3>字符问题</h3>
<p>字符串是个比较简单的概念: 一个字符串是一个字符序列. 但是关于 <code>"字符"</code> 的定义却五花八门, 其中, <code>"字符"</code> 的最佳定义是 <code>Unicode 字符</code> . 因此, python3中的 <code>str</code> 对象中获得的元素就是 unicode 字符.</p>
<p>把码位转换成字节序列的过程就是 <code>编码</code>, 把字节序列转换成码位的过程就是 <code>解码</code> :</p>
<pre><code>>>> s = 'café'
>>> len(s)
4
>>> b = s.encode('utf8')
>>> b
b'caf\xc3\xa9'
>>> len(b)
5
>>> b.decode('utf8') #'café</code></pre>
<p>码位可以认为是人类可读的文本, 而字符序列则可以认为是对机器更友好. 所以要区分 <code>.decode()</code> 和 <code>.encode()</code> 也很简单. 从字节序列到人类能理解的文本就是解码(decode). 而把人类能理解的变成人类不好理解的字节序列就是编码(encode).</p>
<h3>字节概要</h3>
<p>python3有两种字节序列, 不可变的 <code>bytes</code> 类型和可变的 <code>bytearray</code> 类型. 字节序列中的各个元素都是介于 <code>[0, 255]</code> 之间的整数.</p>
<h3>处理编码问题</h3>
<p>python自带了超过100中编解码器. 每个编解码器都有一个名称, 甚至有的会有一些别名, 如 <code>utf_8</code> 就有 <code>utf8</code>, <code>utf-8</code>, <code>U8</code> 这些别名.</p>
<p>如果字符序列和预期不符, 在进行解码或编码时容易抛出 <code>Unicode*Error</code> 的异常. 造成这种错误是因为目标编码中没有定义某个字符(没有定义某个码位对应的字符), 这里说说解决这类问题的方式.</p>
<ul>
<li>使用python3, python3可以避免95%的字符问题.</li>
<li>主流编码尝试下: latin1, cp1252, cp437, gb2312, utf-8, utf-16le</li>
<li>留意BOM头部 <code>b'\xff\xfe'</code> , UTF-16编码的序列开头也会有这几个额外字节.</li>
<li>找出序列的编码, 建议使用 <code>codecs</code> 模块</li>
</ul>
<h3>规范化unicode字符串</h3>
<pre><code>s1 = 'café'
s2 = 'caf\u00e9'</code></pre>
<p>这两行代码完全等价. 而有一种是要避免的是, 在Unicode标准中 <code>é</code> 和 <code>e\u0301</code> 这样的序列叫 <code>"标准等价物"</code>. 这种情况用NFC使用最少的码位构成等价的字符串:</p>
<pre><code>>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False</code></pre>
<p>改进后:</p>
<pre><code>>>> from unicodedata import normalize
>>> s1 = 'café' # 把"e"和重音符组合在一起
>>> s2 = 'cafe\u0301' # 分解成"e"和重音符
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) == normalize('NFC', s2)
True
>>> normalize('NFD', s1) == normalize('NFD', s2)
True</code></pre>
<h3>unicode文本排序</h3>
<p>对于字符串来说, 比较的码位. 所以在非 ascii 字符时, 得到的结果可能会不尽人意.</p>
<h2>第五章: 一等函数</h2>
<p>在python中, 函数是一等对象. 编程语言把 <code>"一等对象"</code> 定义为满足下列条件:</p>
<ul>
<li>在运行时创建</li>
<li>能赋值给变量或数据结构中的元素</li>
<li>能作为参数传给函数</li>
<li>能作为函数的返回结果</li>
</ul>
<p>在python中, 整数, 字符串, 列表, 字典都是一等对象.</p>
<h3>把函数视作对象</h3>
<p>Python即可以函数式编程,也可以面向对象编程. 这里我们创建了一个函数, 然后读取它的 <code>__doc__</code> 属性, 并且确定函数对象其实是 <code>function</code> 类的实例:</p>
<pre><code>def factorial(n):
'''
return n
'''
return 1 if n < 2 else n * factorial(n-1)
print(factorial.__doc__)
print(type(factorial))
print(factorial(3))
'''
OUT
return n
<class 'function'>
6
'''</code></pre>
<h3>高阶函数</h3>
<p>高阶函数就是接受函数作为参数, 或者把函数作为返回结果的函数. 如 <code>map</code>, <code>filter</code> , <code>reduce</code> 等.</p>
<p>比如调用 <code>sorted</code> 时, 将 <code>len</code> 作为参数传递:</p>
<pre><code>fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
# ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']</code></pre>
<h3>匿名函数</h3>
<p><code>lambda</code> 关键字是用来创建匿名函数. 匿名函数一些限制, 匿名函数的定义体只能使用纯表达式. 换句话说, <code>lambda</code> 函数内不能赋值, 也不能使用while和try等语句.</p>
<pre><code>fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])
# ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']</code></pre>
<h3>可调用对象</h3>
<p>除了用户定义的函数, 调用运算符即 <code>()</code> 还可以应用到其他对象上. 如果像判断对象能否被调用, 可以使用内置的 <code>callable()</code> 函数进行判断. python的数据模型中有7种可是可以被调用的:</p>
<ul>
<li>用户定义的函数: 使用def语句或lambda表达式创建</li>
<li>内置函数:如len</li>
<li>内置方法:如dict.get</li>
<li>方法:在类定义体中的函数</li>
<li>类</li>
<li>类的实例: 如果类定义了 <code>__call__</code> , 那么它的实例可以作为函数调用.</li>
<li>生成器函数: 使用 <code>yield</code> 关键字的函数或方法.</li>
</ul>
<h3>从定位参数到仅限关键字参数</h3>
<p>就是可变参数和关键字参数:</p>
<pre><code>def fun(name, age, *args, **kwargs):
pass</code></pre>
<p>其中 <code>*args</code> 和 <code>**kwargs</code> 都是可迭代对象, 展开后映射到单个参数. args是个元组, kwargs是字典.</p>
<h2>第六章: 使用一等函数实现设计模式</h2>
<p>虽然设计模式与语言无关, 但这并不意味着每一个模式都能在每一个语言中使用. Gamma 等人合著的 <code>《设计模式:可复用面向对象软件的基础》</code> 一书中有 <code>23</code> 个模式, 其中有 <code>16</code> 个在动态语言中"不见了, 或者简化了".</p>
<p>这里不举例设计模式, 因为书里的模式不常用.</p>
<h2>第七章: 函数装饰器和闭包</h2>
<blockquote><p>函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功<br>能,但是若想掌握,必须理解闭包。</p></blockquote>
<p>修饰器和闭包经常在一起讨论, 因为修饰器就是闭包的一种形式. 闭包还是回调式异步编程和函数式编程风格的基础.</p>
<h3>装饰器基础知识</h3>
<p>装饰器是可调用的对象, 其参数是另一个函数(被装饰的函数). 装饰器可能会处理被<br>装饰的函数, 然后把它返回, 或者将其替换成另一个函数或可调用对象.</p>
<pre><code>@decorate
def target():
print('running target()')</code></pre>
<p>这种写法与下面写法完全等价:</p>
<pre><code>def target():
print('running target()')
target = decorate(target)</code></pre>
<p>装饰器是语法糖, 它其实是将函数作为参数让其他函数处理. 装饰器有两大特征:</p>
<ul>
<li>把被装饰的函数替换成其他函数</li>
<li>装饰器在加载模块时立即执行</li>
</ul>
<p>要理解立即执行看下等价的代码就知道了, <code>target = decorate(target)</code> 这句调用了函数. 一般情况下装饰函数都会将某个函数作为返回值.</p>
<h3>变量作用域规则</h3>
<p>要理解装饰器中变量的作用域, 应该要理解闭包, 我觉得书里将闭包和作用域的顺序换一下比较好. 在python中, 一个变量的查找顺序是 <code>LEGB</code> (L:Local 局部环境,E:Enclosing 闭包,G:Global 全局,B:Built-in 内建).</p>
<pre><code>base = 20
def get_compare():
base = 10
def real_compare(value):
return value > base
return real_compare
compare_10 = get_compare()
print(compare_10(5))</code></pre>
<p>在闭包的函数 <code>real_compare</code> 中, 使用的变量 <code>base</code> 其实是 <code>base = 10</code> 的. 因为base这个变量在闭包中就能命中, 而不需要去 <code>global</code> 中获取.</p>
<h3>闭包</h3>
<p>闭包其实挺好理解的, 当匿名函数出现的时候, 才使得这部分难以掌握. 简单简短的解释闭包就是:</p>
<p><strong>名字空间与函数捆绑后的结果被称为一个闭包(closure).</strong></p>
<p>这个名字空间就是 <code>LEGB</code> 中的 <code>E</code> . 所以闭包不仅仅是将函数作为返回值. 而是将名字空间和函数捆绑后作为返回值的. 多少人忘了理解这个 <code>"捆绑"</code> , 不知道变量最终取的哪和哪啊. 哎.</p>
<h3>标准库中的装饰器</h3>
<p>python内置了三个用于装饰方法的函数: <code>property</code> 、 <code>classmethod</code> 和 <code>staticmethod</code> .<br>这些是用来丰富类的.</p>
<pre><code>class A(object):
@property
def age():
return 12</code></pre>
<h2>第八章: 对象引用、可变性和垃圾回收</h2>
<h3>变量不是盒子</h3>
<p>很多人把变量理解为盒子, 要存什么数据往盒子里扔就行了.</p>
<pre><code>a = [1,2,3]
b = a
a.append(4)
print(b) # [1, 2, 3, 4]</code></pre>
<p>变量 <code>a</code> 和 <code>b</code> 引用同一个列表, 而不是那个列表的副本. 因此赋值语句应该理解为将变量和值进行引用的关系而已.</p>
<h3>标识、相等性和别名</h3>
<p>要知道变量a和b是否是同一个值的引用, 可以用 <code>is</code> 来进行判断:</p>
<pre><code>>>> a = b = [4,5,6]
>>> c = [4,5,6]
>>> a is b
True
>>> x is c
False</code></pre>
<p>如果两个变量都是指向同一个对象, 我们通常会说变量是另一个变量的 <code>别名</code> .</p>
<p><strong>在==和is之间选择</strong><br>运算符 <code>==</code> 是用来判断两个对象值是否相等(注意是对象值). 而 <code>is</code> 则是用于判断两个变量是否指向同一个对象, 或者说判断变量是不是两一个的别名, is 并不关心对象的值. 从使用上, <code>==</code> 使用比较多, 而 <code>is</code> 的执行速度比较快.</p>
<h3>默认做浅复制</h3>
<pre><code>l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1) # 通过构造方法进行复制
l2 = l1[:] #也可以这样想写
>>> l2 == l1
True
>>> l2 is l1
False</code></pre>
<p>尽管 l2 是 l1 的副本, 但是复制的过程是先复制(即复制了最外层容器,副本中的元素是源容器中元素的引用). 因此在操作 l2[1] 时, l1[1] 也会跟着变化. 而如果列表中的所有元素是不可变的, 那么就没有这样的问题, 而且还能节省内存. 但是, 如果有可变元素存在, 就可能造成意想不到的问题.</p>
<p>python标准库中提供了两个工具 <code>copy</code> 和 <code>deepcopy</code> . 分别用于浅拷贝与深拷贝:</p>
<pre><code>import copy
l1 = [3, [55, 44], (7, 8, 9)]
l2 = copy.copy(l1)
l2 = copy.deepcopy(l1)</code></pre>
<h3>函数的参数做引用时</h3>
<p>python中的函数参数都是采用共享传参. 共享传参指函数的各个形式参数获得实参中各个引用的副本. 也就是说, 函数内部的形参<br>是实参的别名.</p>
<p>这种方案就是当传入参数是可变对象时, 在函数内对参数的修改也就是对外部可变对象进行修改. 但这种参数试图重新赋值为一个新的对象时则无效, 因为这只是相当于把参数作为另一个东西的引用, 原有的对象并不变. 也就是说, 在函数内, 参数是不能把一个对象替换成另一个对象的.</p>
<h3>不要使用可变类型作为参数的默认值</h3>
<p>参数默认值是个很棒的特性. 对于开发者来说, 应该避免使用可变对象作为参数默认值. 因为如果参数默认值是可变对象, 而且修改了它的内容, 那么后续的函数调用上都会收到影响.</p>
<h3>del和垃圾回收</h3>
<p>在python中, 当一个对象失去了最后一个引用时, 会当做垃圾, 然后被回收掉. 虽然python提供了 <code>del</code> 语句用来删除变量. 但实际上只是删除了变量和对象之间的引用, 并不一定能让对象进行回收, 因为这个对象可能还存在其他引用.</p>
<p>在CPython中, 垃圾回收主要用的是引用计数的算法. 每个对象都会统计有多少引用指向自己. 当引用计数归零时, 意味着这个对象没有在使用, 对象就会被立即销毁.</p>
<h2>符合Python风格的对象</h2>
<blockquote><p>得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的<br>行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所<br>需的方法即可。</p></blockquote>
<h3>对象表示形式</h3>
<p>每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了<br>两种方式。</p>
<ul>
<li>
<code>repr()</code> : 以便于开发者理解的方式返回对象的字符串表示形式。</li>
<li>
<code>str()</code> : 以便于用户理解的方式返回对象的字符串表示形式。</li>
</ul>
<h3>classmethod 与 staticmethod</h3>
<p>这两个都是python内置提供了装饰器, 一般python教程都没有提到这两个装饰器. 这两个都是在类 <code>class</code> 定义中使用的, 一般情况下, class 里面定义的函数是与其类的实例进行绑定的. 而这两个装饰器则可以改变这种调用方式.</p>
<p>先来看看 <code>classmethod</code> , 这个装饰器不是操作实例的方法, 并且将类本身作为第一个参数. 而 <code>staticmethod</code> 装饰器也会改变方法的调用方式, 它就是一个普通的函数, </p>
<p><code>classmethod</code> 与 <code>staticmethod</code> 的区别就是 <code>classmethod</code> 会把类本身作为第一个参数传入, 其他都一样了.</p>
<p>看看例子:</p>
<pre><code>>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args
... @staticmethod
... def statmeth(*args):
... return args
...
>>> Demo.klassmeth()
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth()
()
>>> Demo.statmeth('spam')
('spam',)</code></pre>
<h3>格式化显示</h3>
<p>内置的 <code>format()</code> 函数和 <code>str.format()</code> 方法把各个类型的格式化方式委托给相应的 <code>.__format__(format_spec)</code> 方法. <code>format_spec</code> 是格式说明符,它是:</p>
<ul>
<li>
<code>format(my_obj, format_spec)</code> 的第二个参数</li>
<li>
<code>str.format()</code> 方法的格式字符串,{} 里代换字段中冒号后面的部分</li>
</ul>
<h3>Python的私有属性和"受保护的"属性</h3>
<p>python中对于实例变量没有像 <code>private</code> 这样的修饰符来创建私有属性, 在python中, 有一个简单的机制来处理私有属性.</p>
<pre><code>class A:
def __init__(self):
self.__x = 1
a = A()
print(a.__x) # AttributeError: 'A' object has no attribute '__x'
print(a.__dict__)</code></pre>
<p>如果属性以 <code>__name</code> 的 <code>两个下划线为前缀, 尾部最多一个下划线</code> 命名的实例属性, python会把它名称前面加一个下划线加类名, 再放入 <code>__dict__</code> 中, 以 <code>__name</code> 为例, 就会变成 <code>_A__name</code> .</p>
<p>名称改写算是一种安全措施, 但是不能保证万无一失, 它能避免意外访问, 但不能阻止故意做坏事.</p>
<p>只要知道私有属性的机制, 任何人都能直接读取和改写私有属性. 因此很多python程序员严格规定: <code>遵守使用一个下划线标记对象的私有属性</code> . Python 解释器不会对使用单个下划线的属性名做特殊处理, 由程序员自行控制, 不在类外部访问这些属性. 这种方法也是所推荐的, 两个下划线的那种方式就不要再用了. 引用python大神的话:</p>
<blockquote><p>绝对不要使用两个前导下划线,这是很烦人的自私行为。如果担心名称冲突,应该明<br>确使用一种名称改写方式(如 _MyThing_blahblah)。这其实与使用双下划线一<br>样,不过自己定的规则比双下划线易于理解。</p></blockquote>
<p><strong>Python中的把使用一个下划线前缀标记的属性称为"受保护的"属性</strong></p>
<h3>使用 <strong>slots</strong> 类属性节省空间</h3>
<p>默认情况下, python在各个实例中, 用 <code>__dict__</code> 的字典存储实例属性. 因此实例的属性是动态变化的, 可以在运行期间任意添加属性. 而字典是消耗内存比较大的结构. 因此当对象的属性名称确定时, 使用 <code>__slots__</code> 可以节约内存.</p>
<pre><code>class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
# 下面是各个方法(因排版需要而省略了)</code></pre>
<p>在类中定义<code> __slots__</code> 属性的目的是告诉解释器:"这个类中的所有实例属性都在这儿<br>了!" 这样, Python 会在各个实例中使用类似元组的结构存储实例变量, 从而避免使用消<br>耗内存的 <code>__dict__</code> 属性. 如果有数百万个实例同时活动, 这样做能节省大量内存.</p>
<h2>第十章: 序列的修改、散列和切片</h2>
<h3>协议和鸭子类型</h3>
<p>在python中, 序列类型不需要使用继承, 只需要符合序列协议的方法即可. 这里的协议就是实现 <code>__len__</code> 和 <code>__getitem__</code> 两个方法. 任何类, 只要实现了这两个方法, 它就满足了序列操作, 因为它的行为像序列.</p>
<p>协议是非正式的, 没有强制力, 因此你知道类的具体使用场景, 通常只要实现一个协议的部分. 例如, 为了支持迭代, 只需实现 <code>__getitem__</code> 方法, 没必要提供 <code>__len__</code> 方法, 这也就解释了 <code>鸭子类型</code> :</p>
<blockquote><p>当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,<br>那么这只鸟就可以被称为鸭子</p></blockquote>
<h3>可切片的序列</h3>
<p>切片(Slice)是用来获取序列某一段范围的元素. 切片操作也是通过 <code>__getitem__</code> 来完成的:</p>
<pre><code>class Vector:
# 省略了很多行
# ...
def __len__(self):
return len(self._components)
# 省略了很多
def __getitem__(self, index):
cls = type(self) # 获取实例的类型
if isinstance(index, slice): # 如果index参数值是切片的对象
# 调用Vector的构造方法,建立一个新的切片后的Vector类
return cls(self._components[index])
elif isinstance(index, numbers.Integral): # 如果参数是整数类型
return self._components[index] # 我们就对数组进行切片
else: # 否则我们就抛出异常
msg = '{cls.__name__} indices must be integers'
raise TypeError(msg.format(cls=cls))</code></pre>
<h3>动态存取属性</h3>
<p>通过访问分量名来获取属性:</p>
<pre><code>shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self) # 获取类型
if len(name) == 1: # 判断属性名是否在我们定义的names中
pos = cls.shortcut_names.find(name)
if 0 <= pos < len(self._components):
return self._components[pos]
msg = '{} objects has no attribute {}'
raise AttributeError(msg.format(cls, name))
test = Vector([3, 4, 5])
print(test.x)
print(test.y)
print(test.z)
print(test.c)</code></pre>
<h3>散列和快速等值测试</h3>
<p>实现 <code>__hash__</code> 方法。加上现有的 <code>__eq__</code> 方法,这会把实例变成可散列的对象. </p>
<p>当序列是多维是时候, 我们有一个效率更高的方法:</p>
<pre><code>def __eq__(self, other):
if len(self) != len(other): # 首先判断长度是否相等
return False
for a, b in zip(self, other): # 接着逐一判断每个元素是否相等
if a != b:
return False
return True
# 我们也可以写的漂亮点
def __eq__(self, other):
return (len(self) == len(other)) and all(a == b for a, b in zip(self, other))</code></pre>
<h2>第十一章: 接口:从协议到抽象基类</h2>
<p>这些协议定义为非正式的接口, 是让编程语言实现多态的方式. 在python中, 没有 <code>interface</code> 关键字, 而且除了抽象基类, 每个类都有接口: 所有类都可以自行实现 <code>__getitem__</code> 和 <code>__add__</code> .</p>
<p>有写规定则是程序员在开发过程中慢慢总结出来的, 如受保护的属性命名采用单个前导下划线, 还有一些编码规范之类的.</p>
<p>协议是接口, 但不是正式的, 这些规定并不是强制性的, 一个类可能只实现部分接口, 这是允许的.</p>
<p>既然有非正式的协议, 那么有没有正式的协议呢? 有, 抽象基类就是一种强制性的协议.</p>
<p>抽象基类要求其子类需要实现定义的某个接口, 且抽象基类不能实例化.</p>
<h3>Python文化中的接口和协议</h3>
<p>引入抽象基类之前, python就已经非常成功了, 即使现在也很少使用抽象基类. 通过鸭子类型和协议, 我们把协议定义为非正式接口, 是让python实现多态的方式.</p>
<p>另一边面, 不要觉得把公开数据属性放入对象的接口中不妥, 如果需要, 总能实现读值和设值方法, 把数据属性变成特性. 对象公开方法的自己, 让对象在系统中扮演特定的角色. 因此, 接口是实现特定角色的方法集合.</p>
<p>序列协议是python最基础的协议之一, 即便对象只实现那个协议最基本的一部分, 解释器也会负责地处理.</p>
<h3>水禽和抽象基类</h3>
<p>鸭子类型在很多情况下十分有用, 但是随着发展, 通常由了更好的方式.</p>
<p>近代, 属和种基本是根据表型系统学分类的, 鸭科属于水禽, 而水禽还包括鹅, 鸿雁等. 水禽是对某一类表现一致进行的分类, 他们有一些统一"描述"部分.</p>
<p>因此, 根据分类的演化, 需要有个水禽类型, 只要 <code>cls</code> 是抽象基类, 即 <code>cls</code> 的元类是 <code>abc.ABCMeta</code> , 就可以使用 <code>isinstance(obj, cls)</code> 来进行判断.</p>
<p>与具类相比, 抽象基类有很多理论上的优点, 被注册的类必须满足抽象基类对方法和签名的要求, 更重要的是满足底层语义契约.</p>
<h3>标准库中的抽象基类</h3>
<p>大多数的标准库的抽象基类在 <code>collections.abc</code> 模块中定义. 少部分在 <code>numbers</code> 和 <code>io</code> 包中有一些抽象基类. 标准库中有两个 <code>abc</code> 模块, 这里只讨论 <code>collections.abc</code> .</p>
<p>这个模块中定义了 16 个抽象基类.</p>
<p><strong>Iterable、Container 和 Sized</strong><br>各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。<code>Iterable</code> 通过 <code>__iter__</code> 方法支持迭代,Container 通过 <code>__contains__</code> 方法支持 in 运算符,Sized<br>通过 <code>__len__</code> 方法支持 len() 函数。</p>
<p><strong>Sequence、Mapping 和 Set</strong><br> 这三个是主要的不可变集合类型,而且各自都有可变的子类。</p>
<p><strong>MappingView</strong><br> 在 Python3 中,映射方法 <code>.items()</code>、<code>.keys()</code> 和 <code>.values()</code> 返回的对象分别是<br>ItemsView、KeysView 和 ValuesView 的实例。前两个类还从 Set 类继承了丰富的接<br>口。</p>
<p><strong>Callable 和 Hashable</strong><br> 这两个抽象基类与集合没有太大的关系,只不过因为 <code>collections.abc</code> 是标准库中<br>定义抽象基类的第一个模块,而它们又太重要了,因此才把它们放到 <code>collections.abc</code><br>模块中。我从未见过 <code>Callable</code> 或 <code>Hashable</code> 的子类。这两个抽象基类的主要作用是为内<br>置函数 <code>isinstance</code> 提供支持,以一种安全的方式判断对象能不能调用或散列。</p>
<p><strong>Iterator</strong><br> 注意它是 Iterable 的子类。<br> </p>
<h2>第十二章: 继承的优缺点</h2>
<p>很多人觉得多重继承得不偿失, 那些不支持多继承的编程语言好像也没什么损失.</p>
<h3>子类化内置类型很麻烦</h3>
<p>python2.2 以前, 内置类型(如list, dict)是不能子类化的. 它们是不能被其他类所继承的, 原因是内置类型是C语言实现的, 不会调用用户定义的类覆盖的方法.</p>
<p>至于内置类型的子类覆盖的方法会不会隐式调用, CPython 官方也没有制定规则. 基本上, 内置类型的方法不会调用子类覆盖的方法. 例如, dict 的子类覆盖的 <code>__getitem__</code> 方法不会覆盖内置类型的 <code>get()</code> 方法调用.</p>
<h3>多重继承和方法解析顺序</h3>
<p>任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名<br>方法引起。这种冲突称为“菱形问题”,如图.</p>
<p><img src="https://www.hongweipeng.com/usr/uploads/2017/10/3337953654.png" alt="20171010144742.png" title="20171010144742.png"></p>
<p>Python 会按照特定的顺序遍历继承<br>图。这个顺序叫方法解析顺序(Method Resolution Order,MRO)。类都有一个名为<br><strong>mro</strong> 的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直<br>向上,直到 object 类。</p>
<p><img src="https://www.hongweipeng.com/usr/uploads/2017/10/928562845.png" alt="20171010145104.png" title="20171010145104.png"></p>
<h2>第十三章: 正确重载运算符</h2>
<p>在python中, 大多数的运算符是可以重载的, 如 <code>==</code> 对应了 <code>__eq__</code> , <code>+</code> 对应 <code>__add__</code> . </p>
<p>某些运算符不能重载, 如 <code>is, and, or, and</code>.</p>
<h2>第十四章: 可迭代的对象、迭代器和生成器</h2>
<p>迭代是数据处理的基石. 扫描内存中放不下的数据集时, 我们要找到一种惰性获取数据的方式, 即按需一次获取一个数据. 这就是 <code>迭代器模式</code> .</p>
<p>python中有 <code>yield</code> 关键字, 用于构建 <code>生成器(generator)</code>, 其作用用于迭代器一样.</p>
<p>所有的生成器都是迭代器, 因为生成器完全实现了迭代器的接口.</p>
<p>检查对象 x 是否迭代, 最准确的方法是调用 <code>iter(x)</code> , 如果不可迭代, 则抛出 <code>TypeError</code> 异常. 这个方法比 <code>isinstance(x, abc.Iterable)</code> 更准确, 因为它还考虑到遗留的 <code>__getitem__</code> 方法.</p>
<h3>可迭代的对象与迭代器的对比</h3>
<p>我们需要对可迭代的对象进行一下定义:</p>
<blockquote><p>使用 iter 内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的<br><strong>iter</strong> 方法,那么对象就是可迭代的。序列都可以迭代;实现了 <strong>getitem</strong> 方<br>法,而且其参数是从零开始的索引,这种对象也可以迭代。</p></blockquote>
<p>我们要明确可迭代对象和迭代器之间的关系: 从可迭代的对象中获取迭代器.</p>
<p>标准的迭代器接口有两个方法:</p>
<ul>
<li>
<code>__next__</code>: 返回下一个可用的元素, 如果没有元素了, 抛出 <code>StopIteration</code> 异常.</li>
<li>
<code>__iter__</code>: 返回 <code>self</code> , 以便咋应该使用可迭代对象的地方使用迭代器.</li>
</ul>
<h3>典型的迭代器</h3>
<p>为了清楚地说明可迭代对象与迭代器之间的重要区别, 我们将两者分开, 写成两个类:</p>
<pre><code>import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
# 返回一个字符串列表、元素为正则所匹配到的非重叠匹配】
self.words = RE_WORD.findall(text)
def __repr__(self):
# 该函数用于生成大型数据结构的简略字符串的表现形式
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
'''明确表明该类型是可以迭代的'''
return SentenceIterator(self.words) # 创建一个迭代器
class SentenceIterator:
def __init__(self, words):
self.words = words # 该迭代器实例应用单词列表
self.index = 0 # 用于定位下一个元素
def __next__(self):
try:
word = self.words[self.index] # 返回当前的元素
except IndexError:
raise StopIteration()
self.index += 1 # 索引+1
return word # 返回单词
def __iter__(self):
return self # 返回self</code></pre>
<p>这个例子主要是为了区分可迭代对象和迭代器, 这种情况工作量一般比较大, 程序员也不愿这样写. </p>
<p>构建可迭代对象和迭代器经常会出现错误, 原因是混淆了二者. 要知道, 可迭代的对象有个 <code>__iter__</code> 方法, 每次都实例化一个新的迭代器; 而迭代器是要实现 <code>__next__</code> 方法, 返回单个元素, 同时还要提供 <code>__iter__</code> 方法返回迭代器本身.</p>
<p>可迭代对象一定不能是自身的迭代器. 也就是说, 可迭代对象必须实现 <code>__iter__</code> 方法, 但不能实现 <code>__next__</code> 方法.</p>
<p>小结下, 迭代器可以迭代, 但是可迭代对象不是迭代器.</p>
<h3>生成器函数</h3>
<p>实现相同功能, 覆盖python习惯的方式, 就是用生成器代替迭代器 <code>SentenceIterator</code> . 将上个例子改成生成器的方式:</p>
<pre><code>import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
# 返回一个字符串列表、元素为正则所匹配到的非重叠匹配】
self.words = RE_WORD.findall(text)
def __repr__(self):
# 该函数用于生成大型数据结构的简略字符串的表现形式
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
'''生成器版本'''
for word in self.words: # 迭代实例的words
yield word # 生成单词
return</code></pre>
<p>在这个例子中, 迭代器其实就是生成器对象, 每次调用 <code>__iter__</code> 都会自动创建, 因为这里的 <code>__iter__</code> 方法是生成器函数.</p>
<p><strong>生成器函数的工作原理</strong><br>只要python函数的定义体中有 <code>yield</code> 关键字, 改函数就是生成器函数. 调用生成器函数时, 会返回一个生成器对象. 也就是说, 生成器函数是生成器工厂.</p>
<p>普通函数与生成器函数的唯一区别就是, 生成器函数里面有 <code>yield</code> 关键字.</p>
<p>生成器函数会创建一个生成器对象, 包装生成器函数的定义体. 吧生成器传给 <code>next(...)</code> 函数时, 生成器函数会向前, 执行函数体中下一个 <code>yield</code> 语句, 返回产出的值, 并在函数定义体的当前位置暂停.</p>
<h3>惰性实现</h3>
<p>惰性的方式就是索性把所有数据都产出, 这是区别于 <code>next(...)</code> 一次生成一次元素的.</p>
<pre><code>import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for match in RE_WORD.finditer(self.text):
yield match.group()</code></pre>
<h3>生成器表达式</h3>
<p>生成器表达式可以理解为列表推导的惰性版本: 不会迫切地构建列表, 而是返回一个生成器, 按需惰性生成元素. 也就是, 如果列表推导是产出列表的工厂, 那么生成器表达式就是产出生成器的工厂.</p>
<pre><code>def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end.')
res1 = [x*3 for x in gen_AB()]
for i in res1:
print('-->', i)</code></pre>
<p>可以看出, 生成器表达式会产出生成器, 因此可以使用生成器表达式减少代码:</p>
<pre><code>import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))</code></pre>
<p>这里的 <code>__iter__</code> 不是生成器函数了, 而是使用生成器表达式构建生成器, 最终的效果一样. 调用 <code>__iter__</code> 方法会得到一个生成器对象.</p>
<p>生成器表达式是语法糖, 完全可以替换生成器函数.</p>
<h3>标准库中的生成器函数</h3>
<p>标准库提供了很多生成器, 有用于逐行迭代纯文本文件的对象, 还有出色的 <code>os.walk</code> 函数. 这个函数在遍历目录树的过程中产出文件名, 因此递归搜索文件系统像 for 循环那样简单.</p>
<p>标准库中的生成器大多在 <code>itertools</code> 和 <code>functools</code> 中, 表格中不代表所有.</p>
<p><strong>用于过滤的生成器函数</strong></p>
<table>
<thead><tr>
<th>模块</th>
<th>函数</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>itertools</td>
<td>compress(it, selector_it)</td>
<td>并行处理两个可迭代的对象;如果 selector_it 中的元素是真值,产出 it 中对应的元素</td>
</tr>
<tr>
<td>itertools</td>
<td>dropwhile(predicate, it)</td>
<td>处理 it,跳过 predicate 的计算结果为真值的元素,然后产出剩下的各个元素(不再进一步检查)</td>
</tr>
<tr>
<td>(内置)</td>
<td>filter(predicate, it)</td>
<td>把 it 中的各个元素传给 predicate,如果 predicate(item) 返回真值,那么产出对应的元素;如果 predicate 是 None,那么只产出真值元素</td>
</tr>
</tbody>
</table>
<p><strong>用于映射的生成器函数</strong></p>
<table>
<thead><tr>
<th>模块</th>
<th>函数</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>itertools</td>
<td>accumulate(it, [func])</td>
<td>产出累积的总和;如果提供了 func,那么把前两个元素传给它,然后把计算结果和下一个元素传给它,以此类推,最后产出结果</td>
</tr>
<tr>
<td>(内置)</td>
<td>enumerate(iterable, start=0)</td>
<td>产出由两个元素组成的元组,结构是 (index, item),其中 index 从 start 开始计数,item 则从 iterable 中获取</td>
</tr>
<tr>
<td>(内置)</td>
<td>map(func, it1, [it2, ..., itN])</td>
<td>把 it 中的各个元素传给func,产出结果;如果传入 N 个可迭代的对象,那么 func 必须能接受 N 个参数,而且要并行处理各个可迭代的对象</td>
</tr>
</tbody>
</table>
<p><strong>合并多个可迭代对象的生成器函数</strong></p>
<table>
<thead><tr>
<th>模块</th>
<th>函数</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>itertools</td>
<td>chain(it1, ..., itN)</td>
<td>先产出 it1 中的所有元素,然后产出 it2 中的所有元素,以此类推,无缝连接在一起</td>
</tr>
<tr>
<td>itertools</td>
<td>chain.from_iterable(it)</td>
<td>产出 it 生成的各个可迭代对象中的元素,一个接一个,无缝连接在一起;it 应该产出可迭代的元素,例如可迭代的对象列表</td>
</tr>
<tr>
<td>(内置)</td>
<td>zip(it1, ..., itN)</td>
<td>并行从输入的各个可迭代对象中获取元素,产出由 N 个元素组成的元组,只要有一个可迭代的对象到头了,就默默地停止</td>
</tr>
</tbody>
</table>
<h3>新的句法:yield from</h3>
<p>如果生成器函数需要产出另一个生成器生成的值, 传统的方式是嵌套的 for 循环, 例如, 我们要自己实现 <code>chain</code> 生成器:</p>
<pre><code>>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]</code></pre>
<p><code>chain</code> 生成器函数把操作依次交给接收到的可迭代对象处理. 而改用 <code>yield from</code> 语句可以简化:</p>
<pre><code>>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]</code></pre>
<p>可以看出, <code>yield from i</code> 取代一个 for 循环. 并且让代码读起来更流畅.</p>
<h3>可迭代的归约函数</h3>
<p>有些函数接受可迭代对象, 但仅返回单个结果, 这类函数叫规约函数.</p>
<table>
<thead><tr>
<th>模块</th>
<th>函数</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>(内置)</td>
<td>sum(it, start=0)</td>
<td>it 中所有元素的总和,如果提供可选的 start,会把它加上(计算浮点数的加法时,可以使用 math.fsum 函数提高精度)</td>
</tr>
<tr>
<td>(内置)</td>
<td>all(it)</td>
<td>it 中的所有元素都为真值时返回 True,否则返回 False;all([]) 返回 True</td>
</tr>
<tr>
<td>(内置)</td>
<td>any(it)</td>
<td>只要 it 中有元素为真值就返回 True,否则返回 False;any([]) 返回 False</td>
</tr>
<tr>
<td>(内置)</td>
<td>max(it, [key=,] [default=])</td>
<td>返回 it 中值最大的元素;*key 是排序函数,与 sorted 函数中的一样;如果可迭代的对象为空,返回 default</td>
</tr>
<tr>
<td>functools</td>
<td>reduce(func, it, [initial])</td>
<td>把前两个元素传给 func,然后把计算结果和第三个元素传给 func,以此类推,返回最后的结果;如果提供了 initial,把它当作第一个元素传入</td>
</tr>
</tbody>
</table>
<h2>第十五章: 上下文管理器和 else 块</h2>
<p>本章讨论的是其他语言不常见的流程控制特性, 正因如此, python新手往往忽视或没有充分使用这些特性. 下面讨论的特性有:</p>
<ul>
<li>with 语句和上下文管理器</li>
<li>for while try 语句的 else 子句</li>
</ul>
<p><code>with</code> 语句会设置一个临时的上下文, 交给上下文管理器对象控制, 并且负责清理上下文. 这么做能避免错误并减少代码量, 因此API更安全, 而且更易于使用. 除了自动关闭文件之外, with 块还有其他很多用途.</p>
<p><code>else</code> 子句先做这个,选择性再做那个的作用.</p>
<h3>if语句之外的else块</h3>
<p>这里的 else 不是在在 if 语句中使用的, 而是在 for while try 语句中使用的.</p>
<pre><code>for i in lst:
if i > 10:
break
else:
print("no num bigger than 10")</code></pre>
<p><code>else</code> 子句的行为如下:</p>
<ul>
<li>
<code>for</code> : 仅当 for 循环运行完毕时(即 for 循环没有被 <code>break</code> 语句中止)才运行 else 块。</li>
<li>
<code>while</code> : 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 <code>break</code> 语句中止)才运行 else 块。</li>
<li>
<code>try</code> : 仅当 try 块中没有异常抛出时才运行 else 块。</li>
</ul>
<p>在所有情况下, 如果异常或者 <code>return</code> , <code>break</code> 或 <code>continue</code> 语句导致控制权跳到了复合语句的住块外, <code>else</code> 子句也会被跳过.</p>
<p>这一些情况下, 使用 else 子句通常让代码更便于阅读, 而且能省去一些麻烦, 不用设置控制标志作用的变量和额外的if判断.</p>
<h3>上下文管理器和with块</h3>
<p>上下文管理器对象的目的就是管理 <code>with</code> 语句, with 语句的目的是简化 <code>try/finally</code> 模式. 这种模式用于保证一段代码运行完毕后执行某项操作, 即便那段代码由于异常, <code>return</code> 或者 <code>sys.exit()</code> 调用而终止, 也会执行执行的操作. <code>finally</code> 子句中的代码通常用于释放重要的资源, 或者还原临时变更的状态.</p>
<p>上下文管理器协议包含 <code>__enter__</code> 和 <code>__exit__</code> 两个方法. with 语句开始运行时, 会在上下文管理器上调用 <code>__enter__</code> 方法, 待 with 语句运行结束后, 再调用 <code>__exit__</code> 方法, 以此扮演了 <code>finally</code> 子句的角色.</p>
<p>with 最常见的例子就是确保关闭文件对象.</p>
<p>上下文管理器调用 <code>__enter__</code> 没有参数, 而调用 <code>__exit__</code> 时, 会传入3个参数:</p>
<ul>
<li>
<code>exc_type</code> : 异常类(例如 ZeroDivisionError)</li>
<li>
<code>exc_value</code> : 异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 <code>exc_value.args</code> 获取</li>
<li>
<code>traceback</code> : <code>traceback</code> 对象</li>
</ul>
<h3>contextlib模块中的实用工具</h3>
<p>在ptyhon的标准库中, contextlib 模块中还有一些类和其他函数,使用范围更广。</p>
<ul>
<li>
<code>closing</code>: 如果对象提供了 <code>close()</code> 方法,但没有实现 <code>__enter__/__exit__</code> 协议,那么可以使用这个函数构建上下文管理器。</li>
<li>
<code>suppress</code>: 构建临时忽略指定异常的上下文管理器。</li>
<li>
<code>@contextmanager</code>: 这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。</li>
<li>
<code>ContextDecorator</code>: 这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。</li>
<li>
<code>ExitStack</code>: 这个上下文管理器能进入多个上下文管理器。with 块结束时,ExitStack 按照后进先出的顺序调用栈中各个上下文管理器的 <code>__exit__</code> 方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。</li>
</ul>
<p>显然,在这些实用工具中,使用最广泛的是 <code>@contextmanager</code> 装饰器,因此要格外留心。这个装饰器也有迷惑人的一面,因为它与迭代无关,却要使用 yield 语句。</p>
<h3>使用@contextmanager</h3>
<p>@contextmanager 装饰器能减少创建上下文管理器的样板代码量, 因为不用定义 <code>__enter__</code> 和 <code>__exit__</code> 方法, 只需要实现一个 <code>yield</code> 语句的生成器.</p>
<pre><code>import sys
import contextlib
@contextlib.contextmanager
def looking_glass():
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
yield 'JABBERWOCKY'
sys.stdout.write = original_write
with looking_glass() as f:
print(f) # YKCOWREBBAJ
print("ABCD") # DCBA
</code></pre>
<p><code>yield</code> 语句起到了分割的作用, yield 语句前面的所有代码在 with 块开始时(即解释器调用 <code>__enter__</code> 方法时)执行, yield 语句后面的代码在 with 块结束时(即调用 <code>__exit__</code> 方法时)执行.</p>
<h2>第十六章: 协程</h2>
<p>为了理解协程的概念, 先从 <code>yield</code> 来说. <code>yield item</code> 会产出一个值, 提供给 <code>next(...)</code> 调用方; 此外还会做出让步, 暂停执行生成器, 让调用方继续工作, 直到需要使用另一个值时再调用 <code>next(...)</code> 从暂停的地方继续执行.</p>
<p>从句子语法上看, 协程与生成器类似, 都是通过 <code>yield</code> 关键字的函数. 可是, 在协程中, <code>yield</code> 通常出现在表达式的右边(datum = yield), 可以产出值, 也可以不产出(如果yield后面没有表达式, 那么会产出None). 协程可能会从调用方接收数据, 不过调用方把数据提供给协程使用的是 <code>.send(datum)</code> 方法. 而不是 <code>next(...)</code> . 通常, 调用方会把值推送给协程.</p>
<p>生成器调用方是一直索要数据, 而协程这是调用方可以想它传入数据, 协程也不一定要产出数据.</p>
<p>不管数据如何流动, <code>yield</code> 都是一种流程控制工具, 使用它可以实现写作式多任务: 协程可以把控制器让步给中心调度程序, 从而激活其他的协程.</p>
<h3>生成器如何进化成协程</h3>
<p>协程的底层框架实现后, 生成器API中增加了 <code>.send(value)</code> 方法. 生成器的调用方可以使用 <code>.send(...)</code> 来发送数据, 发送的数据会变成生成器函数中 <code>yield</code> 表达式的值. 因此, 生成器可以作为协程使用. 除了 <code>.send(...)</code> 方法, 还添加了 <code>.throw(...)</code> 和 <code>.close()</code> 方法, 用来让调用方抛出异常和终止生成器.</p>
<h3>用作协程的生成器的基本行为</h3>
<pre><code>>>> def simple_coroutine():
... print('-> coroutine started')
... x = yield
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
...
StopIteration</code></pre>
<p>在 <code>yield</code> 表达式中, 如果协程只需从调用那接受数据, 那么产出的值是 <code>None</code> . 与创建生成器的方式一样, 调用函数得到生成器对象. 协程都要先调用 <code>next(...)</code> 函数, 因为生成器还没启动, 没在 yield 出暂定, 所以一开始无法发送数据. 如果控制器流动到协程定义体末尾, 会像迭代器一样抛出 <code>StopIteration</code> 异常.</p>
<p>使用协程的好处是不用加锁, 因为所有协程只在一个线程中运行, 他们是非抢占式的. 协程也有一些状态, 可以调用 <code>inspect.getgeneratorstate(...)</code> 来获得, 协程都是这4个状态中的一种:</p>
<ul>
<li>
<code>'GEN_CREATED'</code> 等待开始执行。</li>
<li>
<code>'GEN_RUNNING'</code> 解释器正在执行。</li>
<li>
<code>'GEN_SUSPENDED'</code> 在 yield 表达式处暂停。</li>
<li>
<code>'GEN_CLOSED'</code> 执行结束。</li>
</ul>
<p>只有在多线程应用中才能看到这个状态。此外,生成器对象在自己身上调用 <code>getgeneratorstate</code> 函数也行,不过这样做没什么用。</p>
<p>为了更好理解继承的行为, 来看看产生两个值的协程:</p>
<pre><code>>>> from inspect import getgeneratorstate
>>> def simple_coro2(a):
... print('-> Started: a =', a)
... b = yield a
... print('-> Received: b =', b)
... c = yield a + b
... print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> getgeneratorstate(my_coro2) # 协程处于未启动的状态
'GEN_CREATED'
>>> next(my_coro2) # 向前执行到yield表达式, 产出值 a, 暂停并等待 b 赋值
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2) # 协程处于暂停状态
'GEN_SUSPENDED'
>>> my_coro2.send(28) # 数字28发给协程, yield 表达式中 b 得到28, 协程向前执行, 产出 a + b 值
-> Received: b = 28
42
>>> my_coro2.send(99) # 同理, c 得到 99, 但是由于协程终止, 导致生成器对象抛出 StopIteration 异常
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2) # 协程处于终止状态
'GEN_CLOSED'</code></pre>
<p>关键的一点是, 协程在 <code>yield</code> 关键字所在的位置暂停执行. 对于 <code>b = yield a</code> 这行代码来说, 等到客户端代码再激活协程时才会设定 b 的值. 这种方式要花点时间才能习惯, 理解了这个, 才能弄懂异步编程中 <code>yield</code> 的作用. 对于实例的代码中函数 <code>simple_coro2</code> 的执行过程可以分为三个阶段:</p>
<p><img src="https://www.hongweipeng.com/usr/uploads/2017/10/3194048676.png" alt="20171011110645.png" title="20171011110645.png"></p>
<h3>示例:使用协程计算移动平均值</h3>
<pre><code>def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count</code></pre>
<p>这是一个动态计算平均值的协程代码, 这个无限循环表明, 它会一直接收值然后生成结果. 只有当调用方在协程上调用 <code>.close()</code> 方法, 或者没有该协程的引用时, 协程才会终止.</p>
<p>协程的好处是, 无需使用实例属性或者闭包, 在多次调用之间都能保持上下文.</p>
<h3>预激协程的装饰器</h3>
<p>如果没有执行 <code>next(...)</code> , 协程没什么用. 为了简化协程的用法, 有时会使用一个预激装饰器.</p>
<pre><code>from functools import wraps
def coroutine(func):
"""装饰器:向前执行到第一个`yield`表达式,预激`func`"""
@wraps(func)
def primer(*args,**kwargs): # 调用 primer 函数时,返回预激后的生成器
gen = func(*args,**kwargs) # 调用被装饰的函数,获取生成器对象。
next(gen) # 预激生成器
return gen # 返回生成器
return primer</code></pre>
<h3>终止协程和异常处理</h3>
<p>协程中未处理的异常会向上冒泡, 传给 <code>next()</code> 函数或者 <code>send()</code> 的调用方. 如果这个异常没有处理, 会导致协程终止.</p>
<pre><code>>>> coro_avg.send(40)
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('spam') # 传入会产生异常的值
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro_avg.send(60)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration</code></pre>
<p>这要求在协程内部要处理这些异常, 另外, 客户端代码也可以显示的发送异常给协程, 方法是 <code>throw</code> 和 <code>close</code> :</p>
<pre><code>coro_avg.throw(ZeroDivisionError)</code></pre>
<p>协程内部如果不能处理这个异常, 就会导致协程终止.</p>
<p>而 <code>close</code> 是致使在暂停的 <code>yield</code> 表达式处抛出 <code>GeneratorExit</code> 异常. 协程内部当然允许处理这个异常, 但收到这个异常时一定不能产出值, 不然解释器会抛出 <code>RuntimeError</code> 异常.</p>
<h3>让协程返回值</h3>
<pre><code>def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return (count, average)
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
try:
coro_avg.send(None) # 发送 None 让协程终止
except StopIteration as exc:
result = exc.value</code></pre>
<p>为了返回值, 协程必须正常终止, 而正常终止的的协程会抛出 <code>StopIteration</code> 异常, 因此需要调用方处理这个异常.</p>
<h3>使用yield from</h3>
<p><code>yield from</code> 是全新的语法结构. 它的作用比 <code>yield</code> 多很多.</p>
<pre><code>>>> def gen():
... for c in 'AB':
... yield c
... for i in range(1, 3):
... yield i
...
>>> list(gen())
['A', 'B', 1, 2]</code></pre>
<p>可以改写成:</p>
<pre><code>>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]</code></pre>
<p>在生成器 <code>gen</code> 中使用 <code>yield form subgen()</code> 时, subgen 会得到控制权, 把产出的值传给 gen 的调用方, 既调用方可以直接调用 subgen. 而此时, gen 会阻塞, 等待 subgen 终止.</p>
<p><code>yield from x</code> 表达式对 <code>x</code> 对象所做的第一件事是, 调用 <code>iter(x)</code> 获得迭代器. 因此, x 对象可以是任何可迭代对象.</p>
<p>这个语义过于复杂, 来看看作者 <code>Greg Ewing</code> 的解释:</p>
<blockquote><p>“把迭代器当作生成器使用,相当于把子生成器的定义体内联在 yield from 表达式<br>中。此外,子生成器可以执行 return 语句,返回一个值,而返回的值会成为 yield<br>from 表达式的值。”</p></blockquote>
<p>子生成器是从 <code>yield from <iterable></code> 中获得的生成器. 而后, 如果调用方使用 <code>send()</code> 方法, 其实也是直接传给子生成器. 如果发送的是 <code>None</code> , 那么会调用子生成器的 <code>__next__()</code> 方法. 如果不是 <code>None</code> , 那么会调用子生成器的 <code>send()</code> 方法. 当子生成器抛出 <code>StopIteration</code> 异常, 那么委派生成器恢复运行. 任何其他异常都会向上冒泡, 传给委派生成器.</p>
<p>生成器在 <code>return expr</code> 表达式中会触发 <code>StopIteration</code> 异常.</p>
<h2>第十七章: 使用期物处理并发</h2>
<p><code>"期物"</code> 是什么概念呢? 期物指一种对象, 表示异步执行的操作. 这个概念是 <code>concurrent.futures</code> 模块和 <code>asyncio</code> 包的基础.</p>
<h3>示例:网络下载的三种风格</h3>
<p>为了高效处理网络io, 需要使用并发, 因为网络有很高的延迟, 所以为了不浪费 CPU 周期去等待.</p>
<p>以一个下载网络 20 个图片的程序看, 串行下载的耗时 7.18s . 多线程的下载耗时 1.40s, asyncio的耗时 1.35s . 两个并发下载的脚本之间差异不大, 当对于串行的来说, 快了很多.</p>
<h3>阻塞型I/O和GIL</h3>
<p>CPython解释器不是线程安全的, 因此有全局解释锁(GIL), 一次只允许使用一个线程执行 python 字节码, 所以一个python进程不能同时使用多个 CPU 核心.</p>
<p>python程序员编写代码时无法控制 GIL, 然而, 在标准库中所有执行阻塞型I/O操作的函数, 在登台操作系统返回结果时都会释放GIL. 这意味着IO密集型python程序能从中受益.</p>
<h3>使用concurrent.futures模块启动进程</h3>
<p>一个python进程只有一个 GIL. 多个python进程就能绕开GIL, 因此这种方法就能利用所有的 CPU 核心. <code>concurrent.futures</code> 模块就实现了真正的并行计算, 因为它使用 <code>ProcessPoolExecutor</code> 把工作交个多个python进程处理.</p>
<p><code>ProcessPoolExecutor</code> 和 <code>ThreadPoolExecutor</code> 类都实现了通用的 <code>Executor</code> 接口, 因此使用 <code>concurrent.futures</code> 能很轻松把基于线程的方案转成基于进程的方案.</p>
<pre><code>def download_many(cc_list):
workers = min(MAX_WORKERS, len(cc_list))
with futures.ThreadPoolExecutor(workers) as executor:
res = executor.map(download_one, sorted(cc_list))</code></pre>
<p>改成:</p>
<pre><code>def download_many(cc_list):
with futures.ProcessPoolExecutor() as executor:
res = executor.map(download_one, sorted(cc_list))</code></pre>
<p><code>ThreadPoolExecutor.__init__</code> 方法需要 <code>max_workers</code> 参数,指定线程池中线程的数量; 在 <code>ProcessPoolExecutor</code> 类中, 这个参数是可选的.</p>
<h2>第十八章: 使用 asyncio 包处理并发</h2>
<blockquote><p>并发是指一次处理多件事。<br>并行是指一次做多件事。<br>二者不同,但是有联系。<br>一个关于结构,一个关于执行。<br>并发用于制定方案,用来解决可能(但未必)并行的问题。—— Rob Pike Go 语言的创造者之一</p></blockquote>
<p>并行是指两个或者多个事件在同一时刻发生, 而并发是指两个或多个事件在同一时间间隔发生. 真正运行并行需要多个核心, 现在笔记本一般有 4 个 CPU 核心, 但是通常就有超过 100 个进程同时运行. 因此, 实际上大多数进程都是并发处理的, 而不是并行处理. 计算机始终运行着 100 多个进程, 确保每个进程都有机会取得发展, 不过 CPU 本身同时做的事情不会超过四件.</p>
<p>本章介绍 <code>asyncio</code> 包, 这个包使用事件循环驱动的协程实现并发. 这个库有龟叔亲自操刀. <code>asyncio</code> 大量使用 <code>yield from</code> 表达式, 因此不兼容 python3.3 以下的版本.</p>
<h3>线程与协程对比</h3>
<p>一个借由 <code>threading</code> 模块使用线程, 一个借由 <code>asyncio</code> 包使用协程实现来进行比对.</p>
<pre><code>import threading
import itertools
import time
def spin(msg, done): # 这个函数会在单独的线程中运行
for char in itertools.cycle('|/-\\'): # 这其实是个无限循环,因为 itertools.cycle 函数会从指定的序列中反复不断地生成元素
status = char + ' ' + msg
print(status)
if done.wait(.1): # 如果进程被通知等待, 那就退出循环
break
def slow_function(): # 假设这是耗时的计算
# pretend waiting a long time for I/O
time.sleep(3) # 调用 sleep 函数会阻塞主线程,不过一定要这么做,以便释放 GIL,创建从属线程
return 42
def supervisor(): # 这个函数设置从属线程,显示线程对象,运行耗时的计算,最后杀死线程。
done = threading.Event()
spinner = threading.Thread(target=spin,
args=('thinking!', done))
print('spinner object:', spinner) # 显示从属线程对象。输出类似于 <Thread(Thread-1, initial)>
spinner.start() # 启动从属线程
result = slow_function() # 运行 slow_function 函数,阻塞主线程。同时,从属线程以动画形式显示旋转指针
done.set() # 改变 signal 的状态;这会终止 spin 函数中的那个 for 循环
spinner.join() # 等待 spinner 线程结束
return result
if __name__ == '__main__':
result = supervisor()
print('Answer:', result)</code></pre>
<p>这是使用 <code>threading</code> 的案例, 让子线程在 3 秒内不断打印, 在python中, 没有提供终止线程的API. 若想关闭线程, 必须给线程发送消息.</p>
<p>下面看看使用 <code>@asyncio.coroutine</code> 装饰器替代协程, 实现相同的行为:</p>
<pre><code>import asyncio
import itertools
@asyncio.coroutine # 交给 asyncio 处理的协程要使用 @asyncio.coroutine 装饰
def spin(msg):
for char in itertools.cycle('|/-\\'):
status = char + ' ' + msg
print(status)
try:
yield from asyncio.sleep(.1) # 使用 yield from asyncio.sleep(.1) 代替 time.sleep(.1),这样的休眠不会阻塞事件循环。
except asyncio.CancelledError: # 如果 spin 函数苏醒后抛出 asyncio.CancelledError 异常,其原因是发出了取消请求,因此退出循环。
break
@asyncio.coroutine
def slow_function(): # slow_function 函数是协程,在用休眠假装进行 I/O 操作时,使用 yield from 继续执行事件循环。
# pretend waiting a long time for I/O
yield from asyncio.sleep(3) # yield from asyncio.sleep(3) 表达式把控制权交给主循环,在休眠结束后恢复这个协程。
return 42
@asyncio.coroutine
def supervisor(): # supervisor 函数也是协程
spinner = asyncio.async(spin('thinking!')) # asyncio.async(...) 函数排定 spin 协程的运行时间,使用一个 Task 对象包装spin 协程,并立即返回。
print('spinner object:', spinner)
result = yield from slow_function() # 驱动 slow_function() 函数。结束后,获取返回值。
# 同时,事件循环继续运行,因为slow_function 函数最后使用 yield from asyncio.sleep(3) 表达式把控制权交回给了主循环。
spinner.cancel() # Task 对象可以取消;取消后会在协程当前暂停的 yield 处抛出 asyncio.CancelledError 异常。协程可以捕获这个异常,也可以延迟取消,甚至拒绝取消。
return result
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 获取事件循环的引用
result = loop.run_until_complete(supervisor()) # 驱动 supervisor 协程,让它运行完毕;这个协程的返回值是这次调用的返回值。
loop.close()
print('Answer:', result)</code></pre>
<p><code>asyncio</code> 包使用的协程是比较严格的定义, 适合 asyncio API 的协程在定义体中必须使用 <code>yield from</code> , 而不是使用 <code>yield</code> . 此外, <code>asyncio</code> 的协程要由调用方驱动, 例如 <code>asyncio.async(...)</code> , 从而驱动协程. 最后由 <code>@asyncio.coroutine</code> 装饰器应用在协程上.</p>
<p>这两种 <code>supervisor</code> 实现之间的主要区别概述如下:</p>
<ul>
<li>
<code>asyncio.Task</code> 对象差不多与 <code>threading.Thread</code> 对象等效。“Task对象像是实现协作式多任务的库(例如 gevent)中的绿色线程(green thread)”。</li>
<li>
<code>Task</code> 对象用于驱动协程,<code>Thread</code> 对象用于调用可调用的对象。</li>
<li>
<code>Task</code> 对象不由自己动手实例化,而是通过把协程传给 <code>asyncio.async(...)</code> 函数或 <code>loop.create_task(...)</code> 方法获取。</li>
<li>获取的 <code>Task</code> 对象已经排定了运行时间(例如,由 <code>asyncio.async</code> 函数排定);Thread 实例则必须调用 start 方法,明确告知让它运行。</li>
<li>在线程版 <code>supervisor</code> 函数中,<code>slow_function</code> 函数是普通的函数,直接由线程调用。在异步版 <code>supervisor</code> 函数中,<code>slow_function</code> 函数是协程,由 <code>yield from</code> 驱动。</li>
<li>没有 API 能从外部终止线程,因为线程随时可能被中断,导致系统处于无效状态。如果想终止任务,可以使用 <code>Task.cancel()</code> 实例方法,在协程内部抛出 <code>CancelledError</code> 异常。协程可以在暂停的 <code>yield</code> 处捕获这个异常,处理终止请求。</li>
<li>
<code>supervisor</code> 协程必须在 <code>main</code> 函数中由 <code>loop.run_until_complete</code> 方法执行。</li>
</ul>
<p>多线程编程是比较困难的, 因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断.</p>
<p>而协程默认会做好全方位保护, 以防止中断. 我们必须显示产出才能让程序的余下部分运行. 对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行.</p>
<h3>从期物、任务和协程中产出</h3>
<p>在 <code>asyncio</code> 包中, 期物和协程关系紧密, 因为可以使用 <code>yield from</code> 从 <code>asyncio.Future</code> 对象中产出结果. 也就是说, 如果 <code>foo</code> 是协程函数, 或者是返回 <code>Future</code> 或 <code>Task</code> 实例的普通函数, 那么可以用 <code>res = yield from foo()</code> .</p>
<p>为了执行这个操作, 必须排定协程的运行时间, 然后使用 <code>asyncio.Task</code> 对象包装协程. 对协程来说, 获取 <code>Task</code> 对象主要有两种方式:</p>
<ul>
<li>
<code>asyncio.async(coro_or_future, *, loop=None)</code> : 这个函数统一了协程和期物:第一个参数可以是二者中的任何一个。如果是 Future 或 Task 对象,那就原封不动地返回。如果是协程,那么 async 函数会调用 <code>loop.create_task(...)</code> 方法创建 Task 对象。loop 关键字参数是可选的,用于传入事件循环;如果没有传入,那么 async 函数会通过调用 <code>asyncio.get_event_loop()</code> 函数获取循环对象。</li>
<li>
<code>BaseEventLoop.create_task(coro)</code> : 这个方法排定协程的执行时间,返回一个 <code>asyncio.Task</code> 对象。如果在自定义的 <code>BaseEventLoop</code> 子类上调用,返回的对象可能是外部库(如 Tornado)中与 Task 类兼容的某个类的实例。</li>
</ul>
<p><code>asyncio</code> 包中有多个函数会自动(使用 <code>asyncio.async</code> 函数) 把参数指定的协程包装在 <code>asyncio.Task</code> 对象中.</p>
<h3>使用asyncio和aiohttp包下载</h3>
<p><code>asyncio</code> 包只直接支持 TCP 和 UDP. 如果像使用 HTTP 或其他协议, 就需要借助第三方包. 使用的几乎都是 <code>aiohttp</code> 包. 以下载图片为例:</p>
<pre><code>import asyncio
import aiohttp
from flags import BASE_URL, save_flag, show, main
@asyncio.coroutine
def get_flag(cc): # 协程应该使用 @asyncio.coroutine 装饰。
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
resp = yield from aiohttp.request('GET', url) # 阻塞的操作通过协程实现
image = yield from resp.read() # 读取响应内容是一项单独的异步操作
return image
@asyncio.coroutine
def download_one(cc): # download_one 函数也必须是协程,因为用到了 yield from
image = yield from get_flag(cc)
show(cc)
save_flag(image, cc.lower() + '.gif')
return cc
def download_many(cc_list):
loop = asyncio.get_event_loop() # 获取事件循环底层实现的引用
to_do = [download_one(cc) for cc in sorted(cc_list)] # 调用 download_one 函数获取各个国旗,然后构建一个生成器对象列表
wait_coro = asyncio.wait(to_do) # 虽然函数的名称是 wait,但它不是阻塞型函数。wait 是一个协程,等传给它的所有协程运行完毕后结束
res, _ = loop.run_until_complete(wait_coro) # 执行事件循环,直到 wait_coro 运行结束
loop.close() # 关闭事件循环
return len(res)
if __name__ == '__main__':
main(download_many)</code></pre>
<p><code>asyncio.wait(...)</code> 协程参数是一个由期物或协程构成的可迭代对象, wait 会分别把各个协程装进一个 <code>Task</code> 对象. 最终的结果是, wait 处理的所有对象都通过某种方式变成 <code>Future</code> 类的实例. wait 是协程函数, 因此返回的是一个协程或生成器对象. 为了驱动协程, 我们把协程传给 <code>loop.run_until_complete(...)</code> 方法.</p>
<p><code>loop.run_until_complete</code> 方法的参数是一个期物或协程. 如果是协程, <code>run_until_complete</code> 方法与 wait 函数一样, 把协程包装进一个 <code>Task</code> 对象中. 因为协程都是由 <code>yield from</code> 驱动, 这正是 <code>run_until_complete</code> 对 wait 返回返回的 wait_coro 对象所做的事. 运行结束后返回两个元素, 第一个是是结束的期物, 第二个是未结束的期物.</p>
<h3>避免阻塞型调用</h3>
<p>有两种方法能避免阻塞型调用中止整个应用程序的进程:</p>
<ul>
<li>在单独的线程中运行各个阻塞型操作</li>
<li>把每个阻塞型操作转换成非阻塞的异步调用使用</li>
</ul>
<p>多线程是可以的, 但是会消耗比较大的内存. 为了降低内存的消耗, 通常使用回调来实现异步调用. 这是一种底层概念, 类似所有并发机制中最古老最原始的那种--硬件中断. 使用回调时, 我们不等待响应, 而是注册一个函数, 在发生某件事时调用. 这样, 所有的调用都是非阻塞的.</p>
<p>异步应用程序底层的事件循环能依靠基础设置的中断, 线程, 轮询和后台进程等待等, 确保多个并发请求能取得进展并最终完成, 这样才能使用回调. 事件循环获得响应后, 会回过头来调用我们指定的回调. 如果做法正确, 事件循环和应用代码公共的主线程绝不会阻塞.</p>
<p>把生成器当做协程使用是异步编程的另一种方式. 对事件循环来说, 调用回调与在暂停的协程上调用 <code>.send()</code> 效果差不多.</p>
<h3>使用Executor对象,防止阻塞事件循环</h3>
<p>访问本地文件会阻塞, 而CPython底层在阻塞型I/O调用时会释放 GIL, 因此另一个线程可以继续.</p>
<p>因为 <code>asyncio</code> 事件不是通过多线程来完成, 因此 <code>save_flag</code> 用来保存图片的函数阻塞了与 <code>asyncio</code> 事件循环共用的唯一线程, 因此保存文件时, 真个应用程序都会冻结. 这个问题的解决办法是, 使用事件循环对象的 <code>run_in_executor</code> 方法.</p>
<p><code>asyncio</code> 的事件循环背后维护者一个 <code>ThreadPoolExecutor</code> 对象, 我们可以调用 <code>run_in_executor</code> 方法, 把可调用的对象发给它执行:</p>
<pre><code>@asyncio.coroutine
def download_one(cc, base_url, semaphore, verbose):
try:
with (yield from semaphore):
image = yield from get_flag(base_url, cc)
except web.HTTPNotFound:
status = HTTPStatus.not_found
msg = 'not found'
except Exception as exc:
raise FetchError(cc) from exc
else:
loop = asyncio.get_event_loop() # 获取事件循环对象的引用
loop.run_in_executor(None, # run_in_executor 方法的第一个参数是 Executor 实例;如果设为 None,使用事件循环的默认 ThreadPoolExecutor 实例。
save_flag, image, cc.lower() + '.gif') # 余下的参数是可调用的对象,以及可调用对象的位置参数
status = HTTPStatus.ok
msg = 'OK'
if verbose and msg:
print(cc, msg)
return Result(status, cc)</code></pre>
<h2>第十九章: 动态属性和特性</h2>
<p>在python中, 数据的属性和处理数据的方法都可以称为 <code>属性</code> . 除了属性, pythpn 还提供了丰富的 API, 用于控制属性的访问权限, 以及实现动态属性, 如 <code>obj.attr</code> 方式和 <code>__getattr__</code> 计算属性.</p>
<p>动态创建属性是一种元编程,</p>
<h3>使用动态属性转换数据</h3>
<p>通常, 解析后的 json 数据需要形如 <code>feed['Schedule']['events'][40]['name']</code> 形式访问, 必要情况下我们可以将它换成以属性访问方式 <code>feed.Schedule.events[40].name</code> 获得那个值.</p>
<pre><code>from collections import abc
class FrozenJSON:
"""一个只读接口,使用属性表示法访问JSON类对象
"""
def __init__(self, mapping):
self.__data = dict(mapping)
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FrozenJSON.build(self.__data[name]) # 从 self.__data 中获取 name 键对应的元素
@classmethod
def build(cls, obj):
if isinstance(obj, abc.Mapping):
return cls(obj)
elif isinstance(obj, abc.MutableSequence):
return [cls.build(item) for item in obj]
else: # 如果既不是字典也不是列表,那么原封不动地返回元素
return obj</code></pre>
<h3>使用 <strong>new</strong> 方法以灵活的方式创建对象</h3>
<p>我们通常把 <code>__init__</code> 成为构造方法, 这是从其他语言借鉴过来的术语. 其实, 用于构造实例的特殊方法是 <code>__new__</code> : 这是个类方法, 必须返回一个实例. 返回的实例将作为以后的 <code>self</code> 传给 <code>__init__</code> 方法.</p>
<h2>第二十章: 属性描述符</h2>
<p>描述符是实现了特性协议的类, 这个协议包括 <code>__get__</code>, <code>__set__</code> 和 <code>__delete__</code> 方法. 通常, 可以实现部分协议.</p>
<h3>覆盖型与非覆盖型描述符对比</h3>
<p>python存取属性的方式是不对等的. 通过实例读取属性时, 通常返回的是实例中定义的属性, 但是, 如果实例中没有指定的属性, 那么会从获取类属性. 而实例中属性赋值时, 通常会在实例中创建属性, 根本不影响类.</p>
<p>这种不对等的处理方式对描述符也有影响. 根据是否定义 <code>__set__</code> 方法, 描述符可分为两大类: 覆盖型描述符和与非覆盖型描述符.</p>
<p>实现 <code>__set__</code> 方法的描述符属于覆盖型描述符, 因为虽然描述符是类属性, 但是实现 <code>__set__</code> 方法的话, 会覆盖对实例属性的赋值操作. 因此作为类方法的 <code>__set__</code> 需要传入一个实例 <code>instance</code> . 看个例子:</p>
<pre><code>def print_args(*args): # 打印功能
print(args)
class Overriding: # 设置了 __set__ 和 __get__
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
def __set__(self, instance, value):
print_args('set', self, instance, value)
class OverridingNoGet: # 没有 __get__ 方法的覆盖型描述符
def __set__(self, instance, value):
print_args('set', self, instance, value)
class NonOverriding: # 没有 __set__ 方法,所以这是非覆盖型描述符
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
class Managed: # 托管类,使用各个描述符类的一个实例
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverriding()
def spam(self):
print('-> Managed.spam({})'.format(repr(self)))</code></pre>
<p><strong>覆盖型描述符</strong></p>
<pre><code>obj = Managed()
obj.over # ('get', <__main__.Overriding object>, <__main__.Managed object>, <class '__main__.Managed'>)
obj.over = 7 # ('set', <__main__.Overriding object>, <__main__.Managed object>, 7)
obj.over # ('get', <__main__.Overriding object>, <__main__.Managed object>, <class '__main__.Managed'>)</code></pre>
<p>名为 <code>over</code> 的实例属性, 会覆盖读取和赋值 <code>obj.over</code> 的行为.</p>
<p><strong>没有 <code>__get__</code> 方法的覆盖型描述符</strong></p>
<pre><code>obj = Managed()
obj.over_no_get
obj.over_no_get = 7 # ('set', <__main__.OverridingNoGet object>, <__main__.Managed object>, 7)
obj.over_no_get</code></pre>
<p>只有在赋值操作的时候才回覆盖行为.</p>
<h3>方法是描述符</h3>
<p>python的类中定义的函数属于绑定方法, 如果用户定义的函数都有 <code>__get__</code> 方法, 所以依附到类上, 就相当于描述符.</p>
<p><code>obj.spam</code> 和 <code>Managed.spam</code> 获取的是不同的对象. 前者是 <code><class method></code> 后者是 <code><class function></code> .</p>
<p>函数都是非覆盖型描述符. 在函数上调用 <code>__get__</code> 方法时传入实例作为 <code>self</code> , 得到的是绑定到那个实例的方法. 调用函数的 <code>__get__</code> 时传入的 instance 是 <code>None</code> , 那么得到的是函数本身. 这就是形参 <code>self</code> 的隐式绑定方式.</p>
<h3>描述符用法建议</h3>
<p><strong>使用特性以保持简单</strong><br>内置的 <code>property</code> 类创建的是覆盖型描述符, <code>__set__</code> 和 <code>__get__</code> 都实现了.</p>
<p><strong>只读描述符必须有 <strong>set</strong> 方法</strong><br>如果要实现只读属性, <code>__get__</code> 和 <code>__set__</code> 两个方法必须都定义, 柔则, 实例的同名属性会覆盖描述符.</p>
<p><strong>用于验证的描述符可以只有 <strong>set</strong> 方法</strong><br>什么是用于验证的描述符, 比方有个年龄属性, 但它只能被设置为数字, 这时候就可以只定义 <code>__set__</code> 来验证值是否合法. 这种情况不需要设置 <code>__get__</code> , 因为实例属性直接从 <code>__dict__</code> 中获取, 而不用去触发 <code>__get__</code> 方法.</p>
<h2>第二十一章: 类元编程</h2>
<p>类元编程是指在运行时创建或定制类的技艺. 在python中, 类是一等对象, 因此任何时候都可以使用函数创建类, 而无需使用 <code>class</code> 关键字. 类装饰器也是函数, 不过能够审查, 修改, 甚至把被装饰的类替换成其他类.</p>
<p>元类是类元编程最高级的工具. 什么是元类呢? 比如说 <code>str</code> 是创建字符串的类, <code>int</code> 是创建整数的类. 那么元类就是创建类的类. 所有的类都由元类创建. 其他 <code>class</code> 只是原来的"实例".</p>
<p>本章讨论如何在运行时创建类.</p>
<h3>类工厂函数</h3>
<p>标准库中就有一个例子是类工厂函数--具名元组( <code>collections.namedtuple</code> ). 我们把一个类名和几个属性传给这个函数, 它会创建一个 <code>tuple</code> 的子类, 其中的元素通过名称获取.</p>
<p>假设我们创建一个 <code>record_factory</code> , 与具名元组具有相似的功能:</p>
<pre><code>>>> Dog = record_factory('Dog', 'name weight owner')
>>> rex = Dog('Rex', 30, 'Bob')
>>> rex
Dog(name='Rex', weight=30, owner='Bob')
>>> rex.weight = 32
>>> Dog.__mro__
(<class 'factories.Dog'>, <class 'object'>)</code></pre>
<p>我们要做一个在运行时创建类的, 类工厂函数:</p>
<pre><code>def record_factory(cls_name, field_names):
try:
field_names = field_names.replace(',', ' ').split() # 属性拆分
except AttributeError: # no .replace or .split
pass # assume it's already a sequence of identifiers
field_names = tuple(field_names) # 使用属性名构建元组,这将成为新建类的 __slots__ 属性
def __init__(self, *args, **kwargs): # 这个函数将成为新建类的 __init__ 方法
attrs = dict(zip(self.__slots__, args))
attrs.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
def __iter__(self): # 实现 __iter__ 函数, 变成可迭代对象
for name in self.__slots__:
yield getattr(self, name)
def __repr__(self): # 生成友好的字符串表示形式
values = ', '.join('{}={!r}'.format(*i) for i
in zip(self.__slots__, self))
return '{}({})'.format(self.__class__.__name__, values)
cls_attrs = dict(__slots__ = field_names, # 组建类属性字典
__init__ = __init__,
__iter__ = __iter__,
__repr__ = __repr__)
return type(cls_name, (object,), cls_attrs) # 调用元类 type 构造方法,构建新类,然后将其返回</code></pre>
<p><code>type</code> 就是元类, 实例的最后一行会构造一个类, 类名是 <code>cls_name</code>, 唯一直接的超类是 <code>object</code> .</p>
<p>在python中做元编程时, 最好不要用 <code>exec</code> 和 <code>eval</code> 函数. 这两个函数会带来严重的安全风险.</p>
<h3>元类基础知识</h3>
<p>元类是制造类的工厂, 不过不是函数, 本身也是类. <strong>元类是用于构建类的类</strong>.</p>
<p>为了避免无限回溯, <code>type</code> 是其自身的实例. <code>object</code> 类和 <code>type</code> 类关系很独特, <code>object</code> 是 <code>type</code> 的实例, 而 <code>type</code> 是 <code>object</code> 的子类.</p>
<h3>元类的特殊方法 <strong>prepare</strong>
</h3>
<p><code>type</code> 构造方法以及元类的 <code>__new__</code> 和 <code>__init__</code> 方法都会收到要计算的类的定义体, 形式是名称到属性的映像. 在默认情况下, 这个映射是字典, 属性在类的定义体中顺序会丢失. 这个问题的解决办法是, 使用python3引入的特殊方法 <code>__prepare__</code> , 这个方法只在元类中有用, 而且必须声明为类方法(即要使用 <code>@classmethod</code> 装饰器定义). 解释器调用元类的 <code>__new__</code> 方法之前会先调用 <code>__prepare__</code> 方法, 使用类定义体中的属性创建映射.</p>
<p><code>__prepare__</code> 的第一个参数是元类, 随后两个参数分别是要构建类的名称和基类组成的原则, 返回值必须是映射.</p>
<pre><code>class EntityMeta(type):
"""Metaclass for business entities with validated fields"""
@classmethod
def __prepare__(cls, name, bases):
return collections.OrderedDict() # 返回一个空的 OrderedDict 实例,类属性将存储在里面。
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict)
cls._field_names = [] # 中创建一个 _field_names 属性
for key, attr in attr_dict.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
cls._field_names.append(key)
class Entity(metaclass=EntityMeta):
"""Business entity with validated fields"""
@classmethod
def field_names(cls): # field_names 类方法的作用简单:按照添加字段的顺序产出字段的名称
for name in cls._field_names:
yield name</code></pre>
<h2>结语</h2>
<p>python是一门即容易上手又强大的语言.</p>
[译]将PHP扩展从PHP5升级到NG(PHP7)
https://segmentfault.com/a/1190000007727633
2016-12-07T18:27:56+08:00
2016-12-07T18:27:56+08:00
weapon
https://segmentfault.com/u/weapon
2
<p>许多经常使用的API函数已经更改,例如HashTable API; 这个页面致力于记录尽可能多的实际影响扩展和核心代码的更改。 强烈建议在阅读本指南之前阅读phpng-int中有关PHPNG实现的一般信息。</p>
<p>这不是一个涵盖所有可能情况的完整指南。 这是一个在大多数情况下有用的汇总。 我希望它对大多数用户级扩展来说是足够的。 然而,如果你没有在这里找到一些信息,发现一个解决方案,因为它可能对其他人有用 - 随时完善您的方法。</p>
<h2>一般建议</h2>
<p>尝试使用PHPNG编译扩展。 查看编译错误和警告。 他们可以显示出75%需要修改的地方。</p>
<p>在调试模式下编译和测试扩展(使用 <code>-enable-debug</code> 来配置PHP)。它将在运行时使用 <code>assert()</code> 函数捕获一些错误。 您还将看到有关内存泄漏的信息。</p>
<h2>zval</h2>
<p>PHPNG不需要任何指向指向zval的指针的参与。大多数<code>zval**</code>变量和参数必须更改为<code>zval*</code>。 使用这些变量的相应<code>Z_*_ PP()</code>宏应该更改为<code>Z_*_P()</code>。</p>
<p>在许多地方PHPNG直接使用zval(消除了分配和释放的需求)。 在这些情况下,应将相应的<code>zval *</code>变量转换为纯<code>zval</code>,使用此变量从<code>Z_*_P()</code>到<code>Z_*()</code>和相应的创建宏从<code>ZVAL_*(var,...)</code>到<code>ZVAL_*(&var,...)</code>。 一定要小心传递zval和&运算的地址。 PHPNG几乎从 <code>不需要</code> 传递 <code>zval *</code> 的地址。 在某些地方应该删除 <code>&</code> 运算。</p>
<p>有关zval分配的宏 <code>ALLOC_ZVAL</code> , <code>ALLOC_INIT_ZVAL</code> 和 <code>MAKE_STD_ZVAL</code> 被移除。 在大多数情况下,它们的用法表明<code>zval *</code>需要更改为纯<code>zval</code>。 宏<code>INIT_PZVAL</code>也被删除,它的用法在大多数情况下应该被删除。</p>
<pre><code>- zval *zv;
- ALLOC_INIT_ZVAL();
- ZVAL_LONG(zv, 0);
+ zval zv;
+ ZVAL_LONG(&zv, 0);</code></pre>
<p>zval结构已完全更改。 现在它的定义是:</p>
<pre><code>struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* various IS_VAR flags */
} v;
zend_uint type_info;
} u1;
union {
zend_uint var_flags;
zend_uint next; /* hash collision chain */
zend_uint str_offset; /* string offset */
zend_uint cache_slot; /* literal cache slot */
} u2;
};</code></pre>
<p>zend_value如下:</p>
<pre><code>typedef union _zend_value {
long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
} zend_value;</code></pre>
<p>主要的区别是,现在我们处理标量和复杂类型不同。 PHP不在堆中分配标量值,而是直接在VM堆栈上,在HashTables和对象内部。 它们 <code>不再</code> 是引用计数和垃圾收集的主体。 标量值没有引用计数器,不再支持 <code>Z_ADDREF *()</code> , <code>Z_DELREF *()</code> , <code>Z_REFCOUNT *()</code> 和 <code>Z_SET_REFCOUNT *()</code> 宏。 在大多数情况下,你应该判断zval是否支持这些宏,然后再调用它们。 否则你会得到一个<code>assert()</code>或崩溃。</p>
<pre><code>- Z_ADDREF_P(zv)
+ if (Z_REFCOUNTED_P(zv)) {Z_ADDREF_P(zv);}
# or equivalently
+ Z_TRY_ADDREF_P(zv);</code></pre>
<ul>
<li><p>应使用 <code>ZVAL_COPY_VALUE()</code> 宏复制zval值。</p></li>
<li><p>如果需要,可以使用 <code>ZVAL_COPY()</code> 宏复制和增加引用计数器。</p></li>
<li><p>可以使用 <code>ZVAL_DUP()</code> 宏来完成 <code>zval(zval_copy_ctor)</code> 的复制。</p></li>
<li><p>如果将<code>zval *</code>转换为<code>zval</code>并且提前使用<code>NULL</code>来指示未定义的值,那么现在可以改用<code>IS_UNDEF</code>类型。 它可以使用 <code>ZVAL_UNDEF(&zv)</code> 设置并可以使用<code>if(Z_ISUNDEF(zv))</code>进行检查。</p></li>
<li><p>如果要使用cast-semantics而不修改原始zval来获取zval的long/double/string值,现在可以使用 <code>zval_get_long(zv)</code> , <code>zval_get_double(zv)</code> 和<code>zval_get_string(zv)</code> API简化代码:</p></li>
</ul>
<pre><code>- zval tmp;
- ZVAL_COPY_VALUE(&tmp, zv);
- zval_copy_ctor(&tmp);
- convert_to_string(&tmp);
- // ...
- zval_dtor(&tmp);
+ zend_string *str = zval_get_string(zv);
+ // ...
+ zend_string_release(str);</code></pre>
<p>查看 <code>zend_types.h</code> 代码获取更多详细信息: <a href="https://link.segmentfault.com/?enc=GNLdZcWAgbg4LHbEY1Xn1g%3D%3D.VtVDz%2FkKF56zc%2FM53pSWD9mwaYK0JReiOwUhJmrH21ZAm1gA%2BL6nt%2FLj%2FgST1SmpxfFLAtE9e6%2Fmlt6Ha3K2yA%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=qgBM8tkyWzKWM1UO9F4%2BCw%3D%3D.eIBQ6fCWlyVLTIj%2F6HjO8aalmdZ8DAp8vexA1GWhQWt6PvtZcaksttljXhqIJJTntPwNIHQZ9XlfCslaVx9qOA%3D%3D" rel="nofollow">https://github.com/php/php-sr...</a></p>
<h2>参考</h2>
<p>PHPNG中的 <code>zval</code> 不再有 <code>is_ref</code> 标志。 引用是使用单独的复数引用计数类型 <code>IS_REFERENCE</code> 实现的。 仍然可以使用 <code>Z_ISREF *()</code> 宏来检查给定的 <code>zval</code> 是否被引用。 实际上,它只是检查给定的zval的类型是否等于<code>IS_REFERENCE</code>。 因此使用<code>is_ref</code>标志的宏被移除:<code>Z_SET_ISREF *()</code>,<code>Z_UNSET_ISREF *()</code> 和 <code>Z_SET_ISREF_TO *()</code> 。 它们的用法应该以下列方式改变:</p>
<pre><code>- Z_SET_ISREF_P(zv);
+ ZVAL_MAKE_REF(zv);
- Z_UNSET_ISREF_P(zv);
+ if (Z_ISREF_P(zv)) {ZVAL_UNREF(zv);}</code></pre>
<p>以前的引用可以直接检查引用的类型。 现在我们必须通过 <code>Z_REFVAL *()</code> 宏来间接检查它。</p>
<pre><code>- if (Z_ISREF_P(zv) && Z_TYPE_P(zv) == IS_ARRAY) {}
+ if (Z_ISREF_P(zv) && Z_TYPE_P(Z_REFVAL_P(zv)) == IS_ARRAY) {}</code></pre>
<p>或使用 <code>ZVAL_DEREF()</code> 宏执行手动取消引用:</p>
<pre><code>- if (Z_ISREF_P(zv)) {...}
- if (Z_TYPE_P(zv) == IS_ARRAY) {
+ if (Z_ISREF_P(zv)) {...}
+ ZVAL_DEREF(zv);
+ if (Z_TYPE_P(zv) == IS_ARRAY) {</code></pre>
<h2>Booleans</h2>
<p><code>IS_BOOL</code>不再存在,但<code>IS_TRUE</code>和<code>IS_FALSE</code>是依然是它的类型:</p>
<pre><code>- if ((Z_TYPE_PP(item) == IS_BOOL || Z_TYPE_PP(item) == IS_LONG) && Z_LVAL_PP(item)) {
+ if (Z_TYPE_P(item) == IS_TRUE || (Z_TYPE_P(item) == IS_LONG && Z_LVAL_P(item))) {</code></pre>
<p>将删除 <code>Z_BVAL *()</code> 宏。 注意, <code>IS_FALSE/IS_TRUE</code> 在 <code>Z_LVAL *()</code> 的返回值里是没有定义的。</p>
<h2>Strings</h2>
<p>可以使用相同的宏 <code>Z_STRVAL *()</code> 和 <code>Z_STRLEN *()</code> 来访问字符串的值/长度。 但是现在字符串表示的下划线数据结构是 <code>zend_string</code> (在单独的部分中描述)。 zend_string可以通过 <code>Z_STR *()</code> 宏从zval中检索。 它也可以通过 <code>Z_STRHASH *()</code> 获取字符串的哈希值。</p>
<p>如果代码需要检查给定的字符串是否是可转为int,现在应该使用<code>zend_string</code>(不是char *):</p>
<pre><code>- if (IS_INTERNED(Z_STRVAL_P(zv))) {
+ if (IS_INTERNED(Z_STR_P(zv))) {</code></pre>
<p>创建字符串zvals有点改变。 以前的宏,如 <code>ZVAL_STRING()</code> 有一个额外的参数,告诉是否应该复制给定的字符。 现在这些宏总是必须创建 <code>zend_string</code> 结构,所以这个参数变得没用了。 但是,如果它的实际值为0,则可以释放原始字符串,以避免内存泄漏。</p>
<pre><code>- ZVAL_STRING(zv, str, 1);
+ ZVAL_STRING(zv, str);
- ZVAL_STRINGL(zv, str, len, 1);
+ ZVAL_STRINGL(zv, str, len);
- ZVAL_STRING(zv, str, 0);
+ ZVAL_STRING(zv, str);
+ efree(str);
- ZVAL_STRINGL(zv, str, len, 0);
+ ZVAL_STRINGL(zv, str, len);
+ efree(str);</code></pre>
<p>类似的宏,如 <code>RETURN_STRING()</code> , <code>RETVAL_STRINGS()</code> 等等和一些内部API函数也是如此。</p>
<pre><code>- add_assoc_string(zv, key, str, 1);
+ add_assoc_string(zv, key, str);
- add_assoc_string(zv, key, str, 0);
+ add_assoc_string(zv, key, str);
+ efree(str);</code></pre>
<p>可以直接使用 <code>zend_string</code> API并直接从zend_string创建zval来避免双重新分配。</p>
<pre><code>- char * str = estrdup("Hello");
- RETURN_STRING(str);
+ zend_string *str = zend_string_init("Hello", sizeof("Hello")-1, 0);
+ RETURN_STR(str);</code></pre>
<p><code>Z_STRVAL *()</code> 现在应该用作只读对象。 它不可能分配任何东西。 它可以修改单独的字符,但在做之前,你必须确保这个字符串没有被引用到其他地方(它不是interned,它的reference-counter是1)。 此外,在字符串修改后,可能需要重置计算的哈希值。</p>
<pre><code> SEPARATE_ZVAL(zv);
Z_STRVAL_P(zv)[0] = Z_STRVAL_P(zv)[0] + ('A' - 'a');
+ zend_string_forget_hash_val((Z_STR_P(zv))</code></pre>
<h2>zend_string API</h2>
<p>Zend有一个新的 <code>zend_string</code> API,除了zend_string是在zval中的字符串表示的下划线结构,这些结构也被用于以前使用 <code>char *</code> 和 <code>int</code> 的大部分代码库。</p>
<p>可以使用 <code>zend_string_init(char * val,size_t len,int persistent)</code> 函数创建<code>zend_strings</code>(不是<code>IS_STRING</code> zvals)。 实际字符可以作为 <code>str→val</code> 和字符串长度作为 <code>str→len</code> 访问。 字符串的哈希值应通过 <code>zend_string_hash_val</code> 函数访问。 如果需要,它将重新计算哈希值。</p>
<p>字符串应该使用 <code>zend_string_release()</code> 函数释放,这不需要空闲内存,因为相同的字符串可能从几个地方引用。</p>
<p>如果你打算在某个地方保持 <code>zend_string</code> 指针,你应该增加它的reference-counter或使用 <code>zend_string_copy()</code> 函数,它会为你做。 在许多地方,代码复制字符只是为了保持值(不修改),可以使用这个函数。</p>
<pre><code>- ptr->str = estrndup(Z_STRVAL_P(zv), Z_STRLEN_P(zv));
+ ptr->str = zend_string_copy(Z_STR_P(zv));
...
- efree(str);
+ zend_string_release(str);</code></pre>
<p>如果复制的字符串要更改,您可以使用 <code>zend string_dup()</code> :</p>
<pre><code>- char *str = estrndup(Z_STRVAL_P(zv), Z_STRLEN_P(zv));
+ zend_string *str = zend_string_dup(Z_STR_P(zv));
...
- efree(str);
+ zend_string_release(str);</code></pre>
<p>具有旧宏的代码也是支持的,因此无需切换到新宏。</p>
<p>在某些情况下,在实际字符串数据已知之前分配字符串缓冲区是有意义的。 您可以使用 <code>zend_string_alloc()</code> 和 <code>zend_string_realloc()</code> 函数来完成。</p>
<pre><code>- char *ret = emalloc(16+1);
- md5(something, ret);
- RETURN_STRINGL(ret, 16, 0);
+ zend_string *ret = zend_string_alloc(16, 0);
+ md5(something, ret->val);
+ RETURN_STR(ret);</code></pre>
<p>不是所有的扩展代码都必须将 <code>char *</code> 转换为 <code>zend_string</code> 。 由扩展维护者决定哪种类型在每种特定情况下更合适。</p>
<p>查看 <code>zend_string.h</code> 代码了解更多详细信息:<a href="https://link.segmentfault.com/?enc=3tYIGBCUz81bKcHLqrx0kw%3D%3D.FOqcsGduqR92fZoD%2FLCMI2ahIkkUjV5Qt6xW0Bzx2giuvx4wEWR6hlRsSoXZ0mcnUtM5GAh8%2FP%2BA7Auuys2%2F2A%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=5oppaOeNrHN%2F2L1Zp9fr1w%3D%3D.%2BDFwOlR7ZeUsczmenTcfI1AHok5%2B3xUS3YXb9UpJQeM75OubrsVTCLDvbuN4O7UJMmwL6VuB56P0GfIVrRu2Hw%3D%3D" rel="nofollow">https://github.com/php/php-sr...</a></p>
<h2>smart_str 和 smart_string</h2>
<p>为了一致的命名约定,旧的<code>smart_str</code> API被重命名为<code>smart_string</code>。 它可以像以前一样使用,除了新的名称。</p>
<pre><code>- smart_str str = {0};
- smart_str_appendl(str, " ", sizeof(" ") - 1);
- smart_str_0(str);
- RETURN_STRINGL(implstr.c, implstr.len, 0);
+ smart_string str = {0};
+ smart_string_appendl(str, " ", sizeof(" ") - 1);
+ smart_string_0(str);
+ RETVAL_STRINGL(str.c, str.len);
+ smart_string_free(&str);</code></pre>
<p>此外,引入了一个新的 <code>zend_str</code> API,它直接与 <code>zend_string</code> 一起工作:</p>
<pre><code>- smart_str str = {0};
- smart_str_appendl(str, " ", sizeof(" ") - 1);
- smart_str_0(str);
- RETURN_STRINGL(implstr.c, implstr.len, 0);
+ smart_str str = {0};
+ smart_str_appendl(&str, " ", sizeof(" ") - 1);
+ smart_str_0(&str);
+ if (str.s) {
+ RETURN_STR(str.s);
+ } else {
+ RETURN_EMPTY_STRING();
+ }</code></pre>
<p><code>smart_str</code> 定义如下:</p>
<pre><code>typedef struct {
zend_string *s;
size_t a;
} smart_str;</code></pre>
<p><code>smart_str</code>和<code>smart_string</code>的API非常相似,实际上它们重复PHP5中使用的API。 所以采用代码不是一个大问题。 最大的问题是自动为每个特定情况选择什么,但它取决于最终结果的使用方式。</p>
<p>请注意,可能需要更改先前检查的空 <code>smart_str</code> :</p>
<pre><code>- if (smart_str->c) {
+ if (smart_str->s) {</code></pre>
<h2>strprintf</h2>
<p>除了 <code>sprintf()</code> 和 <code>vsprintf()</code> 函数,我们引入了类似的函数,产生<code>zend_string</code> ,而不是 <code>char *</code> 。 它取决于您决定何时应该更改为新的变体。</p>
<pre><code>PHPAPI zend_string *vstrpprintf(size_t max_len, const char *format, va_list ap);
PHPAPI zend_string *strpprintf(size_t max_len, const char *format, ...);</code></pre>
<h2>Arrays</h2>
<p>数组实现或多或少相同,但是,如果以前的下划线结构被实现为指向 <code>HashTable</code> 的指针,现在我们在这里有一个指向 <code>zend_array</code> 的内部保持 <code>HashTable</code> 。 <code>HashTable</code> 可以像之前一样使用 <code>Z_ARRVAL *()</code> 宏读取,但现在不可能将指针更改为HashTable。 它只能通过宏<code>Z_ARR *()</code>获取或设置指向整个zend_array的指针。</p>
<p>创建数组的最好方法是使用旧的 <code>array_init()</code> 函数,但也可以使用 <code>ZVAL_NEW_ARR()</code> 创建新的未初始化数组,或者通过 <code>ZVAL_ARR()</code> 使用 <code>zend_array</code> 结构初始化数组。</p>
<p>一些数组可能是不可变的(可以使用 <code>Z_IMMUTABLE()</code> 宏来检查)。 如果代码需要修改它们,它们必须首先复制。 使用内部位置指针通过不可变数组迭代也是不可能的。 可以使用带有外部位置指针的旧迭代API或使用在单独部分中描述的新的HashTable迭代API来遍历这些数组。</p>
<h2>HashTable API</h2>
<p><code>HashTable API</code> 明显的改变,它可能会导致扩展兼容中的一些麻烦。</p>
<p>首先,现在<code>HashTables</code>总是使用<code>zval</code>。 即使我们存储一个任意指针,它被打包到zval与特殊类型<code>IS_PTR</code>。 无论如何,这简化了zval的工作:</p>
<pre><code>- zend_hash_update(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)+1, (void*)&zv, sizeof(zval**), NULL) == SUCCESS) {
+ if (zend_hash_update(EG(function_table), Z_STR_P(key), zv)) != NULL) {</code></pre>
<p>大多数API函数直接返回请求的值(而不是通过引用参数使用附加参数并返回<code>SUCCESS</code> / <code>FAILURE</code>):</p>
<pre><code>- if (zend_hash_find(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)+1, (void**)&zv_ptr) == SUCCESS) {
+ if ((zv = zend_hash_find(ht, Z_STR_P(key))) != NULL) {</code></pre>
<p>键表示为<code>zend_string</code>。 大多数函数有两种形式。 一个以<code>zend_string</code>作为键,另一个以<code>char *</code>作为键,长度对。</p>
<p>重要说明:当键值字符串的长度不包括尾随零(<code>0</code>)。 在某些地方,必须删除或添加<code>+1 / -1</code>:</p>
<pre><code>- if (zend_hash_find(ht, "value", sizeof("value"), (void**)&zv_ptr) == SUCCESS) {
+ if ((zv = zend_hash_str_find(ht, "value", sizeof("value")-1)) != NULL) {</code></pre>
<p>这也适用于zend_hash之外的其他hashtable相关的API。 例如:</p>
<pre><code>- add_assoc_bool_ex(&zv, "valid", sizeof("valid"), 0);
+ add_assoc_bool_ex(&zv, "valid", sizeof("valid") - 1, 0);</code></pre>
<p>API提供了一组单独的函数来处理任意指针。 这些函数与 <code>_ptr</code> 后缀具有相同的名称。</p>
<pre><code>- if (zend_hash_find(EG(class_table), Z_STRVAL_P(key), Z_STRLEN_P(key)+1, (void**)&ce_ptr) == SUCCESS) {
+ if ((ce_ptr = zend_hash_find_ptr(EG(class_table), Z_STR_P(key))) != NULL) {
- zend_hash_update(EG(class_table), Z_STRVAL_P(key), Z_STRLEN_P(key)+1, (void*)&ce, sizeof(zend_class_entry*), NULL) == SUCCESS) {
+ if (zend_hash_update_ptr(EG(class_table), Z_STR_P(key), ce)) != NULL) {</code></pre>
<p>API提供了一组单独的函数来存储任意大小的内存块。 这些函数与 <code>_mem</code> 后缀具有相同的名称,并且它们实现为相应 <code>_ptr</code> 函数的内联封装。 这不意味着如果使用_mem或_ptr变量存储某些内容。 它总是可以使用 <code>zend_hash_find_ptr()</code> 找回来。</p>
<pre><code>- zend_hash_update(EG(function_table), Z_STRVAL_P(key), Z_STRLEN_P(key)+1, (void*)func, sizeof(zend_function), NULL) == SUCCESS) {
+ if (zend_hash_update_mem(EG(function_table), Z_STR_P(key), func, sizeof(zend_function))) != NULL) {</code></pre>
<p>增加了新的元素插入的新的优化功能。 它们旨在用于代码仅添加新元素(不能与现有键重叠)的情况。 例如,当您将一个HashTable的一些元素复制到一个新的。 所有这些函数都有 <code>_new </code>后缀。</p>
<pre><code>zval* zend_hash_add_new(HashTable *ht, zend_string *key, zval *zv);
zval* zend_hash_str_add_new(HashTable *ht, char *key, int len, zval *zv);
zval* zend_hash_index_add_new(HashTable *ht, pzval *zv);
zval* zend_hash_next_index_insert_new(HashTable *ht, pzval *zv);
void* zend_hash_add_new_ptr(HashTable *ht, zend_string *key, void *pData);
...</code></pre>
<p>HashTable析构函数现在总是接收<code>zval *</code>(即使我们使用zend_hash_add_ptr或zend_hash_add_mem来添加元素)。 <code>Z_PTR_P()</code> 宏可以用于在析构函数中达到实际的指针值。 另外,如果使用 <code>zend_hash_add_mem</code> 添加元素,析构函数也负责指针本身的解除分配。</p>
<pre><code>- void my_ht_destructor(void *ptr)
+ void my_ht_destructor(zval *zv)
{
- my_ht_el_t *p = (my_ht_el_t*) ptr;
+ my_ht_el_t *p = (my_ht_el_t*) Z_PTR_P(zv);
...
+ efree(p); // this efree() is not always necessary
}
);</code></pre>
<p>所有 <code>zend_hash_apply_*()</code> 函数的回调,以及 <code>zend_hash_copy()</code> 和 <code>zend_hash_merge()</code> 的回调应该改变为接收 <code>zval *</code> 而不是 <code>void * &&</code> ,与析构函数相同。 这些函数中的一些还接收指向 <code>zend_hash_key</code> 结构的指针。 它的定义以下面的方式改变。 对于字符串键,h包含hash函数的值,key是实际的字符串。 对于整数键,h包含数字键值,键为 <code>NULL</code> 。</p>
<pre><code>typedef struct _zend_hash_key {
ulong h;
zend_string *key;
} zend_hash_key;</code></pre>
<p>在某些情况下,将 <code>zend_hash_apply_*()</code> 函数的用法更改为使用新的HashTable<code>迭代API</code>是有意义的。 这可能导致更小和更有效的代码。</p>
<p>可参考<code>zend_hash.h</code> :<a href="https://link.segmentfault.com/?enc=%2FFiOJEtYaL%2B74IqW5AYXrg%3D%3D.BDwjInSF6fM6AQKxahvaSGnA30aBHMFCSc4sy55tT6k9jG%2F%2FwBCIPXJTG3%2BScdRL3w4%2FYAfmZ7clxvsPVt3czg%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=Cr25kNnQXMuhpBh4AKe7og%3D%3D.Ys33krdxa0WhESMi24qYl3rnhF9fKFto%2BL62uILSqFHR9R%2FXnPrJfHEKpgLB1afEMdbC%2BsNXE%2BYV2x11UjTxMA%3D%3D" rel="nofollow">https://github.com/php/php-sr...</a></p>
<h2>HashTable Iteration API</h2>
<p>我们提供几个专门的宏来遍历<code>HashTables</code>的元素(和键)。 宏的第一个参数是哈希表,其他是在每个迭代步骤上分配的变量。</p>
<pre><code>ZEND_HASH_FOREACH_VAL(ht, val)
ZEND_HASH_FOREACH_KEY(ht, h, key)
ZEND_HASH_FOREACH_PTR(ht, ptr)
ZEND_HASH_FOREACH_NUM_KEY(ht, h)
ZEND_HASH_FOREACH_STR_KEY(ht, key)
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val)
ZEND_HASH_FOREACH_KEY_VAL(ht, h, key, val)</code></pre>
<p>应使用最佳的宏,而不是旧的reset, current, 和move功能。</p>
<pre><code>- HashPosition pos;
ulong num_key;
- char *key;
- uint key_len;
+ zend_string *key;
- zval **pzv;
+ zval *zv;
-
- zend_hash_internal_pointer_reset_ex(&ht, &pos);
- while (zend_hash_get_current_data_ex(&ht, (void**)&ppzval, &pos) == SUCCESS) {
- if (zend_hash_get_current_key_ex(&ht, &key, &key_len, &num_key, 0, &pos) == HASH_KEY_IS_STRING){
- }
+ ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, key, val) {
+ if (key) { //HASH_KEY_IS_STRING
+ }
........
- zend_hash_move_forward_ex(&ht, &pos);
- }
+ } ZEND_HASH_FOREACH_END();</code></pre>
<h2>Objects</h2>
<p>TODO: …</p>
<h2>Custom Objects</h2>
<p>TODO: …</p>
<p><code>zend_object</code> struct定义为:</p>
<pre><code>struct _zend_object {
zend_refcounted gc;
zend_uint handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
HashTable *guards; /* protects from __get/__set ... recursion */
zval properties_table[1];
};</code></pre>
<p>我们内联了<code>properties_table</code>以获得更好的访问性能,但这也带来了一个问题,我们习惯于这样定义一个自定义对象:</p>
<pre><code>struct custom_object {
zend_object std;
void *custom_data;
}
zend_object_value custom_object_new(zend_class_entry *ce TSRMLS_DC) {
zend_object_value retval;
struct custom_object *intern;
intern = emalloc(sizeof(struct custom_object));
zend_object_std_init(&intern->std, ce TSRMLS_CC);
object_properties_init(&intern->std, ce);
retval.handle = zend_objects_store_put(intern,
(zend_objects_store_dtor_t)zend_objects_destroy_object,
(zend_objects_free_object_storage_t) custom_free_storage,
NULL TSRMLC_CC);
intern->handle = retval.handle;
retval.handlers = &custom_object_handlers;
return retval;
}
struct custom_object* obj = (struct custom_object *)zend_objects_get_address(getThis());</code></pre>
<p>但现在,<code>zend_object</code>是变量长度现在(内联的<code>properties_table</code>)。 因此上述代码应改为:</p>
<pre><code>struct custom_object {
void *custom_data;
zend_object std;
}
zend_object * custom_object_new(zend_class_entry *ce TSRMLS_DC) {
# Allocate sizeof(custom) + sizeof(properties table requirements)
struct custom_object *intern = ecalloc(1,
sizeof(struct custom_object) +
zend_object_properties_size(ce));
# Allocating:
# struct custom_object {
# void *custom_data;
# zend_object std;
# }
# zval[ce->default_properties_count-1]
zend_object_std_init(&intern->std, ce TSRMLS_CC);
...
custom_object_handlers.offset = XtOffsetOf(struct custom_obj, std);
custom_object_handlers.free_obj = custom_free_storage;
intern->std.handlers = custom_object_handlers;
return &intern->std;
}
# Fetching the custom object:
static inline struct custom_object * php_custom_object_fetch_object(zend_object *obj) {
return (struct custom_object *)((char *)obj - XtOffsetOf(struct custom_object, std));
}
#define Z_CUSTOM_OBJ_P(zv) php_custom_object_fetch_object(Z_OBJ_P(zv));
struct custom_object* obj = Z_CUSTOM_OBJ_P(getThis());</code></pre>
<h2>zend_object_handlers</h2>
<p>一个新的项目偏移被添加到<code>zend_object_handlers</code>,你应该总是将它定义为在你的自定义对象结构中的<code>zend_object</code>偏移量。</p>
<p>它用 <code>zend_objects_store_*</code> 来查找分配的内存的正确起始地址。</p>
<pre><code>// An example in spl_array
memcpy(&spl_handler_ArrayObject, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
spl_handler_ArrayObject.offset = XtOffsetOf(spl_array_object, std);</code></pre>
<p>对象的内存现在将由 <code>zend_objects_store_*</code> 释放,因此您不应释放自定义对象<code>free_obj</code>处理程序中的内存。</p>
<h2>Resources</h2>
<p>类型 <code>IS_RESOURCE</code> 的zvals不再保留资源句柄。 无法使用 <code>Z_LVAL_*()</code> 检索资源句柄。 相反,应该使用 <code>Z_RES_*()</code> 宏直接检索资源记录。 资源记录由 <code>zend_resource</code> 结构表示。 它包含:</p>
<ul>
<li><p>tyep - 资源类型,</p></li>
<li><p>ptr - 指向实际数据的指针,</p></li>
<li><p>handle - 数字资源索引(用于兼容性)以及引用计数器的服务字段。</p></li>
</ul>
<p>实际上,这个<code>zend_resurce</code>结构是间接引用的<code>zend_rsrc_list_entry</code>的替代。 所有出现的<code>zend_rsrc_list_entry</code>应替换为<code>zend_resource</code>。</p>
<p><del>zend_list_find()</del> 函数被删除,因为资源被直接访问。</p>
<pre><code>- long handle = Z_LVAL_P(zv);
- int type;
- void *ptr = zend_list_find(handle, &type);
+ long handle = Z_RES_P(zv)->handle;
+ int type = Z_RES_P(zv)->type;
+ void *ptr = = Z_RES_P(zv)->ptr;</code></pre>
<p>删除 <code>Z_RESVAL_*()</code> 宏可以改用 <code>Z_RES*()</code>:</p>
<pre><code>- long handle = Z_RESVAL_P(zv);
+ long handle = Z_RES_P(zv)->handle;</code></pre>
<p>ZEND_REGISTER_RESOURCE / ZEND_FETCH_RESOURCE()被删除</p>
<pre><code>- ZEND_FETCH_RESOURCE2(ib_link, ibase_db_link *, &link_arg, link_id, LE_LINK, le_link, le_plink);
//if you are sure that link_arg is a IS_RESOURCE type, then use :
+if ((ib_link = (ibase_db_link *)zend_fetch_resource2(Z_RES_P(link_arg), LE_LINK, le_link, le_plink)) == NULL) {
+ RETURN_FALSE;
+}
//otherwise, if you know nothing about link_arg's type, use
+if ((ib_link = (ibase_db_link *)zend_fetch_resource2_ex(link_arg, LE_LINK, le_link, le_plink)) == NULL) {
+ RETURN_FALSE;
+}
- REGISTER_RESOURCE(return_value, result, le_result);
+ RETURN_RES(zend_register_resource(result, le_result);</code></pre>
<p><code>zend_list_addref()</code>和<code>zend_list_delref()</code>函数被删除。 资源使用与所有zval相同的引用计数机制。</p>
<pre><code>- zend_list_addref(Z_LVAL_P(zv));
+ Z_ADDREF_P(zv);</code></pre>
<p>同样的:</p>
<pre><code>- zend_list_addref(Z_LVAL_P(zv));
+ Z_RES_P(zv)->gc.refcount++;</code></pre>
<p><code>zend_list_delete()</code>将指针指向<code>zend_resource</code>结构,而不是资源句柄:</p>
<pre><code>- zend_list_delete(Z_LVAL_P(zv));
+ zend_list_delete(Z_RES_P(zv));</code></pre>
<p>在大多数用户扩展函数(如<code>mysql_close()</code>)中,应该使用<code>zend_list_close()</code>而不是<code>zend_list_delete()</code>。 这将关闭实际连接并释放扩展特定的数据结构,但不释放<code>zend_reference</code>结构。 可能仍然从<code>zval(s)</code>引用。 这也不会递减资源引用计数器。</p>
<pre><code>- zend_list_delete(Z_LVAL_P(zv));
+ zend_list_close(Z_RES_P(zv));</code></pre>
<h2>Parameters Parsing API changes</h2>
<p><code>'l'</code>说明符现在期望一个<code>zend_long</code>参数,而不是一个<code>long</code>参数。</p>
<pre><code>- long lval;
+ zend_long lval;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &lval) == FAILURE) {</code></pre>
<p><code>'s'</code>说明符的长度参数现在需要一个<code>size_t</code>变量,而不是一个<code>int</code>变量。</p>
<pre><code> char *str;
- int len;
+ size_t len;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &len) == FAILURE) {</code></pre>
<p>除了需要字符串的<code>'s'</code>说明符,PHPNG引入了<code>'S'</code>说明符,它也期望字符串,但将参数放在<code>zend_string</code>变量中。 在某些情况下,直接使用<code>zend_string</code>是首选。 (例如,当接收到的字符串用作HashTable API中的键时。</p>
<pre><code>- char *str;
- int len;
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &len) == FAILURE) {
+ zend_string *str;
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "S", &str) == FAILURE) {</code></pre>
<p>PHPNG不再使用<code>zval **</code>,所以它不再需要<code>'Z'</code>说明符了。 它必须替换为<code>'z'</code>。</p>
<pre><code>- zval **pzv;
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z", &pzv) == FAILURE) {
+ zval *zv;
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zv) == FAILURE) {</code></pre>
<p><code>'+'</code>和<code>'*'</code>说明符现在只返回zval数组(而不是之前的<code>zval **</code>数组)</p>
<pre><code>- zval ***argv = NULL;
+ zval *argv = NULL;
int argn;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "+", &argv, &argn) == FAILURE) {</code></pre>
<p>通过引用传递的参数应该分配到引用的值。 有可能分离这样的参数,得到引用值在第一位。</p>
<pre><code>- zval **ret;
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z", &ret) == FAILURE) {
+ zval *ret;
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &ret) == FAILURE) {
return;
}
- ZVAL_LONG(*ret, 0);
+ ZVAL_LONG(ret, 0);</code></pre>
<h2>Call Frame Changes (zend_execute_data)</h2>
<p>关于记录在<code>zend_execute_data</code>结构链中的每个函数调用的信息。 <code>EG(current_execute_data)</code> 指向当前执行函数的调用帧(以前的<code>zend_execute_data</code>结构仅为用户级PHP函数创建)。 我将尝试逐个字段解释旧的和新的调用框架结构之间的区别。</p>
<ul>
<li><p><code>zend_execute_data.opline</code> - 当前执行的用户函数的指令指针。 对于内部函数,其值未定义。 (以前为内部函数,其值为NULL)</p></li>
<li><p><code>zend_execute_data.function_state</code> - 此字段已删除。 应该使用zend_execute_data.call。</p></li>
<li><p><code>zend_execute_data.call</code> - 以前它是一个指向当前<code>call_slot</code>的指针。 目前它是一个指向当前调用函数的<code>zend_execute_data</code>的指针。 此字段最初为<code>NULL</code>,然后由ZEND_INIT_FCALL(或类似)操作码更改,然后由<code>ZEND_FO_FCALL</code>恢复。 语法嵌套函数调用,像<code>foo($ a,bar($ c))</code>,通过<code>zend_execute_data.prev_nested_call</code>构造一个这样的结构链</p></li>
<li><p><code>zend_execute_data.op_array</code> - 此字段由zend_execute_data.func替换,因为现在它可能不仅表示用户函数,而且表示内部函数。</p></li>
<li><p><code>zend_execute_data.func</code> - 当前执行的函数</p></li>
<li><p><code>zend_execute_data.object</code> - $ this当前执行的函数(以前它是一个zval <em>,现在它是一个zend_object </em>)</p></li>
<li><p><code>zend_execute_data.symbol_table</code> - 当前符号表或NULL</p></li>
<li><p><code>zend_execute_data.prev_execute_data</code> - 回溯调用链的链接</p></li>
<li><p><code>original_return_value</code>,<code>current_scope</code>,<code>current_called_scope</code>,<code>current_this</code> - 这些字段保留旧值以在调用后恢复它们。 现在他们被删除。</p></li>
<li><p><code>zend_execute_data.scope</code> - 当前执行函数的作用域(这是一个新字段)。</p></li>
<li><p><code>zend_execute_data.called_scope</code> - called_scope当前执行的函数(这是一个新字段)。</p></li>
<li><p><code>zend_execute_data.run_time_cache</code> - 当前执行函数的运行时缓存。 这是一个新字段,实际上它是op_array.run_time_cache的副本。</p></li>
<li><p><code>zend_execute_data.num_args</code> - 传递给函数的参数数量(这是一个新字段)</p></li>
<li><p><code>zend_execute_data.return_value</code> - 指向zval *的指针,其中当前执行的<code>op_array</code>应存储结果。 如果调用不关心返回值,它可以为<code>NULL</code>。 (这是一个新字段)。</p></li>
</ul>
<p>参数存储在zval槽中的函数直接在<code>zend_execute_data</code>结构之后。 它们可以使用 <code>ZEND_CALL_ARG(execute_data,arg_num)</code> 宏访问。 对于用户PHP函数,第一个参数与第一个编译的变量 - CV0等重叠。如果调用者传递了被调用者接收的更多参数,所有额外的参数都被复制到被调用者CV和TMP变量之后。</p>
<h2>Executor Globals - EG() Changes</h2>
<p><code>EG(symbol_table)</code> - 被改为一个zend_array(以前它是一个HashTable)。 到达下划线HashTable不是一个大问题</p>
<pre><code>- symbols = zend_hash_num_elements(&EG(symbol_table));
+ symbols = zend_hash_num_elements(&EG(symbol_table).ht);</code></pre>
<p>删除<code>EG(uninitialized_zval_ptr)</code>和<code>EG(error_zval_ptr)</code>。 使用&<code>EG(uninitialized_zval)</code>和<code>&EG(error_zval)</code>。</p>
<p><code>EG(current_execute_data)</code> - 这个字段的含义改变了一点。 以前它是一个指向最后执行的PHP函数的框架的指针。 现在它是一个指向最后执行的调用框架(如果它的用户或内部函数,不介意)。 可以获得最后一个<code>op_array</code>遍历调用链列表的<code>zend_execute_data</code>结构。</p>
<pre><code> zend_execute_data *ex = EG(current_execute_data);
+ while (ex && (!ex->func || !ZEND_USER_CODE(ex->func->type))) {
+ ex = ex->prev_execute_data;
+ }
if (ex) {</code></pre>
<ul>
<li><p><code>EG(opline_ptr)</code> - 。 请改用<code>execute_data→opline</code>。</p></li>
<li><p><code>EG(return_value_ptr_ptr)</code> - 已删除。 请改用<code>execute_data→return_value</code>。</p></li>
<li><p><code>EG(active_symbol_table)</code> - 被删除。 请使用<code>execute_data→symbol_table</code>。</p></li>
<li><p><code>EG(active_op_array)</code> - 被删除。 请使用<code>execute_data→func</code>。</p></li>
<li><p><code>EG(called_scope)</code> - 被删除。 请改用<code>execute_data→called_scope</code>。</p></li>
<li><p><code>EG(This)</code> - 变成了zval,以前它是一个指向zval的指针。 用户代码不应该修改它。</p></li>
<li><p><code>EG(in_execution)</code>)<code>。 如果</code>EG(current_excute_data)`不为NULL,我们正在执行某事。</p></li>
<li><p><code>EG(异常)</code>和<code>EG(prev_exception)</code> - 被转换为指向zend_object的指针,以前它们是指向zval的指针。</p></li>
</ul>
<h2>Opcodes changes</h2>
<ul>
<li><p><code>ZEND_DO_FCALL_BY_NAME</code> - 已删除, <code>ZEND_INIT_FCALL_BY_NAME</code>已添加。</p></li>
<li><p><code>ZEND_BIND_GLOBAL</code> - 被添加到处理“全局$ var”</p></li>
<li><p><code>ZEND_STRLEN</code> - 已添加以替换strlen函数</p></li>
<li><p><code>ZEND_TYPE_CHECK</code> - 已添加以替换<code>is_array / is_int / is_ *</code>(如果可能)</p></li>
<li><p><code>ZEND_DEFINED</code> - 被添加来替换<code>zif_defined</code>如果可能(如果只有一个参数,它的常量字符串,它不在命名空间样式)</p></li>
<li><p><code>ZEND_SEND_VAR_EX</code> - 是为了做比 <code>ZEND_SEND_VAR</code>更多的检查,如果条件无法在编译时间内解决</p></li>
<li><p><code>ZEND_SEND_VAL_EX</code> - 已添加,以进行比 <code>ZEND_SEND_VAL</code>更多的检查,如果条件无法在编译时间内解决</p></li>
<li><p><code>ZEND_INIT_USER_CALL</code> - 被添加以替换<code>call_user_func(_array)</code>如果可能的话,如果在编译时无法找到该函数,否则它可以转换为 ZEND_INIT_FCALL</p></li>
<li><p><code>ZEND_SEND_ARRAY</code> - 被添加发送第二个参数,<code>call_user_func_array</code>的数组在被转换为操作码</p></li>
<li><p><code>ZEND_SEND_USER</code> - 被添加以发送<code>call_user_func</code>的参数,在它被转换为操作码之后</p></li>
</ul>
<h2>temp_variable</h2>
<h2>PCRE</h2>
<p>一些pcre API使用或返回zend_string现在。 F.e. php_pcre_replace返回一个zend_string,并将zend_string作为第一个参数。 仔细检查他们的声明以及编译器警告,这很可能是错误的参数类型。</p>
<p>phpng-upgrading.txt · Last modified: 2016/01/21 17:18 by nikic</p>
PHP7扩展开发(五):回调php函数与开发一个并行扩展
https://segmentfault.com/a/1190000007648157
2016-11-30T14:56:55+08:00
2016-11-30T14:56:55+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2>
<p>很多时候,需要把控制权限交给用户,或者在扩展里完成某件事后去回调用户的方法。</p>
<p>在PHP扩展里是通过 <code>call_user_function_ex</code> 函数来调用用户空间的函数的。</p>
<h2>定义</h2>
<p>它的定义在 <code>Zend/zend_API.h</code> :</p>
<pre><code>#define call_user_function_ex(function_table, object, function_name, retval_ptr, param_count, params, no_separation, symbol_table)
_call_user_function_ex(object, function_name, retval_ptr, param_count, params, no_separation)</code></pre>
<p>通过宏定义替换为<code>_call_user_function_ex</code>,其中参数 <code>function_table</code> 被移除了,它之所以在API才存在大概是为了兼容以前的写法。函数的真正定义是:</p>
<pre><code>ZEND_API int _call_user_function_ex(
zval *object,
zval *function_name,
zval *retval_ptr,
uint32_t param_count,
zval params[],
int no_separation);</code></pre>
<p>参数分析:</p>
<ul>
<li><p><code>zval *object</code>:这个是用来我们调用类里的某个方法的对象。</p></li>
<li><p><code>zval *function_name</code>:要调用的函数的名字。</p></li>
<li><p><code>zval *retval_ptr</code>:收集回调函数的返回值。</p></li>
<li><p><code>uint32_t param_count</code>:回调函数需要传递参数的个数。</p></li>
<li><p><code>zval params[]</code>: 参数列表。</p></li>
<li><p><code>int no_separation</code>:是否对zval进行分离,如果设为1则直接会出错,分离的作用是为了优化空间。</p></li>
</ul>
<h2>回调功能的实现</h2>
<pre><code>PHP_FUNCTION(hello_callback)
{
zval *function_name;
zval retval;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &function_name) == FAILURE) {
return;
}
if (Z_TYPE_P(function_name) != IS_STRING) {
php_printf("Function require string argumnets!");
return;
}
//TSRMLS_FETCH();
if (call_user_function_ex(EG(function_table), NULL, function_name, &retval, 0, NULL, 0, NULL TSRMLS_CC) != SUCCESS) {
php_printf("Function call failed!");
return;
}
*return_value = retval;
zval_copy_ctor(return_value);
zval_ptr_dtor(&retval);
}</code></pre>
<p><code>zval_copy_ctor()</code>原始(zval)的内容拷贝给它。<code>zval_ptr_dtor()</code>释放空间。<code>return_value</code>不是一个函数外的变量,它的由函数声明里的变量。<code>PHP_FUNCTION(hello_callback)</code>这个声明是简写,最终会被预处理宏替换为:</p>
<pre><code>void zif_hello_callback(zend_execute_data *execute_data, zval *return_value)</code></pre>
<p><code>return_value</code>变量其实也就是最终返回给调用脚本的,<code>RETURN_STR(s)</code> 等返回函数最终也都是宏替换为对该变量的操作。</p>
<p>测试脚本:</p>
<pre><code><?php
function fun1() {
for ($i = 0; $i < 5; $i++) {
echo 'fun1:'.$i."\n";
}
return 'call end';
}
echo hello_callback('fun1');</code></pre>
<h2>一个并行扩展</h2>
<p>早期的php不支持多进程多线程的,现在随着发展有很多扩展不断完善它,诸如<code>pthread</code>,<code>swoole</code>等,不仅能多线程,而且能实现异步。</p>
<p>利用c语言多线程pthread库来实现一个简单的并行扩展。</p>
<p>先声明我们一会用到的结构:</p>
<pre><code>struct myarg
{
zval *fun;
zval ret;
};</code></pre>
<p>线程函数:</p>
<pre><code>static void my_thread(struct myarg *arg) {
zval *fun = arg->fun;
zval ret = arg->ret;
if (call_user_function_ex(EG(function_table), NULL, fun, &ret, 0, NULL, 0, NULL TSRMLS_CC) != SUCCESS) {
return;
}
}</code></pre>
<p>函数的实现:</p>
<pre><code>PHP_FUNCTION(hello_thread)
{
pthread_t tid;
zval *fun1, *fun2;
zval ret1, ret2;
struct myarg arg;
int ret;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zz", &fun1, &fun2) == FAILURE) {
return;
}
arg.fun = fun1;
arg.ret = ret1;
ret = pthread_create(&tid, NULL, (void*)my_thread, (void*)&arg);
if(ret != 0) {
php_printf("Thread Create Error\n");
exit(0);
}
if (call_user_function_ex(EG(function_table), NULL, fun2, &ret2, 0, NULL, 0, NULL TSRMLS_CC) != SUCCESS) {
return;
}
pthread_join(tid, NULL);
RETURN_NULL();
}</code></pre>
<p>测试脚本:</p>
<pre><code><?php
function fun1() {
for ($i = 0; $i < 5; $i++) {
echo 'fun1:'.$i.'\n';
}
}
function fun2() {
for ($i = 0; $i < 5; $i++) {
echo 'fun2:'.$i.'\n';
}
}
hello_thread('fun1', 'fun2');
echo 'after 多并发';</code></pre>
<p>输出:</p>
<p><img src="/img/remote/1460000007648160?w=540&h=350" alt="20161130143035.png" title="20161130143035.png"></p>
<p>两次的输出结果不一样,并且<code>echo 'after 多并发';</code>是在两个函数都运行完后才执行的。</p>
PHP7扩展开发(四):拷贝与引用
https://segmentfault.com/a/1190000007648108
2016-11-30T14:53:58+08:00
2016-11-30T14:53:58+08:00
weapon
https://segmentfault.com/u/weapon
3
<h2>引用计数</h2>
<p>迄今为止,我们向<code>HashTables</code>中加入的<code>zval</code>要么是新建的,要么是刚拷贝的。它们都是独立的,只占用自己的资源且只存在于某个HashTable中。作为一个语言设计的概念,创建和拷贝变量的方法是“很好”的,但是习惯了C程序设计就会知道,通过避免拷贝大块的数据(除非绝对必须)来节约内存和CPU时间并不少见。考虑这段用户代码:</p>
<pre><code><?php
$a = file_get_contents('fourMegabyteLogFile.log');
$b = $a;
unset($a);</code></pre>
<p>如果执行<code>zval_copy_ctor()</code>(将会对字符串内容执行estrndup())将$a拷贝给$b,那么这个简短的脚本实际会用掉8M内存来存储同一4M文件的两份相同的副本。在最后一步取消$a只会更糟,因为原始字符串被efree()了。用C做这个将会很简单,大概是这样:b = a; a = NULL;。</p>
<p>Zend引擎的做法更聪明。当创建$a时,会创建一个潜在的string类型的zval,它含有日至文件的内容。这个zval通过调用zend_hash_add()被<br>赋给$a变量。当$a被拷贝给$b,引擎做类似下面的事情:</p>
<pre><code>{
zval **value;
zend_hash_find(EG(active_symbol_table), "a", sizeof("a"), (void**)&value);
ZVAL_ADDREF(*value);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), value,sizeof(zval*));
}</code></pre>
<p>实际代码会更复杂点,简单的说,就是通过引用计数来记录zval在符号表中、数组中、或其他地方被引用的次数。这样<code>$b = $a</code>赋值只要将其引用计数+1,而不用去进行内容拷贝。</p>
<p>当用户空间代码调用<code>unset($a)</code>,引擎对该变量执行<code>zval_ptr_dtor()</code>。在前面用到的<code>zval_ptr_dtor()</code>中,你看不到的事实是,这个调用没有必要销毁该zval和它的内容。实际工作是减少refcount。如果,且仅仅是如果,引用计数变成了0,Zend引擎会销毁该zval。</p>
<p>有些简单数据类型不需要单独分配内存,也不需要计数;PHP7中zval的long和double类型是 <code>不需要</code> 引用计数的。</p>
<p>php7的zval结构重新定义了,都有一个同样的头(zend_refcounted)用来存储引用计数:</p>
<pre><code>typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;</code></pre>
<h2>拷贝 vs 引用</h2>
<p>有两种方法引用zval。第一种,如上文示范的,被称为写复制引用(copy-on-write referencing)。第二种形式是完全引用(full referencing);当说起“引用”时,用户空间代码的编写者更熟悉这种, 以用户空间代码的形式出现类似于:<code>$a = &$b;</code>。</p>
<p>在zval中,这两种类型的区别在于它的is_ref成员的值,0表示写复制引用,非0表示完全引用。注意,一个zval不可能同时具有两种引用类型。所以,如果变量起初是is_ref(即完全引用-译注),然后以拷贝的方式赋给新的变量,那么必将执行一个完全拷贝。考虑下面的用户空间代码:</p>
<pre><code><?php
$a = []; //$a -> zend_array_1(refcount=1, value=[])
$b = &$a; //$a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])
$c = $a; //// $a, $b, $c -> zend_array_1(refcount=3, value=[])</code></pre>
<p>在这段代码中,为$a创建并初始化了一个zval,将is_ref设为0,将refcount设为1。当$a被$b引用时,is_ref变为1,refcount递增至2。当拷贝至$c时,Zend引擎不能只是递增refcount至3,因为如此则$c变成了$a的完全引用。关闭is_ref也不行,因为如此会使$b看起来像是$a的一份拷贝而不是引用。所以此时分配了一个新的zval,并使用zval_copy_ctor()把原始(zval)的值拷贝给它。原始zval仍为is_ref==1、refcount==2,同时新zval则为is_ref=0、refcount=1。现在来看另一块内容相同的代码块,只是顺序稍有不同:</p>
<pre><code><?php
$a = []; //$a -> zend_array_1(refcount=1, value=[])
$c = $a; // $a, $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$b = &$a; // $c -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
// $b, $a -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
// $b 是 $a 的引用, 但却不是 $a 的 $c, 所以这里 zval 还是需要进行复制
// 这样我们就有了两个 zval, 一个 is_ref 的值是 0, 一个 is_ref 的值是 1.</code></pre>
<p>所有的变量都可以共享同一个数组,最终结果不变,$b是$a的完全引用,并且$c是$a的一份拷贝。然而这次的内部效果稍有区别。如前,开始时为$a创建一个is_ref==0并且refcount=1的新zval。$c = $a;语句将同一个zval赋给$c变量,同时将refcount增至2,is_ref仍是0。当Zend引擎遇到$b = &$a;,它想要只是将is_ref设为1,但是当然不行,因为那将影响到$c。所以改为创建新的zval并用zval_copy_ctor()将原始(zval)的内容拷贝给它。然后递减原始zval的refcount以表明$a不再使用该zval。代替地,(Zend)设置新zval的is_ref为1、refcount为2,并且更新$a和$b变量指向它(新zval)。</p>
<pre><code><?php
$a = []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
$b = $a; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
// $b = zval_2(type=IS_ARRAY) ---^
// zval 分离在这里进行
$a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
unset($a); // $a = zval_1(type=IS_UNDEF), zend_array_2 被销毁
// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])</code></pre>
<p>这个过程其实挺简单的。现在整数不再是共享的,变量直接就会分离成两个单独的 zval,由于现在 zval 是内嵌的所以也不需要单独分配内存,所以这里的注释中使用 = 来表示的而不是指针符号 ->,unset 时变量会被标记为 IS_UNDEF。</p>
<h2>总结</h2>
<p>PHP7 中最重要的改变就是 zval 不再单独从堆上分配内存并且不自己存储引用计数。需要使用 zval 指针的复杂类型(比如字符串、数组和对象)会自己存储引用计数。这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。</p>
插件发布:悬浮式文章目录树MenuTree_for_typecho
https://segmentfault.com/a/1190000007599195
2016-11-25T13:52:43+08:00
2016-11-25T13:52:43+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>悬浮式文章目录树,定在右侧。</p>
<h2>使用方法</h2>
<p>第一步:下载本插件,放在 <code>usr/plugins/</code> 目录中;<br>第二步:激活插件;</p>
<h2>预览</h2>
<p><img src="/img/remote/1460000007599198?w=367&h=432" alt="20160401112707.png" title="20160401112707.png"></p>
<p>github开源地址:<a href="https://link.segmentfault.com/?enc=Xj1vD7N3YHOd3cmuZMmh8A%3D%3D.%2FZO%2Fsb32Z1YnPhCCX6Qx4khInlGMzdUPbPunVL4hYx%2B1U1zwzy%2BGId7276JTu8HfHxWAoroj%2BFsY3sE0Et8QHQ%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=Cnb6jZ%2B4dSHONGtNBinKKA%3D%3D.JoCLxQV8So5jysMlh9aSAIHRPNjKZsZCzID7Qr0%2F%2F9R0x%2Fux4SSRQM7zOTs9vss9ptqqSN0zTMpQtQNksHuZeA%3D%3D" rel="nofollow">https://github.com/hongweipen...</a></p>
<h2>与我联系:</h2>
<p>作者:hongweipeng<br>主页:<a href="https://link.segmentfault.com/?enc=mK4XNQ9bGBY7w86Jc2xNhg%3D%3D.KY2JFQg9Ocdj50Z9ymJJUv4tG%2B1ryAYjKPAMjbIvsGw%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=AA%2BRagq%2FH8WdYFPA1Hw7yg%3D%3D.QwHMeeh305k3%2BWbpuGndMnICZOdJOGWYCsNOKJAGkjs%3D" rel="nofollow">https://www.hongweipeng.com/</a><br>或者通过 Emai: hongweichen8888<a href="/u/gzg">@sina</a> .com<br>有任何问题也可评论留言</p>
PHP7扩展开发(三):参数、数组和Zvals
https://segmentfault.com/a/1190000007575322
2016-11-23T15:01:04+08:00
2016-11-23T15:01:04+08:00
weapon
https://segmentfault.com/u/weapon
15
<h2>起步</h2>
<p>到这已经能声明简单函数,返回静态或者动态值了。定义INI选项,声明内部数值或全局数值。本章节将介绍如何接收从调用脚本(php文件)传入参数的数值,以及 <code>PHP内核</code> 和 <code>Zend引擎</code> 如何操作内部变量。</p>
<h2>接收参数</h2>
<p>与用户控件的代码不同,内部函数的参数实际上并不是在函数头部声明的,函数声明都形如: <code>PHP_FUNCTION(func_name)</code> 的形式,参数声明不在其中。参数的传入是通过参数列表的地址传入的,并且是传入每一个函数,不论是否存在参数。</p>
<p>通过定义函数<code>hello_str()</code>来看一下,它将接收一个参数然后把它与问候的文本一起输出。</p>
<pre><code>PHP_FUNCTION(hello_greetme)
{
char *name = NULL;
size_t name_len;
zend_string *strg;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) {
RETURN_NULL();
}
strg = strpprintf(0, "你好: %s", name);
RETURN_STR(strg);
}</code></pre>
<p>大多数 <code>zend_parse_parameters()</code> 块看起来都差不多。 <code>ZEND_NUM_ARGS()</code> 告诉Zend引擎要取的参数的信息, <code>TSRMLS_CC</code> 用来确保线程安全,返回值检测是<code>SUCCESS</code>还是<code>FAILURE</code>。通常情况下返回是SUCCESS的。除非传入的参数太少或太多或者参数不能被转为适当的类型,Zend会自动输出一条错误信息并将控制权还给调用脚本。</p>
<p>指定 "s" 表明此函数期望只传入一个参数,并且该参数被转化为string数据类型,地址传入char * 变量。</p>
<p>此外,还有一个int变量通过地址传递到 <code>zend_parse_parameters()</code> 。这使Zend引擎提供字符串的字节长度,如此二进制安全的函数不再依赖<code>strlen(name)</code>来确定字符串的长度。因为实际上使用<code>strlen(name)</code>甚至得不到正确的结果,因为name可能在字符串结束之前包含了<code>NULL</code>字符。</p>
<p>在php7中,提供另一种获取参数的方式<code>FAST_ZPP</code>,是为了提高参数解析的性能。</p>
<pre><code>#ifdef FAST_ZPP
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STR(type)
Z_PARAM_OPTIONAL
Z_PARAM_ZVAL_EX(value, 0, 1)
ZEND_PARSE_PARAMETERS_END();
#endif</code></pre>
<h2>参数类型表</h2>
<table>
<thead><tr>
<th>类型</th>
<th align="center">代码</th>
<th align="right">变量类型</th>
</tr></thead>
<tbody>
<tr>
<td>Boolean</td>
<td align="center">b</td>
<td align="right">zend_bool</td>
</tr>
<tr>
<td>Long</td>
<td align="center">l</td>
<td align="right">long</td>
</tr>
<tr>
<td>Double</td>
<td align="center">d</td>
<td align="right">double</td>
</tr>
<tr>
<td>String</td>
<td align="center">s</td>
<td align="right">char*, int</td>
</tr>
<tr>
<td>Resource</td>
<td align="center">r</td>
<td align="right">zval *</td>
</tr>
<tr>
<td>Array</td>
<td align="center">a</td>
<td align="right">zval *</td>
</tr>
<tr>
<td>Object</td>
<td align="center">o</td>
<td align="right">zval *</td>
</tr>
<tr>
<td>zval</td>
<td align="center">z</td>
<td align="right">zval *</td>
</tr>
</tbody>
</table>
<p>最后四个类型都是<code>zvals *</code>.这是因为在php的实际使用中,zval数据类型存储所有的用户空间变量。三种“复杂”数据类型:<code>资源、数组、对象</code>。当它们的数据类型代码被用于<code>zend_parse_parameters()</code>时,Zend引擎会进行类型检查,但是因为在C中没有与它们对应的数据类型,所以不会执行类型转换。</p>
<h2>Zval</h2>
<p>一般而言,zval和php用户空间变量是很伤脑筋的,概念很难懂。到了PHP7,它的结构在Zend/zend_types.h中有定义:</p>
<pre><code>struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* literal cache slot */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
} u2;
};</code></pre>
<p>可以看到,变量是通过_zval_struct结构体存储的,而变量的值是zend_value类型的:</p>
<pre><code>typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;</code></pre>
<p>虽然结构体看起来很大,但细细看,其实都是联合体,value的扩充,u1是type_info,u2是其他各种辅助字段。</p>
<h3>zval 类型</h3>
<p>变量存储的数据是有数据类型的,php7中总体有以下类型,Zend/zend_types.h中有定义:</p>
<pre><code>/* regular data types */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10
/* constant expressions */
#define IS_CONSTANT 11
#define IS_CONSTANT_AST 12
/* fake types */
#define _IS_BOOL 13
#define IS_CALLABLE 14
#define IS_ITERABLE 19
#define IS_VOID 18
/* internal types */
#define IS_INDIRECT 15
#define IS_PTR 17
#define _IS_ERROR 20</code></pre>
<h3>测试</h3>
<p>书写一个类似<code>gettype()</code>来取得变量的类型的<code>hello_typeof()</code>:</p>
<pre><code>PHP_FUNCTION(hello_typeof)
{
zval *userval = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &userval) == FAILURE) {
RETURN_NULL();
}
switch (Z_TYPE_P(userval)) {
case IS_NULL:
RETVAL_STRING("NULL");
break;
case IS_TRUE:
RETVAL_STRING("true");
break;
case IS_FALSE:
RETVAL_STRING("false");
break;
case IS_LONG:
RETVAL_STRING("integer");
break;
case IS_DOUBLE:
RETVAL_STRING("double");
break;
case IS_STRING:
RETVAL_STRING("string");
break;
case IS_ARRAY:
RETVAL_STRING("array");
break;
case IS_OBJECT:
RETVAL_STRING("object");
break;
case IS_RESOURCE:
RETVAL_STRING("resource");
break;
default:
RETVAL_STRING("unknown type");
}
}</code></pre>
<p>这里使用<code>RETVAL_STRING()</code>与之前的<code>RETURN_STRING()</code>差别并不大,它们都是宏。只不过RETURN_STRING中包含了RETVAL_STRING的<code>宏代替</code>,详细在 <code>Zend/zend_API.h</code> 中有定义:</p>
<pre><code>#define RETVAL_STRING(s) ZVAL_STRING(return_value, s)
#define RETVAL_STRINGL(s, l) ZVAL_STRINGL(return_value, s, l)
#define RETURN_STRING(s) { RETVAL_STRING(s); return; }
#define RETURN_STRINGL(s, l) { RETVAL_STRINGL(s, l); return; }</code></pre>
<h3>创建zval</h3>
<p>前面用到的zval是由Zend引擎分配空间,也通过同样的途径释放。然而有时候需要创建自己的zval,可以参考如下代码:</p>
<pre><code>{
zval temp;
ZVAL_LONG(&temp, 1234);
}</code></pre>
<h2>数组</h2>
<p>数组作为运载其他变量的变量。内部实现上使用了众所周知的 <code>HashTable</code> .要创建将被返回PPHP的数组,最简单的方法:</p>
<table>
<thead><tr>
<th>PHP语法</th>
<th align="left">C语法(arr是zval*)</th>
<th>意义</th>
</tr></thead>
<tbody>
<tr>
<td>$arr = array();</td>
<td align="left">array_init(arr);</td>
<td>初始化一个新数组</td>
</tr>
<tr>
<td>$arr[] = NULL;</td>
<td align="left">add_next_index_null(arr);</td>
<td>向数字索引的数组增加指定类型的值</td>
</tr>
<tr>
<td>$arr[] = 42;</td>
<td align="left">add_next_index_long(arr, 42);</td>
<td> </td>
</tr>
<tr>
<td>$arr[] = true;</td>
<td align="left">add_next_index_bool(arr, 1);</td>
<td> </td>
</tr>
<tr>
<td>$arr[] = 3.14;</td>
<td align="left">add_next_index_double(arr, 3.14);</td>
<td> </td>
</tr>
<tr>
<td>$arr[] = 'foo';</td>
<td align="left">add_next_index_string(arr, "foo", 1);</td>
<td> </td>
</tr>
<tr>
<td>$arr[] = $myvar;</td>
<td align="left">add_next_index_zval(arr, myvar);</td>
<td> </td>
</tr>
<tr>
<td>$arr[0] = NULL;</td>
<td align="left">add_index_null(arr, 0);</td>
<td>向数组中指定的数字索引增加指定类型的值</td>
</tr>
<tr>
<td>$arr[1] = 42;</td>
<td align="left">add_index_long(arr, 1, 42);</td>
<td> </td>
</tr>
<tr>
<td>$arr[2] = true;</td>
<td align="left">add_index_bool(arr, 2, 1);</td>
<td> </td>
</tr>
<tr>
<td>$arr[3] = 3.14;</td>
<td align="left">add_index_double(arr, 3, 3.14);</td>
<td> </td>
</tr>
<tr>
<td>$arr[4] = 'foo';</td>
<td align="left">add_index_string(arr, 4, "foo", 1);</td>
<td> </td>
</tr>
<tr>
<td>$arr[5] = $myvar;</td>
<td align="left">add_index_zval(arr, 5, myvar);</td>
<td> </td>
</tr>
<tr>
<td>$arr['abc'] = NULL;</td>
<td align="left">add_assoc_null(arr, "abc");</td>
<td> </td>
</tr>
<tr>
<td>$arr['def'] = 711;</td>
<td align="left">add_assoc_long(arr, "def", 711);</td>
<td>向关联索引的数组增加指定类型的值</td>
</tr>
<tr>
<td>$arr['ghi'] = true;</td>
<td align="left">add_assoc_bool(arr, "ghi", 1);</td>
<td> </td>
</tr>
<tr>
<td>$arr['jkl'] = 1.44;</td>
<td align="left">add_assoc_double(arr, "jkl", 1.44);</td>
<td> </td>
</tr>
<tr>
<td>$arr['mno'] = 'baz';</td>
<td align="left">add_assoc_string(arr, "mno", "baz", 1);</td>
<td> </td>
</tr>
<tr>
<td>$arr['pqr'] = $myvar;</td>
<td align="left">add_assoc_zval(arr, "pqr", myvar);</td>
<td> </td>
</tr>
</tbody>
</table>
<p>做一个测试:</p>
<pre><code>PHP_FUNCTION(hello_get_arr)
{
array_init(return_value);
add_next_index_null(return_value);
add_next_index_long(return_value, 42);
add_next_index_bool(return_value, 1);
add_next_index_double(return_value, 3.14);
add_next_index_string(return_value, "foo");
add_assoc_string(return_value, "mno", "baz");
add_assoc_bool(return_value, "ghi", 1);
}</code></pre>
<p><img src="https://www.hongweipeng.com/usr/uploads/2016/11/1262688200.png" alt="20161122192253.png" title="20161122192253.png"></p>
<p>add_*_string()函数参数从四个改为了三个。</p>
<h3>数组遍历</h3>
<p>假设我们需要一个取代以下功能的扩展:</p>
<pre><code><?php
function hello_array_strings($arr) {
if (!is_array($arr)) {
return NULL;
}
printf("The array passed contains %d elements\n", count($arr));
foreach ($arr as $data) {
if (is_string($data))
echo $data.'\n';
}
}</code></pre>
<p>php7的遍历数组和php5差很多,7提供了一些专门的宏来遍历元素(或keys)。宏的第一个参数是HashTable,其他的变量被分配到每一步迭代:</p>
<blockquote><p>ZEND_HASH_FOREACH_VAL(ht, val)<br>ZEND_HASH_FOREACH_KEY(ht, h, key) <br>ZEND_HASH_FOREACH_PTR(ht, ptr)<br>ZEND_HASH_FOREACH_NUM_KEY(ht, h) <br>ZEND_HASH_FOREACH_STR_KEY(ht, key)<br>ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val)<br>ZEND_HASH_FOREACH_KEY_VAL(ht, h, key, val)</p></blockquote>
<p>因此它的对应函数实现如下:</p>
<pre><code>PHP_FUNCTION(hello_array_strings)
{
ulong num_key;
zend_string *key;
zval *val, *arr;
HashTable *arr_hash;
int array_count;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) {
RETURN_NULL();
}
arr_hash = Z_ARRVAL_P(arr);
array_count = zend_hash_num_elements(arr_hash);
php_printf("The array passed contains %d elements\n", array_count);
ZEND_HASH_FOREACH_KEY_VAL(arr_hash, num_key, key, val) {
//if (key) { //HASH_KEY_IS_STRING
//}
PHPWRITE(Z_STRVAL_P(val), Z_STRLEN_P(val));
php_printf("\n");
}ZEND_HASH_FOREACH_END();
}</code></pre>
<p>因为这是新的遍历方法,而我看的还是php5的处理方式,调试出上面的代码花了不少功夫,总的来说,用宏的方式遍历大大减少了编码体积。哈希表是php中很重要的一个内容,有时间再好好研究一下。</p>
<h2>遍历数组的其他方式</h2>
<p>遍历 <code>HashTable</code> 还有其他方法。Zend引擎针对这个任务展露了三个非常类似的函数:<code>zend_hash_apply(), zend_hash_apply_with_argument(), zend_hash_apply_with_arguments</code>。第一个形式仅仅遍历HashTable,第二种形式允许传入单个<code>void*</code>参数,第三种形式通过var arg列表允许数量不限的参数。hello_array_walk()展示个他们各自的行为。</p>
<pre><code>static int php_hello_array_walk(zval *ele TSRMLS_DC)
{
zval temp = *ele; // 临时zval,避免convert_to_string 污染原元素
zval_copy_ctor(&temp); // 分配新 zval 空间并复制 ele 的值
convert_to_string(&temp); // 字符串类型转换
//简单的打印
PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
php_printf("\n");
zval_dtor(&temp); //释放临时的 temp
return ZEND_HASH_APPLY_KEEP;
}
static int php_hello_array_walk_arg(zval *ele, char *greeting TSRMLS_DC)
{
php_printf("%s", greeting);
php_hello_array_walk(ele TSRMLS_CC);
return ZEND_HASH_APPLY_KEEP;
}
static int php_hello_array_walk_args(zval *ele, int num_args, va_list args, zend_hash_key *hash_key)
{
char *prefix = va_arg(args, char*);
char *suffix = va_arg(args, char*);
TSRMLS_FETCH();
php_printf("%s", prefix);
// 打印键值对结果
php_printf("key is : [ ");
if (hash_key->key) {
PHPWRITE(ZSTR_VAL(hash_key->key), ZSTR_LEN(hash_key->key));
} else {
php_printf("%ld", hash_key->h);
}
php_printf(" ]");
php_hello_array_walk(ele TSRMLS_CC);
php_printf("%s\n", suffix);
return ZEND_HASH_APPLY_KEEP;
}</code></pre>
<p>用户调用的函数:</p>
<pre><code>PHP_FUNCTION(hello_array_walk)
{
zval *arr;
HashTable *arr_hash;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) {
RETURN_NULL();
}
arr_hash = Z_ARRVAL_P(arr);
//第一种遍历 简单遍历各个元素
zend_hash_apply(arr_hash, (apply_func_t)php_hello_array_walk TSRMLS_CC);
//第二种遍历 带一个参数的简单遍历各个元素
zend_hash_apply_with_argument(arr_hash, (apply_func_arg_t)php_hello_array_walk_arg, "Hello " TSRMLS_CC);
//第三种遍历 带多参数的遍历key->value
zend_hash_apply_with_arguments(arr_hash, (apply_func_args_t)php_hello_array_walk_args, 2, "Hello ", "Welcome to my extension!");
RETURN_TRUE;
}</code></pre>
<p>为了复用,在输出值时调用<code>php_hello_array_walk(ele TSRMLS_CC)</code>。传入<code>hello_array_walk()</code>的数组被遍历了三次,一次不带参数,一次带单个参数,一次带两给参数。三个遍历的函数返回了<code>ZEND_HASH_APPLY_KEEP</code>。这告诉<code>zend_hash_apply()</code>函数离开HashTable中的(当前)元素,继续处理下一个。</p>
<p>这儿也可以返回其他值:<code>ZEND_HASH_APPLY_REMOVE</code>删<br>除当前元素并继续应用到下一个;<code>ZEND_HASH_APPLY_STOP</code>在当前元素中止数组的遍历并退出<code>zend_hash_apply()</code>函数。</p>
<p><code>TSRMLS_FETCH()</code> 是一个关于线程安全的动作,用于避免各线程的作用域被其他的侵入。因为<code>zend_hash_apply()</code>的多线程版本用了vararg列表,tsrm_ls标记没有传入walk()函数。</p>
<pre><code><?php
$arr = ["99", "fff", "key1"=>"888", "key2"=>"aaa"];
hello_array_walk($arr);</code></pre>
<p><img src="https://www.hongweipeng.com/usr/uploads/2016/11/3459167216.png" alt="20161128104657.png" title="20161128104657.png"></p>
PHP7扩展开发(二):配置项与全局数值
https://segmentfault.com/a/1190000007571391
2016-11-23T10:32:52+08:00
2016-11-23T10:32:52+08:00
weapon
https://segmentfault.com/u/weapon
1
<h2>起步</h2>
<p>Zend引擎提供了另种管理设置值(INI)的途径。现在弄个简单的,我们经常看到php.ini里有诸如 <code>display_errors = On</code> 这样的全局设置。假设我们需要为我们扩展定义一个值: <code>hello.greeting</code> 并用函数 <code>hello_ini()</code> 返回它的内容。</p>
<p>在php.ini:</p>
<pre><code>[hello]
hello.greeting=1</code></pre>
<p>为了避免命名空间的冲突,我们扩展的名字作为所有值的前缀。仅仅是一种约定,一个句点用来分隔扩展名和说明性的初始设定名字。</p>
<h2>声明变量 php_hello.h</h2>
<p>用<code>ext_skel</code> 工具初始化的扩展有个好处就是它能帮我们在特定的位置写上注释。</p>
<pre><code>/*
Declare any global variables you may need between the BEGIN
and END macros here:
ZEND_BEGIN_MODULE_GLOBALS(hello)
zend_long global_value;
char *global_string;
ZEND_END_MODULE_GLOBALS(hello)
*/</code></pre>
<p>这是说如果我们需要声明全局变量,需要将放置在 <code>宏</code> BEBIN与END之间。并提供了示例,因此在这边添加:</p>
<pre><code>ZEND_BEGIN_MODULE_GLOBALS(hello)
zend_long greeting;
ZEND_END_MODULE_GLOBALS(hello)</code></pre>
<p><code>ZEND_BEGIN_MODULE_GLOBALS()</code>和<code>ZEND_END_MODULE_GLOBALS()</code>用来创建一个名为<code>zend_hello_globals</code>的结构,它包含一个long型的变量。然后有条件地将<code>HELLO_G()</code>定义为从线程池中取得数值,或者从全局作用域中得到-如果你编译的目标是非多线程环境。</p>
<p>工具还为我生成了:</p>
<pre><code>/* Always refer to the globals in your function as HELLO_G(variable).
You are encouraged to rename these macros something shorter, see
examples in any other php module directory.
*/
#define HELLO_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(hello, v)
#if defined(ZTS) && defined(COMPILE_DL_HELLO)
ZEND_TSRMLS_CACHE_EXTERN()
#endif
#endif</code></pre>
<p>这是一个简化变量获取操作的宏设置,可以使用 <code>HELLO_G(greeting)</code> 来获得全局设置的变量。</p>
<h2>源码实现 hello.c</h2>
<pre><code>/* {{{ PHP_INI
*/
/* Remove comments and fill if you need to have entries in php.ini
PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("hello.global_value", "42", PHP_INI_ALL, OnUpdateLong, global_value, zend_hello_globals, hello_globals)
STD_PHP_INI_ENTRY("hello.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_hello_globals, hello_globals)
PHP_INI_END()
*/
/* }}} */</code></pre>
<p>注释说明可以自己看一下,在这下方添加:</p>
<pre><code>ZEND_DECLARE_MODULE_GLOBALS(hello)
PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("hello.greeting","0", PHP_INI_ALL, OnUpdateLong, greeting, zend_hello_globals, hello_globals)
PHP_INI_END()</code></pre>
<p>用<code>ZEND_DECLARE_MODULE_GLOBALS()</code>宏来例示<code>zend_hello_globals</code>结构.初始值 <code>"0"</code> 是在php.ini里没有对应实体的时候生效的。</p>
<p>全局初始函数:</p>
<pre><code>static void php_hello_init_globals(zend_hello_globals *hello_globals)
{
//hello_globals->global_value = 0;
//hello_globals->global_string = NULL;
}</code></pre>
<p><code>php_hello_init_globals()</code>实际上什么也没做,却得多声明个RINIT将变量greeting初始化为0,为什么?</p>
<p>关键在于这两个函数何时调用。php_hello_init_globals()只是在一个新的进程或线程时被调用;然而,每个进程都能处理多个请求,所以这个函数将变量初始化为0将只在第一个页面请求时运行。</p>
<p>接下来就是hello_ini()函数的实现了:</p>
<pre><code>PHP_FUNCTION(hello_ini)
{
RETURN_LONG(HELLO_G(greeting));
}
const zend_function_entry hello_functions[] = {
PHP_FE(hello, NULL)
PHP_FE(hello_ini, NULL) /*添加到编译中去*/
PHP_FE(confirm_hello_compiled, NULL)
PHP_FE_END
};</code></pre>
<p>一些诸如<code>PHP_MINIT_FUNCTION</code>也要修改,这些函数目前不知道作用是什么:</p>
<pre><code>PHP_MINIT_FUNCTION(hello)
{
REGISTER_INI_ENTRIES();
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(hello)
{
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}</code></pre>
<h2>修改配置</h2>
<p>写个<code>hello_change_ini()</code>来修改配置项:</p>
<pre><code>PHP_FUNCTION(hello_change_ini)
{
HELLO_G(greeting) ++;
}</code></pre>
<p>同样要加到 <code>hello_functions[]</code> 中。</p>
<h2>测试</h2>
<pre><code><?php
echo hello_ini(); //1
hello_change_ini();
echo "<br>";
echo hello_ini(); //2</code></pre>
PHP7扩展开发(一):hello world
https://segmentfault.com/a/1190000007571341
2016-11-23T10:30:33+08:00
2016-11-23T10:30:33+08:00
weapon
https://segmentfault.com/u/weapon
8
<h2>起步</h2>
<p>最近在看 <code>《PHP扩展开发中文教程》</code> 的pdf版。PHP的解释器是用C语言写的,所以PHP扩展自然也是用<code>C 语言</code>了。</p>
<h2>扩展是什么</h2>
<p>用过php的人一定也用过php扩展。php本身带有86个扩展,扩展是对php语言功能的一个延伸,php的核心由两部分组成:最底层的 <code>Zend引擎</code> 和 <code>PHP内核</code> 。ze把脚本解析成机器可读的符号,也会处理内存管理,变量作用域,程序调度。PHP内核则主要涉及主机环境(Apache,IIS,Nginx),处理与主机的通信。</p>
<h2>动机</h2>
<p>当php自身不满足需求的时候就可以自己去造轮子了。采用C语言开发还能一定程度上解决性能问题,而php是我最喜欢的一门编程语言,写扩展的机会自然不放过,更重要的是可以 <code>装逼</code> 。</p>
<h2>新建扩展</h2>
<p>我们要写个扩展代替以下的功能:</p>
<pre><code><?php
function hello() {
return 'hello world';
}</code></pre>
<p>我的开发环境是:</p>
<ul>
<li><p>系统: Ubuntu 16.04</p></li>
<li><p>PHP: 7.0+</p></li>
<li><p>gcc :4.8.4<br>PHP已经提供了工具用来创建扩展,并初始化代码:<code>ext_skel</code></p></li>
</ul>
<pre><code>$ cd php-src/ext
$ ./ext_skel --extname=hello</code></pre>
<p>工具会在当前目录生成 <code>hello</code> 文件夹。</p>
<h2>修改配置文件</h2>
<p>cd到hello,工具已经初始化了目录,打开配置文件 <code>config.m4</code>:</p>
<pre><code>dnl If your extension references something external, use with:
dnl PHP_ARG_WITH(hello, for hello support,
dnl Make sure that the comment is aligned:
dnl [ --with-hello Include hello support])
dnl Otherwise use enable:
dnl PHP_ARG_ENABLE(hello, whether to enable hello support,
dnl Make sure that the comment is aligned:
dnl [ --enable-hello Enable hello support])</code></pre>
<p>dnl 是注释符,表示当前行是注释。这段话是说如果此扩展依赖其他扩展,去掉<code>PHP_ARG_WITH</code>段的注释符;否则去掉<code>PHP_ARG_ENABLE</code>段的注释符。显然我们不依赖其他扩展或lib库,所以去掉<code>PHP_ARG_ENABLE</code>段的注释符:</p>
<pre><code>PHP_ARG_ENABLE(hello, whether to enable hello support,
Make sure that the comment is aligned:
[ --enable-hello Enable hello support])</code></pre>
<h2>书写代码</h2>
<p>工具生成的<code>hello.c</code>,写上我们的实现:</p>
<pre><code>PHP_FUNCTION(hello)
{
zend_string *strg;
strg = strpprintf(0, "hello world.");
RETURN_STR(strg);
}</code></pre>
<p>添加到编译列表里:</p>
<pre><code>const zend_function_entry hello_functions[] = {
PHP_FE(hello, NULL) /*添加这行*/
PHP_FE(confirm_hello_compiled, NULL) /* For testing, remove later. */
PHP_FE_END /* Must be the last line in hello_functions[] */
};</code></pre>
<h2>编译与安装</h2>
<pre><code>$ phpize
$ ./configure --with-php-config=/usr/local/php7/bin/php-config
$ make & make install</code></pre>
<p>修改<code>php.ini</code>,开启扩展,若找不到可以用<code>phpinfo()</code>查看使用哪个配置文件.</p>
<pre><code>extension=hello.so</code></pre>
<p>写个脚本:<code><?php echo hello();</code> 不出意外就能看到输出了。</p>
<h2>额外:不使用工具写扩展</h2>
<p>一个扩展(为避免与写过的hello冲突,采用world作为名字),至少包含3个文件: <code>config.m4</code>、 <code>php_world.h</code> 、 <code>world.c</code> 。一个是phpize用来准备编译扩展的配置文件,一个是引用包含的头文件,一个是源码文件。</p>
<p>config.m4</p>
<pre><code>PHP_ARG_ENABLE(world, whether to enable world support,
Make sure that the comment is aligned:
[ --enable-world Enable hello support])
if test "$PHP_WORLD" != "no"; then
AC_DEFINE(HAVE_WORLD,1,[ ])
PHP_NEW_EXTENSION(world, world.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi</code></pre>
<p>php_world.h</p>
<pre><code>#ifndef PHP_WORLD_H
#define PHP_WORLD_H
extern zend_module_entry hello_module_entry;
#define phpext_hello_ptr &hello_module_entry
#define PHP_WORLD_VERSION "0.1.0"
#define PHP_WORLD_EXTNAME "world"
#endif</code></pre>
<p>world.c</p>
<pre><code>#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_world.h"
PHP_FUNCTION(world)
{
zend_string *strg;
strg = strpprintf(0, "hello world. (from world module)");
RETURN_STR(strg);
}
const zend_function_entry world_functions[] = {
PHP_FE(world, NULL)
PHP_FE_END
};
zend_module_entry world_module_entry = {
STANDARD_MODULE_HEADER,
PHP_WORLD_EXTNAME,
world_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
PHP_WORLD_VERSION,
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_WORLD
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(world)
#endif</code></pre>
<p>编译安装:</p>
<pre><code>$ phpize
$ ./configure --with-php-config=/usr/local/php7/bin/php-config
$ make & make install</code></pre>
<p>测试:<br>一样需要在<code>php.ini</code>添加<code>extension=world.so</code></p>
<p><img src="https://www.hongweipeng.com/usr/uploads/2016/11/848961722.png" alt="20161118144823.png" title="20161118144823.png"></p>
<p>不使用工具的精简的一个扩展完成。</p>
编译php源码错误集与解决
https://segmentfault.com/a/1190000006107786
2016-07-29T09:44:32+08:00
2016-07-29T09:44:32+08:00
weapon
https://segmentfault.com/u/weapon
3
<h2>起步</h2>
<p>服务器Ubuntu14.04已后lamp开发环境,却还是没有不能顺利编译php源码,在此整理编译过程。</p>
<h2>获取源码与编译</h2>
<p>确保已安装了git<code>sudo apt-get install git -y</code>,因为这可以看到PHP每次修改的内容及日志信息和跟进作者的更新。</p>
<pre><code>git clone https://github.com/php/php-src.git
cd php-src
sudo apt-get install build-essential
./buildconf
./configure --disable-all # 为了尽快得到可以测试的环境,我们仅编译一个最精简的PHP
make
./sapi/cli/php -v</code></pre>
<p>-v参数表示输出版本号,如果命令执行完后看到输出php版本信息则说明编译成功。</p>
<p><img src="/img/remote/1460000006765952" alt="20160628170052.png" title="20160628170052.png"></p>
<h2>错误集</h2>
<p>错误</p>
<pre><code>configure: error: xml2-config not found. Please check your libxml2 installation.</code></pre>
<p>解决<br><code>apt-get install libxml2-dev</code></p>
<p>错误</p>
<pre><code>/usr/bin/mysql_config: No such file or directory</code></pre>
<p>解决<br><code>apt-get install mysql-server mysql-client libmysqlclient-dev</code></p>
<p>错误</p>
<pre><code>Warning: Declaration of PEAR_Installer::download() should be compatible with & PEAR_Downloader::download($params) in phar:///root/php7/php-src/pear/install-pear-nozlib.phar/PEAR/Installer.php on line 43
Warning: Declaration of PEAR_PackageFile_Parser_v2::parse() should be compatible with PEAR_XMLParser::parse($data) in phar:///root/php7/php-src/pear/install-pear-nozlib.phar/PEAR/PackageFile/Parser/v2.php on line 113
[PEAR] Archive_Tar - installed: 1.3.13
[PEAR] Console_Getopt - installed: 1.3.1
[PEAR] Structures_Graph- installed: 1.0.4
Warning: Declaration of PEAR_Task_Replace::init() should be compatible with PEAR_Task_Common::init($xml, $fileAttributes, $lastVersion) in phar:///root/php7/php-src/pear/install-pear-nozlib.phar/PEAR/Task/Replace.php on line 31
[PEAR] XML_Util - installed: 1.2.3
Warning: Declaration of PEAR_Task_Windowseol::init() should be compatible with PEAR_Task_Common::init($xml, $fileAttributes, $lastVersion) in phar:///root/php7/php-src/pear/install-pear-nozlib.phar/PEAR/Task/Windowseol.php on line 76
Warning: Declaration of PEAR_Task_Unixeol::init() should be compatible with PEAR_Task_Common::init($xml, $fileAttributes, $lastVersion) in phar:///root/php7/php-src/pear/install-pear-nozlib.phar/PEAR/Task/Unixeol.php on line 76
[PEAR] PEAR - installed: 1.9.5
Wrote PEAR system config file at: /root/php7/usr/etc/pear.conf</code></pre>
<p>解决<br>`You may want to add: /root/php7/usr/lib/php to your php.ini include_path<br>/root/php7/php-src/build/shtool install -c ext/phar/phar.phar /root/php7/usr/bin`</p>
<p>错误</p>
<pre><code>configure: WARNING: unrecognized options: --with-mysql</code></pre>
<p>错误</p>
<pre><code>checking for bison version... invalid
configure: WARNING: This bison version is not supported for regeneration of the Zend/PHP parsers (found: none, min: 204, excluded: ).
checking for re2c... no
configure: WARNING: You will need re2c 0.13.4 or later if you want to regenerate PHP parsers.
configure: error: bison is required to build PHP/Zend when building a GIT checkout!</code></pre>
<p>解决<br><code>apt-get install bison</code></p>
<p>错误</p>
<pre><code>configure: error: Cannot find OpenSSL's</code></pre>
<p>解决<br><code>apt-get install libssl-dev</code></p>
<p>错误</p>
<pre><code>configure: error: Cannot find OpenSSL's libraries</code></pre>
<p>解决<br><code>apt-get install libssl-dev</code></p>
<p>错误</p>
<pre><code>checking for BZip2 in default path… not found
configure: error: Please reinstall the BZip2 distribution</code></pre>
<p>解决<br><code>apt-get install libbz2-dev</code></p>
<p>错误</p>
<pre><code>configure: error: Please reinstall the libcurl distribution –
easy.h should be in /include/curl/</code></pre>
<p>解决<br><code>apt-get install libcurl4-openssl-dev</code></p>
<p>错误</p>
<pre><code>If configure fails try --with-vpx-dir=
configure: error: jpeglib.h not found.</code></pre>
<p>解决<br><code>apt-get install libjpeg-dev</code></p>
<p>错误</p>
<pre><code>configure: error: png.h not found.</code></pre>
<p>解决<br><code>apt-get install libpng12-dev</code></p>
<p>错误</p>
<pre><code>configure: error: freetype-config not found.</code></pre>
<p>解决<br><code>apt-get install libfreetype6-dev</code></p>
<p>错误</p>
<pre><code>configure: error: mcrypt.h not found. Please reinstall libmcrypt.</code></pre>
<p>解决<br><code>apt-get install libmcrypt-dev</code></p>
<p>错误</p>
<pre><code>configure: error: Cannot find pspell</code></pre>
<p>解决<br><code>apt-get install libpspell-dev</code></p>
<p>错误</p>
<pre><code>PEAR package PHP_Archive not installed: generated phar will require PHP's phar extension be enabled.</code></pre>
<p>解决<br><code>pear install pear/PHP_Archive</code></p>
<p>错误</p>
<pre><code>checking for recode support... yes
configure: error: Can not find recode.h anywhere under /usr /usr/local /usr /opt.</code></pre>
<p>解决<br><code>apt-get install librecode-dev</code></p>
安装php7,与php5共存
https://segmentfault.com/a/1190000006107718
2016-07-29T09:40:37+08:00
2016-07-29T09:40:37+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2>
<p>之前在服务器搭建了<code>lamp</code>环境,想换用性能更强的<code>nginx</code>作为服务器软件,又想将php5升级为php7.<br>安装nginx无需赘述:<code>sudo apt-get install nginx</code>,启动ng前修改apache的端口。</p>
<h2>安装php7</h2>
<p>源码在<a href="https://link.segmentfault.com/?enc=6kbood2ztKukMrWI%2FJ6JBQ%3D%3D.jMHdr%2Bi%2FPd8voN9DJFWAU6HtniUgRtbZRLnLSQgr62Y%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=npA5JLIFPXCpUy%2FXN7F1%2Bw%3D%3D.zcjTLdc89gqeFc0ML42vM1yZS9zc3EwSpY29Gk138b0%3D" rel="nofollow">http://php.net/downloads.php</a> 下载,并解压。</p>
<pre><code># cd php7***
# ./configure --prefix=/usr/local/php7 --with-config-file-path=/usr/local/php7/etc --with-mcrypt=/usr/include --with-mysql=mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-gd --with-iconv --with-zlib --enable-xml --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --enable-mbregex --enable-fpm --enable-mbstring --enable-ftp --enable-gd-native-ttf --with-openssl --enable-pcntl --enable-sockets --with-xmlrpc --enable-zip --enable-soap --without-pear --with-gettext --enable-session --with-curl --with-jpeg-dir --with-freetype-dir --enable-opcache
# make
# make install</code></pre>
<p>为不与5冲突,文件夹都用php7,安装过程中报错的安装响应的依赖。</p>
<h2>对接nginx</h2>
<p>nginx本身不能处理php脚本,需要发给php解释器处理。nginx一般是把请求发fastcgi管理进程处理,fascgi管理进程选择cgi子进程处理结果并返回被nginx。</p>
<pre><code># cp php.ini-production /usr/local/php7/etc/php.ini
# cp sapi/fpm/init.d.php-fpm /etc/init.d/php7-fpm
# chmod +x /etc/init.d/php7-fpm
# cp /usr/local/php7/etc/php-fpm.conf.default /usr/local/php7/etc/php-fpm.conf
# cp /usr/local/php7/etc/php-fpm.d/www.conf.default /usr/local/php7/etc/php-fpm.d/www.conf</code></pre>
<h2>启动php-fpm</h2>
<pre><code># service php7-fpm start</code></pre>
<p>中途如遇到日志文件路径不存在就手动创建并给予写的权限。</p>
<pre><code># service php7-fpm start
Starting php-fpm [07-Apr-2016 11:16:11] ERROR: [pool www] cannot get gid for group 'nobody'
[07-Apr-2016 11:16:11] ERROR: FPM initialization failed
failed</code></pre>
<p>遇到这个错误时,要添加个nobody组<code>groupadd nobody</code>再重新启动。</p>
<h3>nginx的配置</h3>
<p>这是访问php文件是变成下载文件,因为ng并未配置响应处理。</p>
<pre><code>location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}</code></pre>
<p>Thank you for using PHP.</p>
插件发布:tyepcho编辑器左右编辑
https://segmentfault.com/a/1190000004555452
2016-03-07T14:44:32+08:00
2016-03-07T14:44:32+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>最近深深的迷上了<code>Markdown</code>,无论是编写代码说明文档还是记日记写博客,没有markdown来排版感觉浑身难受。也是因为wordpress对markdown的不友好,选择了typecho作为我的博客系统,自带的编辑器只有撰写的浏览两种,要看效果还要切换,没有像<code>sengmentfault</code>那样有三种编辑模式,还是实时看到渲染效果。</p>
<p><!--more--></p>
<p>大概程序员就是这样一类人,看到一些不完善的东西就想去改进它,于是写了插件来改善这个编辑器。效果图如下:</p>
<p><img src="https://www.hongweipeng.com/usr/uploads/2015/12/1703409017.png" alt="左右编辑器效果图.png" title="左右编辑器效果图.png"></p>
<h2>功能:</h2>
<ul>
<li><p>添加三种模式:编辑,实时,浏览。</p></li>
<li><p>左右滚动条同步。</p></li>
<li><p>浏览代码块简单高亮。</p></li>
</ul>
<h2>使用方法:</h2>
<ol>
<li><p>把 <code>EditorLR</code> 文件夹上传到插件目录</p></li>
<li><p>启用插件</p></li>
</ol>
<p>github:<a href="https://link.segmentfault.com/?enc=cBcB1V52AUEU%2B4sQ2SljXA%3D%3D.5JPU%2B1zNss5IOqO2qJIGkTlkRTASjBAI%2FD7X%2BQu%2B%2FytqxX4F7p1UUEeUKVP47rYK5Csid5QU43rtEFbMn2uQFA%3D%3D" rel="nofollow">https://github.com/hongweipeng/EditorLR_for_typecho</a></p>
<h2>与我联系:</h2>
<p>作者:hongweipeng <br>主页:<a href="https://link.segmentfault.com/?enc=XbmGyEQZXUbpkQeyM5ys2w%3D%3D.pZxz%2FFgE4LGS5%2BDErHED9YUA1sL6zoLWhq7lNOjjprM%3D" rel="nofollow">https://www.hongweipeng.com</a> <br>Emai: hongweichen8888@sina.com</p>
wamp尝鲜php7
https://segmentfault.com/a/1190000004132555
2015-12-11T15:06:31+08:00
2015-12-11T15:06:31+08:00
weapon
https://segmentfault.com/u/weapon
0
<h2>起步</h2>
<p>php7终于正式发布了,迫不及待想去试下,先拿公司的电脑开刀,环境是wamp,还是32位的,从官网<a href="https://link.segmentfault.com/?enc=dxi%2BXPmIlaeXJpNbOwaSsA%3D%3D.o8F%2BHSf0NvQ6ErT1lkNSg2zj5fPOPxFKMqVe2R5Si5%2BV3fzSBWMvuaAxPdDO15bl" rel="nofollow">php7.0.0</a>下载windows版本<br><code>VC14 x86 Thread Safe</code>,升级wamp的php版本如下:</p>
<h2>第一步</h2>
<p>wamp的php放在<code>wamp/bin/php</code>,我们在该目录新建<code>php7.0.0</code>文件夹</p>
<p><img src="http://blog.west2online.com/usr/uploads/2015/12/3920202643.png" alt="微信截图_20151207091933.png" title="微信截图_20151207091933.png"></p>
<p>把下载的压缩包解压到刚建的php7.0.0文件夹中。<br><img src="http://blog.west2online.com/usr/uploads/2015/12/1984394572.png" alt="微信截图_20151207092026.png" title="微信截图_20151207092026.png"></p>
<p><!--more--></p>
<h2>第二步</h2>
<blockquote><p>如果这时急着切换php版本那肯定是失败的</p></blockquote>
<p>从其他php版本目录那(之前能用版本,我的是5.3,好像有点旧了。。。),复制1个文件到php7.0.0下:<code>wampserver.conf</code> 修改</p>
<blockquote><p>$phpConf'apache'['LoadModuleName'] = 'php5_module';<br>$phpConf'apache'['LoadModuleFile'] = 'php5apache2_4.dll';</p></blockquote>
<p>为</p>
<blockquote><p>$phpConf'apache'['LoadModuleName'] = 'php7_module';<br>$phpConf'apache'['LoadModuleFile'] = 'php7apache2_4.dll';</p></blockquote>
<p>复制<code>php.ini-development</code>并改名为<code>phpForApache.ini</code>,再拷贝一次命名为<code>php.ini</code>覆盖<code>apache/bin/</code>的<code>php.ini</code></p>
<p>如果解压出来双击<code>php.exe</code>报了缺少dll错误</p>
<p><img src="http://blog.west2online.com/usr/uploads/2015/12/3734953681.jpg" alt="21e5582309f79052f87acf140ef3d7ca7bcbd570.jpg" title="21e5582309f79052f87acf140ef3d7ca7bcbd570.jpg"></p>
<p>那时因为缺少了<a href="https://link.segmentfault.com/?enc=fhnoT3XcBmt%2BfKtA6TGN9w%3D%3D.TRHWo1wybo1e5n5MhMWwitimtlTyU1UJ15Q5q%2BCXfBi%2FbU0l%2BUJ3YDULcLZk4btqWxuC2FE3lrwVY%2FavFdem7w%3D%3D" rel="nofollow">vc++2015</a>运行库下载对应的的位数的即可。另外php7需要<code>apache2.4</code>版本的,解压包里有<br><code>php7apache2_4.dll</code>,如果还是用2.2的就应该升级了。</p>
<h2>第三步</h2>
<p>重新打开wamp,在php->version就看到<code>7.0.0</code>了。</p>
<p><img src="http://blog.west2online.com/usr/uploads/2015/12/715316288.png" alt="微信截图_20151207092839.png" title="微信截图_20151207092839.png"></p>
<p>大胆的点开它,等wamp图标变绿后,访问<code>http://localhost</code> 版本变为7啦</p>
<p><img src="http://blog.west2online.com/usr/uploads/2015/12/1360801090.png" alt="微信截图_20151211092226.png" title="微信截图_20151211092226.png"></p>
<p><img src="http://blog.west2online.com/usr/uploads/2015/12/211474231.png" alt="微信截图_20151211092333.png" title="微信截图_20151211092333.png"></p>
<p><img src="http://blog.west2online.com/usr/uploads/2015/12/3398207861.png" alt="微信截图_20151207105238.png" title="微信截图_20151207105238.png"></p>
<p><strong>Let's enjoy it!</strong></p>
让json更懂中文
https://segmentfault.com/a/1190000003968722
2015-11-09T10:06:16+08:00
2015-11-09T10:06:16+08:00
weapon
https://segmentfault.com/u/weapon
2
<h2>起步</h2>
<p>相信很多人用php搭后台时候,当ajax用于交互时候,由于字符都被urf-8处理,所以用PHP的json_encode来处理中文的时候, 中文都会被编码, 变成不可读的, 类似”\u<em>*</em>”的格式, 而且还会在一定程度上增加传输的数据量。</p>
<pre><code><?php
$str = "让json更懂中文";
echo json_encode($str);
//输出:"\u8ba9json\u66f4\u61c2\u4e2d\u6587"</code></pre>
<p>总结几种解决方法。</p>
<h2>方法1:自己构造支持中文的 json_encode</h2>
<p>思路是这样的,对字符串进行url加密处理,之后json_encode后再解密</p>
<pre><code><?php
function json_encode_zn($data) {
//处理json的中文问题
if(is_string($data)) {
$data = urlencode($data);
}else if(is_array($data)) {
array_walk_recursive($data, function(&$value) {
if(is_string($value)) {
$value = urlencode($value);
}
});
}
return urldecode(json_encode($data));
}
$str = "让json更懂中文";
$arr = array("id"=>5,"name"=>"中文名字","arr"=>array(1,"weapon","中文"));
echo json_encode_zn($str);//"让json更懂中文"
echo json_encode_zn($arr);//{"id":5,"name":"中文名字","arr":[1,"weapon","中文"]}</code></pre>
<h2>方法二:运用preg_replace替换\u**为中文</h2>
<pre><code><?php
$code = json_encode($str);
echo preg_replace("#\\\u([0-9a-f]+)#ie", "iconv('UCS-2', 'UTF-8', pack('H4', '\\1'))", $code);
//linux使用preg_replace("#\\\u([0-9a-f]{4})#ie", "iconv('UCS-2BE', 'UTF-8', pack('H4', '\\1'))", $code);</code></pre>
<h2>方法三:5.4版本后的直接处理</h2>
<p>自从php5.3的json_encode加入了options参数,5.4版本新加了JSON_UNESCAPED_UNICODE,故名思议, 就是说, json不要编码unicode.</p>
<pre><code>echo json_encode("中文", JSON_UNESCAPED_UNICODE);//中文</code></pre>
用php写wifidog的认证服务器
https://segmentfault.com/a/1190000003851291
2015-10-13T14:47:52+08:00
2015-10-13T14:47:52+08:00
weapon
https://segmentfault.com/u/weapon
4
<h2>路由器上wifidog的设置</h2>
<p><img src="/img/bVqjTH" alt="图片描述" title="图片描述"></p>
<p>主要设置<strong>鉴权服务器主机名</strong>(域名或ip都可以)和<strong>加粗鉴权服务器路径</strong></p>
<p>路由器会请求以下四个地址:</p>
<blockquote><p><a>http://</a>认证服务器/路径/login <br><a>http://</a>认证服务器/路径/auth<br><a>http://</a>认证服务器/路径/ping<br><a>http://</a>认证服务器/路径/portal<br><a>http://</a>认证服务器/路径/gw_message.php</p></blockquote>
<p>所以我们需要每个请求建立一个文件夹下一个index.php</p>
<h2>预备知识</h2>
<p>客户端首次连接wifi,浏览器请求将被重定向到login并携带参数</p>
<blockquote><p>login/?gw_address=路由器ip&gw_port=路由器wifidog的端口&gw_id=用户id&url=被重定向前用户浏览的地址</p></blockquote>
<p>(2013版本的wifidog参数多了mac)</p>
<p>而login/index.php需要做的就是验证通过后再重定向到网关:</p>
<blockquote><p><a>http://</a>网关地址:网关端口/wifidog/auth?token=</p></blockquote>
<p>之后wifidog会启动一个线程周期性报告用户状态:</p>
<blockquote><p>/auth?stage=&ip=&mac=&token=&incoming=&outgoing=</p></blockquote>
<p>/auth/index.php则需要返回是否让该用户继续上网,回复格式:Auth:状态码(0:拒绝, 1:验证通过)</p>
<p>验证成功后,路由器将请求/portal/?gw_id=%s</p>
<p>在/portal/index.php就可以写重定向到第一次请求的url参数或者重定向到自定义网址了</p>
<p>/ping/index.php的作用就是告诉路由器认证服务器还没有崩<br>/gw_message/index.php作用是当认证过程出现错误的时候,想用户显示错误信息</p>
<h2>开工</h2>
<p>我们将完成用户用账号密码方式认证</p>
<h2>1.首次重定向:</h2>
<p>/login/index.php</p>
<pre><code><?php
//获取url传递过来的参数
parse_str($_SERVER['QUERY_STRING'], $parseUrl);
//gw_address、gw_port、gw_id是必需参数,缺少不能认证成功.
if( !array_key_exists('gw_address', $parseUrl) || !array_key_exists('gw_port', $parseUrl) || !array_key_exists('gw_id', $parseUrl)){
exit;
}
//如果提交了账号密码
if(isset($_POST['name']) && isset($_POST['password'])){
$username = $_POST['name'];
$password = $_POST['password'];
$db = new mysqli('localhost', 'root', '', 'test');
if(mysqli_connect_errno()){
echo mysqli_connect_error();
die;
}
$db->query("set names 'utf8'");
$result = $db->query("SELECT * FROM user WHERE username='{$username}' AND password='{$password}'");
if($result && $result->num_rows != 0){
//数据库验证成功
$token = '';
$pattern="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLOMNOPQRSTUVWXYZ";
for($i=0;$i<32;$i++)
$token .= $pattern[ rand(0,35) ];
//把token放到数据库,用于后续验证(auth/index.php)
$time = time();
$sql = "UPDATE user SET token='{$token}',logintime='{$time}'";
$db->query($sql);
$db->close();
//登陆成功,跳转到路由网管指定的页面.
$url = "http://{$parseUrl['gw_address']}:{$parseUrl['gw_port']}/wifidog/auth?token={$token}";
header("Location: ".$url);
}else{
//认证失败
//直接重定向本页 请求变成get
$url='http://'.$_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"];
header("Location: ".$url);
}
}else{
//get请求
//一个简单的表单页面
$html = <<< EOD
<html>
<head>
<title>portal login</title>
</head>
<body>
<form action="#" method="post">
username:<input type="text" name="username" />
password:<input type="password" name="password" />
<input type="submit" value="submit" />
</form>
</body>
</html>
EOD;
echo $html;
}
</code></pre>
<h2>2.用户认证协议:</h2>
<p>/auth/?stage=%s&ip=%s&mac=%s&token=%s&incoming=%s&outgoing=%s<br>参数解释:<br>stage: 认证阶段,就logoin和counters两种<br>token: login页面下发的token<br>incoming: 下载流量<br>outgoing: 上传流量</p>
<p>/auth/index.php</p>
<pre><code><?php
//获取url传递过来的参数
parse_str($_SERVER['QUERY_STRING'], $parseUrl);
//需要多少参数用户可自己顶
if( !array_key_exists('token', $parseUrl) ){
//拒绝
echo "Auth:0";
exit;
}
$db = new mysqli('localhost', 'root', '', 'test');
$db->query("set names 'utf8'");
$token = $parseUrl['token'];
$sql = "SELECT * FROM user WHERE token='{$token}'";
$result = $db->query($sql);
if($result && $result->num_rows != 0){
//token匹配,验证通过
echo "Auth:1";
}else{
echo "Auth:0";
}
</code></pre>
<h2>3.Ping协议</h2>
<p>/ping/?gw_id=%s&sys_uptime=%lu&sys_memfree=%u&sys_load=%.2f&wifidog_uptime=%lu</p>
<p>wifidog会向认证服务器发送一些信息,来报告wifidog现在的情况,这些信息是通过Http协议发送的,如上的链接所示,参数大概如字面意思,没仔细研究过,而作为认证服务器,auth_server应回应一个"Pong"。<br>主要作用是路由确认认证服务器仍然存活,没有死机,另外一个功能是认证服务器可以收集路由的负载等的信息。路由器会定时访问这个脚本,脚本必须回复Pong,否则将认为认证服务器失效而出错。</p>
<p>/ping/index.php</p>
<pre><code><?php
echo "Pong";
?></code></pre>
<h2>4.认证成功后的跳转</h2>
<p>portal/?gw_id=%s</p>
<p>在认证成功后,wifidog会将用户重定向至该页面。</p>
<p>/portal/index.php</p>
<pre><code><?php
//认证前用户访问任意url,然后被重定向登录页面,session记录的是这个“任意url”.
$url = $_SESSION["url"];//如果login参数url保存到session中
//跳转到登陆前页面.
header("Location: ".$url);</code></pre>
<h2>5.若验证失败,则会根据失败原因跳转至如下页面</h2>
<p>gw_message.php?message=denied</p>
<p>gw_message.php?message=activate</p>
<p>gw_message.php?message=failed_validation</p>
<p>/gw_message.php</p>
<pre><code><?php
$message = null;
if(isset($_GET["message"])){
$message = $_GET["message"];
}
echo $message;
?></code></pre>
<h2>总结</h2>
<p>wifidog的认证流程是:</p>
<ol>
<li><p>用户连上wifi,发起一个访问网站的请求,如:segmentfault.com</p></li>
<li><p>网关根据防火墙规则,将请求重定向本地(路由器的ip)的wifidog端口</p></li>
<li><p>wifidog重定向到认证服务器的认证页面</p></li>
<li><p>认证服务器返回登录页面让用户填写</p></li>
<li><p>用户填写后请求认证</p></li>
<li><p>认证服务器根据用户提供数据确定是否符合要求</p></li>
<li><p>如果符合要求,认证服务器将用户重定向路由器网关并携带token</p></li>
<li><p>网关向认证服务器确定用户信息</p></li>
<li><p>如果符合要求,服务器返回用户登录成功的页面</p></li>
<li><p>用户正常上网</p></li>
</ol>
PHP mysqli 操作数据库
https://segmentfault.com/a/1190000003841357
2015-10-11T00:07:30+08:00
2015-10-11T00:07:30+08:00
weapon
https://segmentfault.com/u/weapon
8
<h2>起步</h2>
<hr>
<p>由于mysql连接方式被废除,据说在php7中要使用mysql_connect()还需要额外下载组件。<br>使用mysqli有面向过程和面向对象两种方式。<br>mysqli提供了三个类:</p>
<ol>
<li><p>mysqli 连接相关的</p></li>
<li><p>mysqli_result 处理结果集</p></li>
<li><p>mysqli_stmt 预处理类</p></li>
</ol>
<h2>数据库连接</h2>
<pre><code><?php
$db_host = 'localhost';
$db_name = 'test';
$db_user = 'root';
$db_pwd = '';
//面向对象方式
$mysqli = new mysqli($db_host, $db_user, $db_pwd, $db_name);
//面向对象的昂视屏蔽了连接产生的错误,需要通过函数来判断
if(mysqli_connect_error()){
echo mysqli_connect_error();
}
//设置编码
$mysqli->set_charset("utf8");//或者 $mysqli->query("set names 'utf8'")
//关闭连接
$mysqli->close();
//面向过程方式的连接方式
$mysqli = mysqli_connect($db_host, $db_user, $db_pwd, $db_name);
//判断是否连接成功
if(!$mysqli ){
echo mysqli_connect_error();
}
//关闭连接
mysqli_close($mysqli);
?></code></pre>
<h2>数据库查询</h2>
<p>通用:执行sql语句都可用query(sql),执行失败会返回false,select成功则返回结果集对象,其他返回true,只要不是false就说明sql语句执行成功了。</p>
<pre><code><?php
//无结果集示例
$sql = "insert into table_name (`name`, `address`) values ('xiaoming', 'adddddrrreess')";
$result = $mysqli->query($sql);
//或者
$sql = "delete from table_name where name='xiaoming'";
$result = $mysqli->query($sql);
if($result === false){
echo $mysqli->error;
echo $mysqli->errno;
}
//影响条数
echo $mysqli->num_rows;
//插入的id
echo $mysqli->insert_id;
$mysqli->close();</code></pre>
<p>有结果集</p>
<pre><code><?php
$sql = "select * from table_name";
$result = $mysqli->query($sql);
if($result === false){//执行失败
echo $mysqli->error;
echo $mysqli->errno;
}
//行数
echo $result->num_rows;
//列数 字段数
echo $result->field_count;
//获取字段信息
$field_info_arr = $result->fetch_fields();
//移动记录指针
//$result->data_seek(1);//0 为重置指针到起始
//获取数据
while($row = $result->fetch_assoc()){
echo $row['name'];
echo $row['address'];
}
//也可一次性获取所有数据
//$result->data_seek(0);//如果前面有移动指针则需重置
$data = $result->fetch_all(MYSQLI_ASSOC);
$mysqli->close();</code></pre>
<h2>预处理示例</h2>
<p>预处理能有效的防止sql注入的产生,mysqli_stmt是预处理类</p>
<pre><code><?php
$sql = "insert inro table_name ('name','address') values (?,?)";
//获得预处理对象
$stmt = $mysqli->prepare($sql);
//绑定参数 第一个参数为绑定的数据类型
/*
i:integer 整型
d:double 浮点型
s:string 字符串
b:a blob packets blob数据包
*/
$name = "xiaoming";
$address = "adddressss";
$stmt->bind_param("ss", $name, $address);//绑定时使用变量绑定
//执行预处理
$stmt->execute();
/*
//可重新绑定 多次执行
$stmt->bind_param("ss", $name, $address);
$stmt->execute();
*/
//插入的id 多次插入为最后id
echo $stmt->insert_id;
//影响行数 也是最后一次执行的
echo $stmt->affected_rows;
//错误号
echo $stmt->errno;
//错误信息
echo $stmt->error;
//关闭
$stmt->close();
$mysqli->close();</code></pre>
<p>下面示例select的预处理</p>
<pre><code>//注释部分省略
$sql = "select * from table_name where id<?";
$stmt = $mysqli->prepare($sql);
$id = 30;
$stmt->bind_param("i", $id);
$stmt->execute();
//获取结果集
$result = $stmt->get_result();//结果集取后的操作就和之前一样了
//获取所有数据
$data = $result->fetch_all(MYSQLI_ASSOC);
$result->close();
$mysqli->close();</code></pre>
<p>一次执行多条sql语句multiquery(不推荐),执行结果不是结果集,affectd_rows是最后影响的条数</p>
<pre><code><?php
$sql_arr = array(
"insert into table_name (`name`,`address`) values ('xiaoming','a')",
"insert into table_name (`name`,`address`) values ('xiaohong','a')",
'delete from table_name where id=23',
);
$sql = implode(';', $sql_arr);
$result = $mysqli->multi_query($sql);
if($result === false){
echo $mysqli->error;
}
$mysqli->close();</code></pre>