Tóm tắt buổi trước và nội dung kỳ này

Buổi hôm trước chúng ta đã học qua những Data Structure quan trọng như: list dictionary tuple set. Ngoài ra ta còn được học biểu thức comprehensions giúp tạo một sequence nhanh chóng và linh hoạt.

Do sắp xếp Livestream chưa tối ưu lắm nên phần nội dung kiến thức này rơi vào cuối buổi thứ 6 và toàn bộ thời lượng ngày thứ 7, các bạn chịu khó xem lại xíu nhen:

🔴 Buổi live ngày 7

Trong kỳ này ta đi tiếp về các kiến thức liên quan tới Hàm. Khi trước ở phần Language Semantics (Ngày 4) ta có học qua về cách gọi hàm, nhưng đó chỉ là bề nổi, bữa nay ta sẽ đi vào chi tiết hơn về các kiến thức liên quan như: Namespace là gì? Variable scope là gì? Lambda function hay Anonymous function là gì? Generators là gì? Làm sao để kiểm soát lỗi bằng Exception?

🔴 Buổi live ngày 8:

Functions – hàm (phương thức)

🔥 syntax

▶ Một hàm được khai báo bằng từ khóa def và nó sẽ chứa 1 block of code (khối mã) indentate vào 1 tab (4 spaces). Hàm có thể trả về kết quả return hoặc chỉ dùng để thực thi tác vụ (trong trường hợp này sẽ trả về None)

In [177]: def function_without_return(x):
   .....:     print(x)

In [178]: result = function_without_return("hello!")
hello!

In [179]: print(result)
None
In [173]: def my_function(x, y):
   .....:     return x + y

▶ Khi biên dịch tới return, giá trị hay biểu thức sau return sẽ được gửi tới ngữ cảnh nơi mà hàm được gọi.

In [174]: my_function(1, 2)
Out[174]: 3

In [175]: result = my_function(1, 2)

In [176]: result
Out[176]: 3

▶ Một hàm có thể có nhiều biểu thức return và thậm chí 1 biểu thức return nhưng trả về nhiều giá trị (cơ mà thật ra nó sẽ gộp về 1 tuple).

Ví dụ 1: Có 2 biểu thức return trong cùng 1 hàm

def return_many_2(x, y, z=1.5):
    if z > 1:
        return z * (x + y)
    else:
        return z / (x + y)

# -------------------------

def return_many_1():
  def inner():
    return 1, 2, 3
  return inner()

print(return_many_1())

Ví dụ 2: Kết quả trả về tạo thành 1 tuple

def return_results(a, b):
  sum = a + b
  product = a * b
  return sum, product

print(return_results(1, 2))

🔥 positional argms and keyword argms

Với keyword argms ta thường dùng để chỉ định giá trị default hoặc tham số optional.

Còn positional argms là tên gọi chung của các tham số còn lại cần truyền vô

def my_function2(x, y, z=1.5):
    if z > 1:
        return z * (x + y)
    else:
        return z / (x + y)

xy là positional còn z là optional

In [181]: my_function2(5, 6, z=0.7)
Out[181]: 0.06363636363636363

In [182]: my_function2(3.14, 7, 3.5)
Out[182]: 35.49

In [183]: my_function2(10, 20) # my_function2(y =20, x=10)
Out[183]: 45.0

Và như tập trước có nói thì ta có thể để tên dúng khi gọi như vậy không cần nhớ chính xác vị trí

🍀Namespaces, Scope, Local functions

Thật ra cách sử dụng hàm hay phương thức thì mình đã có đề cập nhiều lần ở các phần trước rồi. Cơ mà phần này ta sẽ đi sâu nhiều hơn về bản chất, đặc biệt là ảnh hưởng của scope (phạm vi) điều mà ta dễ bị “ngộ nhận” nhất và lỗi khiến dữ liệu ta bị thay đổi mà không hề hay biết.

🔥 Namespaces – Chỉ là “tập hợp các tên”?

Có hơi trừu tượng khi nói về phần này, nhưng mình tin sẽ đáng giá vì hiểu rõ namespace thì qua bên scope càng củng cố kiến thức của chúng ta. Vậy nên chịu khó đọc dài dài tí nhé

🎯 Bạn có tự hỏi?

Trong Python, ta có thể tưởng tượng namespace là một mapping (hiểu là dict cũng được) của tất cả tên định danh với những objects tương ứng (Vì cơ bản mọi thứ chương trình Python khởi tạo hay xử lý/thực thi đều là obj nên ta không thể liệt kê hết được)

Ví dụ: x = "foo" thì ta có một tên “tượng trưng” (symbolic name) x và tham chiếu tới obj str có giá trị foo

Như vậy với một phần mềm phức tạp, có khi tới hàng trăm nghìn cái tên được chỉ tới một đối tượng cụ thể nào đó. Như vậy Bạn có tự hỏi làm sao Python quản lý được hết chúng để tụi nó không “bem” lẫn nhau?

Đây là lúc namespace ra đời, tới đây thì các câu hỏi được đặt ra là:

  • Cách Python quản lý các namespaces?
  • Khi nào Python tạo một namespace mới?
  • Cách implement namespace
  • Cách variable scope xác định tên định danh có visibility (hiện hữu) ?

🎍 Các loại namespaces

Python namespaces

Các loại namespaces đều có vòng đời riêng. Khi Python chạy chương trình, nó tạo ra tụi nó lúc cần và xóa đi khi không dùng tới.

Ngoài ra có nhiều namespaces luôn có sẵn bất cứ lúc nào (chỉ khi chương trình vẫn đang chạy – nói cách khác là từ khi start Python interpreter và miễn là interpreter vẫn còn chạy) – đây chính là built-in namespaceglobal namespace

Built-in Namepsace

dir(__builtins__)

['ArithmeticError', 'AssertionError', 'AttributeError',
 'BaseException','BlockingIOError', 'BrokenPipeError', 'BufferError',
 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError',
 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError',
 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError',
 'Exception', 'False', 'FileExistsError', 'FileNotFoundError',
 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError',
 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError',
 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt',
 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError',
 'OverflowError', 'PendingDeprecationWarning', 'PermissionError',
 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning',
 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration',
 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError',
 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError',
 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError',
 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError',
 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__',
 '__doc__', '__import__', '__loader__', '__name__', '__package__',
 '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray',
 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex',
 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate',
 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset',
 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list',
 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct',
 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr',
 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

Như mình đề cập thì rõ ràng các hàm như print() hay id() có thể gọi ở bất cứ phần nào của chương trình.

Global Namespace

Có hai đối tượng gắn liền với global namespace

  • Hàm/chương trình main
  • Bất kỳmodule nào được đưa vào chương trình bằng hàm import

Như vậy rõ ràng các module khác nhau có namespace khác nhau thì cho dù hàm hay thuộc tính có tên giống nhau thì vẫn tách biết khi chạy

Local namespace hay enclosing namespace

Bất cứ khi nào interpreter thực thi một hàm ta khai báo nào thì nó sẽ tạo ra một local namespace tương ứng. Dễ hiểu là do ta tạo hàm chỉ xài ở cục bộ thôi mà! Nên namespace này sẽ xóa khi hàm ngưng chạy.

>>> def f():
...     print('Start f()')
...
...     def g():
...         print('Start g()')
...         print('End g()')
...         return
...
...     g()
...
...     print('End f()')
...     return
...

>>> f()
Start f()
Start g()
End g()
End f()

Trong ví dụ này, g() được định nghĩa trong thân của f():

  • Dòng 1 đến 12: Định nghĩa f(), the enclosing function.
  • Dòng 4 đến 7: Định nghĩa g(), the enclosed function.
  • Dòng 15: Chương trình chính gọi f().
  • Dòng 9: f() gọi g().

Khi chương trình chính gọi f(), Python tạo một namespace mới cho f(). Tương tự, khi f() gọi g(), g() có namespace riêng của mình. Namespace được tạo cho g() là namespace local, và namespace được tạo cho f() là namespace enclosing.

Mỗi namespace này tồn tại cho đến khi hàm tương ứng của nó kết thúc. Python có thể không thu hồi ngay lập tức bộ nhớ được phân bổ cho các namespace đó khi hàm của chúng kết thúc, nhưng tất cả các tham chiếu đến các đối tượng mà chúng chứa sẽ không còn hợp lệ.

🔥 Scope – Phạm vi hiệu lực

Cho ví dụ hàm sau

def func():
    a = []
    for i in range(5):
        a.append(i)

Ở ví dụ này sau khi đã tìm hiểu về local namespace ở trên thì ta sẽ biết a sẽ bị xóa sau khi hàm func() kết thúc. Nhưng nếu

In [184]: a = []

In [185]: def func():
   .....:     for i in range(5):
   .....:         a.append(i)

Lúc này a vẫn còn hiệu lực vì nó không nằm trong scope của hàm func()

In [186]: func()

In [187]: a
Out[187]: [0, 1, 2, 3, 4]

In [188]: func()

In [189]: a
Out[189]: [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]

Nếu ta muốn gán biến bên ngoài function’s scope thì ta dùng từ khóa global hoặc nonlocal

In [190]: a = None

In [191]: def bind_a_variable():
   .....:     global a
   .....:     a = []
   .....: bind_a_variable()
   .....:

In [192]: print(a)
[]

nonlocal cho phép sửa đổi các biến khai báo ở scope cao hơn – Nhưng nó không phải biến gobalnhen. Thường dùng trong các case hàm lồng hàm để sửa đổi các biến trong hàm cha. Ví dụ:

def nonlocal_test():
    x = 1
    def inner():
        nonlocal x
        x += 1
        print(id(x))
        return x
    return inner()

print(nonlocal_test())

Trong ví dụ trên nếu bạn quên sử dụng nonlocal để truy cập biến x trong hàm cha của nó, Python sẽ giả định rằng biến đó là cục bộ và cho ra lỗi UnboundLocalError: local variable 'x' referenced before assignment

def nonlocal_test():
    x = 1
    def inner():
        x += 1
        return x
    return inner()

print(nonlocal_test())

Mặc dù global trông thì khá tiện khi có thể linh hoạt tương tác qua lại giữa các biến nhưng mình không khuyến khích dùng nó vì hệ thống lúc này phải lưu trữ các trạng thái khác nhau (vốn ta không thấy được và phàm cái gì mắt không thấy thì sẽ làm sai).

Nếu thật sự muốn thực hiện việc chỉnh sửa “vượt cấp” như này thì hãy nghĩ tới chuyện ứng dụng OOP (sử dụng các classes)

🔥 Return mutiples values

Như đã đề cập ở trên thì ta có thể trả về cùng lúc nhiều giá trị dưới dạng tuple, nhưng hoàn toàn tách các giá trị đó ra nhờ vào thuộc tính của tuple

def f():
    a = 5
    b = 6
    c = 7
    return a, b, c

a, b, c = f()

Trong các chương trình phân tích hoặc khoa học dữ liệu thì sẽ không hiếm việc ta viết 1 hàm nhưng trả về nhiều giá trị khác nhau. Ngoài để dạng tuple ta còn có thể chuyển nó về các dạng khác tùy ý như dictionary:

def f():
    a = 5
    b = 6
    c = 7
    return {"a" : a, "b" : b, "c" : c}

🔥 Functions are objects

Như chúng ta biết thì để trở thành một object thì nó phải có các đặc điểm như:

  • Có thể gắn cho một biến
  • Có thể truyền làm tham số cho hàm

Thú vị là trong Python bản thân một hàm là đối tượng nên nhiều cấu trúc chương trình có thể dễ dàng biểu diễn mà các ngôn ngữ lập trình khác không làm được.

Ví dụ ta cần làm sách và biến đổi danh sách strings sau:

In [193]: states = ["   Alabama ", "Georgia!", "Georgia", "georgia", "FlOrIda",
   .....:           "south   carolina##", "West virginia?"]

Bất cứ ai đã từng làm việc với dữ liệu khảo sát do người dùng gửi có thể đã thấy những kết quả lộn xộn như thế này. Cần làm nhiều việc để làm cho danh sách các chuỗi này thống nhất và sẵn sàng cho việc phân tích: loại bỏ khoảng trắng, loại bỏ các dấu chấm câu và chuẩn hóa việc viết hoa thích hợp.

🎍 re

Một cách để làm điều này là sử dụng các phương thức tích hợp với thư viện có sẵn re viết tắc của regular expressions mà có lần mình đã đề cập trong tập trước

import re

def clean_strings(strings):
    result = []
    for value in strings:
        value = value.strip()
        value = re.sub("[!#?]", "", value)
        value = value.title()
        result.append(value)
    return result

Kết quả cho ra

In [195]: clean_strings(states)
Out[195]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

🎍 lst functions

Nhưng vì hàm cũng là objs nên ta có thể có một giải pháp thay thế hữu hiệu sau, đó là đưa hết những hàm xử lý vào một danh sách và áp nó vô bộ strings cần biến đổi

def remove_punctuation(value):
    return re.sub("[!#?]", "", value)

clean_ops = [str.strip, remove_punctuation, str.title]

def clean_strings(strings, ops):
    result = []
    for value in strings:
        for func in ops:
            value = func(value)
        result.append(value)
    return result

Ta sẽ có kết quả sau:

In [197]: clean_strings(states, clean_ops)
Out[197]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

Như vậy với cách này ta có thể sửa đổi strings ở high-level. Nói cách khác clean_strings có thể tái sử dụng và generic (đa dụng – chập nhận bất kỳ kiểu dữ liệu nào như hàm sort())

🎍 map()

Một cách khác để dùng hàm trở thành tham số của hàm khác là dùng hàm có sẵn map() giúp áp một hàm xác định vào một chuỗi trình tự thuộc một loại nào đó (như chuỗi loại strings ở trên)

In [198]: for x in map(remove_punctuation, states):
   .....:     print(x)
Alabama 
Georgia
Georgia
georgia
FlOrIda
south   carolina
West virginia

map() thay thế lst comprehension nhưng bỏ đi chức năng lọc

🔥Anonymous (Lambda) Functions

Python hỗ trợ các hàm đặc biệt có tên là anonymous (vô danh) hay lambda, đây là một cách viết các hàm bao gồm một câu lệnh duy nhất, kết quả của nó là giá trị trả về.

Chúng được định nghĩa với từ khóa lambda, từ khóa này không có nghĩa gì khác ngoài “chúng tôi đang khai báo một hàm vô danh”:

In [199]: def short_function(x):
   .....:     return x * 2

In [200]: equiv_anon = lambda x: x * 2

Mặc dù hơi trù tượng nhưng đây là cách khai báo hàm dùng rất phổ biến với những ai lập trình Python và đặc biệt với dân Data. Có rất nhiều trường hợp ta phải truyền các hàm khác nhau làm tham số của một hàm transformation. Và việc phải khai báo đầy đủ cấu trúc sẽ khiến việc khai báo hàm transform trở nên dài dòng gây rối.

In [201]: def apply_to_list(some_list, f):
   .....:     return [f(x) for x in some_list]

In [202]: ints = [4, 0, 1, 5, 6]

In [203]: apply_to_list(ints, lambda x: x * 2)
Out[203]: [8, 0, 2, 10, 12]

Hoặc trong một ví dụ khác ta cần sắp xếp tập hợp các chuỗi ký tự bằng số độ dài của từng chữ:

In [204]: strings = ["foo", "card", "bar", "aaaa", "abab"]

In [205]: strings.sort(key=lambda x: len(set(x)))

In [206]: strings
Out[206]: ['aaaa', 'foo', 'abab', 'bar', 'card']

🔥 Generator

Generator là một hàm hoặc biểu thức đặc biệt tạo một iterable object. Hay để tạo một iterator ta có nhiều cách trong đó ở nội dung bài này gồm:

  • Default behavior khi dùng for loop
  • Dùng hàm iter()
  • Tạo hàm generator bằng yield keyword
  • Generator expression
  • Thư viện itertools

🎍 iterable object là gì?

Dừng lại khoảng chừng 5s, iterable object à … Chẳng phải cái từ iterable này mình đã nhắc tới nhiều khi học về list hay các cấu trúc dữ liệu sequence mà nhỉ ❓

Thật ra ngoài các sequence object vốn chắc chắn có đặc điểm iterable thì nhiều đối tượng khác đều hỗ trợ tính năng lặp lại (iteration) chẳng hạn chính các dòng lệnh trong 1 file code.

Giao thức iterator protocol giúp các đối tượng có khả năng iterable

Fun fact

✔ How to create iterator | object support iteration

In [207]: some_dict = {"a": 1, "b": 2, "c": 3}

In [208]: for key in some_dict:
   .....:     print(key)
a
b
c

Trong mệnh đề for key in some_dict:, trình biên dịch Python ngầm định sẽ tạo một iterator từ biến some_dict và thực chất nó đang duyệt ở trên iterator

🎍 Phân biệt iteratorsequence object

some_dict là một dict nhưng iterator tạo từ nó là sao ❓

▶ Thật ra chúng độc lập với nhau:

  • Iterator là một đối tượng cho phép bạn lặp lại các phần tử của nó một cách tuần tự. Nó được tạo ra bởi các đối tượng có thuộc tính iterable như dict lst str , … trong các ngữ cảnh như for loop ở trên chẳng hạn.
  • Dictionary hay bất kỳ một đối tượng chuỗi trình tự nào khác đóng vai trò là đối tượng lưu trữ dữ liệu.

✔ Hàm iter()

Tới đây thì dễ rồi, chỉ cần dùng hàm iter() để vừa tạo ra iterator vừa kiểm tra đối tượng có iterable không:

In [209]: dict_iterator = iter(some_dict)

In [210]: dict_iterator
Out[210]: <dict_keyiterator at 0x17d60e020>

In [211]:iter(1)
TypeError: 'int' object is not iterable

Ở chiều ngược các phương thức nhận list hoặc list-like object sử dụng được cho các iterable object.

Ví dụ sum max min hay các constructors list tuple:

In [211]: list(dict_iterator)
Out[211]: ['a', 'b', 'c']

yield keyword – Tạo hàm generator() bằng yield

Trong khi các hàm thông thường thực thi và trả về một kết quả duy nhất tại một thời điểm, generator có thể trả về một chuỗi nhiều giá trị bằng cách tạm dừng và tiếp tục thực thi mỗi khi generator được sử dụng (1).

Để tạo một generator, bạn sử dụng từ khóa yield thay vì return trong một hàm.

def squares(n=10):
    print(f"Generating squares from 1 to {n ** 2}")
    for i in range(1, n + 1):
        yield i ** 2

Ở (1) phía trên là sao nhỉ, ta thử tiếp tục ví dụ là sẽ hiểu

In [213]: gen = squares()

In [214]: gen
Out[214]: <generator object squares at 0x17d5fea40>

Đáng lý ra với một hàm bình thường thì ta sẽ được trả output là 1 4 9 16 25 36 49 64 81 100 Nhưng ở đây nó lại trả về địa chỉ của một generator object squares at ___

Đúng như vậy, chỉ khi ta yêu cầu các phần tử của generator thì nó mới bắt đầu thực thi code:

In [215]: for x in gen:
   .....:     print(x, end=" ")
Generating squares from 1 to 100
1 4 9 16 25 36 49 64 81 100

Vì generator tạo ra đầu ra một phần tử tại một thời điểm thay vì toàn bộ danh sách cùng một lúc, nên nó có thể giúp chương trình của bạn sử dụng ít bộ nhớ hơn.

Ví dụ:

  • Nếu bạn có một danh sách lớn các số nguyên và bạn muốn tính tổng của tất cả các số nguyên đó, bạn có thể sử dụng vòng lặp for thông thường để lặp qua danh sách và cộng tất cả các số nguyên lại với nhau. Tuy nhiên, điều này sẽ yêu cầu bạn lưu trữ toàn bộ danh sách trong bộ nhớ cùng một lúc.
  • Thay vào đó bạn sử dụng generator để tạo ra một chuỗi các số nguyên, bạn có thể tính tổng của tất cả các số nguyên mà không cần lưu trữ toàn bộ chuỗi trong bộ nhớ cùng một lúc. Generator sẽ chỉ tạo ra một số nguyên tại một thời điểm, và bạn có thể sử dụng vòng lặp for để lặp qua chuỗi và cộng tất cả các số nguyên lại với nhau.

✔ Generator expressions – Biểu thức Generator

Một cách khác để tạo ra một generator là bằng cách sử dụng generator expression hay biểu thức generator.

Cú pháp tạo generator expression giống với list comprehension nhưng thay vì ngoặc vuông [] thì thay bằng ngoặc tròn ()

In [216]: gen = (x ** 2 for x in range(100))

In [217]: gen
Out[217]: <generator object <genexpr> at 0x17d5feff0>

Đoạn code trên tương tự cách dùng generator cũ dài dòng sau

def _make_gen():
    for x in range(100):
        yield x ** 2
gen = _make_gen()

Một ứng dụng ta hay bắt gặp là ta sẽ sử dụng generator expressions để thay thế list comprehensions trở thành tham số như ví dụ sau:

In [218]: sum(x ** 2 for x in range(100))
Out[218]: 328350

In [219]: dict((i, i ** 2) for i in range(5))
Out[219]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Nếu so về perfomance giữa comprehension expressiongenerator expression thì chỉ khi số lượng phần tử (elements) đủ lớn thì ta mới thấy sự khác biệt được.

🎍 SPEED TEST

Nếu bạn nào kiên nhẫn đợi được thì có thể chạy thử để xem kết quả so sánh nhé 👀 Tạm thời mình skip qua

%timeit sum(x ** 2 for x in range(1_000_000))
%timeit sum([x ** 2 for x in range(1_000_000_000)])

✔ The standard bibrary itertools module

itertools là một thư viện mô-đun có sẵn cho phép tạo ra iterator bằng các generators – hay các hàm của nó (một số hàm thực thi các thuật toán dữ liệu phổ biến)

▶ Ví dụ: groupby() sau đây nhận một sequence và một hàm (trả về các key là các ký tự đầu) sau đó nhóm các elements cùng key.

In [220]: import itertools

In [221]: def first_letter(x):
   .....:     return x[0]

In [222]: names = ["Alan", "Adam", "Wes", "Will", "Albert", "Steven"]

In [223]: for letter, names in itertools.groupby(names, first_letter):
   .....:     print(letter, list(names)) # names is a generator
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']

Một số hàm thông dụng thường dùng mình liệt kê sau đây nhưng bạn hoàn toàn có thể truy cập vào the official Python document để tìm đọc thêm nhé

Useful itertools functions

chain: Kết hợp hai chuỗi

import itertools

numbers = [1, 2, 3, 4, 5]
letters = ["a", "b", "c", "d", "e"]

combined_list = itertools.chain(numbers, letters)

print(list(combined_list))

combination : Phép phân phối tạo ra tuples

import itertools

numbers = [1, 2, 3, 4, 5]

combinations = itertools.combinations(numbers, 2)

for combination in combinations:
  print(combination)

(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 3)
(2, 4)
(2, 5)
(3, 4)
(3, 5)
(4, 5)

permutation: Phép hoán vị

import itertools

numbers = [1, 2, 3]

permutations = itertools.permutations(numbers)

for permutation in permutations:
  print(permutation)

product: Tạo tất cả các tích số có thể có

import itertools

numbers = [1, 2, 3]

products = itertools.product(numbers)

for product in products:
  print(product)

(1, 1, 1)
(1, 1, 2)
(1, 1, 3)
(1, 2, 1)
---
(3, 3, 3)

🔥 Errors and Exception Handling

Trong các ứng dụng phân tích dữ liệu, nhiều hàm chỉ hoạt động trên một số loại đầu vào nhất định. Ví dụ: hàm float của Python có khả năng chuyển đổi chuỗi thành số thực, nhưng nó sẽ báo lỗi ValueError khi đầu vào không hợp lệ:

In [224]: float("1.2345")
Out[224]: 1.2345

In [225]: float("something")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-225-5ccfe07933f4> in <module>
----> 1 float("something")
ValueError: could not convert string to float: 'something'

Trường hợp ta mong muốn chủ động xử lý và kiểm soát lỗi thay vì phó mặt cho hệ thống thì ta có thể viết lại hàm trên bằng tryexcept như sau:

def attempt_float(x):
    try:
        return float(x)
    except:
        return x

Như vậy mỗi khi có lỗi (exception) xảy ra thì chương trình sẽ nhảy vào phần except: để thực thi

In [227]: attempt_float("1.2345")
Out[227]: 1.2345

In [228]: attempt_float("something")
Out[228]: 'something'

Thật ra có rất nhiều kiểu lỗi nên hệ thống cũng có thể trả về rất nhiều loại lỗi, lấy ví dụ như hàm float ở trên thử:

In [229]: float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-229-82f777b0e564> in <module>
----> 1 float((1, 2))
TypeError: float() argument must be a string or a real number, not 'tuple'

Như ta thấy thì ở đây nó trả về lỗi TypeError chứ không phải ValueError nữa rồi, lỗi ở đây ý chỉ rằng có 1 con bug không hợp lệ trong chương trình.

Vậy làm sao để kiểm soát đúng cái lỗi mình muốn theo dõi thôi? Vậy thì ta chỉ cần sử dụng đúng cái Exception type ngay sau except thôi:

def attempt_float(x):
    try:
        return float(x)
    except ValueError:
        return x

Có điểm khác xíu là lúc này với những lỗi chưa catch hay except lúc này sẽ báo lỗi ở ngay tại dòng return của try: thay vì lỗi lúc gọi hàm. Như vậy thì rõ ràng là cụ thể hơn đúng không

In [231]: attempt_float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-231-8b0026e9e6b7> in <module>
----> 1 attempt_float((1, 2))
<ipython-input-230-6209ddecd2b5> in attempt_float(x)
      1 def attempt_float(x):
      2     try:
----> 3         return float(x)
      4     except ValueError:
      5         return x
TypeError: float() argument must be a string or a real number, not 'tuple'

Ngoài ra ta còn có thể bắt nhiều loại exception cùng lúc bằng cách dùng 1 tuple để chứa các loại lỗi:

def attempt_float(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return x

Trong một số trường hợp, bạn muốn thực thi một đoạn chương trình nhất định bất kể try có thành công hay không thì hãy sử dụng finally:

f = open(path, mode="w")

try:
    write_to_file(f)
finally:
    f.close()

Như ở trên thì f lúc nào cũng closed. Tương tự nếu bạn muốn xử lý việc try: có thành công thì làm gì đó, hãy dùng else

f = open(path, mode="w")

try:
    write_to_file(f)
except:
    print("Failed")
else:
    print("Succeeded")
finally:
    f.close()

🎍 Exceptions in IPython

Nếu exception xuất hiện khi chạy %run đoạn script hay biểu thức, IPython mặc định sẽ in ra toàn bộ call stack tree (traceback) với vài dòng ngữ cảnh quanh vị trí được chỉ định trong stack:

In [10]: %run examples/ipython_bug.py
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in <module>()
     13     throws_an_exception()
     14
---> 15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
     11 def calling_things():
     12     works_fine()
---> 13     throws_an_exception()
     14
     15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
      7     a = 5
      8     b = 6
----> 9     assert(a + b == 10)
     10
     11 def calling_things():

AssertionError:

Có thêm ngữ cảnh tự nó là một lợi thế lớn so với trình thông dịch Python tiêu chuẩn (không cung cấp bất kỳ ngữ cảnh bổ sung nào). Bạn có thể kiểm soát lượng ngữ cảnh được hiển thị bằng cách sử dụng magic method %xmode, từ Plain (giống như trình thông dịch Python tiêu chuẩn) đến Verbose (hiển thị trực tiếp giá trị tham số hàm và nhiều hơn thế nữa).

Như bạn sẽ thấy sau này trong Phụ lục B: Chi tiết hơn về Hệ thống IPython, bạn có thể biết thêm về stack (sử dụng %debug hoặc %pdb) sau khi xảy ra lỗi để gỡ lỗi.

Lời kết

Buổi hôm nay có các kiến thức khá là trừu tượng nhưng ảnh hưởng và giúp ích rất nhiều cho quá trình debug cũng nhưng đọc tài liệu vậy nên mình mới cần tường tận giải thích.

Hẹn gặp các bạn và buổi Livestream kế tiếp nhé (8:00 PM – T3 và CN hằng tuần)

Các kênh cộng đồng của mình

  • Facebook: https://www.facebook.com/bocuoilamdata
  • Youtube: https://www.youtube.com/@CarrY4U_VN
  • Website: https://carry4u.org/

© Bản quyền thuộc về CarrY4U và Bò Cười Làm Data
© Copyright by CarrY4U and bocuoilamdata ☞ Do not Reup”