怎樣用 Matlab 寫出優雅的代碼?

看著大家在這個問題(為什麼不少程序員認為Matlab的語言設計不優雅甚至比較丑?能否舉出一些例子來說明? - 數學)下面吐槽,背上涼颼颼的。因為組裡的歷史原因,換語言不現實。。。

我自己的體驗是用matlab寫小規模(1000行以內)的gui很快速,交互性不是特彆強的gui可以直接用guide搞定。但是代碼的複雜度隨著代碼長度的平方上升。

求問怎樣改善用matlab寫代碼的程序風格,寫出簡明易維護的程序呢?


想到哪說哪,有點亂。

用deal實現Python中的「一行賦多值」。

[H, W, N] = deal(42); % H = 42, W = 42, N = 42
[H, W, N] = deal(28, 28, 32); % H = 28, W = 28, N = 32, respectively

Matlab向量化的技巧網上文章很多,此處不表。矩陣操作和循環操作混雜的時候,考慮用structfun, arrayfun, bsxfun, cellfun等等,例如從N幅圖像中減去均值:

A = rand(H, W, N); % N 2D images
B = bsxfun(@minus, A, mean(A, 3)); % take out the mean

需要和高維數組(Tensor)打交道的話,思考時不要想著row, column, page這些術語, 要用dim_1, dim_2,... dim_M來思考。好多地方說Matlab是先存column, 用我們的術語其實就是Matlab按照從左到右:dim_1, dim_2,... dim_M的順序存儲元素。例如想想下面 2 x 4 x 3 x 5 矩陣的存儲順序:

sz = [2, 4, 3, 5]; % dim_1 = 2, dim_2 = 4, dim_3 = 3, dim_4 = 5
A = reshape( 1 : prod(sz), sz );

可能的話,用generic for。例如以下代碼哪個好一望便知:

z = [1, 3, 7, 11, 15];
for each = z
disp( each );
end
for i = 1: numel(z)
disp( z(i) );
end

用Matlab的方式組織自己的數據結構。比如,有時應該把struct當做dictionary用,比Python里的dict還方便。例如:

a.tom = 1; // insert (key = tom, value = 1)
a.jerry = 2; // insert (key = jerry, value = 2)
c = a.tom + 1; // access key = tom

理解cell的()引用和{}引用的區別。

理解Matlab傳遞參數時的逗號語義,以及相關的用cell實現變長參數的傳遞。例如:

x = linspace(0, 2*pi);
sty = {"linewidth", 2, "color","r", "linestyle","--"};
plot(x, sin(x), sty{:});

理解Matlab為面向過程編程、函數式編程提供了哪些語言層面上的支持。可變參數和返回值用varargin, varargout,Nested function相當於其它動態語言裡面的閉包,函數和類名也是first-class type,能方便的被當做參數傳遞(加@)。

Matlab對並行編程提供語言層面的支持,如parfor, spmd等關鍵字,你可以用類似openmp或者cuda C那樣的思維寫並行代碼。

在寫行業相關代碼時,函數參數和函數名可以用長名,別人調用時比較清晰。本地變數用短名,做這行的一看變知含義,自己也方便。例如 e = m * c^2等等。

寫面向過程Matlab代碼時,不要把所有的演算法流程都塞到一個幾百行上千行的函數里。把功能拆分成若干小函數。一個典型m文件的layout

function [r_1, r_2] = do_a (arg1, arg2, varargin)
t = 5
z1 = sub_1 (3);

z2 = sub_2 (4);

r_1 = …;
r_2 = …;
return;

function r = sub_1 (aa)

r = aa + t;
end
end

function t = sub_2 (bb)

end

代碼規模較大時,做好名字空間管理。如果有大量互相獨立的短函數,用靜態方法實現名字空間的管理。

classdef xxu
methods(Static)
function r = fun_1 (arg)
end

function v = fun_2 (arg)
end
end
end

調用時:

xy = xxu.fun_1(A);
vv = xxu.fun_2(A);

如果函數多,每個函數也比較大(比如一個函數就是一個幾百行的文件,裡面還有sub function, nested function),用pakage管理。例如新建一個叫 "+xxu"的文件夾(xxu前面一個加號),文件夾裡面有函數文件fun_1.m和fun_2.m,調用時就可以用名字空間xxu:

xy = xxu.fun_1(A);
vv = xxu.fun_2(A);

如果直接寫fun_1或fun_2就會出錯。

理解Matlab為面向對象編程提供了哪些支持。class的傳值,handle class的傳引用。繼承。定義介面的Abstract Class。將行業的演算法庫封成class是比較推薦的做法,近期的官方toolbox基本都開始轉向這種風格了。當然怎麼用OO的思維編程就是「功夫在Matlab外」了。

最後給題主潑點冷水,做科研的話,太大型的gui不適合用matlab搞。matlab的gui是給工toolbox提供一個可視化的portal。經典例子:信號處理中濾波器設計,matlab提供一個gui讓你手拖帶寬。


看到高票回答很受啟發!聽說 R2016b 已經支持在 script 里定義 function 但是敝廠還在小範圍兼容性測試階段所以呢本喵暫時還沒用上,所以呢下面要說的東西還是有一些參考價值的吧。

有同學在問題你什麼時候對MATLAB感到絕望? - 數學下回答道「我要給每一個函數新建一個文件的時候。 」想必各位跟我一樣深感贊同吧。哪怕寫一點稍微能用的東西,都是以目錄為單位來組織的,有時候少拷一個文件就跑不起來,傳送程序也是發壓縮包,相當的麻煩。於是我就慢慢積累了一些繞過這個限制的辦法:

運行一段代碼。在作圖的時候,經常要對每一個 figure 或者 subplot 執行同樣的一系列操作,會出現很多重複的代碼,比如

subplot(3, 1, 1);
plot(t, x);
grid("on");
xlabel("t");
xlim([tmin, tmax]);
subplot(3, 1, 2);
plot(t, y);
grid("on"); % 1
xlabel("t"); % 2
xlim([tmin, tmax]); % 3 - 這 3 行毫無意義的重複啊
% 後面可能還有

解決方法很簡單,將重複操作定義成 cell array

setfigures = { ...
@() grid("on"); ...
@() xlabel("t"); ...
@() xlim([tmin, tmax]); ...
};

然後

subplot(3, 1, 1);
plot(t, x);
cellfun(@(f) f(), setfigures);
subplot(3, 1, 2);
plot(t, y);
cellfun(@(f) f(), setfigures);

是不是看起來清爽很多?

「那麼老師,一定要完全重複的代碼才能用嗎?帶參數的怎麼辦呢?」很簡單,比如我們要給圖上添加不同名稱的 ylabel 和 title:

setfigures = { ...
@(varargin) grid("on"); ...
@(varargin) xlim([tmin, tmax]); ...
@(varargin) xlabel("t"); ...
@(varargin) ylabel(varargin{1}); ...
@(varargin) title(varargin{2}); ...
};

subplot(3, 1, 1);
plot(t, x);
cellfun(@(f) f("x", "x = sin(t)"), setfigures);
subplot(3, 1, 2);
plot(t, y);
cellfun(@(f) f("y", "y = cos(t)"), setfigures);

這個算「子程序」嗎?還不用 function 定義,可以隨意用在 script 中,真開心啊。而且看起來有點眼熟不是,像是 arrayfun/cellfun 倒過來,或者是 Python 的 map 倒過來對嗎?將一組數據一一送進一個函數里,得到各自結果再組合起來是 arrayfun 和 cellfun 的基本用法,而這裡我們可以把同一組數據送到一個函數列表中,試觀察

x = linspace(0, 2 * pi, 20);
arrayfun(@sin, x); % 20 個數字送到 sin 函數里
cellfun(@sin, num2cell(x)); % 跟上面一樣
cellfun(@(f) f(x), {@sin, @cos}, "UniformOutput", false); % 同樣的 x 送到 sin 和 cos 兩個函數里

那麼以 Python 來對比,既然有了 map,filter 還會遠嗎?其實 MATLAB 的 filter 特別簡單,對比 Python 3

import numpy as np
a = np.random.randn(10)
b = list(filter(lambda x: x &> 0, a)) % filter 函數
c = [x for x in a if x &> 0] % 列表推導

MATLAB

a = randn(1, 10);
b = a(a &> 0); % 用 logical indexing 更加直觀

那麼 reduce 怎麼辦呢?我們知道 reduce (或者叫 fold)本質上是遞歸,而遞歸需要條件判斷,但是 MATLAB 不提供類似 C 語言中的

a = condition ? b : c;

或 Python 中的

a = b if condition else c

這種條件運算符,但是沒關係,我們可以造個輪子

iif = @(varargin) varargin{2 * find([varargin{1 : 2 : end}], 1, "first")}();
a = iif(condition, b, true, c); % 這樣就跟 C 和 Python 的等效了

a = iif(condition1, b, condition2, c, condition3, d, true, e); % 還能擴展,相當於以下語句:
if condition1
a = b;
elseif condition2
a = c;
elseif condition3
a = d;
else
a = e;
end

原理很簡單,iif 的參數列表中,奇數下標的是條件,偶數下標是條件對應的結果,取第一個真值條件對應的結果(可以是函數),最後一個「條件-結果」對的條件恆為 true,表示 by default 最後一個結果,這樣不僅可以充當三目運算符,而且可以當成寫在一行里的 if/elseif/else/end 或者 switch/case/otherwise/end。

用這個造好的輪子,我們來搞個大新聞。但是如果你這樣想的話:

reduce = @(f, a) ...
iif(numel(a) == 2, @() f(a(1), a(2)), ...
true, @() f(reduce(f, a(1 : end - 1)), a(end)));

那就 sometimes naive 了:出現在 iif 里的 reduce 是什麼鬼?所以我們至少需要這樣

reduce_ = @(r, f, a) ...
iif(numel(a) == 2, @() f(a(1), a(2)), ...
true, @() f(r(r, f, a(1 : end - 1)), a(end)));
reduce = @(f, a) reduce_(reduce_, f, a);

m = reduce(@times, [2, 4, 6]); % --&> 48

不過請不要用這個逗比函數跑什麼重型計算,因為 MATLAB 的遞歸速度……

(搬磚去了未完待續


毛遂自薦下自己的項目GitHub - li12242/NDG-FEM。

使用matlab建立一個計算流體力學間斷有限元求解器,計算包括一維/二維對流,淺水方程。

求解器採用模塊化及面向對象進行編寫,另外為了提高計算速度(目前最大問題),部分函數採用C函數重寫然後編譯。

可以從我提交的各個版本查看優化過程,未來或許會採用GPU繼續對計算效率進行優化。

==============分割線==================

關於matlab代碼我覺得前面各個答案都差不多了,就補充點求解器理論部分。

假如有人對如何建立淺水模型有興趣,可以參考下面幾本教材,

  • Toro的Riemann Solver
  • Toro的Shock-capturing methods for free-surface shallow flows
  • Khan和Lai的Modeling shallow water flows using the DGM

有關DG求解器部分則是根據Hesthaven和Warburton兩位的那本Nodal DGM編寫,這個項目最原始框架也是按照隨書的源程序改的(GitHub - tcew/nodal-dg)。最原始的程序是Codes1.1,其最大問題就在於使用了太多的全局變數,計算效率實在是太慢。

最後有關一些淺水方程的細節確實是最難的,如well-balanced,乾濕處理,斜率限制器等,這就要求找到最新文獻才能找到解決方法,我覺得最有幫助的是Xing,Zhang和Shu大牛2010年在ADWR上發表的《Positivity-preserving high order well-balanced discontinuous Galerkin methods for the shallow water equations》,還有Westerink等大牛的《A wetting and drying treatment for the Runge–Kutta discontinuous Galerkin solution to the shallow water equations》。

===============最後一句話====================

由於這個模型只是我自己用的,所以程序的結構或者框架並非沒有缺點,而且現在主要精力在於求解器的構造,有什麼缺點歡迎指正。

最後的最後,估計明年快畢業了(希望),求介紹相關工作(CFD)


可以利用運算符重載實現某些操作。例如類似於 C 語言的條件運算符:

在路徑中創建一個 @cell 文件夾 (mkdir @cell),之後在該文件夾中定義函數:

function varargout = colon(test, trueValue, falseValue)
if test{1}() 1
[varargout{1:nargout}] = trueValue{1}();
else
[varargout{1:nargout}] = falseValue{1}();
end

之後可以:

&>&> result = {rand &< .5} : {"win"} : {"lose"}; &>&> {rand &< .5} : {@()pause(1)} : {@()pause(3)}


一 代碼比較長的話(比如超過500行), 使用 OOP(面向對象編程)。很多用了MATLAB多年的人(包括一個月前的我)都不知道MATLAB還可以面向對象編程。

個人認為,代碼行數超過一定數量,函數個數超過一定個數後,想要保持邏輯清晰,維護容易,面向對象編程是不可避免的。

二 網上有MATLAB代碼風格建議的小冊子.我受益匪淺.

https://cn.mathworks.com/matlabcentral/fileexchange/46056-matlab-style-guidelines-2-0


啊謝謝師兄@渡子厄邀請。

並不是很會寫MATLAB,我只是說一說看到的MATLAB代碼的一些問題和建議。

第一是設計模式,

一些的MATLAB工程師並沒有設計模式,一般都是把一個小過程式的demo擴充成為一個大程序,代碼中有很多重複的代碼,其實在基於命令行的demo完成之後就應該考慮要設計一個怎麼樣的程序,然後推倒重來,減少每一行沒有必要的代碼,利用每個可以利用的數據。

第二是數據結構的管理,

MATLAB提供了很方便的一些數據結構,如何善用他們是一個很大的問題,在這方面,應該說是盡量減少每一行自己手寫的代碼,在用每個功能前Google一下是否有內建支持,花十分鐘看一下文檔理解內建支持有何優劣之處,然後使用MATLAB語言設計者的思維來寫MATLAB。

MATLAB使用者似乎很喜歡使用全局變數,這本身其實無可厚非,全局變數有時候會導致程序出一些問題,可以使用類似於命名空間的方法加以區分。

第三是代碼的管理,

業餘程序員有時候會有一個問題,就使用注釋複製粘貼和壓縮文件來管理代碼,這樣在稍微龐大的系統中代碼就會變得不可讀,而且工程人員自己也不知道自己在幹什麼,這方面我的建議是花一個下午時間學習一下git

第四是結構上的分離,

在設計GUI的時候可以盡量符合MVC,視圖,控制,數據分離,盡量每個.m文件擁有各自的功能,每個函數邏輯分離。當然可以做的輕量級一些。個人不喜歡把文件放的太長。

第五是使用更高階的編程範式,

MATLAB可以寫的OO一些,不過MATLAB對於函數式編程的支持不是很好,因為寫的不多就不多談了。

以上是我讀一些工程師,科學家的代碼看到的一些問題,並不僅限於MATLAB,如有不當還請多指教。


唉我來慢慢更新自己的學習筆記吧。

1. 代碼風格:

提完問題順手谷歌了一下,下面這個guideline可以參考:

MATLAB Style Guidelines 2.0

2. 怎樣優雅地寫gui

Stackoverflow中解決gui代碼糟糕的問題:matlab - What"s the "right" way to organize GUI code?

官方文檔:Writing Apps in MATLAB 。

範例:File Exchange 里一個使用OO寫gui的範例,看起來很棒。

3. 使用APP功能「封裝」程序

4. Google

沒必要自己重新造輪子,大部分問題都是有人遇到過並且解決過的。因此遇到幾分鐘以內解決不了的問題,可以Google一下解決方法。畢竟大部分matlab使用者都不是程序開發專家,大多數時候Get things done才是好的工作態度。

這裡還有另外一個問題:

基礎不好,遇到問題連搜什麼關鍵詞都不知道。

我的笨辦法是問師兄師弟、去stackoverflow和Mathwork網站上提問,或者乾脆把書快速掃一遍,只注意概念和定義,忽略技術細節。

5.用正版

如果有條件用正版的話,就用正版吧,遇到問題可以直接呼叫Mathwork技術支持,也可以節省很多時間。

唉看起來matlab好像也不是那麼無藥可救呢


可以試試 appdesigner, 完全是oop的, 可以使用設計模式, 就是現在提供的組件有限, 貌似暫時還不支持處理mouse event 和keyboard event. 要是GUI太複雜了感覺還是不要用MATLAB寫了, 可以嘗試一下接Java的GUI, 具體的可以參考undocumentedmatlab網站~


我是做工程應用的,我覺得一個好的演算法只有得到應用才是好的演算法,所以我個人不推崇使用natlab的GUI,matlab做演算法分析簡潔高效,邏輯夠複雜,人機交互沒有必要做matlab的GUI。

其實上面的回答都是關於OO以上的風格,因為設計思想的原因。設計模式的思想是可以用,前提是你本來就用得比較熟,在matlab中邊學邊用還是算了吧。

我覺得能夠直接按原文轉化為C++的matlab程序才是好程序,這才是一個演算法脫離理論,投入應用的終極歸宿。

話說回來,當可以沒有障礙地用C++做演算法的時候,你就輕易不想再回來用matlab了,除非是一些中間過程要繁瑣顯示的才拿matlab分析一下。

話說回來,我個人覺得windows和matlab對中國大學生的傷害之大其實難以估量,我們一開始就用上了最為高大上的成熟工具,卻忽視了他們的底層實現和設計思想,知道和實現過是兩種層次,在需要創造性的時候經驗和手段就能顯現作用,不能搶了先機、失了後手,如果有讀大學的孩子的話,建議用用那些開源的操作系統和數值演算法,不要貪圖方便,在最需要思考和實踐的年齡自廢武功,把思考和實踐的樓閣建在了空中。


推薦閱讀:

opencv庫的python版為啥比c++版小這麼多?是功能有區別嗎?
如何用 Python 中的 NLTK 對中文進行分析和處理?
用python去搞數學建模可行性大不大?
python生成器到底有什麼優點?
金融工程現在用python多嗎?

TAG:軟體開發 | Python | MATLAB | 科學計算 | 如何優雅地X |