天天看點

Python 工匠:寫好面向對象代碼幾個原則

以下文章來源于piglei ,作者piglei

Python 工匠:寫好面向對象代碼幾個原則

piglei

電玩愛好者,“Python 工匠” 系列文章作者。

Python 工匠:寫好面向對象代碼幾個原則
Python 工匠:寫好面向對象代碼幾個原則

Python 是一門支援多種程式設計風格的語言,面對相同的需求,擁有不同背景的程式員可能會寫出風格迥異的 Python 代碼。比如一位習慣編寫 C 語言的程式員,通常會定義一大堆函數來搞定所有事情,這是“過程式程式設計”的思想。而一位有 Java 背景的程式員則更傾向于設計許多個互相關聯的類(class),這是 “面向對象程式設計(後簡稱 OOP)”。

雖然不同的程式設計風格各有特點,無法直接比較。但是 OOP 思想在現代軟體開發中起到的重要作用應該是毋庸置疑的。

很多人在學習如何寫好 OOP 代碼時,會選擇從那 23 種經典的“設計模式”開始。不過對于 Python 程式員來說,我認為這并非是一個最佳選擇。

Python 對 OOP 的支援

Python 語言雖然擁有類、繼承、多态等核心 OOP 特性,但和那些完全基于 OOP 思想設計的程式設計語言(比如 Java)相比,它在 OOP 支援方面做了很多簡化工作。比如它 沒有嚴格的類私有成員,沒有接口(Interface)對象 等。

而與此同時,Python 靈活的函數對象、鴨子類型等許多動态特性又讓一些在其他語言中很難做到的事情變得非常簡單。這些語言間的差異共同導緻了一個結果:很多經典的設計模式到了 Python 裡,就丢失了那個“味道”,實用性也大打折扣。

拿大家最熟悉的單例模式來說。你可以花上一大把時間,來學習如何在 Python 中利用

__new__

方法或元類(metaclass)來實作單例設計模式,但最後你會發現,自己 95% 的需求都可以通過直接定義一個子產品級全局變量來搞定。

是以,與具體化的 設計模式 相比,我覺得一些更為抽象的 設計原則 适用性更廣、更适合運用到 Python 開發工作中。而談到關于 OOP 的設計原則,“SOLID” 是衆多原則中最有名的一個。

SOLID 設計原則

著名的設計模式書籍《設計模式:可複用面向對象軟體的基礎》出版于 1994 年,距今已有超過 25 年的曆史。而這篇文章的主角: “SOLID 設計原則”同樣也并不年輕。

早在 2000 年,Robert C. Martin 就在他的文章 "Design Principles and Design Patterns" 中整理并提出了 “SOLID” 設計原則的雛型,之後又在他的經典著作《靈活軟體開發 : 原則、模式與實踐》中将其發揚光大。“SOLID” 由 5 個單詞組合的首字母縮寫組成,分别代表 5 條不同的面向對象領域的設計原則。

在編寫 OOP 代碼時,如果遵循這 5 條設計原則,就更可能寫出可擴充、易于修改的代碼。相反,如果不斷違反其中的一條或多條原則,那麼很快你的代碼就會變得不可擴充、難以維護。

接下來,讓我用一個真實的 Python 代碼樣例來分别向你诠釋這 5 條設計原則。

寫在最前面的注意事項:
  1. “原則”不是“法律”,它隻起到指導作用,并非不可以違反
  2. “原則”的後兩條與接口(Interface)有關,而 Python 沒有接口,是以對這部分的诠釋是我的個人了解,與原版可能略有出入
  3. 文章後面的内容含有大量代碼,請做好心理準備 ☕️
  4. 為了增強代碼的說明性,本文中的代碼使用了 Python3 中的 類型注解特性

SOLID 原則與 Python

Hacker News(後簡稱 HN) 是一個在程式員圈子裡很受歡迎的站點。在它的首頁,有很多由使用者送出後基于推薦算法排序的科技相關内容。

我經常會去上面看一些熱門文章,但我覺得每次打開浏覽器通路有點麻煩。是以,我準備編寫一個腳本,自動抓取 HN 首頁 Top5 的新聞标題與連結,并用純文字的方式寫入到檔案。友善自己用其他工具閱讀。

Python 工匠:寫好面向對象代碼幾個原則

圖:Hacker News 首頁截圖

編寫爬蟲幾乎是 Python 天生的拿手好戲。利用 requests、lxml 等子產品提供的好用功能,我可以輕松實作上面的需求。下面是我第一次編寫好的代碼:

  1. import io

  2. import sys

  3. from typing import Generator

  4. import requests

  5. from lxml import etree

  6. class Post:

  7. """HN(https://news.ycombinator.com/) 上的條目

  8.    :param title: 标題

  9.    :param link: 連結

  10.    :param points: 目前得分

  11.    :param comments_cnt: 評論數

  12.    """

  13. def __init__(self, title: str, link: str, points: str, comments_cnt: str):

  14.        self.title = title

  15.        self.link = link

  16.        self.points = int(points)

  17.        self.comments_cnt = int(comments_cnt)

  18. class HNTopPostsSpider:

  19. """抓取 HackerNews Top 内容條目

  20.    :param fp: 存儲抓取結果的目标檔案對象

  21.    :param limit: 限制條目數,預設為 5

  22.    """

  23.    ITEMS_URL = 'https://news.ycombinator.com/'

  24.    FILE_TITLE = 'Top news on HN'

  25. def __init__(self, fp: io.TextIOBase, limit: int = 5):

  26.        self.fp = fp

  27.        self.limit = limit

  28. def fetch(self) -> Generator[Post, None, None]:

  29. """從 HN 抓取 Top 内容

  30.        """

  31.        resp = requests.get(self.ITEMS_URL)

  32. # 使用 XPath 可以友善的從頁面解析出你需要的内容,以下均為頁面解析代碼

  33. # 如果你對 xpath 不熟悉,可以忽略這些代碼,直接跳到 yield Post() 部分

  34.        html = etree.HTML(resp.text)

  35.        items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]')

  36. for item in items[:self.limit]:

  37.            node_title = item.xpath('./td[@class="title"]/a')[0]

  38.            node_detail = item.getnext()

  39.            points_text = node_detail.xpath('.//span[@class="score"]/text()')

  40.            comments_text = node_detail.xpath('.//td/a[last()]/text()')[0]

  41. yield Post(

  42.                title=node_title.text,

  43.                link=node_title.get('href'),

  44. # 條目可能會沒有評分

  45.                points=points_text[0].split()[0] if points_text else '0',

  46.                comments_cnt=comments_text.split()[0]

  47. )

  48. def write_to_file(self):

  49. """以純文字格式将 Top 内容寫入檔案

  50.        """

  51.        self.fp.write(f'# {self.FILE_TITLE}\n\n')

  52. # enumerate 接收第二個參數,表示從這個數開始計數(預設為 0)

  53. for i, post in enumerate(self.fetch(), 1):

  54.            self.fp.write(f'> TOP {i}: {post.title}\n')

  55.            self.fp.write(f'> 分數:{post.points} 評論數:{post.comments_cnt}\n')

  56.            self.fp.write(f'> 位址:{post.link}\n')

  57.            self.fp.write('------\n')

  58. def main():

  59. # with open('/tmp/hn_top5.txt') as fp:

  60. #     crawler = HNTopPostsSpider(fp)

  61. #     crawler.write_to_file()

  62. # 因為 HNTopPostsSpider 接收任何 file-like 的對象,是以我們可以把 sys.stdout 傳進去

  63. # 實作往控制台标準輸出列印的功能

  64.    crawler = HNTopPostsSpider(sys.stdout)

  65.    crawler.write_to_file()

  66. if __name__ == '__main__':

  67.    main()

你可以把上面的代碼稱之為符合 OOP 風格的,因為在上面的代碼裡,我定義了兩個類:

  1. Post

    :表示單個 HN 内容條目,其中定義了标題、連結等字段,是用來銜接“抓取”和“寫入檔案”兩件事情的資料類
❯ python news_digester.py> TOP 1: Show HN: NoAgeismInTech – Job board for companies fighting ageism in tech> 分數:104 評論數:26> 位址:https://noageismintech.com/------> TOP 2: Magic Leap sues former employee who founded the China-based Nreal for IP theft> 分數:17 評論數:2> 位址:https://www.bloomberg.com/news/articles/2019-06-18/secretive-magic-leap-says-ex-engineer-copied-headset-for-china------... ...