Follow Me: ArcPy(2)

木有想到隔了这么长时间才开始写这个系列的第二篇。2014年就这样过去了。在这段时间里,我在开篇中谈到的使用arcpy的项目已经上线,用户非常满意,邀请我们参加他们的board meeting,在会上狠狠的把我们表扬了一番。主席指着我们说:“你看,他们居然用XX万就作出了这么好的app!”。说实话,我怎么感觉他在骂我,还居然?也许是主席大人也意识到了这个问题,会后专程来和我攀谈,最后握着我的手说:“你放心,我们决定上二期。合同肯定还是你们的!由于funding cycle和fiscal year的原因,金额暂时无法确定,细节让我的小弟和你们老板谈。”听到这里,我觉得他其实可以再侮辱我一次。。。

这个项目中的arcpy完全是由小弟完成的。我只是在最开始时给了他一个用model builder生成的网络分析prototype。后来被他改的面目全非,性能奇差无比。最后不得不逐句打印每个arcpy调用的执行时间。还好,我们发现了原因:是线段截断这个本不该消耗太多时间的函数成了瓶颈!找了一个alternative后,性能大幅提升!至于为毛会这样,借用黄西在RTCA年会上的一句话:who care!木有办法啊,谁叫小笨懒呢!

貌似读者也发现了,其实俺还是没有能够有hands on的使用arcpy!那我们这个系列是不是就要说the end了呢?还好,我找到了一本讲arcpy的书《Programming ArcGIS 10.1 with Python Cookbook》。我决定就结合着这本书的结构和例子,用我自己的方式给大家来播讲这本书。所以首先对Eric Pimpler先生对这个系列提供素材表示感谢。

什么?我听见你在喊:“那是读书笔记而已!” 真的吗?你看了就知道了。Follow me and I won’t fail you!

令人困惑的Python Import系统

在上一节中讲过了,arcpy是封装成一个Python的模块包的。因此在使用arcpy之前,必须从中引入相关的模块。以下是书中给出的第一个代码实例:
import arcpy.mapping as mapping
mxd = mapping.MapDocument("CURRENT")

小笨认为第一行import需要一些额外的解释。首先,请记牢:如果import在最前面,import后面跟着的的只能是模块!(考试必背)

以上的import语句翻译成中文是:从arcpy这个包中中引入mapping模块,并用mapping作为arcpy.mapping模块的别名。正因为如此,在脚本中才可以直接使用mapping.MapDocument(),因为在这里的别名mapping会被替换为arcpy.mapping。这是一个对于Python import在2.0版本以后加入的一个扩充。喜欢打破沙锅问到底的同学请戳这里(PEP221)
因此,如果你有如下的import方式(木有as,怎么听上去像木有菊花。。。),那在代码中引用mapping这个模块中的类型,变量时,必须使用模块的全名,那就是“arcpy.mapping“。
import arcpy.mapping
mxd = arcpy.mapping.MapDocument("CURRENT")

书中例子的等价写法可以是:
from arcpy import mapping
mxd = mapping.MapDocument("CURRENT")

这里你依然可以不使用模块的全名(木有包名)而直接引用其中的对象。一些python程序员推荐这种用法。下面会详细展开。
你可能会开始挖苦我了:“小笨,原来你是孔乙己啊!回字有几种写法啊?“
且慢,容我解释:之所以要在这里把import掰开揉碎的讲,是因为这是Python中一个非常tricky的地方。我更愿意叫它Python Import System。令人疑惑的是,直到Python 3.3以后,我们才能在官方文档中找到import的一整章节的系统介绍:The Import System
下面请允许我细细道来!请注意小笨会多次提及的两个问题:
1、变量/类型/函数是如何被引用的;
2、代码/库/模块是如何被加载的;
小笨要开始掉书袋了,如果你对程序语言考古不感兴趣,请忽略“******“分割线中的部分。
分割线开始**************************************分割线开始
以下凡是涉及操作系统的内容,若无说明,都默认为UNIX/Linux平台:
import这个概念,以小笨接触过的程序语言,最早可以追溯到C。C里面有include这个指示符,目的是引入头文件。我们知道在C这样一个强类型,静态语言中,使用变量和函数前都必须声明的。头文件的作用是提供一个类型定义,宏定义,函数声明的场所,以便多个C代码文件可以引用。从编译的过程来看,头文件的引入和展开是由预处理程序在编译开始前就完成的。顺便说一句,一个C程序的编译流程基本是:预处理-〉编译-〉优化-〉汇编生成-〉机器码生成-〉链接。不了解这个过程的同学请自行脑补!
如果你只是编译目标文件的话,引入头文件而不提供实现,编译器表示毫无压力。让子弹飞一会儿,反正那是链接器的工作。
如果我们试图生成一个可执行文件,而头文件对应的实现已经在二进制的程序库里,这个库的加载时机是由库的实现形式决定的。如果是一个静态库(可以理解为目标文件的合集),对应的目标文件会在编译时由链接器加入可执行文件中,作为可执行文件的一部分。如果这个静态库不在ld的查询路径上,链接器会报告“无法解析的引用”。被链接进可执行文件的静态库的目标文件存在于可执行文件的.text区;有关UNIX/Linux下可执行文件的结构, ELF (Executable and Linkable Format),请参考wiki
如果是一个动态库,对应的库文件是在运行时由动态链接器(dynamic linker, run-time linker 或者也可以叫 loader)map进对应的程序进程内存空间的。可执行文件在编译时,会把这个dynamic linker的路径或者 linker stub 写入可执行文件的.interp区。在运行时,kernel会根据这个信息首先加载dynamic linker,然后由dynamic linker完成对可执行文件所依赖的动态库的加载。如果相应的动态库不在动态链接器的查询路径上,程序运行失败并抛出:“在加载动态库时发生错误,无法找到指定的文件”。
动态链接器(Linux下ld.so, ld-linux.so* – dynamic linker/loader)的搜寻顺序见其MAN Page:
  • (ELF only) Using the DT_RPATH dynamic section attribute of the binary if present and DT_RUNPATH attribute does not exist.  Use of DT_RPATH is deprecated.
  • Using the environment variable LD_LIBRARY_PATH.  Except  if  the executable is a set-user-ID/set-group-ID binary, in which case it is ignored.
  • (ELF only) Using the DT_RUNPATH dynamic section attribute of the  binary if present.
  • From the cache file /etc/ld.so.cache which contains a compiled list of candidate libraries previously found in the augmented library  path.  If, however, the binary was linked with -z node-flib linker option, libraries in the default library paths are skipped.
  • In the default path /lib, and then /usr/lib. If the binary was linked with -z nodeflib linker option, this step is skipped.
后来的C++为了表示自己相对C的高大上,又引入了名空间的概念。目的是解决重名的问题。设想一下,如果一班有一个杨伟同学,二班也有一个杨伟同学,一班二班一起开会,老师叫杨伟起立,我们应该可以看到两个男孩垂头丧气的站起来:“父母不给力啊!起了这么一个烂名字!有木有?”(叫杨伟的同学请不要打我,这是小笨说的。。。)
怎样解决这个问题呢?
#include <iostream>
using namespace std;

像这样,只有iostream这个头文件中定义在std这个名空间之内的类型,函数,变量,才会被引入当前代码文件。如果木有第二行的声明,引入定义在ostream中的标准输出流实例cout,就只能使用包含名空间的全名:

std::cout<<"hello!"

其中的“::“就是C++作用域操作符。

话说C++后来还是蛮拼的,把头文件发挥到了极致,实现了标准模板库STL。这就离题太远了。。。
对于库的加载时机,C和C++木有区别。
下面说说Java。在Java中,同样为了解决“杨伟同学的问题“,隐式的使用了C++的名空间概念,改名叫包(Package),并使用了import这个指示符。下面的代码表示引入util包中的ArrayList这个类(ArrayList存在于java.util这个名空间中):
import java.util.ArrayList;

有上面的import撑腰,在代码中可以直接引用ArrayList:

ArrayList<String> list = new ArrayList<String>

和C++一样,如果不显式引入ArrayList,定义一个ArrayList就必须使用全名:

java.util.ArrayList<String> list = new java.util.ArrayList<String>

是不是多敲了很多字符啊?所以Java的import,其实就是个“语法糖“。小笨比较懒,木有深入过Java编译器,但据小笨猜测,这个全名的替换过程也是在预处理阶段完成的。在运行时木有作用。 那Java里库是如何被加载的呢?当然是运行时呗!大家都知道Java程序的byte code是由虚拟机加载执行的。在虚拟机执行程序之前,class loader会得到一个需要加载的包的名单,然后根据预先指定的包路径按图索骥。如果啥都木有发现,jvm就会抛给你一个经典的运行时异常:ClassNotFoundException。。。这个过程和import没有半毛钱的关系!

Java也允许使用通配符引入一个包中的所有可访问对象:
import java.util.*;
另外需要额外指出的是:由于Java的类定义是可以有访问修饰符的(access modifier),如果在class定义前使用public,
package Barfoo;
public class Foobar{
......
}

这就意味着Barfoo中的类Foobar可以被任何其他类import(引用),无论是Barfoo包内还是包外。反之,如果我们忽略访问修饰符:

package Barfoo;
class Foobar{
......
}

Barfoo包外的任何类是无法访问Foobar的。换句话说,包Barfoo外,无法import类Foobar。
前面说了,包的加载和import无关,那是有Java Class Loader完成的。当虚拟机启动时,default会有三种Class Loaders:
  • Bootstrap class loader // 负责加载 JAVA_HOME/jre/lib 内的包包
  • Extensions class loader // 负责加载 JAVA_HOME/jre/lib/ext 或者 java.ext.dirs 内指定的包包
  • System class loader/Application class loader // 负责加载 CLASSPATH (java.class.path) 上的包包
Java允许插入第三方的Applicatoin Class Loader。小笨觉得,JEE中的那些 container就是靠这个加载WAR的吧?
Java的这些class loaders在一起形成了一个树状结构。还造出了一个听上去灰常fancy的模型:The parent-delegation model
这里说要说明的是:由于C语言和主流操作系统的暧昧关系,以上有关ELF文件加载执行的部分实际上是由操作系统完成的。相比C, Java就要苦逼多了,既然决定喝着咖啡玩儿虚拟机,就不能指望操作系统的帮忙。听着C高唱着游击队员之歌,Java叹了口气:“还是自己动手,丰衣足食吧!“
C#嘛,就不赘述了,和Java相似(其实是因为小笨实在是写烦了。。。)。
分割线结束**************************************分割线结束


写了肿么多,终于绕回Python import系统了!Python作为一种弱类型的,动态,脚本语言,其import系统有一些不同于以上语言的特征(其实我是想说:Python哥,真的要肿么暴吗?)。Python的import系统之所以复杂,是因为它把上面几种语言分步做的事儿,all in one,一步到位了!可俗话说的好啊:步子大了容易扯到蛋。Python import系统就是一扯淡的货!import系统用来加载模块(Module),也可以用来加载包包(Package),还可以用来引入变量,函数,类!import系统还负责进行赋值。。。很多时候小笨看到引入语句,都不清楚到底引入的是个啥货。C++
STL好歹还有个关键字叫typename。
T::bar * p;

上面的代码歧义不?

那这个呢?
typename T::bar * p;
想当年,小笨在论坛Java版翻帖子,看到大神们故作深沉的教导新手:“掌握Java,就要首先搞清楚Java包机制!“当时觉得,噢赛!机制,好高大上的词啊!现在看看Python Import系统,机制就爆弱了!一个小小的肩膀上扛了肿么颗大头,我真怀疑Python的设计者也是个BS!小笨可木有骂人的意思哦,我是想说Python的设计者也想做一个C++出来!C++她爸不是叫Bjarne Stroustrup嘛。。。

当小笨在胡说八道时,我听到你在喊:“笨老师,是你自己说的(还黑粗体了):import后面跟的肯定是模块?现在肿么又说还可以加载包?”。呵呵,小笨早有准备,立即引经据典的翻出Python官方文档:
It’s important to keep in mind that all packages are modules, but not all modules are packages. Or put another way, packages are just a special kind of module. Specifically, any module that contains a __path__ attribute is considered a package.

所有的包都是模块,而不是所有的模块都是包。经典的充分必要条件问题啊!
是不是觉得这个import有72变啊?好了,考试必背又来了:
Python Import系统其实只有五个变化:

  1. import <module/package, full qualified name>
  2. from <module/package, full qualified name> import whatever_the_shit_might_be
  3. import <module/package, full qualified name> as whatever_local_variable_u_want_to_call_this_module
  4. from <module/package, full qualified name> import whatever_the_shit_might_be as whatever_local_variable_u_want_to_call_this_whatever_shit
  5. from  <module/package, full qualified name> import *
*是一个通配符
举例说明一下:
Package Foo有一个Module叫Foobar。Foobar里定义了一个变量叫SB和一个函数叫DSB。以下都是合法的引用:
import Foo
import Foo.Foobar
from Foo import Foobar
import Foo.Foobar as foobar
from Foo import Foobar as foobar
from Foo.Foobar import SB
from Foo.Foobar import SB as sb
from Foo.Foobar import DSB
from Foo.Foobar import DSB as dsb
from Foo.Foobar import SB, DSB
from Foo.Foobar import SB as sb, DSB as dsb
from Foo.Foobar import *

以下是非法的引用:

import Foo.Foobar.SB
import Foo.Foobar.DSB

为啥呢?老师叫你背过了嘛:如果import在最前面,import后面跟着的的只能是模块!SB不是模块,只能通过from…import…来引入。

好,我知道你已经开始皱眉了,是不是已经感觉too much了?呵呵,小笨还没讲完呢!下面讲讲import的赋值意义。
前面讲过的Import的五种变化。其中前两种变化的赋值意义可以用一句话总结:
The first 2 statements load in a module, and either bind that module to a local name, or binds objects from that module to a local name.

第三,第四种变化的赋值意义:
Not like 1 and 2, the third and fourth approaches bind the module/object to an alternative name, for instance to avoid name clashes.
举例说明下:
import arcpy.mapping
mxd = arcpy.mapping.MapDocument("CURRENT")
mapping作为一个当前脚本作用域内的一个对arcpy.mapping的引用。换句话说,在当前的脚本中,有一个指向mapping模块的变量,它的名字叫arcpy.mapping。
而from…import…:
from arcpy import mapping
mxd = arcpy.mapping.MapDocument("CURRENT")

以上的语句等价于:

import arcpy.mapping
mapping=arcpy.mapping
mxd = mapping.MapDocument("CURRENT")
因为mapping是一个模块,上面这个例子还没有展示from…import…一个隐晦的行为。考虑以下module:
#module:Bar.py

A=10
B=[1,10]

下面用from..import…来引入A和B,并改变其值:

#script1.py
from Bar import A, B


A=100
B[0]=1000

如果你再次引入Bar through import Bar,pring Bar.A, Bar.B,你会发现A的值没有改变,而B变为了[1000,10]。在参数传递上,Python也是号称“传值”的。这个坑爹啊!传值是不假,但只在primitive type和string上100%正确(string是因为它的常量性, immutable)。其他的变量,包括List,传递的是变量引用的值,其实就是变量本身!(我听见了C++在偷笑)。再看上面的例子,通过from…import…A作为script1.py的一个本地变量被赋予了Module1.A的值,B作为script1.py的一个本地变量被赋予了Module1.B的引用(Python貌似叫mutable
object)。改变A的值只会影响本地变量;改变B的值,由于是引用,Module1中B也会被改变。

写到这你是不是已经在看表,等待下课铃声呢?
下面结合Package arcpy粗略讲一下Python Import System的加载过程和Python Package的结构。喜欢偷懒的同学可以忽略以下“******“分割线中的部分
分割线开始**************************************分割线开始

当Python的解释器看到import时,default importer (由finder和 loader两部分组成)中的finder 会根据指定的包路径寻找模块。这个路径搜索的顺序是:
首先python内建模块(built-in modules,可以使用sys.builtin_module_names来查看内建模块有哪些。使用如下的代码在小笨的机器上:
import sys

for name in sys.builtin_module_names:
    print name

得到一个长长的名单:

__builtin__
__main__
_ast
_bisect
_codecs
_codecs_cn
_codecs_hk
_codecs_iso2022
_codecs_jp
_codecs_kr
_codecs_tw
_collections
_csv
_functools
_heapq
_hotshot
_io
_json
_locale
_lsprof
_md5
_multibytecodec
_random
_sha
_sha256
_sha512
_sre
_struct
_subprocess
_symtable
_warnings
_weakref
_winreg
array
audioop
binascii
cPickle
cStringIO
cmath
datetime
errno
exceptions
future_builtins
gc
imageop
imp
itertools
marshal
math
mmap
msvcrt
nt
operator
parser
signal
strop
sys
thread
time
xxsubtype
zipimport
zlib
这里面据说既有用C写的模块,也有用Python写的。
如果需要引入的模块不在这个表中,据说loader会按以下的顺序试图找到这个模块:
  • the directory containing the input script (or the current directory). #调用import的当前脚本所在的文件夹
  • PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH). #如果没有,就按PYTHONPATH这个环境变量中指定的路径去寻找
  • the installation-dependent default. #如果PYTHONPATH 木有,就按操作系统默认的Python安装路径下去寻找。在小笨的机器上,这个路径在“C:\\Windows\\system32\\python27.zip“
以上三点所包含的路径的合集可以在sys.path里找到。对了,注意到没?Python标准库的打包方式是zip。这和Java的jar是一样的。
一旦找到了,就查看该模块是否已被编译,如果没有会首先编译该模块为byte code,然后由loader加载并执行这个模块(这个加载并执行的过程在当前脚本执行过程中,只会发生一次)。
第一章里小笨说过,在我的机器上,arcpy这个Python Package是安装在如下路径的:“C:\\Program Files (x86)\\ArcGIS\\Desktop10.2\\arcpy”。这个路径也同时存在于PYTHONPATH里。如果你自己去看看这个文件夹,你会发现这个arcpy里面,还有一个子文件夹也叫arcpy!并且所有的模块其实都在这个子文件夹arcpy里!这是为啥?因为这是Python规范要求的Python package标准。
The leftmost component in a package import path is still relative to a directory included in the sys.path module search path list.
结合arcpy的实例就是:arcpy这个出现在import内容中最左边的字段,必须可以作为sys.path中某个路径的相对路径访问。换句话说:arcpy必须是某个在标准sys.path上的路径的子目录。在我们的sys.path上有:“C:\\Program Files (x86)\\ArcGIS\\Desktop10.2\\arcpy”,在添加上import中最左边的字段arcpy,我们得到了一个新的路径:“C:\\Program Files (x86)\\ArcGIS\\Desktop10.2\\arcpy\\arcpy”。mapping模块应该在这个目录下!
以下是arcpy包的大致文件结构:
arcpy—-
              |
             arcpy—-
                           |
                         __init__.py
                         mapping.py
                         _mapping.py
                         other_module.py
                         sub_module—
                                                   |
                                                 __init__.py
                                                 ……
                          ……
细心的读者肯定注意到了那个奇怪的文件:__init__.py。这个不是Python的类构造函数,这个文件叫包/模块初始化文件(Package/Module Initialization file)。在Python3.3之前,包中每一个用作Module的文件夹中都必须有这么一个文件,不然import就会失败!霸王条款吧!这个__init__.py可以是一个空文件,也可以把初始化的工作,譬如定义变量,引入其他模块等放入这个文件。当一个模块被第一次import的时候,相应的__init__.py就会被执行。其实,__init__.py还有其他一些功能,小笨老师就不展开了,毕竟要下课了嘛!
最后,解释一个Python中特有的下划线变量,函数,文件问题。在PEP8中,有如下命名约定。尽管这个是针对代码的,但小笨认为对这些文件命名同样适用:
  • _single_leading_underscore : weak “internal use” indicator. E.g. from M import * does not import objects whose name starts with an underscore.
  • __double_leading_and_trailing_underscore__ : “magic” objects or attributes that live in user-controlled namespaces. E.g. __init__ , __import__ or __file__
以‘_‘开头的对象,在使用from…import *时,这些变量不会被引入;同样,如果我们考虑文件命名:_mapping.py就表示一个内部实现文件,请不要引用。Don’t touch me! 可是,流氓是不会理会女神的力争言辞的,如果你非要引入她,木有人能拦着你。但是,既然是内部实现,你不该动她,否则 on your own risk! 你肿么知道这不是警察叔叔的圈套?女神也可以是女警。。
以两个‘_‘开头的对象叫“魔术”对象;注意,这里的对象可以是变量,也可以是函数。
分割线结束**************************************分割线结束
好了,写了这么多,终于可以结束了。解释了这么多小笨和Python不得不说的故事,在下一章里,我们终于可以开始以《Programming ArcGIS 10.1 with Python Cookbook》的章节为“主线“,探索一下arcpy的功能和实现了!
So, follow me and let’s explore Arcpy!

转载自:https://blog.csdn.net/micropentium6/article/details/42360983

You may also like...