soliloquize 2018-02-14T16:53:29+08:00 关于程序员的中年危机·续 2018-02-14 16:53:29 numerical /2018/02/14/关于程序员的中年危机续

去年在关于程序员的中年危机里分析过这个问题,最近又有朋友找自己聊这方面的问题,因此又再思索了下。

危机的核心点在于,技术这一行真的能一直做下去吗?或者进一步,也许技术本身可以,但能一直像现在这么拼加班下去吗?

从过去的经验来看,很难。计算机相关技术研究向或偏底层的职位并不多,更多的其实都是各种工程类的项目。这意味着很多方向能够深挖的东西有限,或者说实际的市场并不需要你去做这种深入的事情。那种每个团队都需要去做底层或有技术含量的事情是不现实的。很容易在工作几年后就达到一个技术上的瓶颈期,这种情况下随着年龄的增长,职业竞争力是在下降的。不是每个人都能一直不断学习进步,或早或晚,都会步入一个相对停滞的时期,普通人的通病。

同时还有一点,技术含量高并不代表着收入高。市场才是决定收入的决定因素。过去几年移动互联网的火热再到最近一年AI的浪潮,相应职位在对应当口的收入都相当可观,但当需求平稳之后,坑就少了,收入也会相应回落。很难说资本家们是为技术难度买单,更多的是为稀缺性买单。很多有技术含量的事情,都没有对应的收入水平,计算机相关的职位也不会例外。另外一方面,那些技术含量没那么高的工程类业务类的事情,坑虽然多,但可替代性却也是高的。同样也造成了议价能力的不足。本质上来说,作为程序员的我们是希望不断提升自身的不可替代性,但公司却是需要不断减少这种不可替代性的。公司不希望被依赖在一个产品、一个团队、一个个体身上,对于企业来说,分拆风险是天然的诉求。

那么究竟怎么才能有效渡过危机呢?

除了之前提及的几点,在职业生涯中可能需要多关注下团队。作为普通人的我们,在浩浩荡荡的大势面前基本都是不堪一击的。但如果能以团队的形式来面对呢?可能会好很多吧。对于大公司来说,公司内几十上百个团队,我们需要关注的是自己所在的团队。判定下团队是否值得加入,是否值得坚持。人与人之间的协作、磨合、信任,不像代码那样那么容易复制。作为团队中的一员,完成本职工作是本份,如果力所能及的话,最好去推动团队整体的进步。公司再大实际是和自身无太大关系,所处的团队是更重要的。因此如果在大公司中一直在切换团队,某种程度上来说是很不利的。只是不断的磨合再磨合,融入再融入,重复再重复。

所以,做好自己,提升团队,这些是可以去做也值得去做的事情。

使用ast移除代码中的函数实现 2018-02-10 09:12:56 numerical /2018/02/10/使用ast移除代码中的函数实现

如果想要将Python代码中的函数实现全替换掉,但是保留函数定义,要怎么做?

当然,首先还是看下什么时候需要做这样的事情吧。一种情况就是,当Python代码不想对外开放,但又需要提供文档接口说明的时候。通过pydoc生成文档自然是一种选择,不过如果想配合PyCharm进行自动提示,那么还是有一份不含有具体实现的Python代码最为方便。

那如何实现呢?还是借助Python的ast模块。在之前使用ast移除代码中的print语句里面就曾借用ast模块来移除print语句,这里也是同样的处理思路。通过ast构建抽象语法树,找到Python函数定义节点,然后移除子节点,同时保留docstring。

代码很短,直接来看吧,

class CleanFunc(NodeTransformer):

    def visit_FunctionDef(self, node):
        # __init__特殊处理,成员字段基本都在__init__中进行定义,因此保留函数体
        if node.name == '__init__' or not node.body:
            return node
        # 找到docstring对应的节点,保留,移除其余子节点
        child = node.body[0]
        if type(child) == ast.Expr and type(child.value) == ast.Str:
            node.body = [child, ast.Expr(value=ast.Pass())]
        else:
            node.body = [ast.Expr(value=ast.Pass())]
        return node


def clean_func(input_path, output_path):
    with open(output_path, 'w') as outputs:
        outputs.write(astor.to_source(CleanFunc().visit(astor.parsefile(input_path))))
    outputs.flush()

比如用下面这段简单的定义去运行上述清理逻辑之后,可以得到后面的输出,

输入,

class Foobar(object):
        def __init__(self):
            self.name = None

        def get_name(self):
            """docstring"""
            return self.name

输出,

class Foobar(object):
        def __init__(self):
            self.name = None

        def get_name(self):
            """docstring"""
            pass
Boost.Python C++导出基本用法 2017-12-22 07:30:34 numerical /2017/12/22/BoostPython-C导出基本用法

用一些简单的例子来记录下常见用法。

导出模块

导出一个空的Python模块,

BOOST_PYTHON_MODULE(py_sample) {
}

在相应的Python代码中可以dir查看导出模块的内容,

import py_sample
print dir(py_sample)  # ['__doc__', '__name__', '__package__']

导出函数

在C++中定义函数,并做相应导出,

std::string Foobar() {
        return "foobar";
}

BOOST_PYTHON_MODULE(py_sample) {
    def("foobar", Foobar);
}

对于函数参数以及返回值是简单类型,比如int/long/float/double/bool/string等,Boost会自动进行转化。

导出类

直接来看具体的例子,

class Base {
    public:
        Base(std::string name):
            name_(name),
            value1_(0),
            value2_(0) {
        }

        Base(int value1, int value2): name_("") {
            value1_ = value1;
            value2_ = value2;
        }

        std::string GetName() {
            return name_;
        }

        int getValue2() {
            return value2_;
        }

    public:
        std::string name_;
        int value1_;

    private:
        int value2_;
};

BOOST_PYTHON_MODULE(py_sample) {
    class_<Base>("Base", init<std::string>())
            .def(init<int, int>())
            .def("get_name", &Base::GetName)
            .def_readonly("value1", &Base::value1_)
            .add_property("value2", &Base::getValue2)
            ;
}

构造函数导出

对于构造函数,如果只存在一个构造函数,则直接在类名后跟上init块,如果存在多个构造函数,则其余构造函数以.def(init<...>())进行导出。

成员函数导出

对于函数来说,通过def操作进行导出。

成员变量导出

对于类属性来说,如果是public的字段,可以通过def_readonlydef_readwrite进行导出,对于private字段,则需要通过add_property来导出,提供对应的getter/setter接口。

继承关系处理

考虑C++中的继承关系,实现一个Child类继承Base并直接导出,同时再定义导出一个处理基类指针Base*的函数,

class Child: public Base {
    public:
        Child(std::string name):
            Base(name) {
        }
};

void Process(Base* base) {
}

BOOST_PYTHON_MODULE(py_sample) {
    ...

    class_<Child>("Child", init<std::string>())
            ;

    def("process", &Process);
}

上面的导出方式在Python中会丢失继承关系,并且无法将Child实例传递给Process函数,

from py_sample import Base, Child, process
instance = Child("child")
print isinstance(instance, Child), isinstance(instance, Base)   # True, False

调用,

process(instance);

则会出现如下异常,

Boost.Python.ArgumentError: Python argument types in
    py_sample.process(Child)
did not match C++ signature:
    process(class Base *)

为了处理导出类的继承关系,需要在导出类的时候显示注明其基类,

class_<Child, bases<Base>>("Child", init<std::string>())
        ;

如此上述的isinstance判断会返回预期结果,process也能正常调用。

在Python中继承导出类

如果在C++类中定义了纯虚函数或虚函数,希望在Python中继承类并重载对应的函数,则需要通过wrapper进行一定处理,

class Base {
    public:
        Base(): value_(0) {
        }

        virtual int GetValue() = 0;

        virtual int Multiple(int v) {
            return value_ * v;
        }

        int TryGetValue() {
            return GetValue();
        }

        int TryMultiple(int v) {
            return Multiple(v);
        }

    private:
        int value_;
};

class BaseWrap: public Base, public wrapper<Base> {
    public:
        int GetValue() {
            return this->get_override("get_value")();
        }

        int Multiple(int v) {
            if (override f = this->get_override("multiple")) {
                return f(v);
            }
            return Base::Multiple(v);
        }

        int DefaultMultiple(int v) {
            return this->Base::Multiple(v);
        }
};

BOOST_PYTHON_MODULE(py_sample) {
    class_<BaseWrap, boost::noncopyable>("Base")
        .def("try_multiple", &Base::TryMultiple)
        .def("try_get_value", &Base::TryGetValue)
        .def("get_value", pure_virtual(&Base::GetValue))
        .def("multiple", &Base::Multiple, &BaseWrap::DefaultMultiple)
        ;
}

在Python中进行如下调用,

from py_sample import Base

class Child(Base):
    def multiple(self, v):
        return 1000

    def get_value(self):
        return 1000

child = Child()
base = Base()
print child.try_multiple(1)  # 1000
print base.try_multiple()  # 0

print child.try_get_value()  # 1000
print base.try_get_value()   # exception

上面定义的try_multipletry_get_value两个接口是为了说明以这种方式定义,可以在C++代码中获得Python中定义的重载效果。

函数默认参数处理

普通函数的默认参数处理,

int Foo(int a, int b=1) {
        return a * b;
}

BOOST_PYTHON_FUNCTION_OVERLOADS(FooOverloads, Foo, 1, 2)

BOOST_PYTHON_MODULE(py_sample) {
    def("foo", Foo, FooOverloads());
}

通过BOOST_PYTHON_FUNCTION_OVERLOADS进行包装,上面的1,2数字分别代表最少参数个数与最大参数个数。定义在类上的成员函数则通过BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS进行修饰,

class Base {
    public:
        int Foo(int a, int b=1) {
            return a * b;
        }
};

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(FooOverloads, Base::Foo, 1, 2)

BOOST_PYTHON_MODULE(py_sample) {
    class_<Base>("Base")
            .def("foo", &Base::Foo, FooOverloads())
            ;
}

函数overload处理

C++中可以定义多个overload函数,在导出的时候,对于有公共前缀参数的overload函数来说,可以通过BOOST_PYTHON_FUNCTION_OVERLOADS, BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS来帮助进行导出,

class Base {
    public:
        int Foo(int a, int b) {
            return a * b;
        }

        int Foo(int a, int b, int c) {
            return a * b * c;
        }
};

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(FooOverloads, Base::Foo, 2, 3)

BOOST_PYTHON_MODULE(py_sample) {
    class_<Base>("Base")
            .def("foo", (int(Base::*)(int, int, int)) 0, FooOverloads())
            ;
}

导出枚举

枚举的导出简单很多,

enum Enum {
        FIRST = 0,
        SECOND
};

BOOST_PYTHON_MODULE(py_sample) {
    enum_<Enum>("Enum")
        .value("FIRST", FIRST)
        .value("SECOND", SECOND)
        ;
}

在Python中使用,

from py_sample import Enum
print Enum.FIRST, Enum.SECOND

参考

关于程序员的中年危机 2017-12-17 10:58:13 numerical /2017/12/17/关于程序员的中年危机

2017年关于程序员的中年危机问题从华为的34岁裁员再到最近中兴的事件,一直没有停歇。物伤其类,这里也记录下自己的一些观点吧。

认清残酷现实

最重要的一点就是认清现实的残酷吧。多年前有一篇文章叫“公司不是家”。不论是一线岗位还是中层来说,公司是商业利益场所,当我们创造的价值大于公司提供的报酬时,我们才可能立足。换位一下,如果我们自己创业成了老板,难道会去养闲人吗?恐怕也不会。所以不要寄希望于情感,当个人面临这些残酷的问题时,可能确实有人会同情这样的际遇,但在职场,更多的时候就是“business is business”。不论是称其为“公司”或是温情一点叫其”团队”,当立足其中的理由不在时,就自动成为一个外人了。

保持职业竞争力

新来的有干劲的年轻人就一定比老人有更大的战斗力吗?很多时候并不是。工作久了,要总结要学习,要让经验变成能力的加成。什么是竞争力?个人简单理解,一是对团队内的不可替代性,承担团队中重要的工作内容,遇到困难的时候能顶上,如此在团队健在的情况下,你的位置也就会稳固。二是在市场上的竞争力,一种比裁撤单人更残酷的就是整个团队都被干掉,在这种情况下就需要提升那些在就业市场上需要的技能了。

所以对技能上的要求就是专且广。专是为了在同一领域深入,广是为了避免整个领域被打击之后造成的影响。活到老,学到老。不仅仅是兴趣,也是为了应对现实的压力。就自己的经验来说,在繁忙的工作间隙保持学习的节奏是很困难的,很多时候连以前熟练掌握的内容也因为长期不用而淡忘。但即便如此,也是时常提醒自己去温故知新。“学习如逆水行舟,不进则退”,至少也不要退的太远,这样当有必要的时候,还能再拾得起来。

广结善缘

在工作中要和团队人员建立良好的关系,表现出专业性。这样也许某天自己不走运了,之前的朋友们也能帮忙拉一把。陌生人之间建立信任关系是困难的,曾经合作过的人如果靠谱的话,很多人也会考虑给予机会。所以自己跳槽也好,团队中其他人离开也好,要好聚好散。“苟富贵,勿相忘”,大家最好还是要认清自己无产阶级的本质,在困难时候能互相扶持一把。

加强锻炼

看多了电影,发现电影中表现中年危机的一个手段就是凸显中年人的身体不行了。当表现出身体不行的信号时,中年危机就彻底拉开了序幕。随后就是水银泄地般的溃退,从工作到家庭,处处亮起红灯。所以加强锻炼不仅是空喊口号,当我们自己觉得健康不再,疲惫感不断增加时,在遇到打击时很难有信心来认为可以击败这些打击。身体上的虚弱会放大受到打击时的无力感与虚弱感,最后可能就是彻底认输了。

所以有时间多多锻炼,没时间挤出时间也要锻炼。不仅是为了保持健康,也为了遭遇困难时能够昂扬的说这不是事,咱还能从头来过。

不仅仅是程序员的问题

中年危机并不是程序员专有的问题。有时也不是很能理解为什么老是对着我们这个群体在说。难道别的职业都是人到中年就都人生赢家了?说实在的,统统都面临同样的问题。也许有的职业这方面的问题确实不凸显,比如医生、老师。一方面是职业特性,另一方面则是稀缺性。总而言之,这不是一群人的问题,其实是所有人的问题。所以我们也不用过于在意这一点。

Python代码内存泄漏分析·续 2017-10-01 12:19:32 numerical /2017/10/01/Python代码内存泄漏分析续

Python代码内存泄漏分析分析了一些内存泄漏的情况,对于实际排查,简单介绍了使用objgraph, gc等工具。结合实际遇到的一些情况,这里再来记录下实际在排查内存泄漏时遇到的问题。

内存去哪了

在服务器上观察Python进程的内存变化,当运行时发现Python进程不断增长时,潜在就有了泄漏的风险。通过objgraph等来排查Python对象泄漏时,往往需要在一个相对稳定的状态下才可以。如果当前进程繁忙,频繁的创建对象,即便objgraph中看到了数量众多的对象,也难以判定是否真的是泄漏。在当进程不再处理新的处理请求逻辑的时候,观察当前进程的内存占用,然后再通过objgraph去搜寻Python对象,容易发现一个问题。当前可以统计到的Python对象占据不了那么大的内存。这部分内存是去哪里了?

通过guppy可以去计算统计当前进程中Python对象消耗的内存大小,比对Top中看到的内存占用。通常会发现guppy中得到的内存大小远小于进程当前占据的内存。gdb-heap之类的工具也会返回类似的结果。那么那部分内存到底被什么占据着?实际上很可能只是内存被Python自身的分配机制占用着,参见diagnosing-memory-leaks-python。如何来确定这一点?

直接分析进程内存

这里可以找到一个dump进程内存的脚本,

#!/bin/bash

grep rw-p /proc/$1/maps | sed -n 's/^\([0-9a-f]*\)-\([0-9a-f]*\) .*$/\1 \2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done

对导出生成的文件,通过下面命令,

strings -n 10 ...

来找到文件中长度超过一定范围的字符串内容,从内容数据中去反推是哪部分内容占据了内存。在实际存在内存泄漏情况下,heap对应的文件中应当能找到相关信息。在没有泄漏情况下,heap中的内容就很难分析出什么。之所以出现这种情况可能就是Python的内存机制。当这个问题比较明显的时候,可能隐含的问题是进程的内存分配过多,导致进程总是像系统申请内存,最终表现出的就是进程的内存不断上涨,但等到进程空闲时无法从objgraph中直接看出泄露的对象。

通过objgraph进行分析

回归到objgraph。既然问题的根源在Python代码中,那么应当还是可以通过objgraph来追寻踪迹。前文所说在进程空闲的时候去通过objgraph进行分析相对容易,但为了应对上述问题就需要在进程繁忙时介入。

objgraph的objgraph.show_most_common_types可以返回数量最多的类型信息。如果是自定义类型,那么问题很容易处理。如果是原生类型比如dictlist之类要怎么办?原生的类型往往数量众多,正常数据泄露数据混杂在一起。这种情况下不能够直接通过objgraph.find_backref_chain之类的方法去追寻引用链。一个很简单,但是很有效的方法就是将这些数据导出到文件,然后进行分析处理。如果能找到泄漏的数据,即便没有引用链,也容易去代码中分析。

简述下具体步骤,以dict数据为例。

  • 获取当前所有的dict对象
dict_list = objgraph.by_type('dict')
  • 将获取到的列表按顺序写入到文件中,一个dict输出一行,带上行号
def save_to_file(self, name, items):
  with open(name, 'w') as outputs:
    for idx, item in enumerate(items):
      try:
        outputs.write(str(idx) + " ### ")
        outputs.write(str(item))
        outputs.write('\n')
      except Exception as e:
        print e
    outputs.flush()
  • 实现脚本分析输出文件,结合grep、sed、awk命令进行处理。分析统计不同长度的dict有多少个,有多少dict存在相同结构(key相同)等。通过分析得到引起问题的dict。

  • 根据泄漏dict对应的行号信息,通过objgraph去获取引用关系,或者从代码中直接去分析。

objgraph.find_backref_chain(dict_list[idx], objgraph.is_proper_module)

总结

当进程内存不断增长且不归还系统时,通常一是真正存在泄漏,二是内存申请比释放多。前者相对容易处理。但处理起来的共通思路是去找寻引起问题的对象。如果通过工具如objgraph能直接定位到问题对象,那么最好。如果无法直接定位,那么就考虑笨办法吧,将数据导出进行分析处理。内存一定是被什么东西占用了,从数据上可以相对直观的发现问题。最后容易造成内存问题的通常就是全局单例、全局缓存、长期存活的对象,处理好了这部分代码,一般内存还是不容易有问题的。

Python代码内存泄漏分析 2017-04-22 13:05:31 numerical /2017/04/22/Python代码内存泄漏分析

什么情况下Python代码容易出现内存泄漏?为了便于分析这个问题,先来看下Python的内存回收策略。

内存回收策略

Python的内存管理简而言之就是:引用计数 + gc。一个Python对象内存被回收存在两个时机,

  • 引用计数变为0

当对象引用计数变为0时,会立即进行回收。引用计数无法处理存在循环引用的情况。陷入循环应用的对象们需要通过gc进行回收。

  • gc触发

gc触发时会去回收那些无法从root object访问到的对象。在Python下,gc可以认为是专门用来处理循环引用的情况,作为引用计数的一个补充。

内存泄漏分析

在认清内存回收策略之后,反过来思考就能够发现内存泄漏的可能场景了。

引用计数无法降到0的情况

引用直接传递造成的泄漏

一个对象的引用计数怎么会无法降到0呢,最可能的一个原因是该对象的引用因为种种意想不到的情况被外界持有了,且这个持有信息没有得到有效清理。考虑这种情况的构成要素,

  • 长期存活的外部对象
  • 通过强引用方式进行关联
  • 逻辑异常导致清理失败

那些长期存活的外部对象在实现上很多时候是必不可少的,逻辑异常又只能尽量避免却难以根绝,那么可以考虑在引用传递方式上做文章。之前在Python弱引用的使用与注意事项里进行过分析,在合适的场景下选择weakref可以有效的降低这种问题出现的概率。

lambda造成的引用传递

除了显示的直接传递对象引用让外界进行管理之外,在外部注册回调函数也可能引起泄漏,比如,

class Manager(object):

    def __init__(self):
        self.cb_dict = {}

    def do_something(self, key, cb):
        self.cb_dict[key] = cb
        # ...


manager = Manager()


class Foo(object):
    def foo(self):
        manager.do_something('foo', lambda: self.complete_something())

    def complete_something(self):
        pass

如果因为某些异常,任务没有完成,manager中持有的cb没有得到调用与释放,那么cb关联的Foo对象也就被回调所引用着而得不到释放了。

特别在一些需要异步处理的逻辑中,回调函数无可避免。最好从逻辑结构与代码健壮性上去考虑来规避泄漏,不过也还有个折中方法:让回调函数不持有原对象的强引用。一个方法是通过组合weakref与占位对象来达到既避免强引用传递又不会在原对象销毁后抛出ReferenceError: weakly-referenced object no longer exists

weakref的不当使用

weakref.proxy在创建时候可以设置一个参数用于在对象被销毁时进行回调,这个回调弄得不注意了,也可能导致泄漏。

import weakref


class Manager(object):
    def __init__(self):
        self.objects = {}

    def add(self, obj):
        self.objects[obj.id] = weakref.proxy(obj, lambda _: self.remove(obj.id))

    def remove(self, key):
        self.objects.pop(key, None)

上述代码的愚蠢之处在于在回调的lambda函数中隐式泄漏了obj引用。在使用lambda的时候需要小心注意。

defaultdict的默认行为

一个简单的示例,

from collections import defaultdict


class Manager(object):
    def __init__(self):
        self.objects = defaultdict(set)

    def get_count(self, key):
        return len(self.objects[key])


manager = Manager()


def foobar():
    for i in xrange(1000000):
        manager.get_count(i)


foobar()

print manager.objects

运行上述代码会发现objects里面存在大量元素。问题的根源来自,

len(self.objects[key])

当key不存在的时候defaultdict会默认创建一个条目。因此当对大量对象进行检查的时候对象就这么创建了,万一不巧defaultdict实例是一个需要长期存活的对象,那泄漏就这么发生了。

其它

引用计数不能为0的问题根源在于那些长期存活的对象,这可能是自有代码中实现的,还有就是语言特性或标准库里面就含有的。比如,

  • 函数默认参数
def foobar(tmp=[]):
    tmp.append("foo")
    print len(tmp)

for i in xrange(100):
    print foobar()
  • sys.path
def foobar():
    sys.path.append('/path/to/xxx')


for i in xrange(100):
    print foobar()

gc未能触发的情况

gc未能触发的可能原因有,

  • gc.set_threshold参数的错误设置

  • 关闭gc之后未重启gc

当希望手动去控制gc时,就可能去调整gc的参数或是干脆关闭gc,手动来调用gc.collect。在这种情况下,如果异常错误等打断了手动触发的流程,那就会导致泄漏。这个就需要依靠加强相应代码的健壮性来保证了。

不支持循环引用的情况

使用Python C/C++ extension时需要注意循环引用的处理,Supporting Cyclic Garbage Collection里说了在extension中定义的Python类型需要设置Py_TPFLAGS_HAVE_GC标记。否则gc是无法处理循环引用的,只能等待引用计数为0才会回收,处理不好则非常容易泄露。

内存泄漏排查工具

就个人经验而言objgraph、以及Python标准库中的gc模块是最有效的工具。

objgraph.show_most_common_typesobjgraph.by_typeobjgraph.find_backref_chaingc.get_referrers。这几个是最为有用的接口。

改善Python代码的几个建议 2017-04-09 20:22:28 numerical /2017/04/09/改善Python代码的几个建议

从开始使用Python到现在,一直都在努力尝试提升代码质量。从实际经验来看,可能如下几个方面会是相对有效的方法。

多读书

在未知的情况下看看别人是怎么做的,这样大有裨益。书本其实是最好的内容总结,比凌乱的各式文章更有条理与逻辑。对于代码改进类的Python书籍,下面几本可以推荐,

几本书结合起来,基本上语言层面的一些用法都差不多涵盖了。

代码实现的好与不好也是需要进行判断的,同样的,同样可以从一些书上获取建议,

虽然这不是以Python为例的书,但很多想法都很值得思考。

多实践

多多实践当然是最有效的方法之一,条件允许下用余闲时间写点自己的代码自然好,如果限于时间精力只能处理工作相关代码的话,也同样可以不断改进。隔一段时间翻看下之前的一些模块代码,看看自己是否还能理解。如果连自己都忘记了,那要么是需求过于复杂,要么是实现不够清晰。在已经发现问题的情况下,带着问题去寻找解决方案往往能有所收获。

阅读代码也是不错的方法,然而漫无目的的去翻看通常不会有太大收益。最好是从当前项目出发去发掘具体关注的局部点,在阅读的同时能带上问题或思考,这样终归是能积少成多的获得进步的。

多思考

语言的细节并不会有那么多,而且在实际项目中通常也不会去使用各种奇技淫巧。实际项目中最影响代码质量的是如何去分析处理需求。好的处理方案、好的模块设计、再到好的代码实现。代码首要的是要解决问题,然后是要优雅的解决问题,再是要高效的解决问题,最后是避免不断创造问题。

所有这些都需要不断进行思考。但思考说的容易,如何能落地去做呢?一个直白的策略就是不断从自己的感受出发,去发现分析自己在项目开发中遇到的问题,然后再去思考自己遇到的问题有无解决方案。当有了结果之后,可以自己动手去尝试,然后再验证。

使用好工具

Python作为语言本身实际并没有太多特性,通常情况不用太多时间对各项特性就都该有了解了。剩下的其实就是如何能够进一步熟悉与加深印象,以及在实际编码中应用各种Best Practice。

编码环境当然是最重要的工具,依然无脑推荐PyCharm。开箱即用,在项目代码中能够消除PyCharm的各种warning提示,那么代码再差也不会差到哪里去。

StackOverflow

StackOverflow的平均问答质量其实一般,但还是沉淀了很多有价值的内容。那些高Vote的问题与回答可以去细看下,有的时候也会有不少收获。多尝试去上面回答问题,即便是那些很小白的问题,很多时候也是需要熟练度才能在第一时间回答。

使用PyPDF2修改PDF中的内容 2017-03-11 20:31:56 numerical /2017/03/11/使用PyPDF2修改PDF中的内容

尝试了一下使用Python来修改创建PDF文件。试用了PyPDF2pdfrw这两个库之后还是前者提供的功能更丰富些,在这里记录下PyPDF2的简单使用。

方法

因为只是API接口的直接调用,也没什么可分析的,直接附上代码,

class Stripper(object):
    def __init__(self, in_path, out_path, mark):
        self.in_path = in_path
        self.out_path = out_path
        self.mark = mark
        self.reader = None
        self.writer = None

    def execute(self):
        with open(self.in_path, 'rb') as inputs:
            self.reader = PdfFileReader(inputs)
            self.writer = PdfFileWriter()
            self.writer.cloneReaderDocumentRoot(self.reader)
            total_nums = self.reader.getNumPages()
            print 'total_nums', total_nums, self.in_path
            for idx in xrange(total_nums):
                page = self.reader.getPage(idx)
                self.strip_annots(page)
                self.strip_content(page)
            with open(self.out_path, 'wb') as outputs:
                self.writer.write(outputs)
                outputs.flush()

    def strip_annots(self, page):
        annots = page['/Annots']
        for link_info in list(annots):
            obj_info = self.reader.getObject(link_info)
            if '/A' not in obj_info:
                continue
            if self.mark in obj_info['/A'].get('/URI', ''):
                annots.remove(link_info)

    def strip_content(self, page):
        contents = page['/Contents']
        for content_info in list(contents):
            content = self.reader.getObject(content_info)
            if not hasattr(content, 'getData'):
                continue
            if self.mark not in content.getData():
                continue
            contents.remove(content_info)

上述 Stripper 类的功能是移除PDF中不想看到的某些内容,具体其实是移除来自it-ebooks.info加在页脚上的水印。大体步骤如下,

  1. PdfFileReader拷贝内容至PdfFileWriter
  2. 分析处理PDF每一页,对其中的内容进行相应的修改移除
  3. 将修改后的结果进行保存

PDF本身的格式没有花时间去了解,上述两个库的文档也都很简略。通过它们的示例能简单了解其功能,更具体的接口需要直接看代码。自己尝试的是用PyCharm断点跟踪来分析相应接口的用法与返回的数据结构。

问题

最先想使用PdfFileReaderaddPage接口一页一页从PdfFileWriter中进行添加,然后进行处理与输出。按照这个思路实现后先后遇到两个问题。

首先是导航书签的缺失,在分析相关接口之后实现了手动添加导航书签的方法,同时好像也发现了库的一出bug。虽然追后没采用这个方法,这里也权且记录一下,

def add_bookmarks(self):
    self._add_bookmark_imp(self.reader.outlines, None)

def _add_bookmark_imp(self, destinations, parent=None):
    bookmark = None
    for destination in destinations:
        if isinstance(destination, list):
            self._add_bookmark_imp(destination, bookmark)
        else:
            page_num = self.reader.getDestinationPageNumber(destination)
            title = unicode(destination.title)
            bookmark = self.writer.addBookmark(title, page_num, parent)

PdfFileWriteraddBookmark存在bug,在标签名称包含unicode字符的情况输出的文件中会显示奇怪字符,对PyPDF2/pdf.py需要修改713行,将,

dest = Destination(NameObject("/"+title + " bookmark"), pageRef, NameObject(fit), *zoomArgs)

修改为,

dest = Destination(createStringObject("/"+title + " bookmark"), pageRef, NameObject(fit), *zoomArgs)

另一个问题是内部跳转关系丢失。在分析代码之后,应当是可以通过PdfFileWriteraddNamedDestinationObject接口来手动创建的。但自己没找到正确的调用姿势,怎么添加在输出的文件中这个关系都还是建立的不正确。

最后还有一个问题是对某些PDF文件直接调用PdfFileReaderPdfFileWriter进行读写会报错,估计是库有问题。

使用ast移除代码中的print语句 2017-02-08 21:44:21 numerical /2017/02/08/使用ast移除代码中的print语句

Python标准库中的ast模块一直没怎么使用,这个过于底层的模块在一般逻辑中很少涉及,刚好最近有个改动,清理代码中残存的print语句,需要通过它来完成。

针对这个问题最直接的想法是直接对代码文件通过正则进行匹配,查找print开头的代码行进行替换。不过粗暴的正则匹配只能解决单行问题,对于跨行的语句是无能为力的。为了保证正确性,需要另外想办法。在一番搜索之后找到的方法是通过标准库中的ast模块将代码解析成AST之后,遍历AST,替换掉print语句节点,再将修改后的语法树导出成代码文件。步骤非常简单,实现也并不复杂。

AST的解析与导出

StackOverflow的这个问题的答案中推荐了astor用于将AST导出成代码文件。代码解析可以使用此模块也可以直接使用ast。

def parse_and_export(input_path, output_path):
    with open(output_path, 'w') as outputs:
        outputs.write(astor.to_source(astor.parsefile(input_path)))
    outputs.flush()

替换AST节点

上面parse_and_export只是进行了简单的解析与导出并未对AST进行修改,那么如何进行修改替换?

ast文档中对此进行了介绍,

class ast.NodeTransformer
    A NodeVisitor subclass that walks the abstract syntax tree and allows modification of nodes.
    The NodeTransformer will walk the AST and use the return value of the visitor methods to replace or remove the old node. If the return value of the visitor method is None, the node will be removed from its location, otherwise it is replaced with the return value. The return value may be the original node in which case no replacement takes place.

由此可知,继承NodeTransformer,并提供相应的visit_xxx方法即能够完成需求。

class RemovePrint(NodeTransformer):

    def visit_Print(self, node):
        return ast.Pass()

将上述代码整合,

def remove_print(input_path, output_path):
    with open(output_path, 'w') as outputs:
        outputs.write(astor.to_source(RemovePrint().visit(astor.parsefile(input_path))))
    outputs.flush()

参考

BSON增加自定义类型处理 2016-12-03 20:02:16 numerical /2016/12/03/BSON增加自定义类型处理

在通过bson进行数据传输时经常会遇到BSON无法转化自定义类型的异常。从其代码中没有发现提供转换接口,一般只能在外界先将数据转化之后,再交由bson进行编码。

很多时候这种转化代码并不方便,看着也不简洁。因此考虑直接在BSON库中增加自定义类型的转化处理。

修改bson/__init__.py

bson主体逻辑都在bson/__init__.py内,从其代码分析,如果能动态增加bson._ENCODERSbson._ELEMENT_GETTER中的匹配关系就能够实现自定义类型的转化处理。

def update_bson(identifier, klass, encoder_func, decoder_dunc):
    bson._ENCODERS.update({
        klass: encoder_func,
    })
    bson._ELEMENT_GETTER.update({
        identifier: decoder_dunc,
    })

def encoder_func(name, value, dummy0, dummy1):
    pass

def decoder_func(data, position, dummy0, dummy1, dummy2):
    pass

bson数据进行编码时其格式为,

ID + NAME + DATA1 + DATA2 + ...

解析的时候也是按照写入顺序进行解析。

实现一个简单的样例,

IDENTIFIER = b'\x91'

class Foobar(object):
    def __init__(self, value):
        self.value = value

def encode_foobar(name, value, dummy0, dummy1):
    return IDENTIFIER + name + bson._PACK_FLOAT(value.value)

def decode_foobar(data, position, dummy0, dummy1, dummy2):
    step = 8
    end = position + step
    value = bson._UNPACK_FLOAT(data[position:end])[0]
    return Foobar(value), end

在不更新之前执行,

print bson.BSON(bson.BSON.encode({'foobar': Foobar(42)})).decode()

会遇到如下异常,

bson.errors.InvalidDocument: cannot convert value of type <class '__main__.Foobar'> to bson

在更新处理之后,

update_bson(IDENTIFIER, Foobar, encode_foobar, decode_foobar)
print bson.BSON(bson.BSON.encode({'foobar': Foobar(42)})).decode()

可以获得正常输出,

{u'foobar': <__main__.Foobar object at 0x02376570>}

处理cbson

在存在cbson时,bson/__init__.py中的编解码函数会被替换成cbson中的实现。因此无法再动态修改,如果想继续使用cbson,那么就需要修改bson/_cbsonmodule.c中的实现。

encode数据需要修改,

static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
                                    int type_byte, PyObject* value,
                                    unsigned char check_keys,
                                    const codec_options_t* options)

decode数据需要修改,

static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
                           unsigned* position, unsigned char type,
                           unsigned max, const codec_options_t* options)

在上述两个函数中分别实现对应的编解码逻辑即可。如果不想每次增加新类型都需要重新编译修改cbson中的实现,那么就可以考虑在cbson中遇到这些自定义数据时反向调用脚本层中的处理逻辑。