Python中lambda的使用,與它的三個好基友介紹!

匿名函數lambda

除了def語句,python還提供了一種生成函數對象的表達式形式。由於它與LISP語言中的一個工具類似,所以稱為lambda。

就像def一樣,這個表達式創建了一個之後能夠調用的函數,但是它返回一個函數而不是將這個函數賦值給一個變量。這些就是lambda叫做匿名函數的原因。實際上,他常常以一種行內進行函數定義的方式使用,或者用作推遲執行一些代碼。

lambda的一般形式是關鍵字lambda之後跟着一個或多個參數(與一個def頭部內用括號括起來的參數列表類似),緊跟着是一個冒號,之後是表達式

lambda arg1,arg2,argn:expression using arguments

由lambda表達式所返回的函數對象與由def創建並複製后的函數對象工作起來是完全一致的,但lambda有一些不同之處,讓其扮演特定的角色時更有用:

lambda是一個表達式,而不是一個語句

因為這一點,lambda可以出現在python語法不允許def出現的地方。
此外,作為一個表達式,lambda返回一個值(一個新的函數),可以選擇性的賦值給一個變量
相反,def語句總是得在頭部將一個新的函數賦值給一個變量,而不是將這個函數作為結果返回。

lambda的主題是單個表達式,而不是一個代碼塊

這個lambda的主題簡單的就好像放在def主體return語句中的代碼一樣。
簡單的將結果寫成一個順暢的表達式,而不是明確的返回。
但由於它僅限於表達式,故lambda通常要比def功能少…你僅能夠在lambda主體中封裝有限的邏輯進去,因為他是一個為編寫簡單函數而設計的。
除了上述這些差別,def和lambda都能過做同樣種類的工作

def與lambda的相同用法

x = lambda x, y, z: x + y + z
x(2, 3, 4)
>>> 9

y = (lambda a='hello', b='world': a + b)
y(b='Python')
>>> 'hellopython'

為什麼使用lambda

看過上面的兩個小例子,很多人會說這個和def沒什麼差別,我們又為什麼要使用lambda呢?

通常來說,lambda起到一種函數的速寫作用,允許在使用的代碼內嵌一個函數的定義,他完全是可選的(是可以使用def代替他們),但是在你僅需要切入一段可執行代碼的情況下,它會帶來一個更簡潔的書寫效果。

lambda通常用來編寫跳轉表,也就是行為的列表或者字典,能夠按照需求執行操作,比如:

l = [lambda x: x ** 2, lambda x: x ** 3, lambda x: x ** 4]
for f in l:
    print(f(2))
>>> 4
>>> 8
>>> 16
print(l[0](3))
>>> 9

當需要把小段的可執行代碼編寫進def語句從語法上不能實現的地方是,lambda表達式作為def的一種速寫來說,是最為有用的,如果上面的代碼用def編寫,則變為:

def f1(x):
    return x ** 2
 
def f2(x):
    return x ** 3
 
def f3(x):
    return x ** 4
 
l = [f1, f2, f3]

for f in l:
    print(f(2))
print(l[0](3))

實際上,我們可以用python中的字典或者其他的數據結構來構建更多種類的行為表,從而做同樣的事情。

lambda中實現if-else

Python中具備的單行表達式:if a:b else c語法在lambda中同樣適用:

lower = lambda x,y:x if x<y else y
lower(4,5)
>>> 4

看了半天,大家可能也並未覺得lambda在python中到底比def優越與便利在哪裡,那麼說到lambda,就必須要提及三個函數map、filter、reduce,當你接觸了這三個函數,那麼你才能感受到lambda真實的方便之處

map 函數

程序對列表或者其他序列常常要做的一件事就是對每個元素進行一個操作,並把其結果集合起來。
python提供了一個工具map,它會對一個序列對象中的每一個元素應用該的函數,並返回一個包含了所有函數調用結果的列表。

舉個栗子,我們有一個列表,需要將列表的每一個字段+10,我們該如何操作?

list_show = [1, 2, 3, 4]
# 方式1
new_list_show = []
for i in list_show:
    new_list_show.append(i + 10)

print(new_list_show)

# 方式2
def adds(x):
    return x + 10

print(list(map(adds, list_show)))

# 更優雅的方式3:
print(list(map(lambda x: x + 10, list_show)))

看看上面三個實現方式,你覺得那種更加Pythonic?

eg:需要注意一點,map在python3中是一個可迭代對象,引入需要使用列表調用來使它生成所有的結果用於显示,python2不必如此。

當然map的闡述函數,不僅僅支持自己編寫的,同樣也支持python自帶的多種函數,比如:

list_show = [1, -2, 3, -4, 5, -6]
print(list(map(abs, list_show)))
>>> [1, 2, 3, 4, 5, 6]

filter函數

filter通過字面意思,大家就知道它的用處了,用於數據的過濾操作,它也是lambda的一個好基友,舉個栗子。
我們需要過濾0-9中,能被2整除的数字組成一個列表,我們該如何操作?只需要一行代碼:

print(list(filter(lambda x: x % 2 == 0, range(10))))
>>> [0, 2, 4, 6, 8]

沒錯,filter就是這麼的簡單實用….

reduce的妙用

reduce在python2中是一個簡單的函數,但在python3中它責備收錄與functools中。
它接收一個迭代器來處理並返回一個單個的結果。

list_show = [1, 2, 3, 4]
print(reduce(lambda x, y: x + y, list_show))
>>> 10
print(reduce(lambda x, y: x * y, list_show))
>>> 24

lambda的實用與它的好基友就介紹到這裏,希望對大家有所幫助。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

如何使用C#調用C++類虛函數(即動態內存調用)

  本文講解如何使用C#調用只有.h頭文件的c++類的虛函數(非實例函數,因為非虛函數不存在於虛函數表,無法通過類對象偏移計算地址,除非用export導出,而gcc默認是全部導出實例函數,這也是為什麼msvc需要.lib,如果你不清楚但希望了解,可以選擇找我擺龍門陣),並以COM組件的c#直接調用(不需要引用生成introp.dll)舉例。

  我們都知道,C#支持調用非託管函數,使用P/Inovke即可方便實現,例如下面的代碼

[DllImport("msvcrt", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl)]
public static extern void memcpy(IntPtr dest, IntPtr src, int count);

不過使用DllImport只能調用某個DLL中標記為導出的函數,我們可以使用一些工具查看函數導出,如下圖

一般會導出的函數,都是c語言格式的。

  C++類因為有多態,所以內存中維護了一個虛函數表,如果我們知道了某個C++類的內存地址,也有它的頭文件,那麼我們就能自己算出想要調用的某個函數的內存地址從而直接call,下面是一個簡單示例

#include <iostream>

class A_A_A {
public:
    virtual void hello() {
        std::cout << "hello from A\n";
    };
};

//typedef void (*HelloMethod)(void*);

int main()
{
    A_A_A* a = new A_A_A();
    a->hello();

    //HelloMethod helloMthd = *(HelloMethod *)*(void**)a;
    
    //helloMthd(a);
    (*(void(**)(void*))*(void**)a)(a);

    int c;
    std::cin >> c;
}

(上文中將第23行註釋掉,然後將其他註釋行打開也是一樣的效果,可能更便於閱讀)
從代碼中大家很容易看出,c++的類的內存結構是一個虛函數表二級指針(數組,多重繼承時可能有多個),每個虛函數表又是一個函數二級指針(數組,多少個虛函數就有多少個指針)。上文中我們假使只知道a是一個類對象,它的第一個虛函數是void (*) (void)類型的,那麼我們可以直接call它的函數。

  接下來開始騷操作,我們嘗試用c#來調用一個c++的虛函數,首先寫一個c++的dll,並且我們提供一個c格式的導出函數用於提供一個new出的對象(畢竟c++的new操作符很複雜,而且實際中我們經常是可以拿到這個new出來的對象,後面的com組件調用部分我會詳細說明),像下面這樣

dll.h

class DummyClass {
private:
    virtual void sayHello();
};

dll.cpp

#include "dll.h"
#include <stdio.h>

void DummyClass::sayHello() {
    printf("Hello World\n");
}

extern "C" __declspec(dllexport) DummyClass* __stdcall newObj() {
    return new DummyClass();
}

我們編譯出的dll長這樣

讓我們編寫使用C#來調用sayHello

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp2
{
    class Program
    {
        [DllImport("Dll1", EntryPoint = "newObj")]
        static extern IntPtr CreateObject();

        [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
        delegate void voidMethod1(IntPtr thisPtr);

        static void Main(string[] args)
        {
            IntPtr dummyClass = CreateObject();
            IntPtr vfptr = Marshal.ReadIntPtr(dummyClass);
            IntPtr funcPtr = Marshal.ReadIntPtr(vfptr);
            voidMethod1 voidMethod = (voidMethod1)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(voidMethod1));
            voidMethod(dummyClass);

            Console.ReadKey();
        }
    }
}

(因為調用的是c++的函數,所以this指針是第一個參數,當然,不同調用約定時它入棧方式和順序不一樣)
下面有一種另外的寫法

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace ConsoleApp2
{
    class Program
    {
        [DllImport("Dll1", EntryPoint = "newObj")]
        static extern IntPtr CreateObject();

        //[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
        //delegate void voidMethod1(IntPtr thisPtr);

        static void Main(string[] args)
        {
            IntPtr dummyClass = CreateObject();
            IntPtr vfptr = Marshal.ReadIntPtr(dummyClass);
            IntPtr funcPtr = Marshal.ReadIntPtr(vfptr);
            /*voidMethod1 voidMethod = (voidMethod1)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(voidMethod1));
            voidMethod(dummyClass);*/

            AssemblyName MyAssemblyName = new AssemblyName();
            MyAssemblyName.Name = "DummyAssembly";
            AssemblyBuilder MyAssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(MyAssemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder MyModuleBuilder = MyAssemblyBuilder.DefineDynamicModule("DummyModule");
            MethodBuilder MyMethodBuilder = MyModuleBuilder.DefineGlobalMethod("DummyFunc", MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[] { typeof(int) });
            ILGenerator IL = MyMethodBuilder.GetILGenerator();

            IL.Emit(OpCodes.Ldarg, 0);
            IL.Emit(OpCodes.Ldc_I4, funcPtr.ToInt32());

            IL.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(void), new Type[] { typeof(int) });
            IL.Emit(OpCodes.Ret);

            MyModuleBuilder.CreateGlobalFunctions();

            MethodInfo MyMethodInfo = MyModuleBuilder.GetMethod("DummyFunc");

            MyMethodInfo.Invoke(null, new object[] { dummyClass.ToInt32() });

            Console.ReadKey();
        }
    }
}

上文中的方法雖然複雜了一點,但……就是沒什麼用。不用懷疑!

文章寫到這裏,可能有童鞋就要發問了。你說這麼多,tmd到底有啥用?那接下來,我舉一個栗子,activex組件的直接調用!
以前,我們調用activex組件需要做很多複雜的事情,首先需要使用命令行調用regsvr32將dll註冊到系統,然後回到vs去引用com組件是吧

  仔細想想,需要嗎?並不需要,因為兩個原因:

  • COM組件規定DLL需要給出一個DllGetClassObject函數,它就可以為我們在DLL內部new一個所需對象
  • COM組件返回的對象其實就是一個只有虛函數的C++類對象(COM組件規定屬性和事件用getter/setter方式實現)
  • COM組件其實不需要用戶手動註冊,執行regsvr32會操作註冊表,而且32位/64位會混淆,其實regsvr32隻是調用了DLL導出函數DllRegisterServer,而這個函數的實現一般只是把自己註冊到註冊表中,這一步可有可無(特別是對於我們已經知道某個activex的dll存在路徑且它能提供的服務時,如果你非要註冊,使用p/invoke調用該dll的DllRegisterServer函數是一樣的效果)

因此,假如我們有一個activex控件(例如vlc),我們希望把它嵌入我們程序中,我們先看看常規的做法(本文沒有討論帶窗體的vlc,因為窗體這塊兒又複雜一些),直接貼圖:

看起來很簡單,但當我們需要打包給客戶使用時就很麻煩,涉及到嵌入vlc的安裝程序。而當我們會動態內存調用之後,就可以不註冊而使用vlc的功能,我先貼出代碼:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp3
{
    class Program
    {
        [DllImport("kernel32")]
        static extern IntPtr LoadLibraryEx(string path, IntPtr hFile, int dwFlags);
        [DllImport("kernel32")]
        static extern IntPtr GetProcAddress(IntPtr dll, string func);

        delegate int DllGetClassObject(Guid clsid, Guid iid, ref IntPtr ppv);

        delegate int CreateInstance(IntPtr _thisPtr, IntPtr unkown, Guid iid, ref IntPtr ppv);

        delegate int getVersionInfo(IntPtr _thisPtr, [MarshalAs(UnmanagedType.BStr)] out string bstr);

        static void Main(string[] args)
        {
            IntPtr dll = LoadLibraryEx(@"D:\Program Files\VideoLAN\VLC\axvlc.dll", default, 8);
            IntPtr func = GetProcAddress(dll, "DllGetClassObject");
            DllGetClassObject dllGetClassObject = (DllGetClassObject)Marshal.GetDelegateForFunctionPointer(func, typeof(DllGetClassObject));

            Guid vlc = new Guid("2d719729-5333-406c-bf12-8de787fd65e3");
            Guid clsid = new Guid("9be31822-fdad-461b-ad51-be1d1c159921");
            Guid iidClassFactory = new Guid("00000001-0000-0000-c000-000000000046");
            IntPtr objClassFactory = default;
            dllGetClassObject(clsid, iidClassFactory, ref objClassFactory);
            CreateInstance createInstance = (CreateInstance)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(Marshal.ReadIntPtr(objClassFactory) + IntPtr.Size * 3), typeof(CreateInstance));
            IntPtr obj = default;
            createInstance(objClassFactory, default, vlc, ref obj);
            getVersionInfo getVersion = (getVersionInfo)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(Marshal.ReadIntPtr(obj) + IntPtr.Size * 18), typeof(getVersionInfo));
            string versionInfo;
            getVersion(obj, out versionInfo);

            Console.ReadKey();
        }
    }
}

  上文中的代碼有幾處可能大家不容易懂,特別是指針偏移量的運算,這裏面有比較複雜的地方,文章篇幅有限,下來咱們細細研究。

  從11年下半年開始學習編程到現在已經很久了,有時候會覺得沒什麼奔頭。其實人生,無外乎兩件事,愛情和青春,我希望大家能有抓住的,就不要放手。兩年前,我為了要和一個女孩子多說幾句話,給人家講COM組件,其實我連c++有虛函數表都不知道,時至今日,我已經失去了她。今後怕是一直會任由靈魂遊盪,半夢半醒,即是人生。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

【python測試開發棧】python內存管理機制(一)—引用計數

什麼是內存

在開始進入正題之前,我們先來回憶下,計算機基礎原理的知識,為什麼需要內存。我們都知道計算機的CPU相當於人類的大腦,其運算速度非常的快,而我們平時寫的數據,比如:文檔、代碼等都是存儲在磁盤上的。磁盤的存取速度完全不能匹配cpu的運算速度,因此就需要一个中間層來適配兩者的不對等,內存由此而來,內存的存取速率很快,但是存儲空間不大。

舉一個圖書館的例子,便於大家理解,我們圖書館的書架就相當於磁盤,存放了大量的圖書可以供我們閱讀,但是如果書放在書架上,我們沒辦法直接閱讀(效率低),只能將書取出來,放在書桌上看,那書桌就相當於內存。

內存回收

內存資源畢竟是有限的,所以在使用之後,必須被回收掉,否則系統運行一段時間后就會因無內存可用而癱瘓。我們軟件測試領域常用的兩種語言:java和python,全部都採用內存自動回收的方法,也就是我們只管申請內存,但是不管釋放內存,由jvm和python解釋器來定期觸發內存回收。作為對比,C語言和C++中,程序員需要使用malloc申請內存,使用free去釋放內存,malloc和free必須成對的出現,否則非常容易出現內存問題。

還拿上面圖書館的例子,假如圖書館的書看完之後放在書桌上就可以(因為圖書可自動回收),那麼很快的,就沒有位置給新進來的同學看書了。這時候就需要圖書館管理員(jvm或python解釋器)定期的回收圖書,清空書桌。不過正常情況下,我們離開圖書館時,要自己清空書桌,將書放回書架(類似C語言和C++的內存回收方式)。

python內存管理

引用計數

python通過引用計數來進行內存管理,每一個python對象,都維護了一個指向該對象的引用計數。python的sys庫提供了getrefcount()函數來獲取對象的引用計數。下面我們看個例子(注意:不同版本的python,運行結果不同,我這裏採用的是python3.7.4):

"""
    @author: xuanke
    @time: 2019/11/27
    @function: 測試python內存
"""
import sys

class RefClass(object):
    def __init__(self):
        print("this is init")

def ref_count_test():
    # 驗證普通字符串
    str1 = "abc"
    print(sys.getrefcount(str1))
    # 驗證稍微複雜點的字符串
    print(sys.getrefcount("xuankeTester"))
    # 驗證小的数字
    print(sys.getrefcount(12))
    # 驗證大的数字
    print(sys.getrefcount(257))
    # 驗證類
    a = RefClass()
    print(sys.getrefcount(a))
    # 驗證引用計數增加
    b = a
    print(sys.getrefcount(a))

    # 驗證引用計數減少
    b = None
    print(sys.getrefcount(a))

if __name__ == '__main__':
    ref_count_test()

大家先來思考下,最終的結果會是什麼?!我覺得應該很多人都會答錯,因為不同版本的python,對引用變量個數有影響(主要是可復用的對象)。我們先貼出來運行結果,再來分析產生結果的原因:

27
4
9
3
this is init
2
3
2

不過提前聲明一點:sys.getrefcount函數在使用時,因為將對象(比如上例中的str1)作為參數傳入,所以會額外增加一個變量(相當於getrefcount持有了str1的引用),因此實際每個對象的實際引用計數都得減1。下面分別介紹下上面的幾種情況:

  • 字符串: str1=’abc’的引用數是27-1=26,是因為字符串’abc’比較簡單,在python解釋器(CPython)中確實可能存在26個引用。作為對比,在python2.7中,str1的引用變量個數是3-1=2。而字符串’xuanketester’,是我自定義的一個字符串,所以不可能會有其他額外的引用,所以其引用變量個數是3-1=2(至於為什麼是2,理論應該是0,是因為python解釋器默認持有了所有字符串的兩個引用)。
  • 数字: 数字12對應的引用計數個數是9-1=8,而257對應的引用計數個數是3-1=2,這主要是因為,在python初始化過程中,就創建了從-5到256的数字,緩存起來,這樣做是為了頻繁的分配內存,提高效率。而對於不在這個區間的数字,則會重新分配內存空間。所以数字12因為被複用,其引用計數個數是8(在python2.7.14中,其引用計數個數是8)。
  • 類: 在上面例子中,創建一個RefClass對象,其引用計數就是2-1=1,因為其是一個我們自定義的類對象,在python解釋器(Cpython)中肯定不會被複用。

我們可以通過打印內存地址的方式來驗證上面這幾種情況:

    def memory_address_test():
    str1 = 'xuankeTester'
    str2 = 'xuankeTester'
    print(id(str1))
    print(id(str2))

    str3 = 'abc'
    str4 = 'abc'
    print(id(str3))
    print(id(str4))

    a = 12
    b = 12
    print(id(a))
    print(id(b))

    c = 257
    d = 257
    print(id(c))
    print(id(d))

按照我們上面的分析,c和d的地址應該是不一樣的,a和b的地址是一樣的,字符串str1和str2、str3和str4內存地址都是一樣的。但是我在pycharm中,直接運行py文件,結果卻和預想的不一致,結果如下:

2854496960176
2854496960176
2854496857840
2854496857840
140724423258720
140724423258720
2854498931120
2854498931120

所有情況的內存地址都是一樣的,這是為什麼呢?我考慮到是不是pycharm對py文件做了優化,於是我又在命令行嘗試執行,結果還是一樣的。所以,我猜測可能是python解釋器在執行文件時,為了提高py文件的執行效率,對文件的內存地址做了優化—相同內容的對象內存地址都一樣。

為了驗證這個想法,我直接在python交互模式下執行,果然得到了我想要的結果:

Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> a=12
>>> b=12
>>> id(a)
140724423258720
>>> id(b)
140724423258720
>>> a=257
>>> b=257
>>> id(a)
2559155778384
>>> id(b)
2559155778192
>>> a='xuankeTester'
>>> b='xuankeTester'
>>> id(a)
2559155711280
>>> id(b)
2559155711280
>>>

從上面可以看到兩個257對應的地址確實是不一樣的,和我們最初判斷的是一致的。

總結

python通過對象的引用計數來管理內存,其實java的JVM也有用引用計數,所以理解了引用計數,為我們理解python的垃圾回收方法打下了基礎。本計劃這一篇文章就將python內存管理的機制講完的,但是發現一個內存引用計數就有很多東西得寫,所以索性就分兩篇文章來寫,之後再寫一篇文章來介紹python的垃圾回收方式。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

JavaScript 是否應該重命名

  在誕生 25 年之後,JavaScript 語言仍然讓很多人困惑不已。所以一個老生常談的問題是:它是否應該重命名?呼籲改名的支持者列舉了一系列理由,包括:

  • JavaScript 本意指的是 ECMAScript 的子集,但使用中它經常被指代多種不同的 ECMAScript 超集
  • JavaScript 是甲骨文公司的商標,這與 JavaScript 作為 Web 平台核心組件的身份不相符合,Web 平台是建立在開放技術和標準基礎上的
  • JavaScript 連官方 logo 都沒有
  • JavaScript 與 Java 沒有一點關係,幾十年來它給非技術人員造成了混淆。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?