---
# System prepended metadata

title: 關於我學習Python這檔事(下)

---

## 資料結構
資料結構是什麼？
答：實際上就是幫助你有效率去存取、修改、新增、整理資料的方法

想像以下場景：
今天做了一個可以自動批改學生成績的程式，你會需要以下步驟來進行
1. 從數位學苑中獲取全部學生的考卷
2. 讀取每個考卷，根據答案進行批改
3. 將每位學生的成績傳回去到數位學苑

解釋一下每個步驟理論上會用到甚麼
1. 獲取考卷且儲存:爬蟲獲取(之後會教)、List儲存
2. 批改:List存取
3. 上傳回去:爬蟲上傳、List存取

這裡可以看到List的使用場景
就是存放同性質的資料到一個地方
![image](https://hackmd.io/_uploads/SyVXunTJbx.png)
Source:https://www.analyticsvidhya.com/blog/2021/06/functions-you-should-know-to-master-lists-in-python/

所以如果沒有使用List來儲存的話
你就會這樣寫
```python=
student1_exam = get_exam("student1")
student2_exam = get_exam("student2")
student3_exam = get_exam("student3")
```
這樣的缺點非常明顯:不靈活
透過List寫的話:
```python=
# 直接把一整個列表爬下來
student_exams = get_all_exams()
for student in student_exams:
    # 開始批改
```

這樣就變成非常靈活

接下來開始介紹python中比較常見的資料結構和他們的使用場景

### List(列表)
#### 作用:
如同前面提到的，List (列表) 就像一個收納盒，主要作用是將一群資料（元素）依照「順序」儲存在一起
```python=
# 建立一個儲存成績的 List
scores = [85, 92, 78, 95, 88]

# 建立一個空的 List，之後再慢慢新增資料
students = []
```
使用索引（index）來取得特定位置的元素。索引從 0 開始
```python=
scores = [85, 92, 78, 95, 88]

# 取得第一個學生的成績 (索引 0)
first_score = scores[0]
print(f"第一個成績: {first_score}") # Output: 第一個成績: 85

# 取得第三個學生的成績 (索引 2)
third_score = scores[2]
print(f"第三個成績: {third_score}") # Output: 第三個成績: 78

# 也可以從後面開始算 (索引 -1 是最後一個)
last_score = scores[-1]
print(f"最後一個成績: {last_score}") # Output: 最後一個成績: 88
```

因為 List 是可變的，你可以直接透過索引重新賦值
```python=
scores = [85, 92, 78, 95, 88]
print(f"修改前的成績: {scores}")

# 老師發現改錯了，第一個學生的成績其實是 87
scores[0] = 87
print(f"修改後的成績: {scores}") 
# Output: 修改後的成績: [87, 92, 78, 95, 88]
```
最常用的是使用 .append() 方法，將新元素加到 List 的「尾端」
```python=
scores = [87, 92, 78]
print(f"新增前: {scores}")

# 新來一位轉學生，成績 90 分
scores.append(90)
print(f"新增後: {scores}") 
# Output: 新增後: [87, 92, 78, 90]
```

刪除元素，有幾種方式，最常用的是 .remove() (刪除第一個符合的值) 或 del (刪除指定索引的元素)
```python=
scores = [87, 92, 78, 90, 92] # 假設有兩個 92 分

# 情況一：想刪除 78 分 (使用 .remove())
scores.remove(78)
print(f"刪除 78 分後: {scores}") 
# Output: [87, 92, 90, 92]

# 情況二：想刪除索引為 1 的元素 (也就是第一個 92 分) (使用 del)
del scores[1]
print(f"刪除索引 1 後: {scores}") 
# Output: [87, 90, 92]
```
使用 len() 函式來知道 List 中有多少個元素

```python=
scores = [87, 90, 92]
count = len(scores)
print(f"總共有 {count} 位學生成績") 
# Output: 總共有 3 位學生成績
```

遍歷，這就是前面「批改考卷」範例中最重要的應用：使用 for 迴圈一個一個取出 List 中的所有元素來處理
```python=
scores = [87, 92, 78, 95, 88]

print("--- 開始列印成績單 ---")
for score in scores:
    # score 這個變數會依序變成 87, 92, 78...
    if score < 90:
        print(f"學生成績: {score} (加油！)")
    else:
        print(f"學生成績: {score} (表現優良！)")
```

這裡可以看到其他更多的資料結構
https://medium.com/@gdgntpu/%E7%A4%BE%E8%AA%B2%E5%9B%9E%E9%A1%A7-python-%E5%85%A5%E9%96%80-4-%E8%B3%87%E6%96%99%E7%B5%90%E6%A7%8B-10f963157164

## 函式
可以理解成可重複利用的程式碼區塊
直接看例子比較快
```python
print("Hello world")
```
一切看起來很美好對吧
那如果我告訴你實際上這個**簡單**的函式背後的樣子長這樣呢?
:::spoiler
```c=
static PyObject *
builtin_print_impl(PyObject *module, PyObject * const *objects,
                   Py_ssize_t objects_length, PyObject *sep, PyObject *end,
                   PyObject *file, int flush)
/*[clinic end generated code: output=38d8def56c837bcc input=ff35cb3d59ee8115]*/
{
    int i, err;

    if (file == Py_None) {
        file = PySys_GetAttr(&_Py_ID(stdout));
        if (file == NULL) {
            return NULL;
        }

        /* sys.stdout may be None when FILE* stdout isn't connected */
        if (file == Py_None) {
            Py_DECREF(file);
            Py_RETURN_NONE;
        }
    }
    else {
        Py_INCREF(file);
    }

    if (sep == Py_None) {
        sep = NULL;
    }
    else if (sep && !PyUnicode_Check(sep)) {
        PyErr_Format(PyExc_TypeError,
                     "sep must be None or a string, not %.200s",
                     Py_TYPE(sep)->tp_name);
        Py_DECREF(file);
        return NULL;
    }
    if (end == Py_None) {
        end = NULL;
    }
    else if (end && !PyUnicode_Check(end)) {
        PyErr_Format(PyExc_TypeError,
                     "end must be None or a string, not %.200s",
                     Py_TYPE(end)->tp_name);
        Py_DECREF(file);
        return NULL;
    }

    for (i = 0; i < objects_length; i++) {
        if (i > 0) {
            if (sep == NULL) {
                err = PyFile_WriteString(" ", file);
            }
            else {
                err = PyFile_WriteObject(sep, file, Py_PRINT_RAW);
            }
            if (err) {
                Py_DECREF(file);
                return NULL;
            }
        }
        err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW);
        if (err) {
            Py_DECREF(file);
            return NULL;
        }
    }

    if (end == NULL) {
        err = PyFile_WriteString("\n", file);
    }
    else {
        err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
    }
    if (err) {
        Py_DECREF(file);
        return NULL;
    }

    if (flush) {
        if (_PyFile_Flush(file) < 0) {
            Py_DECREF(file);
            return NULL;
        }
    }
    Py_DECREF(file);

    Py_RETURN_NONE;
}
``` 
:::
可以想像沒有函式的世界每個地方要print出東西的話，就會需要重新撰寫上面那串程式(實際是這個是c語言)
因此函式的主要作用:避免撰寫重複的程式碼、讓程式的主邏輯看起來更簡潔

那麼python裡面要如何定義函式?
```python
def max(lhs, rhs):
    return lhs if lhs > rhs else rhs
```
上面這個函式的主要目的是根據輸入的兩個數回傳哪個比較大
解釋一下每個部份的意思
```python
def 代表define 在python中代表定義函式的意思
定義一個函式的方式就像是上面的程式一樣

整體的架構像是下面這樣
def 函式名字(參數1, 參數2, 參數3, ..., 參數n): <-像是迴圈一樣接一個冒號代表函式的開始
    #像是一般程式一樣開始撰寫程式邏輯 要記得縮排
    
return 的意思是這個函式的輸出，像是數學中f(x)的值一樣，會有輸出

所以外面呼叫這個函式並且想要得到他的值的話就會這樣寫
value = max(1, 2)
# value = 2
# 這裡是用一個變數叫做value來存max(1, 2)這個函式執行結果
# 也就是return 後的值

當然你也可以完全不回傳值（完全不寫return）編譯器會自動在最後加上return
```

練習：
寫一個函式判斷一個數是否在陣列裡面

:::spoiler
```python=
def is_contains(list, target):
    for num in list:
        if num == target:
            return True
    return False

list = [2,5,5,2,1]

print(is_contains(list, 2))
```
:::

## 模組
在上面的程式碼我們展示了max和is_contains兩個函式，如果我們想要在另一個.py檔中也用這兩個函式的話要怎麼辦?

##### 使用import關鍵字
我們將剛剛定義的max和is_contains兩個函式放在一個.py檔案裡面，並且叫做utils .py
此時我們再定義一個main.py檔案，裡面即可透過import utils將utils.py中的函式等等都import進來
```python=
def max(lhs, rhs):
    return lhs if lhs > rhs else rhs
def is_contains(list, target):
    for num in list:
        if num == target:
            return True
    return False
```
這裡有個細節是如果想要呼叫該檔案的函式的話都會需要先加上utils然後才是該函式的名字
python會有這個設計的原因是防止有同名函式撞名，進而產生錯誤

以下是import的其他寫法
```python=
from utils import max

# 要用的時候直接用max而不是utils.max
# from會將namespace的功能給去掉
print(max(1, 2)) # output: 2
```

```python=
from utils import max as mx

# as 是用來取別名
print(mx(1, 2))

# 所以也可以這樣寫
# 因為沒有使用到from所以每次呼叫都還要加上utils
import utils as ut

print(ut.max(1, 2)) # output: 2
```

:::danger
非常不建議！！！
大招：from utils import *
:::
上面的大招會做的事相當於直接把所有utils.py的東西都輸入到這個檔案中，如果好巧不巧現在的檔案以及utils.py都有同名的函式，就會亂掉
```python=
from utils import *

def is_contains(list, target):
    return "Just blank here"

list = [2,5,7,2,1]

print(is_contains(list, 2))
```
猜猜這個is_contains是utils.py裡面的還是上面定義的？
:::spoiler
答：會輸出Just blank here，也就是上面定義的
:::
這個案例還算是比較簡單的，因為只有兩個檔案，如果有多個檔案的話，那可以想像出來肯定非常亂

實際上模組的import也會因為python的檔案架構有所不同，像是如果透過資料夾把各個python檔案都放在不同的資料夾內的話，就會需要比較特殊的東西將其轉化成模組，這個教學就先不講

## 練習
### 第一題
設計一個程式可以log現在程式的狀況，這個log需要可以印出這個訊息的輕重緩急、訊息的時間、訊息的內容
這個程式的log只要設計成print就好

:::info
註：log就是程式的日誌，在撰寫程式的時候可以在多個地方進行log，之後想要看看程式的狀態時，就只需要去看log日誌就好，而log又可以分為好幾個等級，像是DEBUG、INFO、WARNING、ERROR、CRITICAL等等，這些都是可以自己定義的 
:::

:::info
取得現在時間方式：
```python=
import datetime
now = datetime.datetime.now()
```
:::

:::spoiler
```python=
import datetime

def log(level, message):
    now = datetime.datetime.now()
    
    print(f"[{level.upper()}] {now} - {message}")
```

其實還有更好的寫法，但這裡先簡單寫就好
:::

### 第二題
將剛剛的函式放在utils.py裡面，之後透過其他自己定義的程式去呼叫他
:::spoiler
```python=
# main.py
import utils

a = 5
utils.log("error", a)
```
:::