Hiểu về máy phát điện trong Python


218

Hiện tại tôi đang đọc sách dạy nấu ăn Python và hiện đang xem máy phát điện. Tôi cảm thấy khó khăn để có được vòng đầu của tôi.

Khi tôi đến từ một nền tảng Java, có tương đương với Java không? Cuốn sách đã nói về 'Nhà sản xuất / Người tiêu dùng', tuy nhiên khi tôi nghe rằng tôi nghĩ về việc xâu chuỗi.

Máy phát điện là gì và tại sao bạn sẽ sử dụng nó? Rõ ràng, không trích dẫn bất kỳ cuốn sách nào (trừ khi bạn có thể tìm thấy một câu trả lời đơn giản, đàng hoàng trực tiếp từ một cuốn sách). Có lẽ với các ví dụ, nếu bạn cảm thấy hào phóng!

Câu trả lời:


402

Lưu ý: bài đăng này giả định cú pháp Python 3.x.

Trình tạo chỉ đơn giản là một hàm trả về một đối tượng mà bạn có thể gọi next, như vậy với mỗi cuộc gọi, nó trả về một số giá trị, cho đến khi nó đưa ra một StopIterationngoại lệ, báo hiệu rằng tất cả các giá trị đã được tạo. Một đối tượng như vậy được gọi là một vòng lặp .

Các hàm bình thường trả về một giá trị bằng cách sử dụng return, giống như trong Java. Trong Python, tuy nhiên, có một sự thay thế, được gọi là yield. Sử dụng yieldbất cứ nơi nào trong một chức năng làm cho nó một máy phát điện. Quan sát mã này:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Như bạn có thể thấy, myGen(n)là một hàm mang lại nn + 1. Mỗi lệnh gọi nextmang lại một giá trị duy nhất, cho đến khi tất cả các giá trị đã được mang lại. forvòng lặp gọi nexttrong nền, do đó:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Tương tự như vậy, có các biểu thức máy phát , cung cấp một phương tiện để mô tả ngắn gọn một số loại máy phát phổ biến nhất định:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Lưu ý rằng các biểu thức trình tạo rất giống với việc hiểu danh sách :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Quan sát rằng một đối tượng trình tạo được tạo một lần , nhưng mã của nó không chạy cùng một lúc. Chỉ gọi để nextthực sự thực thi (một phần) mã. Việc thực thi mã trong trình tạo dừng lại sau khi yieldđạt được câu lệnh, khi đó nó trả về một giá trị. Cuộc gọi tiếp theo nextsau đó sẽ khiến việc thực thi tiếp tục ở trạng thái mà trình tạo còn lại sau lần cuối yield. Đây là một sự khác biệt cơ bản với các hàm thông thường: những hàm này luôn bắt đầu thực thi ở "đỉnh" và loại bỏ trạng thái của chúng khi trả về một giá trị.

Có nhiều điều để nói về chủ đề này. Đó là ví dụ có thể senddữ liệu trở lại vào một trình tạo ( tham khảo ). Nhưng đó là điều tôi khuyên bạn không nên tìm hiểu cho đến khi bạn hiểu khái niệm cơ bản của máy phát điện.

Bây giờ bạn có thể hỏi: tại sao sử dụng máy phát điện? Có một vài lý do chính đáng:

  • Một số khái niệm có thể được mô tả ngắn gọn hơn nhiều bằng cách sử dụng máy phát điện.
  • Thay vì tạo một hàm trả về một danh sách các giá trị, người ta có thể viết một trình tạo tạo ra các giá trị một cách nhanh chóng. Điều này có nghĩa là không có danh sách nào cần được xây dựng, có nghĩa là mã kết quả là bộ nhớ hiệu quả hơn. Theo cách này, người ta thậm chí có thể mô tả các luồng dữ liệu đơn giản là quá lớn để phù hợp với bộ nhớ.
  • Máy phát điện cho phép một cách tự nhiên để mô tả các luồng vô hạn . Ví dụ, hãy xem xét các số Fibonacci :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    Mã này sử dụng itertools.isliceđể lấy một số phần tử hữu hạn từ một luồng vô hạn. Bạn nên có một cái nhìn tốt về các chức năng trong itertoolsmô-đun, vì chúng là những công cụ cần thiết để viết các trình tạo tiên tiến rất dễ dàng.


   Về Python <= 2.6: trong ví dụ trên nextlà một chức năng mà gọi phương thức __next__trên đối tượng nhất định. Trong Python <= 2.6 người ta sử dụng một kỹ thuật hơi khác, cụ thể là o.next()thay vì next(o). Python 2.7 có next()cuộc gọi .nextnên bạn không cần sử dụng như sau trong 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3

9
Bạn đề cập đến nó có thể senddữ liệu cho một máy phát điện. Một khi bạn làm điều đó, bạn có một 'coroutine'. Rất đơn giản để thực hiện các mẫu như Người tiêu dùng / Nhà sản xuất được đề cập với coroutines vì ​​họ không có nhu cầu về Locks và do đó không thể bế tắc. Thật khó để mô tả coroutines mà không bash thread, vì vậy tôi sẽ chỉ nói coroutines là một thay thế rất thanh lịch cho luồng.
Jochen Ritzel

Các trình tạo Python về cơ bản là các máy Turing về cách thức hoạt động của chúng?
Phượng hoàng lửa

48

Trình tạo thực sự là một hàm trả về (dữ liệu) trước khi kết thúc, nhưng nó tạm dừng tại thời điểm đó và bạn có thể tiếp tục chức năng tại thời điểm đó.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

và như thế. Lợi ích (hoặc một) của máy phát điện là bởi vì chúng xử lý dữ liệu từng mảnh một, bạn có thể xử lý một lượng lớn dữ liệu; với danh sách, yêu cầu bộ nhớ quá mức có thể trở thành một vấn đề. Các trình tạo, giống như các danh sách, có thể lặp lại, vì vậy chúng có thể được sử dụng theo cùng một cách:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Lưu ý rằng máy phát điện cung cấp một cách khác để đối phó với vô cực, ví dụ

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Trình tạo đóng gói một vòng lặp vô hạn, nhưng đây không phải là vấn đề vì bạn chỉ nhận được mỗi câu trả lời mỗi khi bạn yêu cầu.


30

Trước hết, trình tạo thuật ngữ ban đầu có phần không rõ ràng trong Python, dẫn đến nhiều nhầm lẫn. Bạn có thể có nghĩa là lặplặp (xem ở đây ). Sau đó, trong Python cũng có các hàm trình tạo (trả về một đối tượng trình tạo), các đối tượng trình tạo (là các trình lặp) và các biểu thức của trình tạo (được ước tính cho một đối tượng trình tạo).

Theo mục chú giải cho máy phát điện , có vẻ như thuật ngữ chính thức hiện nay là máy phát điện là viết tắt của "hàm tạo". Trước đây, tài liệu đã xác định các thuật ngữ không nhất quán, nhưng may mắn là điều này đã được sửa.

Nó vẫn có thể là một ý tưởng tốt để chính xác và tránh thuật ngữ "trình tạo" mà không có thông số kỹ thuật thêm.


2
Hmm Tôi nghĩ rằng bạn đúng, ít nhất là theo thử nghiệm của một vài dòng trong Python 2.6. Biểu thức trình tạo trả về một trình vòng lặp (còn gọi là 'đối tượng trình tạo'), không phải là trình tạo.
Craig McQueen

22

Các trình tạo có thể được coi là tốc ký để tạo ra một trình vòng lặp. Chúng hoạt động như một Trình lặp Java. Thí dụ:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Hy vọng điều này sẽ giúp / là những gì bạn đang tìm kiếm.

Cập nhật:

Như nhiều câu trả lời khác đang hiển thị, có nhiều cách khác nhau để tạo ra một trình tạo. Bạn có thể sử dụng cú pháp dấu ngoặc đơn như trong ví dụ của tôi ở trên hoặc bạn có thể sử dụng năng suất. Một tính năng thú vị khác là máy phát điện có thể là "vô hạn" - các trình vòng lặp không dừng:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
Bây giờ, Java có Streams, tương tự nhiều hơn với các trình tạo, ngoại trừ việc bạn dường như không thể có được phần tử tiếp theo mà không gặp rắc rối đáng ngạc nhiên.
Vụ kiện của Quỹ Monica

12

Không có tương đương Java.

Đây là một ví dụ về một ví dụ:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

Có một vòng lặp trong trình tạo chạy từ 0 đến n và nếu biến vòng lặp là bội số của 3, nó mang lại biến.

Trong mỗi lần lặp của forvòng lặp, trình tạo được thực thi. Nếu đó là lần đầu tiên trình tạo thực thi, nó khởi động từ đầu, nếu không thì nó tiếp tục từ lần trước nó mang lại.


2
Đoạn cuối rất quan trọng: Trạng thái của hàm tạo được 'đóng băng' mỗi khi nó mang lại sth và tiếp tục ở chính xác trạng thái tương tự khi nó được gọi vào lần tiếp theo.
Julian Charra

Không có cú pháp tương đương trong Java với "biểu thức trình tạo", nhưng các trình tạo - một khi bạn đã có - về cơ bản chỉ là một trình vòng lặp (các đặc điểm cơ bản giống như trình lặp Java).
lật đổ

@overthink: Chà, các trình tạo có thể có các tác dụng phụ khác mà các trình vòng lặp Java không thể có. Nếu tôi đặt print "hello"sau x=x+1ví dụ của mình, "xin chào" sẽ được in 100 lần, trong khi phần thân của vòng lặp for vẫn sẽ chỉ được thực thi 33 lần.
Wernsey

@iWerner: Khá chắc chắn hiệu ứng tương tự có thể có trong Java. Việc triển khai next () trong trình lặp Java tương đương vẫn sẽ phải tìm kiếm từ 0 đến 99 (sử dụng ví dụ mygen (100)), do đó bạn có thể System.out.println () mỗi lần nếu bạn muốn. Bạn chỉ trở lại 33 lần kể từ tiếp theo (). Những gì Java thiếu là cú pháp năng suất rất tiện dụng, dễ đọc hơn (và viết).
lật đổ

Tôi thích đọc và ghi nhớ một dòng def này: Nếu đó là lần đầu tiên trình tạo thực thi, nó bắt đầu từ đầu, nếu không, nó tiếp tục từ lần trước nó mang lại.
Iqra.

8

Tôi thích mô tả các máy phát điện, cho những người có nền tảng tốt về ngôn ngữ lập trình và điện toán, về các khung stack.

Trong nhiều ngôn ngữ, có một ngăn xếp trên đầu là "khung" ngăn xếp hiện tại. Khung ngăn xếp bao gồm không gian được phân bổ cho các biến cục bộ cho hàm bao gồm các đối số được truyền vào hàm đó.

Khi bạn gọi một hàm, điểm thực hiện hiện tại ("bộ đếm chương trình" hoặc tương đương) được đẩy lên ngăn xếp và một khung ngăn xếp mới được tạo. Thực thi sau đó chuyển đến đầu của hàm được gọi.

Với các hàm thông thường, tại một số điểm, hàm trả về một giá trị và ngăn xếp được "bật". Khung ngăn xếp của hàm bị loại bỏ và thực thi lại tại vị trí trước đó.

Khi một hàm là một trình tạo, nó có thể trả về một giá trị mà không bị loại bỏ khung ngăn xếp, sử dụng câu lệnh lợi suất. Các giá trị của biến cục bộ và bộ đếm chương trình trong hàm được bảo toàn. Điều này cho phép máy phát điện được nối lại sau đó, với việc thực thi tiếp tục từ câu lệnh lợi suất và nó có thể thực thi nhiều mã hơn và trả về giá trị khác.

Trước Python 2.5, đây là tất cả các trình tạo. Python 2.5 cũng đã thêm khả năng chuyển các giá trị trở lại vào trình tạo. Khi làm như vậy, giá trị truyền vào có sẵn dưới dạng một biểu thức kết quả từ câu lệnh lợi tức đã tạm thời trả lại quyền điều khiển (và giá trị) từ trình tạo.

Lợi thế chính của máy phát điện là "trạng thái" của chức năng được bảo toàn, không giống như các chức năng thông thường khi mỗi lần bỏ khung ngăn xếp, bạn sẽ mất tất cả "trạng thái" đó. Một lợi thế thứ cấp là một số chức năng gọi qua chức năng (tạo và xóa các khung ngăn xếp) được tránh, mặc dù đây thường là một lợi thế nhỏ.


6

Điều duy nhất tôi có thể thêm vào câu trả lời của Stephan202 là một lời khuyên rằng bạn hãy xem bài thuyết trình "Trình tạo thủ thuật cho các lập trình viên hệ thống" của David Beazley, đó là lời giải thích duy nhất về cách thức và lý do của các trình tạo mà tôi đã thấy bất cứ nơi nào Đây là điều đã đưa tôi từ "Python có vẻ vui" đến "Đây là thứ tôi đang tìm kiếm." Đó là tại http://www.dabeaz.com/generators/ .


6

Nó giúp phân biệt rõ ràng giữa hàm foo và hàm tạo foo (n):

def foo(n):
    yield n
    yield n+1

foo là một chức năng. foo (6) là một đối tượng tạo.

Cách điển hình để sử dụng một đối tượng trình tạo là trong một vòng lặp:

for n in foo(6):
    print(n)

Các vòng lặp in

# 6
# 7

Hãy nghĩ về một máy phát điện như là một chức năng có thể nối lại.

yieldhành xử giống như returntrong ý nghĩa rằng các giá trị được mang lại sẽ được "trả về" bởi trình tạo. Tuy nhiên, không giống như trả về, lần sau khi trình tạo được yêu cầu một giá trị, hàm của trình tạo, foo, sẽ tiếp tục ở nơi nó dừng lại - sau câu lệnh lợi suất cuối cùng - và tiếp tục chạy cho đến khi nó đạt được một câu lệnh lợi suất khác.

Đằng sau hậu trường, khi bạn gọi bar=foo(6)thanh đối tượng trình tạo được xác định để bạn có một nextthuộc tính.

Bạn có thể tự gọi nó để lấy các giá trị mang lại từ foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Khi foo kết thúc (và không còn giá trị nào mang lại), việc gọi next(bar)sẽ gây ra lỗi StopInteration.


5

Bài đăng này sẽ sử dụng các số Fibonacci như một công cụ để xây dựng để giải thích tính hữu ích của các trình tạo Python .

Bài đăng này sẽ có cả mã C ++ và Python.

Các số Fibonacci được định nghĩa là chuỗi: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

Hoặc nói chung:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Điều này có thể được chuyển vào một chức năng C ++ cực kỳ dễ dàng:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Nhưng nếu bạn muốn in sáu số Fibonacci đầu tiên, bạn sẽ tính toán lại rất nhiều giá trị với hàm trên.

Ví dụ : Fib(3) = Fib(2) + Fib(1), nhưng Fib(2)cũng tính toán lại Fib(1). Giá trị bạn muốn tính toán càng cao, bạn sẽ càng tệ.

Vì vậy, người ta có thể bị cám dỗ để viết lại ở trên bằng cách theo dõi trạng thái trong main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Nhưng điều này rất xấu, và nó làm phức tạp logic của chúng tôi main. Sẽ tốt hơn nếu không phải lo lắng về trạng thái trong mainchức năng của chúng tôi .

Chúng ta có thể trả về một vectorgiá trị và sử dụng iteratorđể lặp lại tập hợp các giá trị đó, nhưng điều này đòi hỏi rất nhiều bộ nhớ cùng một lúc cho một số lượng lớn các giá trị trả về.

Vì vậy, trở lại với cách tiếp cận cũ của chúng tôi, điều gì xảy ra nếu chúng tôi muốn làm một cái gì đó khác ngoài việc in các con số? Chúng tôi phải sao chép và dán toàn bộ khối mã vào mainvà thay đổi các câu lệnh đầu ra thành bất cứ điều gì khác mà chúng tôi muốn làm. Và nếu bạn sao chép và dán mã, thì bạn nên bị bắn. Bạn không muốn bị bắn, phải không?

Để giải quyết những vấn đề này và để tránh bị bắn, chúng tôi có thể viết lại khối mã này bằng chức năng gọi lại. Mỗi khi gặp phải một số Fibonacci mới, chúng ta sẽ gọi hàm gọi lại.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Đây rõ ràng là một sự cải tiến, logic của bạn mainkhông bị lộn xộn và bạn có thể làm bất cứ điều gì bạn muốn với các số Fibonacci, chỉ cần xác định các cuộc gọi lại mới.

Nhưng điều này vẫn chưa hoàn hảo. Điều gì sẽ xảy ra nếu bạn muốn chỉ nhận được hai số Fibonacci đầu tiên, và sau đó làm một cái gì đó, sau đó nhận thêm một số, sau đó làm một cái gì đó khác?

Chà, chúng ta có thể tiếp tục như chúng ta đã từng và chúng ta có thể bắt đầu thêm trạng thái trở lại main, cho phép GetFibNumbers bắt đầu từ một điểm tùy ý. Nhưng điều này sẽ làm tăng thêm mã của chúng tôi và nó có vẻ quá lớn đối với một tác vụ đơn giản như in các số Fibonacci.

Chúng tôi có thể thực hiện một mô hình sản xuất và tiêu dùng thông qua một vài chủ đề. Nhưng điều này làm phức tạp mã hơn nữa.

Thay vào đó hãy nói về máy phát điện.

Python có một tính năng ngôn ngữ rất hay giúp giải quyết các vấn đề như những máy phát điện này.

Một trình tạo cho phép bạn thực thi một chức năng, dừng tại một điểm tùy ý và sau đó tiếp tục lại nơi bạn rời đi. Mỗi lần trả lại một giá trị.

Hãy xem xét đoạn mã sau sử dụng trình tạo:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Cung cấp cho chúng tôi kết quả:

0 1 1 2 3 5

Câu yieldlệnh được sử dụng kết hợp với trình tạo Python. Nó lưu trạng thái của hàm và trả về giá trị yeilded. Lần sau khi bạn gọi hàm next () trên trình tạo, nó sẽ tiếp tục ở nơi sản lượng còn lại.

Điều này là sạch hơn nhiều so với mã chức năng gọi lại. Chúng tôi có mã sạch hơn, mã nhỏ hơn và chưa kể mã chức năng nhiều hơn (Python cho phép số nguyên lớn tùy ý).

Nguồn


3

Tôi tin rằng sự xuất hiện đầu tiên của các trình vòng lặp và trình tạo là trong ngôn ngữ lập trình Icon, khoảng 20 năm trước.

Bạn có thể thưởng thức tổng quan về Biểu tượng , cho phép bạn quấn đầu xung quanh chúng mà không tập trung vào cú pháp (vì Biểu tượng là ngôn ngữ mà bạn có thể không biết và Griswold đang giải thích lợi ích của ngôn ngữ của mình đối với những người đến từ các ngôn ngữ khác).

Sau khi đọc chỉ một vài đoạn ở đó, tiện ích của trình tạo và lặp có thể trở nên rõ ràng hơn.


2

Kinh nghiệm với việc hiểu danh sách đã cho thấy tiện ích rộng rãi của họ trên khắp Python. Tuy nhiên, nhiều trường hợp sử dụng không cần phải có một danh sách đầy đủ được tạo trong bộ nhớ. Thay vào đó, họ chỉ cần lặp đi lặp lại các yếu tố một lần.

Chẳng hạn, mã tổng hợp sau đây sẽ xây dựng một danh sách đầy đủ các ô vuông trong bộ nhớ, lặp lại các giá trị đó và khi tham chiếu không còn cần thiết nữa, hãy xóa danh sách:

sum([x*x for x in range(10)])

Bộ nhớ được bảo tồn bằng cách sử dụng biểu thức trình tạo thay thế:

sum(x*x for x in range(10))

Lợi ích tương tự được trao cho các nhà xây dựng cho các đối tượng container:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Biểu thức trình tạo đặc biệt hữu ích với các hàm như sum (), min () và max () làm giảm đầu vào lặp lại thành một giá trị duy nhất:

max(len(line)  for line in file  if line.strip())

hơn


1

Tôi đưa ra đoạn mã này giải thích 3 khái niệm chính về máy phát điện:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.