Không sử dụng Python nếu bạn bắt đầu một dự án lớn
Trong sự nghiệp phát triển phần mềm, có một thời điểm nào đó mà bạn chuyển từ việc đóng góp vào các dự án sang việc sáng tạo điều của riêng mình. Đối với một số người, điều này xảy ra sớm hơn, đối với một số người là sau, và có người lại không bao giờ trải qua điều này.
Hầu hết các nhà phát triển có sự nghiệp dài hạn đều trải qua giai đoạn này. Tôi gọi đó là điểm tự xây dựng.
Nếu bạn đã đến đây, bạn biết rằng những câu hỏi đầu tiên là gì: Nó hoạt động như thế nào? Người dùng có trải nghiệm như thế nào? Kiến trúc là gì? Dữ liệu di chuyển như thế nào? Và nhiều câu hỏi khác như vậy.
Tôi sẽ không trả lời những câu hỏi này ở đây. Chúng rất cụ thể cho dự án bạn đang bắt đầu. Và mỗi câu hỏi như vậy đều xứng đáng có ít nhất một bài viết riêng.
Tuy nhiên, tôi sẽ trả lời một câu hỏi: Ngôn ngữ nào là tốt nhất cho dự án?
Có thể bạn nghĩ rằng điều này quá cụ thể cho từng dự án, và bạn không hoàn toàn sai.
Nhưng mỗi ngôn ngữ lập trình đều có một số rủi ro. Và Python, như vậy, có khá nhiều rủi ro. Đặc biệt là khi bạn cố gắng xây dựng một chương trình lớn với nó.
Khai báo biến không tồn tại và đó là một vấn đề
Triết lý của Python nói: Rõ ràng tốt hơn là ngụ ý.
Nhưng khi nói đến việc khai báo biến, ngụ ý thường xuất hiện nhiều hơn là rõ ràng trong Python.
Hãy xem, so sánh với đoạn mã C nhỏ này:
char notpython[50] = "Điều này không phải là Python.";
Hãy đào sâu vào điều này trước khi quay lại với Python.
‘char’ là một loại định danh và cho bạn biết mọi thứ sau đó liên quan đến một chuỗi. Phần ‘notpython’ là tên mà tôi đã đặt cho chuỗi này. [50] cho bạn biết rằng C sẽ dành 50 ký tự để lưu trữ cho chuỗi này. Tuy nhiên, trong trường hợp này, tôi có thể thoải mái với 19 — một cho mỗi ký tự cộng với một ký tự null \0 ở cuối cùng. Và cuối cùng, một dấu chấm phẩy để kết thúc mọi thứ một cách gọn gàng.
Cách khai báo rõ ràng này là bắt buộc trong C. Trình biên dịch sẽ điều đóng nếu bạn bỏ qua nó!
Cách làm này có vẻ ngớ ngẩn và phiền toái ban đầu.
Nhưng nó đáng giá. Rất đáng giá.
Khi bạn đọc mã C hai tuần hoặc hai năm sau và bạn vấp phải một biến mà bạn không biết, bạn chỉ cần kiểm tra phần khai báo. Nếu bạn đặt tên có ý nghĩa cho nó, điều đó đã mang lại một gợi ý lớn về nó là gì, nó đang làm gì và nó cần ở đâu.
So sánh điều đó với Python.
Ở đó, bạn khá là sáng tạo với biến khi bạn tiến triển. Nếu bạn không đặt cho nó một cái tên có ý nghĩa hoặc ít nhất là để lại một ghi chú về nó, tương lai của bạn sẽ bị rối bời.
Trong Python, không có cách nào để hiểu được biến đang làm gì ngoại trừ việc đào sâu ngay vào mã nguồn.
Nhưng nếu bạn có một lỗi chính tả duy nhất trong một biến, bạn có thể làm hỏng toàn bộ mã của mình. Không có khai báo bảo vệ như trong C.
Điều đó hoàn toàn ổn khi bạn làm việc trên các dự án nhỏ, khoảng vài nghìn dòng mã chẳng hạn. Hoặc nếu dự án của bạn không phức tạp lắm.
Nhưng với các dự án lớn... Mọi thứ trở nên lộn xộn.
Bạn có thể thực hiện khai báo biến rõ ràng trong Python. Nhưng chỉ có những lập trình viên chăm chỉ nhất mới làm điều đó. Và khi trình biên dịch không phàn nàn, nhiều người quên hoàn toàn về các dòng mã bổ sung như thế này.
Lập trình Python nhanh chóng.
Đọc Python dễ dàng, đối với các dự án nhỏ và đơn giản.
Đọc và duy trì các dự án Python lớn — bạn tốt nhất là một siêu anh hùng tìm tên biến mô tả và để lại bình luận cho toàn bộ mã của bạn, nếu không bạn đã làm rối bời mọi thứ.
Ôi module, nơi bạn thuộc về ở đâu?
Nếu bạn nghĩ rằng mọi thứ không thể tồi tệ hơn, bạn đã nhầm.
Câu hỏi về nơi mà một biến bắt đầu "sống" trong mã của bạn không chỉ đến từ các khai báo ngụ ý.
Biến có thể đến từ các module khác nữa. Thông thường, chúng có dạng như my_module.my_variable(). Nếu bạn bối rối bởi một biến như vậy, bạn không hoàn thành khi chỉ kiểm tra nó xuất hiện ở đâu khác trong tệp chính.
Bạn cũng phải kiểm tra xem có một dòng có một trong hai điều này hay không:
import my_module from another_module import my_module
Ở dòng thứ hai, bạn đang nói với trình biên dịch bạn cần hàm hoặc biến nào từ một module chứa nhiều thứ khác nhau.
Điều này làm phiền vì có nhiều module hơn là những gì bạn có thể tìm thấy trên PyPI. Bạn cũng có thể import bất kỳ tệp Python nào trên máy tính của bạn. Vì vậy, việc tìm kiếm nhanh chóng chức năng hoặc biến của bạn trên Google không phải lúc nào cũng giúp ích.
Nhưng nó còn tồi tệ hơn.
Các module có thể phụ thuộc vào các module khác. Vì vậy, nếu bạn không may mắn, bạn đã nhập các module A, B và C, nhưng chúng phụ thuộc vào các module E, F, G và H, và những module này lại phụ thuộc vào I, J và K. Và đột nhiên bạn không quản lý được ba mà là mười module.
Điều tồi tệ hơn, đôi khi nó không phải là một cây đơn giản như vậy. Nói B và C cũng phụ thuộc vào M và N, và J cũng phụ thuộc vào M, và C và H cũng phụ thuộc vào Q... Không cần theo dõi, bạn hiểu ý tưởng.
Đó là một mê cung. Địa ngục phụ thuộc là một thứ thực sự, được đặt tên bởi người sử dụng Python.
Phụ thuộc vòng tròn là quái vật xấu xí nhất trong mê cung. Nếu module A phụ thuộc vào module B, nhưng B cũng sử dụng một số phần của module A — ouch.
Không có gì lớn lao trong các dự án nhỏ. Nhưng trong các dự án lớn... chào mừng bạn đến khu rừng rậm.
Xung đột phụ thuộc hàng loạt
Ồ, tôi vẫn chưa kết thúc sự phàn nàn của mình về các module. Không chỉ là các module chính chúng, mà còn là phiên bản của chúng.
Về nguyên tắc, việc Python có một cộng đồng người dùng tích cực và nhiều module được cập nhật thường xuyên là tuyệt vời. Chỉ có một vấn đề: không phải tất cả các phiên bản của một module luôn tương thích với các module khác.
Chẳng hạn, bạn đang sử dụng các module A và B. Cả hai đều phụ thuộc vào module C. Nhưng A yêu cầu C ở phiên bản 3.2 trở lên, và B cần C ở phiên bản 2.9 trở xuống.
Bạn không quan tâm đến C. Bạn chỉ muốn A và B.
Không có công cụ nào trên thế giới có thể giúp bạn giải quyết xung đột này. Nếu may mắn, bạn sẽ tìm thấy một bản vá được viết bởi ai đó đã gặp vấn đề tương tự như bạn. Nếu không may, bạn sẽ phải viết bản vá.
Hoặc bạn sử dụng một gói khác. Hoặc bạn viết lại một trong những gói, A hoặc B, hoàn toàn và tìm giải pháp phụ ở mọi nơi nơi cần phiên bản sai của C.
Trong mọi trường hợp, bạn sẽ cần thêm thời gian cho điều này.
Đó là một khu rừng, và bạn sẽ cần lòng kiên nhẫn và một số công cụ để điều hướng nó.
Chưa kể đến va chạm phụ thuộc, có một số công cụ tốt xung quanh. Có 'pip' giúp dễ dàng cài đặt gói. Với một 'requirements.txt' đơn giản, bạn có thể xác định các gói và phiên bản bạn muốn sử dụng thay vì làm ô nhiễm các tiêu đề tệp của bạn. Và môi trường ảo giữ tất cả các gói tại một nơi và riêng biệt khỏi cài đặt Python chính của bạn.
Đối với các dự án lớn và lộn xộn hơn, còn có 'conda', tệp YAML và nhiều hơn.
Nhưng bạn vẫn cần học cách sử dụng mỗi công cụ. Và bạn cần dành một lượng thời gian tối thiểu để giải quyết những vấn đề này.
Các máy khác nhau, Python khác nhau
Liên quan đến cả thế giới khó khăn của địa ngục phụ thuộc là một chủ đề không thoải mái khác.
Ngay cả khi bạn đã giải quyết mọi vấn đề phụ thuộc trên máy của mình và Python của bạn chạy mượt như một con ngựa sơ sinh, không có đảm bảo rằng nó sẽ chạy trên máy của người khác.
Ngựa sơ sinh có chạy không? Tôi không biết nhưng có vẻ như tôi đang cố gắng trông có vẻ thông thái hơn trong sinh học hơn bao giờ hết. Dù sao, quay trở lại với Python.
Các công cụ như ‘pip’, ‘requirements.txt’ và môi trường ảo sẽ giúp bạn điều hướng qua những dạng nhẹ của địa ngục phụ thuộc. Nhưng chỉ ở cấp độ local.
Trên mỗi máy mới, bạn sẽ cần kiểm tra và có thể cài đặt lại từng yêu cầu và phiên bản của nó.
Các giải pháp duy nhất thực sự di động là Jupyter notebooks. Ở đây, bạn có thể viết các điều trong bất kỳ phiên bản nào bạn muốn. Trong Jupyter, mọi thứ chạy trên một máy chủ trực tuyến, vì vậy bạn có thể gửi những tệp này cho bất kỳ ai và họ sẽ có thể sử dụng chúng ngay mà không cần cấu hình thêm.
Tuy nhiên, có một điều đáng kể: Jupyter notebooks chỉ có giao diện đồ họa.
Tôi không muốn nghe có vẻ như là một người hâm mộ cứng rắn của terminal. Nhưng với giao diện đồ họa, việc xử lý các dự án lớn với nhiều tệp liên kết khá khó khăn.
Có lẽ đó là lý do tại sao tôi chưa bao giờ thấy một dự án lớn trong Jupyter notebooks. Mặc dù chúng chắc chắn tồn tại.
Ngôn ngữ khác chỉ cần máy ảo. Vấn đề giải quyết.
Thế giới ngoài pip
Giả sử bạn đã quản lý được dự án của mình trên các máy khác nhau bằng cách sử dụng Jython hoặc PyPy hoặc một giải pháp tương tự.
Tất cả đều hơi vụng trộm hơn so với máy ảo. Nhưng héy, ít nhất chúng hoạt động.
Nếu bạn đang xây dựng một dự án lớn, bạn có thể tích hợp các gói C, gói Fortran, và nhiều thứ khác. Có nhiều ưu điểm ở đây: Gói C có thể không tồn tại trong Python và thường thường nhanh hơn. Gói khoa học thường chỉ tồn tại trong Fortran vì lý do kế thừa.
Kết quả là, bạn sẽ phải sử dụng các trình biên dịch như ‘gcc’, ‘gfortran’, và có thể là nhiều thứ khác nữa.
Và đó là một sự phiền toái! Tài liệu cho việc tích hợp các mô-đun C vào mã Python của bạn có hơn 4.500 từ — gấp đôi so với bài viết này! Và tài liệu cho Fortran cũng không ngắn hơn nhiều.
Xây dựng toàn bộ dự án của bạn bằng C có thể chậm hơn khi lập trình ban đầu. Nhưng bạn sẽ ngăn chặn những tình huống phải rối loạn với nhiều trình biên dịch và giao diện.
Cổng C là quá cũ nên có gói cho gần như mọi thứ. Ngay cả các gói máy học thân thiện với người dùng.
Khóa hiệu suất bằng khóa thông dịch viên toàn cầu
Khóa thông dịch viên toàn cầu, hay GIL, tồn tại từ ngày đầu tiên của Python. Nó làm cho quản lý bộ nhớ trở nên vô cùng đơn giản cho người dùng cuối.
Ít nhất trong các dự án nhỏ, những người phát triển không cần phải nghĩ về bộ nhớ máy tính khi họ sử dụng Python. So sánh với C, nơi bạn đặt dành từng bit bộ nhớ cho từng biến!
Đơn giản, GIL đếm xem biến đã được tham chiếu bao nhiêu lần trong mỗi phần của mã. Nếu biến không còn cần thiết, nó sẽ giải phóng không gian bộ nhớ mà nó chiếm giữ.
Trong các dự án nhỏ, GIL giúp tăng hiệu suất vì không gian bộ nhớ không cần thiết được xóa sạch.
Nhưng trong các dự án lớn, có một vấn đề: GIL không ưa đa luồng.
Đây là một cách thực hiện chương trình tăng hiệu suất rất mà nhiều luồng lệnh chạy độc lập trên cùng tài nguyên quy trình. Mô hình máy học là tuyệt vời để đào tạo theo cách này.
Chỉ có một vấn đề nhỏ: GIL chỉ làm việc trên một luồng mỗi lần.
Vì vậy, nếu biến A đang được thực thi trên luồng 1, trong khi luồng 2 đã hoàn thành A, thì bộ nhớ của nó có thể bị xóa. Điều này chỉ phụ thuộc vào việc GIL đang ở đâu vào thời điểm đó.
Điều này có thể dẫn đến các lỗi rất kỳ lạ, như bạn có thể tưởng tượng...
Có cách giải quyết cho điều này, nhưng chúng không phải là quá đẹp. Một lựa chọn khác là sử dụng multiprocessing. Nhưng nó thường không nhanh bằng so với đa luồng trong các ngôn ngữ không có GIL.
Concurrency và song song vẫn còn rối bời và lộn xộn
Chúng ta đã thấy một nhược điểm của đồng thời. Khi bạn thực hiện đa luồng, khóa toàn cầu trình thông dịch có thể làm chậm mọi thứ. Hoặc gây ra lỗi kỳ quặc.
Nhược điểm tương tự áp dụng cho coroutines của Python.
Có một số sự khác biệt tinh subtile giữa đa luồng và coroutines, nhưng điểm chung là coroutines thực hiện một nhiệm vụ mỗi lần, trong khi đa luồng có thể thực hiện nhiều nhiệm vụ cùng một lúc. Cả hai đều là các triển khai của đồng thời.
Coroutines hữu ích khi bạn có các nhiệm vụ đòi hỏi phải chờ đợi nhiều, như khi bạn đọc dữ liệu trang web và chờ đợi máy chủ phản hồi. Thay vì để máy tính ngồi yên, coroutines gán một nhiệm vụ khác cho nó.
Ngược lại, đa luồng hữu ích khi bạn có nhiều nhiệm vụ mất thời gian, nhưng không tốn nhiều CPU và không yêu cầu chờ đợi quá nhiều. Dữ liệu trực tuyến có thể được đưa ra làm ví dụ.
Nếu bạn có một nhiệm vụ tốn CPU và bạn muốn tận dụng tối đa phần cứng của mình, bạn có thể muốn thử nghiệm song song.
Multiprocessing là người bạn đồng hành tốt nhất trong trường hợp này. Nó basically bảo máy tính sử dụng nhiều lõi để tiết kiệm thời gian.
Tuy nhiên, tất cả ba kỹ thuật, đa luồng, coroutines và multiprocessing, đều đối mặt với các vấn đề tương tự. Chúng không khó triển khai trong Python. Nhưng mã nguồn trông rối rắm và khó đọc, đặc biệt là đối với người mới học.
Ngôn ngữ như Clojure, Go và Haskell tốt hơn nhiều cho đồng thời và song song.
Không đáng nghĩ nếu bạn không xử lý các quy trình chậm hoặc tốn nhiều tài nguyên. Nhưng nếu bạn đang làm, bạn có thể muốn xem xét các lựa chọn của mình.
Sử dụng cái gì thay thế Python
Python không phải là tất cả điều ác. Chẳng qua.
Nhưng nó cũng có nhược điểm của mình.
Nếu bạn muốn biến rõ ràng và gói phần mềm phát triển tốt mà không dễ dàng đưa bạn đến dependency hell, thì C là người bạn của bạn.
Nếu bạn muốn một cái gì đó có thể chuyển giao được cho bất kỳ máy tính nào, thì Java, Clojure hoặc Scala là các lựa chọn tuyệt vời. Chúng chạy trên máy ảo, vì vậy bạn sẽ không gặp vấn đề giống như với Python.
Và nếu bạn muốn chạy các nhiệm vụ lớn và chậm, bạn có thể thử Go hoặc Haskell. Ban đầu chúng khó học hơn Python, nhưng thời gian bạn đầu tư sẽ đáng giá.
Và bạn có thể luôn kết hợp ngôn ngữ.
Python tuyệt vời cho việc viết kịch bản nhanh, viết dự thảo và thậm chí là các dự án trung bình. Nhiều nhà phát triển mà tôi biết tạo bản nháp và chạy thử nghiệm đầu tiên của họ bằng Python, sau đó viết lại các phần quan trọng bằng C, Go hoặc Clojure.
Điều này giúp mã chạy nhanh hơn, và bạn vẫn có cơ hội tận hưởng các lợi ích mà Python mang lại.
Trong các dự án lớn, Python không bị cấm. Nhưng nó có thể không phải là ngôn ngữ duy nhất được sử dụng.
Bạn có thể sử dụng Python như keo để ghép các phần trong C, Go, hoặc Clojure.
Nếu bạn đã đạt đến điểm tự xây dựng của mình, hãy nhớ rằng không có ngôn ngữ nào là Chén Thánh.
Mặc dù có nhược điểm, nhưng Python là ngôn ngữ tuyệt vời và tiện lợi. Bạn luôn có thể vượt qua những điểm đau đầu bằng cách tích hợp mã vào các ngôn ngữ khác.
Chúc bạn xây dựng vui vẻ!
Một lời cảm ơn lớn đến Janek Schleicher đã truyền cảm hứng để tôi viết câu chuyện này.
Bài viết này được xuất bản ban đầu trên Medium. Bạn có thể đọc nó tại đây.
