一些 Python 技巧和陷阱(译)

介绍

python(以及他的库)是很庞大有用的。他可以用来做系统自动化、web应用、大数据、数据分析以及安全软件。这篇文章旨在说明一些可以帮你更快更好地开发、debug的鲜为人知的小技巧。
对于每一种语言来说,一旦你学了这个语言,你真正得到的并不是这个语言本身的特性能力,而是你学会他的编程风格、学会用他的库以及python社区里的知识。

探索标准数据类型

低调的enumerate

迭代python里的任何内容都是很容易的,你只要这样就行了:for foo in bar:

drinks=["coffee","tea","milk","water"]
for drink in drinks:
  print("thirsty for",drink)
#thirsty for coffee
#thirsty for tea
#thirsty for milk
#thirsty for water

但是除了这些items之外,我们经常也会需要知道item的index。我们经常会看到一些程序员通过len()和range() 用index迭代一个list,这里有一种更简单的方式:

drinks=["coffee","tea","milk","water"]
for index, drink in enumerate(drinks):
    print("Item {} is {}".format(index,drink))
#Item 0 is coffee
#Item 1 is tea
#Item 2 is milk
#Item 3 is water

内建的enumerate方法生成了index以及item。

集合

有很多的场景是可以归结到集合操作的。我们需要确保list没有重复元素?需要看两个list有哪些共同项?python的set类型可以让这些操作更快并且更可读的。

# deduplicate a list *fast*
print(set(["ham","eggs","bacon","ham"]))
# {'bacon', 'eggs', 'ham'}
# compare lists to find differences/similarities
# {} without "key":"value" pairs makes a set
menu={"pancakes","ham","eggs","bacon"}
new_menu={"coffee","ham","eggs","bacon","bagels"}
new_items=new_menu.difference(menu)
print("Try our new",", ".join(new_items))
# Try our new bagels, coffee
discontinued_items=menu.difference(new_menu)
print("Sorry, we no longer have",", ".join(discontinued_items))
# Sorry, we no longer have pancakes
old_items=new_menu.intersection(menu)
print("Or get the same old",", ".join(old_items))
# Or get the same old eggs, bacon, ham
full_menu=new_menu.union(menu)
print("At one time or another, we've served:",", ".join(full_menu))
# At one time or another, we've served: coffee, ham, pancakes, bagels, bacon, eggs

intersection函数比较所有的items,然后只把两个集合里都有的items返回。

collections.namedtuple

当你不需要类的方法,但是又想要foo.prop这样的简便时,没有什么比namedtuple更好了。你可以提前定义fields,然后初始化一个比完整对象占用内存更少的轻量级的类。

LightObject=namedtuple('LightObject',['shortname','otherprop'])
m=LightObject()
m.shortname='athing'
> Traceback(most recent call last):
> AttributeError:can't set attribute

就像你不能改变一个tuple的成员一样,你也不能设置namedtuple的属性。你需要做的是当你初始化namedtuple时设置他的属性。

LightObject=namedtuple('LightObject',['shortname','otherprop'])
n=LightObject(shortname='something',otherprop='something else')
n.shortname# something

collections.defaultdict

在python app里我们经常会看到这样的逻辑:我们需要去考虑key初始时是不是存在,类似这样:

login_times = {}
for t in logins:
    if login_times.get(t.username, None):
        login_times[t.username].append(t.datetime)
    else:
        login_times[t.username] = [t.datetime]

通过defaulltdict我们可以跳过上面的逻辑,直接访问undefined key。

login_times = collections.defaultdict(list)
for t in logins:
    login_times[t.username].append(t.datetime)

你甚至可以自定义类,给出构造函数去build一个类。

from datetime import datetime
class Event(object):
    def __init__(self, t=None):
    if t is None:
        self.time = datetime.now()
    else:
        self.time = t
events = collections.defaultdict(Event)
for e in user_events:
    print(events[e.name].time)

除了用defaultdict,我们可以用addict去设置嵌套的key。

normal_dict = {
    'a': {
        'b': {
            'c': {
                'd': {
                    'e': 'really really nested dict'
                }
            }
        }
    }
}

from addict import Dict
addicted = Dict()
addicted.a.b.c.d.e = 'really really nested'
print(addicted)
# {'a': {'b': {'c': {'d': {'e': 'really really nested'}}}}}

这段代码比标准的dict更简单,那么defaultdict可以吗?看起来应该可以。

from collections import defaultdict 
default = defaultdict(dict) 
default['a']['b']['c']['d']['e'] = 'really really nested dict' # fails

这样看起来应该是可以的,但是他抛出了KeyError异常,因为default['a']是一个dict,不是defaultdict
如果你只是需要一个counter,你可以用collections.Counter来提供像most_common这样的功能。

控制流

当我们学习python里面的控制结构的时候,通常我们会接触到for,while,if-elif-else,以及try-except。合理地利用这些控制结构我们可以解决绝大多数的问题。不过python也提供了一些其他不常用的结构,这些结构有时可以让你的代码可读性更好,而且更易于维护。

异常处理

当处理数据库,sockets,文件或者任何其他处理时可能失败的资源时,Exception是一种很常见的流程控制模式。通过标准的try except,比如操作数据库:

try:
    # get API data
    data = db.find(id='foo') # may raise exception
    # manipulate the data
    db.add(data)
    # save it again
    db.commit() # may raise exception
except Exception:
    # log the failure
    db.rollback()

db.close()

你可以指出这段代码的问题吗?
这里有两个可能出现的异常,他们会触发相同的except代码块。这意味着find数据失败会引起rollback。这绝不是我们希望的,因为在那个点发生的异常甚至还没有开始一个数据库事务。对于一个连接失败来说,rollback并不是一个正确的处理方式,所以我们要把上面的代码拆分开来。
首先我们需要处理find数据。

try:
    # get API data
    data = db.find(id='foo') # may raise exception
except Exception:
    # log the failure and bail out
    log.warn("Could not retrieve FOO")
    return

# manipulate the data
db.add(data)

现在取数据有他自己的try-except了。如果没有数据我们的代码也没有什么可以做的了,所以我们可以退出函数或者我们也可以创建一个默认对象然后重试这个query,或者kill整个程序。
现在,我们处理commit。

try:
    db.commit() # may raise exception
except Exception:
    log.warn("Failure committing transaction, rolling back")
    db.rollback()
else:
    log.info("Saved the new FOO")
finally:
    db.close()

这里我们加了两句话。首先我们看else,当没有异常时会执行他。在我们的例子里面,他只是在日志里打了事务成功,但是你也可以根据需要放些需要的行为。比如你可能会执行一个后台的job或者一个通知。
这里finally是为了确保db.close()总能执行的。我们可以看到我们所有的代码都是跟commit这个操作绑在一起的。

上下文和控制流程

我们已经看到了上面怎么用exceptions去控制流程。总的来说步骤如下:

  1. Attempt to acquire a resource (file, network connection, whatever)
  2. If it fails, clean up anything left behind
  3. Otherwise, perform actions on the resource
  4. Log what happened
  5. Program complete

知道这个以后,我们用第二种方法来处理前面的数据库的例子。我们用try-except-finally来确保任何事务要么就committed,要么就rolled back。

try:
    # attempt to acquire a resource
    db.commit()
except Exception:
    # If it fails, clean up anything left behind
    log.warn("Failure committing transaction, rolling back")
    db.rollback()
else:
    # If it works, perform actions
    # In this case, we just log success
    log.info("Saved the new FOO")
finally:
    # Clean up
    db.close()
# Program complete

我们之前的例子就是上面这几步,那么我们可以对这个逻辑做些什么修改呢?
对于每次我们保存数据,我们都需要准确地执行上面的这些步骤。所以我们可以把这些逻辑放进方法里,或者我们可以用一个上下文管理器。

db = db_library.connect("fakesql://")
# as a function
commit_or_rollback(db)

# context manager
with transaction("fakesql://") as db:
    # retrieve data here
    # modify data here

context manager可以很容易地把一些运行时设置资源(context)需要的代码块维护起来。在我们的例子里,我们需要一个这样的数据库事务:

  1. Connected to a database
  2. Started at the beginning of the block
  3. Committed or rolled back at the end of the block
  4. Cleaned up at the end of the block

我们构造一个可以隐藏所有数据库setup细节的context manager。contextmanager接口很简单。这个对象需要有一个__enter__()方法用来setup所需的context,还需要一个__exit__(exc_type, exc_val, exc_tb)方法,这个方法会在代码块结束的时候调用。如果没有异常,那么exc_*参数都是None. __enter__方法非常简单,所以我们先介绍这个方法.

class DatabaseTransaction(object):
    def __init__(self, connection_info):
        self.conn = db_library.connect(connection_info)

    def __enter__(self):
        return self.conn

事实上__enter__方法除了返回一个数据库连接之外什么都不干,我们可以用在代码块里使用这个数据库连接查询或者存储数据。__init__方法是连接实际创建的地方,如果这个过程fail的话,整个代码块都无法运行。
下面我们在__exit__方法里面定义如何结束一个事务。因为要处理所有代码块里抛出的异常以及关闭事务,所以这个方法需要做的事情更多了。

 def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            self.conn.rollback()

        try:
            self.conn.commit()
        except Exception:
            self.conn.rollback()
        finally:
            self.conn.close()

现在我们可以在我们的代码里使用DatabaseTransaction这个contextmanager了。在这个框架下面,__enter____exit__方法会运行和建立数据库连接以及当结束时做tear down。

# context manager
with DatabaseTransaction("fakesql://") as db:
    # retrieve data here
    # modify data here

待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容