天天看點

c 字元串轉數字_Python工匠:數字與字元串(上)

程式設計某種意義上是一門『手藝』,因為優雅而高效的代碼,就如同完美的手工藝品一樣讓人賞心悅目。

緻“匠人”

數字是幾乎所有程式設計語言裡最基本的資料類型,它是我們通過代碼連接配接現實世界的基礎。在 Python 裡有三種數值類型:整型(int)、浮點型(float)和複數(complex)。絕大多數情況下,我們隻需要和前兩種打交道。

整型在 Python 中比較讓人省心,因為它不區分有無符号并且永不溢出。但浮點型仍和絕大多數其他程式設計語言一樣,依然有着精度問題,經常讓很多剛進入程式設計世界大門的新人們感到困惑:"Why Are Floating Point Numbers Inaccurate?"。

相比數字,Python 裡的字元串要複雜的多。要掌握它,你得先弄清楚 bytes 和 str 的差別。如果更不巧,你還是位 Python2 使用者的話,光 unicode 和字元編碼問題就夠你喝上好幾壺了(趕快遷移到 Python3 吧,就在今天!)。

在這篇文章裡,我們将讨論一些 更細微、更不常見 的程式設計實踐。來幫助你寫出更好的 Python 代碼。

1少寫數字字面量

“數字字面量(integer literal)” 是指那些直接出現在代碼裡的數字。它們分布在代碼裡的各個角落,比如代碼 

del users[0]

 裡的 

 就是一個數字字面量。它們簡單、實用,每個人每天都在寫。但是,當你的代碼裡不斷重複出現一些特定字面量時,你的“代碼品質告警燈”就應該亮起黃燈 ? 了。

舉個例子,假如你剛加入一家心儀已久的新公司,同僚轉交給你的項目裡有這麼一個函數:

def mark_trip_as_featured(trip):    """将某個旅程添加到推薦欄目    """    if trip.source == 11:        do_some_thing(trip)    elif trip.source == 12:        do_some_other_thing(trip)    ... ...    return
           

這個函數做了什麼事?你努力想搞懂它的意思,不過 

trip.source == 11

 是什麼情況?那 

== 12

 呢?這兩行代碼很簡單,沒有用到任何魔法特性。但初次接觸代碼的你可能需要花費一整個下午,才能弄懂它們的含義。

問題就出在那幾個數字字面量上。 最初寫下這個函數的人,可能是在公司成立之初加入的那位元老程式員。而他對那幾個數字的含義非常清楚。但如果你是一位剛接觸這段代碼的新人,就完全是另外一碼事了。

使用 enum 枚舉類型改善代碼

那麼,怎麼改善這段代碼?最直接的方式,就是為這兩個條件分支添加注釋。不過在這裡,“添加注釋”顯然不是提升代碼可讀性的最佳辦法(其實在絕大多數其他情況下都不是)。我們需要用有意義的名稱來代替這些字面量,而

枚舉類型(enum)

用在這裡最合适不過了。

enum

 是 Python 自 3.4 版本引入的内置子產品,如果你使用的是更早的版本,可以通過 

pip install enum34

 來安裝它。下面是使用 enum 的樣例代碼:

# -*- coding: utf-8 -*-from enum import IntEnumclass TripSource(IntEnum):    FROM_WEBSITE = 11    FROM_IOS_CLIENT = 12def mark_trip_as_featured(trip):    if trip.source == TripSource.FROM_WEBSITE:        do_some_thing(trip)    elif trip.source == TripSource.FROM_IOS_CLIENT:        do_some_other_thing(trip)    ... ...    return
           

将重複出現的數字字面量定義成枚舉類型,不光可以改善代碼的可讀性,代碼出現 Bug 的幾率也會降低。

試想一下,如果你在某個分支判斷時将 

11

 錯打成了 

111

 會怎麼樣?我們時常會犯這種錯,而這類錯誤在早期特别難被發現。将這些數字字面量全部放入枚舉類型中可以比較好的規避這類問題。類似的,将字元串字面量改寫成枚舉也可以獲得同樣的好處。

使用枚舉類型代替字面量的好處:

  • 提升代碼可讀性:所有人都不需要記憶某個神奇的數字代表什麼
  • 提升代碼正确性:減少打錯數字或字母産生 bug 的可能性

當然,你完全沒有必要把代碼裡的所有字面量都改成枚舉類型。 代碼裡出現的字面量,隻要在它所處的上下文裡面容易了解,就可以使用它。 比如那些經常作為數字下标出現的 

 和 

-1

 就完全沒有問題,因為所有人都知道它們的意思。

2别在裸字元串處理上走太遠

什麼是“裸字元串處理”?在這篇文章裡,它指隻使用基本的加減乘除和循環、配合内置函數/方法來操作字元串,獲得我們需要的結果。

所有人都寫過這樣的代碼。有時候我們需要拼接一大段發給使用者的告警資訊,有時我們需要構造一大段發送給資料庫的 SQL 查詢語句,就像下面這樣:

def fetch_users(conn, min_level=None, gender=None, has_membership=False, sort_field="created"):    """擷取使用者清單       :param int min_level: 要求的最低使用者級别,預設為所有級别    :param int gender: 篩選使用者性别,預設為所有性别    :param int has_membership: 篩選所有會員/非會員使用者,預設非會員    :param str sort_field: 排序字段,預設為按 created "使用者建立日期"    :returns: 清單:[(User ID, User Name), ...]    """    # 一種古老的 SQL 拼接技巧,使用 "WHERE 1=1" 來台灣字元串拼接操作    # 區分查詢 params 來避免 SQL 注入問題    statement = "SELECT id, name FROM users WHERE 1=1"    params = []    if min_level is not None:        statement += " AND level >= ?"        params.append(min_level)    if gender is not None:        statement += " AND gender >= ?"        params.append(gender)    if has_membership:        statement += " AND has_membership == true"    else:        statement += " AND has_membership == false"        statement += " ORDER BY ?"    params.append(sort_field)    return list(conn.execute(statement, params))
           

我們之是以用這種方式拼接出需要的字元串 - 在這裡是 SQL 語句 - 是因為這樣做簡單、直接,符合直覺。但是這樣做最大的問題在于:随着函數邏輯變得更複雜,這段拼接代碼會變得容易出錯、難以擴充。事實上,上面這段 Demo 代碼也隻是僅僅做到看上去沒有明顯的 bug 而已 (誰知道有沒有其他隐藏問題)。

其實,對于 SQL 語句這種結構化、有規則的字元串,用對象化的方式建構和編輯它才是更好的做法。下面這段代碼用 SQLAlchemy 子產品完成了同樣的功能:

def fetch_users_v2(conn, min_level=None, gender=None, has_membership=False, sort_field="created"):"""擷取使用者清單    """
    query = select([users.c.id, users.c.name])if min_level is not None:
        query = query.where(users.c.level >= min_level)if gender is not None:
        query = query.where(users.c.gender == gender)
    query = query.where(users.c.has_membership == has_membership).order_by(users.c[sort_field])return list(conn.execute(query))
           

上面的 

fetch_users_v2

 函數更短也更好維護,而且根本不需要擔心 SQL 注入問題。是以,當你的代碼中出現複雜的裸字元串處理邏輯時,請試着用下面的方式替代它:

Q: 目标/源字元串是結構化的,遵循某種格式嗎?

  • 是:找找是否已經有開源的對象化子產品操作它們,或是自己寫一個
    • SQL:SQLAlchemy
    • XML:lxml
    • JSON、YAML ...
  • 否:嘗試使用模闆引擎而不是複雜字元串處理邏輯來達到目的
    • Jinja2
    • Mako
    • Mustache

3不必預計算字面量表達式

我們的代碼裡偶爾會出現一些比較複雜的數字,就像下面這樣:

def f1(delta_seconds):    # 如果時間已經過去了超過 11 天,不做任何事    if delta_seconds > 950400:        return     ...
           

話說在前頭,上面的代碼沒有任何毛病。

首先,我們在小本子(當然,和我一樣的聰明人會用 IPython)上算了算:

11天一共包含多少秒?

。然後再把結果 

950400

 這個神奇的數字填進我們的代碼裡,最後心滿意足的在上面補上一行注釋:告訴所有人這個神奇的數字是怎麼來的。

我想問的是:“為什麼我們不直接把代碼寫成 

if delta_seconds < 11 * 24 * 3600:

 呢?”

性能,答案一定會是“性能”。

我們都知道 Python 是一門(速度欠佳的)解釋型語言,是以預先計算出 

950400

 正是因為我們不想讓每次對函數 

f1

 的調用都帶上這部分的計算開銷。不過事實是:即使我們把代碼改成 

if delta_seconds < 11 * 24 * 3600:

,函數也不會多出任何額外的開銷。

Python 代碼在執行時會被解釋器編譯成位元組碼,而真相就藏在位元組碼裡。讓我們用 dis 子產品看看:

def f1(delta_seconds):    if delta_seconds < 11 * 24 * 3600:        returnimport disdis.dis(f1)# dis 執行結果  5           0 LOAD_FAST                0 (delta_seconds)              2 LOAD_CONST               1 (950400)              4 COMPARE_OP               0 (              6 POP_JUMP_IF_FALSE       12  6           8 LOAD_CONST               0 (None)             10 RETURN_VALUE        >>   12 LOAD_CONST               0 (None)             14 RETURN_VALUE
           

看見上面的 

2 LOAD_CONST 1 (950400)

 了嗎?這表示 Python 解釋器在将源碼編譯成成位元組碼時,會計算 

11 * 24 * 3600

 這段整表達式,并用 

950400

 替換它。

是以,當我們的代碼中需要出現複雜計算的字面量時,請保留整個算式吧。它對性能沒有任何影響,而且會增加代碼的可讀性。

Hint:Python 解釋器除了會預計算數值字面量表達式以外,還會對字元串、清單做類似的操作。一切都是為了性能。誰讓你們老吐槽 Python 慢呢?

結束語

由于篇幅原因,一些常用的操作比如字元串格式化等,文章裡并沒有涵蓋到。以後有機會再寫吧。

原文作者:騰訊進階工程師 朱雷

來源:騰訊内部KM論壇

  -Python好課-  

c 字元串轉數字_Python工匠:數字與字元串(上)

從零開始,開發Wukong-robot

多方位Python體系,滿足不同技能需求

c 字元串轉數字_Python工匠:數字與字元串(上)

騰訊NEXT學院

求職幹貨 | 前輩blog  | 前端課程

↓ 戳