Python中的一些陷阱與技巧小結

原文傳送門:Python中的一些陷阱與技巧小結python

Python是一種被普遍使用的強大語言,讓咱們深刻這種語言,而且學習一些控制語句的技巧,標準庫的竅門和一些常見的陷阱。程序員

Python(和它的各類庫)很是龐大。它被用於系統自動化、web應用、大數據、數據分析及安全軟件。這篇文件旨在展現一些知之甚少的技巧,這些技巧將帶領你走上一條開發速度更快、調試更容易而且充滿趣味的道路。web

學習Python和學習全部其餘語言同樣,真正有用的資源不是各個語言繁瑣的超大官方文檔,而是使用經常使用語法、庫和Python社區共享知識的能力。sql

**探索標準數據類型數據庫

謙遜的enumerate**編程

遍歷在Python中很是簡單,使用「for foo in bar:」就能夠。json

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
複製代碼

可是同時使用元素的序號和元素自己也是常見的需求。咱們常常看到一些程序員使用len()和range()來經過下標迭代列表,可是有一種更簡單的方式。小程序

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 函數能夠同時遍歷元素及其序號。api

Set類型安全

許多概念均可以歸結到對集合(set)的操做。例如:確認一個列表沒有重複的元素;查看兩個列表共同的元素等等。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 函數比較列表中全部元素,返回兩個集合的交集。在咱們的例子中,早餐的主食爲bacon、eggs和ham。

collections.namedtuple

若是你不想給一個類添加方法,但又想使用foo.prop的調用方式,那麼你須要的就是namedtuple。你提早定義好類屬性,而後就能夠實例化一個輕量級的類,這樣的方式會比完整的對象佔用更少的內存。

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

用這種方式你沒法設置namedtuple的屬性,正如你不能修改元組(tuple)中元素的值。你須要在實例化namedtuple的時候設置屬性的值。

LightObject = namedtuple('LightObject', ['shortname', 'otherprop'])
n = LightObject(shortname='something', otherprop='something else')
n.shortname
# something
collections.defaultdict
複製代碼

在寫Python應用使用字典時,不少時候有些關鍵字一開始並不存在,例以下面的例子。

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]

複製代碼

使用defaultdict 咱們能夠跳過檢查關鍵字是否存在的邏輯,對某個未定義key的任意訪問,都會返回一個空列表(或者其餘數據類型)。

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

複製代碼

你甚至可使用自定義的類,這樣調用的時候實例化一個類。

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的特性,同時還想用訪問屬性的方式來處理嵌套的key,那麼能夠了解一下 addict。

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.讓咱們構造一個value是defaulted dictionaries類型的defaultdict,這樣也只能解決兩級嵌套。

若是你只是須要一個默認計數器,你可使用collection.Counter,這個類提供了許多方便的函數,例如 most_common.

控制流 當學習Python中的控制結構時,一般要認真學習 for, while,if-elif-else, 和 try-except。只要正確使用,這幾個控制結構可以處理絕大多數的狀況。也是基於這個緣由,幾乎你所遇到的全部語言都提供相似的控制結構語句。在基本的控制結構之外,Python也額外提供一些不經常使用的控制結構,這些結構會使你的代碼更具可讀性和可維護性。

Great Exceptations

Exceptions做爲一種控制結構,在處理數據庫、sockets、文件或者任何可能失敗的資源時很是經常使用。使用標準的 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模塊。這意味着查找數據失敗(或者爲查詢數據創建鏈接失敗)會引起回退操做。這絕對不是咱們想要的,由於在這個時間點上事務並無開始。一樣回退也不該該是數據庫鏈接失敗的正確響應,所以讓咱們將不一樣的狀況分開處理。

首先,咱們將處理查詢數據。

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,這樣當咱們沒有取得數據時,咱們能夠採起任何處理方式。沒有數據咱們的代碼不大可能再作有用的事,所以咱們將僅僅退出函數。除了退出你也能夠構造一個默認對象,從新進行檢索或者結束整個程序。

如今讓咱們將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,當沒有異常發生時會執行這裏的代碼。在咱們的例子中,這裏只是將事務成功的信息寫入日誌,可是你能夠按照須要進行更多有趣的操做。一種可能的應用是啓動後臺任務或者通知。

很明顯finally 子句在這裏的做用是保證db.close() 老是可以運行。回顧一下,咱們能夠看到全部和數據存儲相關的代碼最終都在相同的縮進級別中造成了漂亮的邏輯分組。之後須要進行代碼維護時,將很直觀的看出這幾行代碼都是用於完成 commit操做的。

Context and Control

以前,咱們已經看到使用異常來進行處理控制流。一般,基本步驟以下:

嘗試獲取資源(文件、網絡鏈接等)
    若是失敗,清除留下的全部東西
    成功得到資源則進行相應操做
    寫日誌
    程序結束
複製代碼

考慮到這一點,讓咱們再看一下上一章數據庫的例子。咱們使用try-except-finally來保證任何咱們開始的事務要麼提交要麼回退。

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

複製代碼

咱們前面的例子幾乎精確的映射到剛剛提到的步驟。這個邏輯變化的多嗎?並很少。

差很少每次存儲數據,咱們都將作相同的步驟。咱們能夠將這些邏輯寫入一個方法中,或者咱們可使用上下文管理器(context manager)

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
複製代碼

上下文管理器經過設置代碼段運行時須要的資源(上下文環境)來保護代碼段。在咱們的例子中,咱們須要處理一個數據庫事務,那麼過程將是這樣的:

鏈接數據庫
    在代碼段的開頭開始操做
    在代碼段的結尾提交或者回滾
    在代碼段的結尾清除資源
複製代碼

讓咱們創建一個上下文管理器,使用上下文管理器爲咱們隱藏數據庫的設置工做。contextmanager 的接口很是簡單。上下文管理器的對象須要具備一個__enter__()方法用來設置所需的上下文環境,還須要一個__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 方法中創建的,所以若是數據庫創建鏈接失敗,那麼代碼段將不會執行。

如今讓咱們定義事務將如何在 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 類做爲咱們例子中的上下文管理器了。在類內部, enterexit 方法將開始和設置數據鏈接而且處理善後工做。

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

爲了改進咱們的(簡單)事務管理器,咱們能夠添加各類異常處理。即便是如今的樣子,這個事務管理器已經爲咱們隱藏了許多複雜的處理,這樣你不用每次從數據庫拉取數據時都要擔憂與數據庫相關的細節。

生成器

Python 2中引入的生成器(generators)是一種實現迭代的簡單方式,這種方式不會一次產生全部的值。Python中典型的函數行爲是開始執行,而後進行一些操做,最後返回結果(或者不返回)。

生成器的行爲卻不是這樣的。

def my_generator(v):
  yield 'first ' + v
  yield 'second ' + v
  yield 'third ' + v
  
print(my_generator('thing'))
# 
複製代碼

使用 yield 關鍵字代替 return ,這就是生成器的獨特之處。當咱們調用 my_generator('thing') 時,我獲得的不是函數的結果而是一個生成器對象,這個生成器對象能夠在任何咱們使用列表或其餘可迭代對象的地方使用。

更常見的用法是像下面例子那樣將生成器做爲循環的一部分。循環會一直進行,直到生成器中止 yield值。

for value in my_generator('thing'):
  print value
  
# first thing
# second thing
# third thing
  
gen = my_generator('thing')
next(gen)
# 'first thing'
next(gen)
# 'second thing'
next(gen)
# 'third thing'
next(gen)
# raises StopIteration exception
複製代碼

生成器實例化以後不作任何事直到被要求產生數值,這時它將一直執行到遇到第一個 yield 而且將這個值返回給調用者,而後生成器保存上下文環境後掛起一直到調用者須要下一個值。

如今咱們來寫一個比剛纔返回三個硬編碼的值更有用的生成器。經典的生成器例子是一個無窮的斐波納契數列生成器,咱們來試一試。數列從1開始,依次返回前兩個數之和。

def fib_generator():
  a = 0
  b = 1
  while True:
    yield a
    a, b = b, a + b
複製代碼

函數中的 while True 循環一般狀況下應該避免使用,由於這會致使函數沒法返回,可是對於生成器卻無所謂,只要保證循環中有 yield 。咱們在使用這種生成器的時候要注意添加結束條件,因該生成器能夠持續不斷的返回數值。

如今,使用咱們的生成器來計算第一個大於10000的斐波納契數列值。

min = 10000
for number in fib_generator():
  if number > min:
    print(number, "is the first fibonacci number over", min)
    break
複製代碼

這很是簡單,咱們能夠把數值定的任意大,代碼最終都會產生斐波納契數列中第一個大於X的值。

讓咱們看一個更實際的例子。翻頁接口是應對應用限制和避免向移動設備發送大於50兆JSON數據包的一種常見方法。首先,咱們定義須要的API,而後咱們爲它寫一個生成器在咱們的代碼中隱藏翻頁邏輯。

咱們使用的API來自Scream,這是一個用戶討論他們吃過的或想吃的餐廳的地方。他們的搜索API很是簡單,基本是下面這樣。

GET http://scream-about-food.com/search?q=coffee
{
  "results": [
    {"name": "Coffee Spot",
     "screams": 99
    },
    {"name": "Corner Coffee",
     "screams": 403
    },
    {"name": "Coffee Moose",
     "screams": 31
    },
    {...}
  ]
  "more": true,
  "_next": "http://scream-about-food.com/search?q=coffee?p=2"
}
複製代碼

他們將下一頁的連接嵌入到API應答中,這樣當須要得到下一頁時就很是簡單了。咱們可以不考慮頁碼,只是獲取第一頁。爲了得到數據,咱們將使用常見的 requests 庫,而且用生成器將其封裝以展現咱們的搜索結果。

這個生成器將處理分頁而且限制重試邏輯,它將按照下述邏輯工做:

收到要搜索的內容
    查詢scream-about-food接口
    若是接口失敗進行重試
    一次yield一個結果
    若是有的話,獲取下一頁
    當沒有更多結果時,退出
複製代碼

很是簡單。我來實現這個生成器,爲了簡化代碼咱們暫時不考慮重試邏輯。

import requests
  
api_url = "http://scream-about-food.com/search?q={term}"
  
def infinite_search(term):
  url = api_url.format(term)
  while True:
    data = requests.get(url).json()
  
    for place in data['results']:
      yield place

# end if we've gone through all the results
    if not data['more']: break
  
    url = data['_next']
複製代碼

當咱們建立了生成器,你只須要傳入搜索的內容,而後生成器將會生成請求,若是結果存在則獲取結果。固然這裏有些未處理的邊界問題。異常沒有處理,當API失敗或者返回了沒法識別的JSON,生成器將拋出異常。

儘管存在這些未處理完善的地方,咱們仍然能使用這些代碼得到咱們的餐廳在關鍵字「coffee」搜索結果中的排序。

# pass a number to start at as the second argument if you don't want
# zero-indexing
for number, result in enumerate(infinite_search("coffee"), 1):
  if result['name'] == "The Coffee Stain":
    print("Our restaurant, The Coffee Stain is number ", number)
    return
print("Our restaurant, The Coffee Stain didnt't show up at all! :(")

複製代碼

若是使用Python 3,當你使用標準庫時你也能使用生成器。調用相似 dict.items() 這樣的函數時,不返回列表而是返回生成器。在Python 2中爲了得到這種行爲,Python 2中添加了 dict.iteritems() 函數,可是用的比較少。

Python 2 & 3 兼容性

從Python 2 遷移到Python3對任何代碼庫(或者開發人員)都是一項艱鉅的任務,可是寫出兩個版本都能運行的代碼也是可能的。Python2.7將被支持到2020年,可是許多新的特性將不支持向後兼容。目前,若是你還不能徹底放棄Python 2, 那最好使用Python 2.7 和 3+兼容的特性。

對於兩個版本支持特性的全面指引,能夠在python.org上看 Porting Python 2 Code 。

讓咱們查看一下在打算寫兼容代碼時,你將遇到的最多見的狀況,以及如何使用 future 做爲變通方案。

print or print()

幾乎每個從Python 2 切換到Python 3的開發者都會寫出錯誤的print 表達式。幸運的是,你可以經過導入 print_function 模塊,將print做爲一個函數而不是一個關鍵字來寫出可兼容的print.

for result in infinite_search("coffee"):
  if result['name'] == "The Coffee Stain":
    print("Our restaurant, The Coffee Stain is number ", result['number'])
    return
print("Our restaurant, The Coffee Stain didn't show up at all! :(")
Divided Over Division
複製代碼

從Python 2 到 Python 3,除法的默認行爲也發生了變化。在Python 2中,整數的除法只進行整除,小數部分所有截去。大多數用戶不但願這樣的行爲,所以在Python 3中即便是整數之間的除法也執行浮點除。

print "hello"
# Python 2
print("hello")
# Python 3
 
from __future__ import print_function
print("hello")
# Python 2
print("hello")
# Python 3
複製代碼

這種行爲的改變會致使編寫同時運行在Python 2 和 Python 3上的代碼時,帶來一連串的小bug。咱們再一次須要 future 模塊。導入 division 將使代碼在兩個版本中產生相同的運行結果。

print(1 / 3) 
# Python 2
# 0
print(1 / 3) 
# Python 3
# 0.3333333333333333
print(1 // 3) 
# Python 3
# 0
複製代碼

更多精彩內容請關注 極智能-專業人工智能社區

極智能是一個助力AI開發者成長的社區。在這裏,你能夠與他人一塊兒學習和分享人工智能相關知識與技術,包括編程語言、數學基礎、機器學習及其相關技術框架等。社區當前主要提供文章分享、話題探討和學習資源分享等功能,內容涉及機器學習,深度學習,天然語言處理,語音識別,機器視覺,機器人等領域。

相關文章
相關標籤/搜索