美文网首页
一些 Python 技巧和陷阱(译)

一些 Python 技巧和陷阱(译)

作者: Aaren | 来源:发表于2015-01-28 21:41 被阅读466次

    介绍

    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
    

    待续

    相关文章

      网友评论

          本文标题:一些 Python 技巧和陷阱(译)

          本文链接:https://www.haomeiwen.com/subject/zlhlxttx.html