python小技巧与坑

python小技巧与坑

这几天打算用python写个小爬虫,就找出先前写python时候整理的一份小文档,顺便把它贴到博客上面来,以后如果还有其他心得,也一并记录在这里好了。

1. 时间处理

import time
print time.strftime( '%Y-%m-%d %X', time.localtime())

python中有四种时间表示方式: 字符串,时间戳,time.struct_time类型,datetime.datetime类型

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
import datetime


def main():
    # python中的时间有四种表示方式

    # 1.字符串表示
    time_string = "20170918123059"
    print type(time_string), time_string

    # 2.时间戳,一个浮点数,表示从基准时间(1970.01.01 00:00:00)到该时间点所经过的秒数,小数点后面表示毫秒数
    time_stamp = time.time()
    print type(time_stamp), time_stamp

    # 3.time.struct_time类型,该类型其实是一个命名元组(named tuple,类似C语言的结构体)
    time_struct = time.localtime()
    print type(time_struct), time_struct

    # 4. datetime.datetime类型 (还有datetime.date类型,只表示日期,没有时间。)
    dt = datetime.datetime.now()
    print type(dt), dt

    print "================ 互相转换 ==================="

    # 时间戳 => struct_time, 使用time.localtime()
    st = time.localtime(time_stamp)
    print st

    # struct_time => 时间戳, 使用time.mktime()
    ts = time.mktime(time_struct)
    print ts

    # struct_time => 字符串, 使用time.strftime(format, t)
    str = time.strftime("%Y-%m-%d %H:%M:%S", time_struct)
    print str

    # 字符串 => struct_time, 使用time.strptime(string, format)
    st = time.strptime(time_string, "%Y%m%d%H%M%S")
    print st

    # datetime => 字符串, 使用datetime.strftime(format)
    str = dt.strftime("%Y-%m-%d %H:%M:%S")
    print str

    # 字符串 => datetime, 使用datetime.strptime(date_string, format)
    dt = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%S")
    print dt

    # datetime => struct_time, 使用datetime.timetuple()
    print dt.timetuple()

    # datetime => timestamp, 使用datetime.timetuple()得到struct_time类型再转行成timestamp
    print time.mktime(dt.timetuple())

    # timestamp => datetime, 使用datetime.fromtimestamp()
    print datetime.datetime.fromtimestamp(time_stamp)

    print "================ 时间差 ==================="

    # 计算两个时间的差,最好用时间戳相减,结果即为相差的秒数(小数点后为毫秒)

    # 计算上个月的最后一天,可以使用datetime.timedetla类型
    now = datetime.datetime.now()
    print "当前时间:",  now
    print "三小时前:", now - datetime.timedelta(hours=3)
    print "三天前:", now + datetime.timedelta(days=-3)
    print "上个月最后一天:", datetime.datetime(day=1, month=now.month, year=now.year) + datetime.timedelta(days=-1)

    pass

if __name__ == "__main__":
    main()

屏幕快照 2017-09-19 上午12.13.14

2. 过程计时

import timeit
StartTime_timeit = timeit.default_timer()
#procdure
EndTime_timeit = timeit.default_timer()
print "Procedure elapsed ", EndTime_timeit - StartTime_timeit, " seconds"

不要用python 2.7中time模块的time.time()或者time.clock()函数,这两个函数都是平台相关的。在python3.3+版本,time.clock()也已经被取消。

3. 输出错误信息

# -*- coding: utf-8 -*-
import sys 
import traceback
import logging

def divisionzero():
    return 1/0
try:
    divisionzero()
except Exception, e:
    #打印错误信息
    print sys.exc_info()[:2]

    #打印traceback信息
    print traceback.format_exc()
    
    #打印错误信息
    #如果用logger就是logger.error()
    logging.error("错误信息: %s", e)
    
    #打印traceback信息
    #logging.execption其实是logging.error的升级版,多加了traceback信息
    #注意这个方法只能用在except下面
    logging.exception("错误信息traceback:")

注意,第一行# -*- coding: utf-8 -*-是为了让python识别文件中的中文。

 

4. 线程不安全

python有不少模块是线程不安全的。

mysql.connector 的cursor、connector变量也是线程不安全的,千万不要在线程里面共享。

logging模块则是线程安全的。

5. 多线程中让主线程等待子线程完成后退出

def main():
    threads = []		# the arry of thread object
    thread_1 = Thread()		# build one thread object
    thread_2 = Thread()		# build another thread object
    threads.append(thread_1)
    threads.append(thread_2)
	
    for t in threads:
        t.start()		# start running a thread in background
        # t.join()              # join will block the calling thread (main-thread) to wait the sub-thread ends.

并不需要额外调用 Thread.join(),因为主线程本来就会等待所有(非 deamon)子线程退出后才终止!(可以设置 Thread.daemon = True,daemon 线程会在主线程结束时候被强制退出)

The entire Python program exits when no alive non-daemon threads are left.

from docs.python

Thread.join() 是阻塞主线程,让主线程等待被调线程结束或者等待超时后才继续运行。

注意:由于全局解释器锁(Global Interpreter Lock,GIL)的作用,python的多线程只会在一个CPU核上运行。所以对于CPU密集型的程序,python的多线程其实根本没有帮助;对于IO密集型的程序的话,还是可以用用的,比如爬虫。

如果想要利用CPU的多个核,最好使用python的多进程模块multiprocessing。参考使用 Python 实现多进程

PS:解释型语言天生都有多线程短板。python如此;javascript根本就没有多线程的概念,不过他有反人类的异步回调机制;php是后来出了pthreads扩展才支持的多线程。

6. 关于urllib2的坑

(1) urllib与urllib2有个缺陷,就是他们是不可重用链接的,即使你在request的hearder中添加connection:keep-alive信息,但在urllib与urllib2的open函数底层,他会自动覆盖这个header为connection:close。即每次发生一个请求后,都会关闭连接,所以每次response响应都返回connection:close的头。下次请求时就重新建立链接。。。 =。=#

要想可重用链接,可以使用urllib3

(2) 如果在多线程中使用不同代理的话,就不能使用urllib2.urlopen()了,因为urllib2.install_opener() 安装的是一个全局变量。而是应该在线程里直接用opener.open()

proxy_ip = random.choice(ip_list)
proxy_support = urllib2.ProxyHandler({'http':ip, 'https':ip})
opener = urllib2.build_opener(proxy_support)
req = urllib2.Request(url)
response = opener.open(req, timeout=3)
html = response.read()

7. 程序入口 if __name__ == '__main__':

python的一个脚本文件,既可以独立执行,又可以当做一个模块被引入另一个文件。

def test1():
    print "i'm in A.test1 function"
    return

def test2():
    print "i'm in A.test2 function"
    return

if __name__ == '__main__':
    test1()

以上面这段代码为例,A.py可以作为一个模块被引用,这时它有两个函数A.test1()跟A.test2();如果单独执行python A.py,那么if __name__ == '__main__':就相当于是该脚本的程序入口,将会执行test1()函数。

这在自定义模块时候非常好用,通常我们会在__main__入口下面放些测试代码,用来单独测试模块的可用性。我觉得这是一个好的python编程习惯。

8. with ... as ...

推荐使用with ... as ...来打开文件

with open("hello.txt") as hello_file:
    for line in hello_file:
        print line

该用法相当于如下这段try ... except ... finally

try:
    open_file = open('hello.txt')
except:
    print 'no such file!'
else:
    try:
        for line in open_file:
            print line
    except:
        print 'read error!'
    finally:
        open_file.close()

9. python的类

9.1 class virables与instance virables

class virables有点像类静态变量的意思,所有对象共享该变量(实际上这样讲不严谨,因为对象有可能覆盖一个class virable为instance virable),

instance virables就是普通的变量,每个对象有一份。

参考https://docs.python.org/2/tutorial/classes.html#class-and-instance-variables

9.2 私有变量、私有方法、类方法、静态方法

# -*- coding: utf-8 -*-

class MyClass(object):
    # class virables
    classVirable = "类变量"

    def __init__(self):
        super(MyClass, self).__init__()

        # instance virables
        # 成员变量最好定义在 __init__ 中,如果暂时无值可以用self.variable=None表示
        self.variable = "成员变量"
        self.__privateVariable = "私有成员变量"  # 「双下划线开头」表示私有,外部无法访问,也无法被继承
            # 另外,通常建议使用「单下划线开头」表示 protected
            # 尽管单下划线开头并不具有实际意义(python 解释器并不认),在外部还是可以正常访问的。
        # 实际上,python 类的成员变量可以无需事先声明。在类实例化后,直接 class_instance.new_attr = 123,也是可以的。。。
        # 不过这也仅是针对该实例的成员变量,其他新建实例默认不会有该变量。所以按惯例,最好是在 __init__ 初始化函数中声明成员变量。
        pass

    # 双下划线开头为私有
    def __privateFunc(self):
        print "这个是私有方法"
        pass

    # python在调用普通实例方法时候,会默认传入实例本身作为第一个实参
    # 因此定义类的实例方法,必须定义第一个参数为self
    def normalFunc(self):
        print "这个是实例方法"
        print "可以直接调用私有变量与私有方法:", self.__privateVariable
        print self
        pass

    # python在调用类方法时候,会默认传入类类型作为第一个实参
    # 因此定义类的类方法,必须定义第一个参数为clazz
    @classmethod
    def classFunc(clazz):
        print "这个是类方法"
        print clazz
        print clazz.classVirable
        pass

    # python在调用类静态方法时候,不会修改要传递的参数
    # 因此定义类的静态方法,无需额外增加self或者clazz参数
    @staticmethod
    def staticFunc():
        print "这个是静态方法"
        pass


def main():
    c = MyClass()

    # 类变量,可以通过类名或实例调用,在__init__之前被赋值给实例
    # 通过实例修改类变量的话,其实是新建了一个同名成员变量,并不会修改到类变量
    print MyClass.classVirable, c.classVirable
    MyClass.classVirable = "通过类名修改类变量"
    print MyClass.classVirable, c.classVirable
    c.classVirable = "通过实例修改类变量"
    print MyClass.classVirable, c.classVirable

    # 成员变量,不能通过类名调用,只能通过实例调用
    print c.variable

    # 直接调用私有变量或者私有方法会报错 >> AttributeError: 'MyClass' object has no attribute '__privateVariable'
    # print c.__privateVariable
    # c.__privateFunc()

    # 私有变量与私有方法,在python中其实并不真的私有!而是被改名了
    print c._MyClass__privateVariable
    c._MyClass__privateFunc()

    # 使用实例调用实例方法,python会自动将实例作为第一个参数传入
    c.normalFunc()
    # 使用类名调用实例方法,必须传递一个实例作为参数
    MyClass.normalFunc(c)

    # 使用实例调用类方法,python会自动将类类型作为第一个参数传入
    c.classFunc()
    # 使用类名调用类方法,python会自动将类类型作为第一个参数传入
    MyClass.classFunc()

    # 使用实例调用静态方法,怎么定义的就怎么调用
    c.staticFunc()
    # 使用类名调用静态方法
    MyClass.staticFunc()
    pass


if __name__ == '__main__':
    main()

 

10. 迭代器与生成器(yield)

http://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/

 

11. 尽量使用logger = logging.getLogger(name)而不是logging

python的logger是有层次关系的,直接使用logging.info()、logging.error()相当于调用的是root logger。logging.getLogger(name)则是从root logger下派生一个名字为name的子logger,一般使用类名进行命名。logging.getLogger()不加参数默认获取root logger。

import logging

LOGGING_FORMAT = "%(asctime)s - %(levelname)5s - %(name)s: %(message)s"

class AAA(object):
    def __init__(self):
        super(AAA, self).__init__()
        self.logger = logging.getLogger(self.__class__.__name__)

class BBB(object):
    def __init__(self):
        super(BBB, self).__init__()
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.setLevel("WARNING")
        
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG, format=LOGGING_FORMAT)
    logging.info("main func starting")

    a = AAA()
    b = BBB()

    a.logger.info("A INFO")
    b.logger.info("B INFO")
    b.logger.warn("B WARNING")

输出:屏幕快照 2016-04-08 下午10.27.02

 

另外,上面我们在连个类中获取了自己的logger,却没有配loghandler,也依然能够输出日志。是因为默认情况下,logger的所有日志都会上传给其父亲logger,父logger再调用自己的loghandler。所以上面的例子中,负责输出日志的其实都是root logger的默认handler。

我们可以给子logger添加一个自己的loghandler。这时候,日志将会通过子logger的loghandler输出一次,同时也会通过父logger的loghandler输出一次。可以通过logger.propagate=False把日志“传播”给关掉,不上传给父logger。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
import logging.config

class myHandler(logging.StreamHandler):
	"""docstring for myHandler"""
	def __init__(self):
		super(myHandler, self).__init__()
		self.setFormatter(logging.Formatter('%(levelname)5s %(asctime)s %(name)s.%(funcName)s >> %(message)s'))
		self.setLevel(logging.WARN)

def main():
	logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)5s %(name)s.%(funcName)s - %(message)s')
	logging.info('main start')

	logger1 = logging.getLogger('[logger 1]')
	# 实际上是回传给root logger的默认handler来输出
	logger1.info('111 test')

	logger2 = logging.getLogger('[logger 2]')
	# 默认情况下,propagate=True。所有日志都会上传给父logger再处理
	# 所以这里如果不关闭propagate,会发现控制台输出了两次日志
	logger2.propagate = False

	# loghandler的日志等级(如果有设置的话)会自动覆盖logger的日志等级
	logger2.addHandler(myHandler())
	logger2.setLevel(logging.INFO)

	logger2.info('222 info')
	logger2.error('222 error')
	pass

if __name__ == '__main__':
	main()

 

12. logging.config读取log配置文件

用过java的log4j,觉得java的log配置特别方便,那么python的logging.config模块其实也可以做到。

新建一个log.config文件

[loggers]
keys = root

[handlers]
keys = consoleHandler, fileHandler, timedRotatingFileHandler

[formatters]
keys = commonFormatter

############################

[logger_root]
level = NOTSET
handlers = consoleHandler, fileHandler, timedRotatingFileHandler

[handler_consoleHandler]
class = logging.StreamHandler
level = NOTSET
formatter = commonFormatter
args=(sys.stdout,)

[handler_fileHandler]
class = logging.FileHandler
level = NOTSET
formatter = commonFormatter
args=("log/file.log", "a")

[handler_timedRotatingFileHandler]
class = logging.handlers.TimedRotatingFileHandler
level = NOTSET
formatter = commonFormatter
args=("log/rotate.log", 'M', 1)

[formatter_commonFormatter]
format = %(asctime)s %(levelname)5s %(name)s.%(funcName)s - %(message)s
datefmt = %Y-%m-%d %H:%M:%S 
# datefmt可以不设置,按asctime的默认格式输出。

然后修改测试代码如下:

import logging
import logging.config

class AAA(object):
    def __init__(self):
        super(AAA, self).__init__()
        self.logger = logging.getLogger(self.__class__.__name__)
 
class BBB(object):
    def __init__(self):
        super(BBB, self).__init__()
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.setLevel("WARNING")
        
if __name__ == '__main__':
    logging.config.fileConfig("log.config")
    logging.info("main func starting")
 
    a = AAA()
    b = BBB()
 
    a.logger.info("A INFO")
    b.logger.info("B INFO")
    b.logger.warn("B WARNING")

运行该代码,就会发现总共有三处日志输入,分别是控制台、file.log日志文件、rotate.log按时间分割的日志文件。

注意,关于logging handler有一些需要注意的地方:

首先,StreamHandler、FileHandler在logging模块下,但TimedRotatingFileHandler是在logging.handlers模块下。

又注,自带的XXXRotatingFileHandler其实都是针对单进程且常驻内存的进程,对于多进程同时运行,或者单进程瞬时运行(每次只运行一小段时间,但频繁启动)的时候,就不适用了。首先对于多进程的切分,会导致日志丢失的情况,这时候可以考虑用WatchedFileHandler,然后自己写crontab进行日志切分。然后对于单进程瞬时运行的情况,我试过按小时切分的测试脚本,1:30运行的一次测试,理论上2:00再运行一次的时候,日志文件应该被切分的,但它不,因为对于新运行的进程来说,他是去判断文件修改时间的,还不足1小时我天。。。。

另外, 在linux下尽量用WatchedFileHandler(windows不适用),因为当日志文件被移动或删除后:

  1. FileHandler会继续将日志输出至原有的文件描述符, 从而导致日志切分后日志丢失;
  2. WatchedFileHandler会检测文件是否被移动或删除, 如果有, 会新建日志文件, 并输出日志到新建的文件。

13. 编码规范

详细参考:http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/

13.1 命名规范

类名使用CapWords的方式,全局变量名使用全大写辅以下划线的方式,模块名、包名、函数名、局部变量名等都使用全小写辅以下划线的方式:

package_name, module_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_CONSTANT_NAME, global_var_name, instance_var_name, function_parameter_name, local_var_name.

参考:Google style guides

13.2 引号的使用规范

自然语言使用双引号,比如输出:print u"hello, 哈呜"

机器标识使用单引号,比如键名:print people['name']

还是放弃这种用法吧,这样单双引号切来切去太麻烦了。google编码规范现在也建议统一用一种就好了,单双任选。

Be consistent with your choice of string quote character within a file. Pick ' or " and stick with it. It is okay to use the other quote character on a string to avoid the need to \\ escape within the string.

我觉得以后还是统一选择单引号吧!因为不用按shift键。

多行字符串以及文档字符串用三重双引号"""... ..."""而非三重单引号'''... ...'''

13.3 空格

在二元操作符两边都加上一个空格, 比如赋值(=), 比较(==, <, >, !=, <>, <=, >=, in, not in, is, is not), 布尔(and, or, not). 至于算术操作符两边的空格该如何使用, 需要你自己好好判断. 不过两侧务必要保持一致.

Yes: x == 1

No:  x<1

当’=’用于指示关键字参数或默认参数值时, 不要在其两侧使用空格.

Yes: def complex(real, imag=0.0): return magic(r=real, i=imag)

No:  def complex(real, imag = 0.0): return magic(r = real, i = imag)

14. 配置文件解析器ConfigParser

之前我还傻逼逼的自己写配置文件解析。(´・_・`) 后来才发现原来python标准库里面就有。参考文章http://www.cnblogs.com/huey/p/4334152.html

新建配置文件myproject.config

# 配置文件由多个section组成
# 中括号代表一个section
# section下面跟着它的options
[db]
host = 127.0.0.1
port = 3306
user = root
pass = root

[ssh]
host = 192.168.1.101
user = huey
pass = huey

火狐截图_2017-01-17T15-52-55.635Z

读取配置文件时候,推荐使用config.readfp(codecs.open(config_file, 'r', 'utf8'))来代替config.read(config_file),前者读入的字符串将会是unicode类型,后者则是str类型。

15. 装饰器(Decorator)

python 装饰器(Decorator)的语法糖看起来就像java的注解,作用就像面向切面编程。

# -*- coding: utf-8 -*-
from functools import wraps


def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwds):
        print "装饰器 start"
        result = func(*args, **kwds)
        print "装饰器 end"
        return result

    return wrapper


@my_decorator
def main():
    """装饰器测试函数"""
    print "main process start"
    print "main process end"
    pass


if __name__ == "__main__":
    main()
    print "=========================="
    # 如果不在上面的装饰器前加@wraps,那么main方法的名字会变成"wrapper",而且无法找到原本的文档字符串
    print "main函数名字:", main.__name__
    print "main函数文档字符串:", main.__doc__

ps:《用装饰器写一个single instance》

16. python的字符编码(the fucking UnicodeEncodeError)

16.1 字符集(charset)与字符编码(encoding)

字符集:即可使用的字符的集合,同时规定了每个字符对应的二进制码。常见的有ascii、gb2312、unicode。

屏幕快照 2017-06-18 14.59.20

字符编码:规定字符二进制码的存储方式。常见的有ascii编码、gb2312编码、utf-8编码。

ps 1:通常,字符集与字符编码成一一对应关系。比如ascii字符集与字符编码,gb2312字符集与字符编码,但是偏偏unicode字符集却又许多种编码方式,utf-8、utf-16、utf-32都针对unicode字符集的字符编码。

ps 2:字符经过编码后存在内存中的二进制值未必会等值于其在字符集中所规定的二进制值,因为编码可能会对二进制码进行相应的转换。

比如ascii编码规定直接按字符的ascii二进制码存储,占用一个字节。

utf-8编码会对字符的unicode二进制码进行转换后再存储,占用字节数从1~6个字节不等。

utf-16编码同样也会对unicode二进制码进行转换后再存储,占用2或4个字节,同时utf-16编码在存储时候还分大尾序和小尾序。

屏幕快照 2017-06-18 15.17.45

16.2 <type 'str'>与<type 'unicode'>,decode与encode

python2有两种字符串类型,一种是str字符串(byte string),一种是unicode字符串(text string)。

(python3的字符串类型不一样了,变成了str类型与bytes类型。str类型即是unicode字符串,bytes类型则表示二进制字符串)

str字符串可以看做是一个字节数组,即将字符串经过字符编码后得到的二进制字节串。(字符编码可以通过encode()方法指定)

unicode字符串则是一unicode字符数组,每个单元就是一个unicode字符。

我们以 a = "a你好" , b = u"b你好" 这两个字符串做个测试。

屏幕快照 2017-06-18 22.07.37

我们可以看到,type(a)是str字符串,由于我的测试环境是mac,系统默认的中文编码是utf-8,对于字符串长度,其中英文a占一个字节,你好各占3个字节,所以总长度为7。

然后再看字符串b,在python中,字符串前面的u表示该字符串为unicode字符串。所以type(b)得到的就是unicode类型。然后长度就是3个unicode字符。

python有一对专门的函数来进行byte string与unicode string之间的相互转换:encode()与decode()。

decode()将byte string(str字符串)解码成text string(unicode字符串),二进制码串 >> 解码 >> 字符串
encode()将text string(unicode字符串)编码成byte string(str字符串),字符串 >> 编码 >> 二进制码串
所以,调用decode的主体,必须是str字符串;调用encode的主体必须是unicode字符串。

# -*- coding: utf-8 -*-
print "a你好".decode('utf-8')
print u"a你好".encode('utf-8')

print type("a你好".decode('utf-8'))
print type(u"a你好".encode('utf-8'))

print len("a你好".decode('utf-8'))
print len(u"a你好".encode('utf-8'))

 

16.3 # -*- coding: utf-8 -*-与sys.setdefaultencoding('utf-8')

通常,我们需要在python脚本的开头写上一行关于utf-8的注释,如下所示。

# -*- coding: utf-8 -*-
或者
# coding=utf-8

这是因为python解释器默认使用ascii编码来读取脚本文件(它认为文件是以ascii编码存储的),如果文件中出现非ascii字符,就会报错。所以必须在文件开头加上这一行,从一开始就告诉python解释器使用utf-8编码来读取脚本,这样才能在脚本中正常使用中文等非ascii字符。

注意:这一行不是代码,不会执行的,它只是影响Python解释器读取脚本文件时候使用的编码。

然后,对于sys.setdefaultencoding('utf-8'),与上面那行注释的作用不同,它用来指定python在运行时所用的默认字符编码。Python2.x为了兼容处理Unicode字符串(unicode)和str字符串,某些时候会自动做两者的转换,但默认使用的是ASCII字符编码,如果有utf-8字符在里面就会出错。这个hack是修改这个默认特性。非常不建议这么做,你应该永远分清str(bytes)和unicode,然后自己调用encode、decode来转换。Python3.x下这两个类型已经不兼容了。

 

17. 浮点数转换的坑

int()函数将浮点数转换成整数时,是直接截取整数位,而不考虑小数位是否需要四舍五入的!所以浮点数转换成整数需要先手工四舍五入下再转int。否则遇到99.9999999这种直接int就会变成99而不是100了。

if __name__ == '__main__':
    f = [89.99991, 91.00001, 97.00002, 95.99998]
    for i in f:
        print i, int(i), round(i), int(round(i))
        pass

18. 包、模块与类

这三者的关系可以简单的理解为:包是目录,模块是目录下的xxx.py文件,类是xxx.py文件定义的Class。

模块 module,是一个 py 文件。它在第一次导入程序的时候被执行(引入模块文件中定义的类、函数,或直接模块文件中的语句)

包 package,是一个目录,目录中必须包含一个 __init__.py 文件(python3 之后好像不是必须的了)。另外还可以有多个模块文件与“子包”。__init__.py文件可以看作是包的默认模块,该文件可以为空,也可以在其中定义变量、函数、类,或者执行import。

18.1 导入模块文件

以如下目录结构为例,主文件main.py以及模块文件common.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import common

def main():
    c = common.Common()
    c.say_hello()
    pass


if __name__ == '__main__':
    main()
#!/usr/bin/env python
# -*- coding: utf-8 -*-

print "导入本模块的话,这句print会被直接执行"

class Common(object):
    """docstring for Common"""
    def __init__(self, arg=None):
        super(Common, self).__init__()
        self.arg = arg

    def say_hello(self):
        print "hello, it's common.py Common.say_hello()"
        pass

这个主程序的执行结果是

18.2 import module与from ... import ...的区别

import common
# 表示导入common模块,
# 这时候想要调用Common类的话,必须加上前缀
# c = common.Common()

from common import Common
# 表示从common模块中导入Common类
# 这时候想要调用Common类的话,只需要
# c = Common()

import关键字后面跟的都是module。即使有时候看起来是包名,其实import package等价于import package.__init__,而__init__是这个包的默认模块。

from ... import ...的用法可以是:

from module import Class/func/variable

from package import module

18.3 导入包中的模块

以如下文件结构为例:

PI = 3.1415926
class BasicColor(object):
    def __init__(self, rgb=(0,0,0)):
        super(BasicColor, self).__init__()
        self.rgb = rgb
class BasicShape(object):
    def __init__(self, arg):
        super(BasicShape, self).__init__()
        self.type = arg
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import draw
from draw import colors
from draw.shapes import BasicShape

def main():
    print draw.PI

    red = colors.BasicColor((1,2,3))
    print red.rgb
    
    triangle = BasicShape('triangle')
    print triangle.type

    pass


if __name__ == '__main__':
    main()

我们来看一下main.py文件中的几个import语句的区别:

import draw 实际上只是导入了draw包目录下的__init__.py文件。所以可以通过draw.PI来引用draw包的默认模块(__init__.py)中定义的PI变量。但是这句并不会导入除__init__之外的其他模块:colors与shapes模块!!!

from draw import colors 从draw包中导入colors模块。所以可以通过colors.BasicColor来引用colors模块中的类。

from draw.shapes import BasicShape 从shapes模块中直接导入BasicShape类。所以可以直接调用BasicShape类了。

 

18.4 __init__.py的用法

可能我们并不想用from draw.shapes import BasicShape这么长的路径来引入一个类(这还不算长的,如果考虑多级包目录的话)。那么,我们可以在draw目录的__init__.py文件中加上一行:

from .shapes import BasicShape
# 表示从当前包的shapes模块中引入BasicShape类

那么,在main.py中,就可以通过import draw来使用draw.BsasicShape了。或者通过from draw import BasicShape来直接使用BasicShape了。

 

18.5 绝对路径导入 和 相对路径导入(从兄弟目录导入)

根据 官方文档 https://docs.python.org/3/reference/import.html#package-relative-importshttps://docs.python.org/3/tutorial/modules.html#intra-package-references

python 支持使用 from ..sibling_package import foo 的相对路径方式从兄弟目录中导入模块。但这里其实有个很大的坑!

Note that relative imports are based on the name of the current module. Since the name of the main module is always "__main__", modules intended for use as the main module of a Python application must always use absolute imports.

这句话说白了就是,在 if __name__ == '__main__': 直接运行的脚本里,是不支持相对路径导入的!因为此时运行环境的 __name__ = "__main__",  __package__ = None。python 解释器无法找到当前包,也就找不到相对包了。此时必须使用 “绝对导入”。

 

所谓的 绝对路径导入,即常用的 import abc 这种语法。python 是如何查找这个 abc 模块的呢?

首先,python 会先去 sys.modules 中查找 abc 模块,sys.modules 保存着已经导入过的模块;

然后,如果找不到的话,python 就会去查找内置的 标准库

最后,如果上述两个位置都找不到 abc 模块,python 就会去 sys.path 变量中的所有路径下查找。(当前工作目录 通常在 sys.path 的第一位,第三方库 site-packages/ 通常排在 sys.path 中最后一位。当前工作目录是指 python example/main.py 入口文件所在的目录。)

上面三个位置都找不到的话,就抛出异常 ModuleNotFoundError。

 

相对路径导入,是基于当前 py 文件的 __name__ 变量进行查找(或者是基于 __package__ 变量,但 __package__ 好像也是来自 __name__)。

 

现在我们来举个例子:

这三个文件的内容分别是:

############################################################
# pack_a/mod_a.py
print('A current env: __package__ = %s, __name__ = %s' % (__package__, __name__))
CONSTANT_A = 'AAA'

############################################################
# pack_b/mod_b.py
print('B current env: __package__ = %s, __name__ = %s' % (__package__, __name__))
CONSTANT_B = 'BBB'

############################################################
# main.py
from pack_a.mod_a import *
from pack_b.mod_b import *

if __name__ == '__main__':
    print(CONSTANT_A + CONSTANT_B)
    print('current env: __package__ = %s, __name__ = %s' % (__package__,__name__))

这时候在当前 sibling_import 目录执行 python main.py命令,运行是正常的。

 

(1)如果将 main.py 改为使用相对路径导入。

from .pack_a.mod_a import *
from .pack_b.mod_b import *

if __name__ == '__main__':
    print(CONSTANT_A + CONSTANT_B)
    print('current env: __package__ = %s, __name__ = %s' % (__package__,__name__))

那么再执行 python main.py 就会报错,ImportError: attempted relative import with no known parent package。因为对于当前运行环境,它的 __package__ 变量是 None,所以没办法使用相对路径。

 

(2)再来,如果我们同时修改 mod_b.py 和 main.py

############################################################
# pack_b/mod_b.py
print('B current env: __package__ = %s, __name__ = %s' % (__package__, __name__))
CONSTANT_B = 'BBB'

from pack_a.mod_a import *
CONSTANT_BA = CONSTANT_B + CONSTANT_A

if __name__ == '__main__':
    print(CONSTANT_BA)

############################################################
# main.py
from pack_a.mod_a import *
from pack_b.mod_b import *

if __name__ == '__main__':
    print(CONSTANT_BA)
    print('current env: __package__ = %s, __name__ = %s' % (__package__, __name__))

这时候执行 python main.py 是正常的,但执行 python pack_b/mod_b.py 则会报错:No module named 'pack_a'

因为当 mod_b.py 作为模块被 main.py 导入的时候,在 mod_b.py 中 from pack_a.mod_a import * 应该用的是相对于当前运行目录的 “绝对路径” 导入,而当前的运行目录是 sibling_import/,在该目录下是可以找到 pack_a 包的。

而当把 mod_b.py 作为脚本独立运行的时候,当前的运行目录其实是 sibling_import/pack_b/,在该路径下确实找不到 pack_a 包。

但此时如果在 sibling_import/ 目录下执行 python -m pack_b.mod_b 命令。会发现是成功的!因为 -m 参数的意思是执行模块。就是把 mod_b.py 当作 pack_b.mod_b 模块来执行,这时的运行环境中,当前路径是 sibling_import/,所以可在 mod_b.py 中用绝对导入 pack_a.mod_a。

 

(3)再来,这时候如果我们把 mod_b.py 导入mod_a 的语句改为相对导入:from ..pack_a.mod_a import *

这时候在 sibling_import/ 目录下不管是执行 python main.py 还是 python -m pack_b.mod_b 都报错了,attempted relative import beyond top-level package

我想是因为此时环境中 mod_b 的__name__ = pack_b.mod_b,它的包是 pack_b,相对包 .. 已经到顶了,是 None。那么 None.pack_a 当然找不到东西。

所以,除非是在 sibling_import/ 的父目录下,执行 python -m sibling_import.pack_b.mod_b 命令。才能成功,因为这时候 mod_b 的 __name__ = sibling_import.pack_b.mod_b。那么相对包 ..pack_a,就是 sibling_import.pack_a 了。

或者是将 main.py 移到 sibling_import/ 的父目录下执行,并修改 import 语句为如下所示,这样也能成功。

from sibling_import.pack_a.mod_a import *
from sibling_import.pack_b.mod_b import *

if __name__ == '__main__':
    print(CONSTANT_A + CONSTANT_B)
    print('current env: __package__ = %s, __name__ = %s' % (__package__, __name__))

 

(4)如果非想维持(2)中的 mod_b.py 代码,直接 from pack_a.mod_a import * 导入兄弟包中的模块。那么有一个 trick 的方法。就是在导入兄弟包之前,使用 sys.path.append 将父目录添加到运行时环境变量中。

print('B current env: __package__ = %s, __name__ = %s' % (__package__, __name__))
CONSTANT_B = 'BBB'

import sys, os
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from pack_a.mod_a import *
CONSTANT_BA = CONSTANT_B + CONSTANT_A

if __name__ == '__main__':
    print(CONSTANT_BA)

这样,不论是直接在子目录执行 python mod_b.py 还是在根目录执行 python main.pypython pack_b/mod_b.py 都是正常的了。

 

(5)但上面的方法(4)实在是有点肮脏 =。=,所以并不推荐在项目的正规代码中使用(我觉得可以在 tests/test.py 中使用)。有网友推荐用 setup.py 来处理兄弟包导入的问题(https://stackoverflow.com/questions/6323860/sibling-package-imports),但我觉得太复杂了。

我更推荐的是,使用规范的代码目录结构。

① 在项目根目录(或者叫根包,即上图中的 MyProject/my_project/)下,所有子包(MyProject/my_project/sub_package/)内的模块都用 相对导入 的方式来导入兄弟包的模块。(这是针对该项目是独立运行的程序。如果项目是一个要公开的第三方库,似乎更推荐使用 setuptools + pip install -e 的方式,然后直接用 import my_project.sub_package.module 的方式来导入。参考 stackoverflow 。)

② 保证不在子包(MyProject/my_project/sub_package/)的模块中编写直接运行的代码 if __name__ == "__main__":

③ 所有要直接运行的代码都放在根目录(MyProject/my_project/)下。

④ 对于测试代码。(a)或者放在项目根目录的 Myproject/my_project/tests/ 子目录下;(b)或者放在与项目根目录同级的 MyProject/tests/ 目录中。将项目代码与测试代码分开。

然后使用 python -m tests.test 方式来运行,或是在 test.py 代码中使用方法(4)里的 trick。

18.6 更规范的项目目录结构

@2023.03.30 发现一个更规范的项目目录结构案例:stackoverflow.com

我自己的总结在:Python 项目目录结构与 pytest

19. string.format与数据库操作时候的参数

执行数据库语句的时候,通常需要传递变量给要执行的语句。这时候有两种方法。一种是用string.format()来格式化要执行的语句,一种的使用数据库cursor.execute('sql_string', argv)的argv参数来传递变量值。

19.1 string.format()用法

sql_query = "select * from {table_name} where id={id}".format(table_name='user', id=1)
cursor.execute(sql_query)
row = cursor.fetchone()

19.2 cursor.execute()用法

通常的数据库驱动实现都支持带参数的cursor.execute()方法,并且都支持%s与%(name)s这两种参数定位。

# 使用%s占位符
cur.execute("""
    INSERT INTO some_table (an_int, a_date, a_string)
    VALUES (%s, %s, %s);
    """,
    (10, datetime.date(2005, 11, 18), "O'Reilly"))

# 使用带名字的%(name)s占位符
cur.execute("""
    INSERT INTO some_table (an_int, a_date, another_date, a_string)
    VALUES (%(int)s, %(date)s, %(date)s, %(str)s);
    """,
    {'int': 10, 'str': "O'Reilly", 'date': datetime.date(2005, 11, 18)})

 

20. 动态定义类成员变量

有时候需要动态定义(调用)变量名字,或者说用字符串定义(调用)变量名。

如果是在函数中定义局部变量,可以使用exec('%s=%s'%('var_name', value))。但这个我试了只在python 2有效,python 3就无效了。

如果是要在类中动态定义成员变量,可以使用类的__dict__特殊变量来定义。举个例子:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class UserModel(object):
    fields = ('id', 'name', 'age', 'gender')

    def __init__(self, row):
        super(UserModel, self).__init__()
        
        assert len(row) == len(self.__class__.fields), '字段数量不一致'

        # 通过__dict__动态定义成员变量并赋值
        # 等价于定义了 self.id=row[0], self.name=row[1], self.age=row[2]
        for i in range(len(row)):
            field = self.__class__.fields[i]
            self.__dict__[field] = row[i]


def main():
    user = UserModel((1, 'funway', 18, 'male'))
    print(user.name)
    pass

if __name__ == '__main__':
    main()

21. ftplib的编码问题

使用python自带的ftplib,在处理中文路径时都会遇到这样一个异常:UnicodeEncodeError: 'latin-1' codec can't encode characters...

这是由于ftplib.py默认使用latin-1编码,而且还硬编码在FTP类的类变量中,简直脑残,设计构造函数可传递的成员变量都好呀。凸=。=

这个情况,有两种方式应对:

1、新建FTP实例后,立即赋值一个encoding成员变量覆盖之(ftplib.py中用到encoding的地方都是用self.encoding来调用,而不是FTP.encoding来调用,所以该方法是有效的)

def ftp_store():
    ftp = FTP('127.0.0.1', 'username', 'password')
    ftp.encoding = 'utf-8'
    file_name = '中文名.txt'
    file_data = 'hello,world'
    if type(file_data) == str:
        file_data = file_data.encode()
    ftp.storbinary('STOR %s' % file_name, io.BytesIO(file_data))
    pass

2、每次遇到中文字符串,都主动改成latin-1编码

def ftp_store():
    ftp = FTP('127.0.0.1', 'username', 'password')
    file_name = '中文名.txt'
    file_data = 'hello,world.'
    if type(file_data) == str:
        file_data = file_data.encode()
    ftp.storbinary('STOR %s' % file_name.encode('utf-8').decode('latin-1'), io.BytesIO(file_data))
    pass

3、网上居然有人说直接修改ftplib.py源码。。。简直👎

22. 函数的可变长参数(任意参数)

在 Python 中,定义函数时可以使用两个特殊符号,以允许它们接受可变数量的参数。这两个特殊符号为 * 和 **。通常将参数写作 *args 和 **kwargs(使用 args 和 kwargs 只是约定俗成,你也可以用任意名字代替)。args 为 arguments 的缩写,表示多个参数。kwargs 为 keyword arguments 的缩写,表示多个关键字参数。

然后在函数中,(*)会把接收到的参数转为一个元组,而(**)会把接收到的参数转为一个字典。所以 *args 表示将传入多个参数(包括0个参数)变为元组,**kwargs 表示将传入多个带名称的参数(包括0个参数)变为字典。

def fun1(*args):
    # 将任意的无名参数变成一个元组赋给 args
    print(type(args), args)
    pass

fun1(1, 'b', 3)     # args = (1, 'b', 3)       args 元组中有三个元素
fun1((1, 'b', 3))   # args = ((1, 'b', 3),)    args 元组中只有一个元素
fun1([1, 'b', 3])   # args = ([1, 'b', 3],)    args 元组中只有一个元素
fun1(*(1, 'b', 3))  # args = (1, 'b', 3)       args 元组中有三个元素,实参前的 *号 将实参元组解包
fun1(*[1, 'b', 3])  # args = (1, 'b', 3)       args 元组中有三个元素,实参前的 *号 将实参列表解包
fun1()              # args = ()

########################################
def fun2(**kwargs):
    # 将任意的关键字参数变成一个字典赋给 kwargs
    print(type(kwargs), kwargs)
    pass

fun2(name='haha', age=18)      # kwargs = {'name': 'haha', 'age': 18}
d = {'name':'haha', 'age':18}  
fun2(**d)                      # kwargs = {'name': 'haha', 'age': 18}    实参前的 **号 将实参字典解包

########################################
# *args 和 **kwargs 以及 指定命名的参数 共同使用
# 注意 **kwargs 要放在所有参数的最后面!
def fun3(name, *args, age=None, **kwargs):
    print('name: ', name)
    print('args: ', args)
    print('age: ', age)
    print('kwargs: ', kwargs)
    pass

fun3('funway', 1, 2, age=18, gender='male', phone='133xxx')
fun3('zoe', 3, 4, 20, gender='female', phone='199xxx')

 

Leave a Reply

Your email address will not be published. Required fields are marked *

TOC